Merge branch 'dev-v2' of https://github.com/google/ExoPlayer into dev-v2
70
.github/ISSUE_TEMPLATE/bug.md
vendored
|
|
@ -6,57 +6,39 @@ labels: bug, needs triage
|
|||
assignees: ''
|
||||
---
|
||||
|
||||
We can only process bug reports that are actionable. Unclear bug reports or
|
||||
reports with insufficient information may not get attention.
|
||||
|
||||
Before filing a bug:
|
||||
-----------------------
|
||||
-------------------------
|
||||
|
||||
- Search existing issues, including issues that are closed:
|
||||
https://github.com/google/ExoPlayer/issues?q=is%3Aissue
|
||||
- Consult our developer website, which can be found at https://exoplayer.dev/.
|
||||
It provides detailed information about supported formats and devices.
|
||||
- Learn how to create useful log output by using the EventLogger:
|
||||
https://exoplayer.dev/listening-to-player-events.html#using-eventlogger
|
||||
- Rule out issues in your own code. A good way to do this is to try and
|
||||
reproduce the issue in the ExoPlayer demo app. Information about the ExoPlayer
|
||||
demo app can be found here:
|
||||
http://exoplayer.dev/demo-application.html.
|
||||
- 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:
|
||||
-----------------------
|
||||
Fill out the sections below, leaving the headers but replacing the content. If
|
||||
you're unable to provide certain information, please explain why in the relevant
|
||||
section. We may close issues if they do not include sufficient information.
|
||||
-------------------------
|
||||
|
||||
### [REQUIRED] Issue description
|
||||
Describe the issue in detail, including observed and expected behavior.
|
||||
|
||||
### [REQUIRED] Reproduction steps
|
||||
Describe how the issue can be reproduced, ideally using the ExoPlayer demo app
|
||||
or a small sample app that you’re able to share as source code on GitHub.
|
||||
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:
|
||||
|
||||
### [REQUIRED] Link to test content
|
||||
Provide a JSON snippet for the demo app’s media.exolist.json file, or a link to
|
||||
media that reproduces the issue. If you don't wish to post it publicly, please
|
||||
submit the issue, then email the link to dev.exoplayer@gmail.com using a subject
|
||||
in the format "Issue #1234", where "#1234" should be replaced with your issue
|
||||
number. Provide all the metadata we'd need to play the content like drm license
|
||||
urls or similar. If the content is accessible only in certain countries or
|
||||
regions, please say so.
|
||||
- Clear reproduction steps including observed and expected behavior
|
||||
- Output of running "adb bugreport" in the console shortly after encountering
|
||||
the issue
|
||||
- URI to test content for reproduction
|
||||
- For protected content:
|
||||
- DRM scheme and license server URL
|
||||
- Authentication HTTP headers
|
||||
|
||||
### [REQUIRED] A full bug report captured from the device
|
||||
Capture a full bug report using "adb bugreport". Output from "adb logcat" or a
|
||||
log snippet is NOT sufficient. Please attach the captured bug report as a file.
|
||||
If you don't wish to post it publicly, please submit the issue, then email the
|
||||
bug report to dev.exoplayer@gmail.com using a subject in the format
|
||||
"Issue #1234", where "#1234" should be replaced with your issue number.
|
||||
- ExoPlayer version number
|
||||
- Android version
|
||||
- Android device
|
||||
|
||||
### [REQUIRED] Version of ExoPlayer being used
|
||||
Specify the absolute version number. Avoid using terms such as "latest".
|
||||
|
||||
### [REQUIRED] Device(s) and version(s) of Android being used
|
||||
Specify the devices and versions of Android on which the issue can be
|
||||
reproduced, and how easily it reproduces. If possible, please test on multiple
|
||||
devices and Android versions.
|
||||
|
||||
<!-- DO NOT DELETE
|
||||
validate_template=true
|
||||
template_path=.github/ISSUE_TEMPLATE/bug.md
|
||||
-->
|
||||
If there's something you don't want to post publicly, please submit the issue,
|
||||
then email the link/bug report to dev.exoplayer@gmail.com using a subject in the
|
||||
format "Issue #1234", where #1234 is your issue number (we don't reply to
|
||||
emails).
|
||||
|
|
|
|||
58
.github/ISSUE_TEMPLATE/content_not_playing.md
vendored
|
|
@ -1,58 +0,0 @@
|
|||
---
|
||||
name: Content not playing correctly
|
||||
about: Issue template for a content not playing issue.
|
||||
title: ''
|
||||
labels: content not playing, needs triage
|
||||
assignees: ''
|
||||
---
|
||||
|
||||
Before filing a content issue:
|
||||
------------------------------
|
||||
- Search existing issues, including issues that are closed:
|
||||
https://github.com/google/ExoPlayer/issues?q=is%3Aissue
|
||||
- Consult our supported formats page, which can be found at
|
||||
https://exoplayer.dev/supported-formats.html.
|
||||
- Learn how to create useful log output by using the EventLogger:
|
||||
https://exoplayer.dev/listening-to-player-events.html#using-eventlogger
|
||||
- Try playing your content in the ExoPlayer demo app. Information about the
|
||||
ExoPlayer demo app can be found here:
|
||||
http://exoplayer.dev/demo-application.html.
|
||||
|
||||
When reporting a content issue:
|
||||
-----------------------------
|
||||
Fill out the sections below, leaving the headers but replacing the content. If
|
||||
you're unable to provide certain information, please explain why in the relevant
|
||||
section. We may close issues if they do not include sufficient information.
|
||||
|
||||
### [REQUIRED] Content description
|
||||
Describe the content and any specifics you expected to play but did not. This
|
||||
could be the container or sample format itself or any features the stream has
|
||||
and you expect to play, like 5.1 audio track, text tracks or drm systems.
|
||||
|
||||
### [REQUIRED] Link to test content
|
||||
Provide a JSON snippet for the demo app’s media.exolist.json file, or a link to
|
||||
media that reproduces the issue. If you don't wish to post it publicly, please
|
||||
submit the issue, then email the link to dev.exoplayer@gmail.com using a subject
|
||||
in the format "Issue #1234", where "#1234" should be replaced with your issue
|
||||
number. Provide all the metadata we'd need to play the content like drm license
|
||||
urls or similar. If the content is accessible only in certain countries or
|
||||
regions, please say so.
|
||||
|
||||
### [REQUIRED] Version of ExoPlayer being used
|
||||
Specify the absolute version number. Avoid using terms such as "latest".
|
||||
|
||||
### [REQUIRED] Device(s) and version(s) of Android being used
|
||||
Specify the devices and versions of Android on which you expect the content to
|
||||
play. If possible, please test on multiple devices and Android versions.
|
||||
|
||||
### [REQUIRED] A full bug report captured from the device
|
||||
Capture a full bug report using "adb bugreport". Output from "adb logcat" or a
|
||||
log snippet is NOT sufficient. Please attach the captured bug report as a file.
|
||||
If you don't wish to post it publicly, please submit the issue, then email the
|
||||
bug report to dev.exoplayer@gmail.com using a subject in the format
|
||||
"Issue #1234", where "#1234" should be replaced with your issue number.
|
||||
|
||||
<!-- DO NOT DELETE
|
||||
validate_template=true
|
||||
template_path=.github/ISSUE_TEMPLATE/content_not_playing.md
|
||||
-->
|
||||
9
.github/ISSUE_TEMPLATE/feature_request.md
vendored
|
|
@ -14,9 +14,7 @@ Before filing a feature request:
|
|||
|
||||
When filing a feature request:
|
||||
-----------------------
|
||||
Fill out the sections below, leaving the headers but replacing the content. If
|
||||
you're unable to provide certain information, please explain why in the relevant
|
||||
section. We may close issues if they do not include sufficient information.
|
||||
Replace the content in the sections below.
|
||||
|
||||
### [REQUIRED] Use case description
|
||||
Describe the use case or problem you are trying to solve in detail. If there are
|
||||
|
|
@ -28,8 +26,3 @@ A clear and concise description of your proposed solution, if you have one.
|
|||
### Alternatives considered
|
||||
A clear and concise description of any alternative solutions you considered,
|
||||
if applicable.
|
||||
|
||||
<!-- DO NOT DELETE
|
||||
validate_template=true
|
||||
template_path=.github/ISSUE_TEMPLATE/feature_request.md
|
||||
-->
|
||||
|
|
|
|||
64
.github/ISSUE_TEMPLATE/question.md
vendored
|
|
@ -6,50 +6,38 @@ labels: question, needs triage
|
|||
assignees: ''
|
||||
---
|
||||
|
||||
Unfortunately we can't answer all questions. Unclear questions or questions with
|
||||
insufficient information may not get attention.
|
||||
|
||||
Before filing a question:
|
||||
-----------------------
|
||||
- This issue tracker is intended ExoPlayer specific questions. If you're asking
|
||||
a general Android development question, please do so on Stack Overflow.
|
||||
- Search existing issues, including issues that are closed. It’s often the
|
||||
quickest way to get an answer!
|
||||
-------------------------
|
||||
|
||||
- Ask general Android development questions on Stack Overflow
|
||||
- Search existing issues, including issues that are closed
|
||||
https://github.com/google/ExoPlayer/issues?q=is%3Aissue
|
||||
- Consult our developer website, which can be found at https://exoplayer.dev/.
|
||||
It provides detailed information about supported formats, devices as well as
|
||||
information about how to use the ExoPlayer library.
|
||||
- The ExoPlayer library Javadoc can be found at
|
||||
https://exoplayer.dev/doc/reference/
|
||||
- Consult our developer website (https://exoplayer.dev/) and Javadoc
|
||||
(https://exoplayer.dev/doc/reference/)
|
||||
|
||||
When filing a question:
|
||||
-----------------------
|
||||
Fill out the sections below, leaving the headers but replacing the content. If
|
||||
you're unable to provide certain information, please explain why in the relevant
|
||||
section. We may close issues if they do not include sufficient information.
|
||||
-------------------------
|
||||
|
||||
### [REQUIRED] Searched documentation and issues
|
||||
Tell us where you’ve already looked for an answer to your question. It’s
|
||||
important for us to know this so that we can improve our documentation.
|
||||
|
||||
### [REQUIRED] Question
|
||||
Describe your question in detail.
|
||||
|
||||
### A full bug report captured from the device
|
||||
In case your question refers to a problem you are seeing in your app, capture a
|
||||
full bug report using "adb bugreport". Please attach the captured bug report as
|
||||
a file. If you don't wish to post it publicly, please submit the issue, then
|
||||
email the bug report to dev.exoplayer@gmail.com using a subject in the format
|
||||
"Issue #1234", where "#1234" should be replaced with your issue number.
|
||||
In case your question refers to a problem you are seeing in your app:
|
||||
|
||||
### Link to test content
|
||||
In case your question is related to a piece of media, which you are trying to
|
||||
play, please provide a JSON snippet for the demo app’s media.exolist.json file,
|
||||
or a link to media that reproduces the issue. If you don't wish to post it
|
||||
publicly, please submit the issue, then email the link to
|
||||
dev.exoplayer@gmail.com using a subject in the format "Issue #1234", where
|
||||
"#1234" should be replaced with your issue number. Provide all the metadata we'd
|
||||
need to play the content like drm license urls or similar. If the content is
|
||||
accessible only in certain countries or regions, please say so.
|
||||
- Output of running `$ adb bugreport` in the console
|
||||
|
||||
<!-- DO NOT DELETE
|
||||
validate_template=true
|
||||
template_path=.github/ISSUE_TEMPLATE/question.md
|
||||
-->
|
||||
In case your question is related to a piece of media:
|
||||
|
||||
- URI to test content
|
||||
- For protected content:
|
||||
- DRM scheme and license server URL
|
||||
- Authentication HTTP headers
|
||||
|
||||
Don't forget to check supported formats and devices
|
||||
(https://exoplayer.dev/supported-formats.html).
|
||||
|
||||
If there's something you don't want to post publicly, please submit the issue,
|
||||
then email the link/bug report to dev.exoplayer@gmail.com using a subject in the
|
||||
format "Issue #1234", where #1234 is your issue number (we don't reply to
|
||||
emails).
|
||||
|
|
|
|||
9
.gitignore
vendored
|
|
@ -47,6 +47,7 @@ bazel-testlogs
|
|||
.DS_Store
|
||||
cmake-build-debug
|
||||
dist
|
||||
jacoco.exec
|
||||
tmp
|
||||
|
||||
# External native builds
|
||||
|
|
@ -57,6 +58,10 @@ extensions/vp9/src/main/jni/libvpx
|
|||
extensions/vp9/src/main/jni/libvpx_android_configs
|
||||
extensions/vp9/src/main/jni/libyuv
|
||||
|
||||
# AV1 extension
|
||||
extensions/av1/src/main/jni/cpu_features
|
||||
extensions/av1/src/main/jni/libgav1
|
||||
|
||||
# Opus extension
|
||||
extensions/opus/src/main/jni/libopus
|
||||
|
||||
|
|
@ -71,7 +76,3 @@ extensions/cronet/jniLibs/*
|
|||
!extensions/cronet/jniLibs/README.md
|
||||
extensions/cronet/libs/*
|
||||
!extensions/cronet/libs/README.md
|
||||
|
||||
# Cast receiver
|
||||
cast_receiver_app/external-js
|
||||
cast_receiver_app/bazel-cast_receiver_app
|
||||
|
|
|
|||
81
.hgignore
|
|
@ -1,81 +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
|
||||
|
||||
# Android generated
|
||||
bin
|
||||
gen
|
||||
libs
|
||||
obj
|
||||
lint.xml
|
||||
|
||||
# IntelliJ IDEA
|
||||
.idea
|
||||
*.iml
|
||||
*.ipr
|
||||
*.iws
|
||||
classes
|
||||
gen-external-apklibs
|
||||
|
||||
# Eclipse
|
||||
.project
|
||||
.classpath
|
||||
.settings
|
||||
.checkstyle
|
||||
.cproject
|
||||
|
||||
# Gradle
|
||||
.gradle
|
||||
build
|
||||
buildout
|
||||
out
|
||||
|
||||
# Maven
|
||||
target
|
||||
release.properties
|
||||
pom.xml.*
|
||||
|
||||
# Ant
|
||||
ant.properties
|
||||
local.properties
|
||||
proguard.cfg
|
||||
proguard-project.txt
|
||||
|
||||
# Bazel
|
||||
bazel-bin
|
||||
bazel-genfiles
|
||||
bazel-out
|
||||
bazel-testlogs
|
||||
|
||||
# Other
|
||||
.DS_Store
|
||||
cmake-build-debug
|
||||
dist
|
||||
tmp
|
||||
|
||||
# VP9 extension
|
||||
extensions/vp9/src/main/jni/libvpx
|
||||
extensions/vp9/src/main/jni/libvpx_android_configs
|
||||
extensions/vp9/src/main/jni/libyuv
|
||||
|
||||
# Opus extension
|
||||
extensions/opus/src/main/jni/libopus
|
||||
|
||||
# FLAC extension
|
||||
extensions/flac/src/main/jni/flac
|
||||
|
||||
# FFmpeg extension
|
||||
extensions/ffmpeg/src/main/jni/ffmpeg
|
||||
|
||||
# Cronet extension
|
||||
extensions/cronet/jniLibs/*
|
||||
!extensions/cronet/jniLibs/README.md
|
||||
extensions/cronet/libs/*
|
||||
!extensions/cronet/libs/README.md
|
||||
|
||||
# Cast receiver
|
||||
cast_receiver_app/external-js
|
||||
cast_receiver_app/bazel-cast_receiver_app
|
||||
74
README.md
|
|
@ -1,4 +1,4 @@
|
|||
# ExoPlayer #
|
||||
# ExoPlayer <img src="https://img.shields.io/github/v/release/google/ExoPlayer.svg?label=latest"/>
|
||||
|
||||
ExoPlayer is an application level media player for Android. It provides an
|
||||
alternative to Android’s MediaPlayer API for playing audio and video both
|
||||
|
|
@ -7,7 +7,7 @@ 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.
|
||||
|
||||
## Documentation ##
|
||||
## Documentation
|
||||
|
||||
* The [developer guide][] provides a wealth of information.
|
||||
* The [class reference][] documents ExoPlayer classes.
|
||||
|
|
@ -20,30 +20,20 @@ and extend, and can be updated through Play Store application updates.
|
|||
[release notes]: https://github.com/google/ExoPlayer/blob/release-v2/RELEASENOTES.md
|
||||
[developer blog]: https://medium.com/google-exoplayer
|
||||
|
||||
## Using ExoPlayer ##
|
||||
## Using ExoPlayer
|
||||
|
||||
ExoPlayer modules can be obtained from JCenter. It's also possible to clone the
|
||||
repository and depend on the modules locally.
|
||||
ExoPlayer modules can be obtained from [the Google Maven repository][]. It's
|
||||
also possible to clone the repository and depend on the modules locally.
|
||||
|
||||
### From JCenter ###
|
||||
[the Google Maven repository]: https://developer.android.com/studio/build/dependencies#google-maven
|
||||
|
||||
#### 1. Add repositories ####
|
||||
### From the Google Maven repository
|
||||
|
||||
#### 1. Add ExoPlayer module dependencies
|
||||
|
||||
The easiest way to get started using ExoPlayer is to add it as a gradle
|
||||
dependency. You need to make sure you have the Google and JCenter repositories
|
||||
included in the `build.gradle` file in the root of your project:
|
||||
|
||||
```gradle
|
||||
repositories {
|
||||
google()
|
||||
jcenter()
|
||||
}
|
||||
```
|
||||
|
||||
#### 2. Add ExoPlayer module dependencies ####
|
||||
|
||||
Next add a dependency in the `build.gradle` file of your app module. The
|
||||
following will add a dependency to the full library:
|
||||
dependency in the `build.gradle` file of your app module. The following will add
|
||||
a dependency to the full library:
|
||||
|
||||
```gradle
|
||||
implementation 'com.google.android.exoplayer:exoplayer:2.X.X'
|
||||
|
|
@ -54,7 +44,7 @@ where `2.X.X` is your preferred version.
|
|||
As an alternative to the full library, you can depend on only the library
|
||||
modules that you actually need. For example the following will add dependencies
|
||||
on the Core, DASH and UI library modules, as might be required for an app that
|
||||
plays DASH content:
|
||||
only plays DASH content:
|
||||
|
||||
```gradle
|
||||
implementation 'com.google.android.exoplayer:exoplayer-core:2.X.X'
|
||||
|
|
@ -62,28 +52,32 @@ implementation 'com.google.android.exoplayer:exoplayer-dash:2.X.X'
|
|||
implementation 'com.google.android.exoplayer:exoplayer-ui:2.X.X'
|
||||
```
|
||||
|
||||
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
|
||||
library is equivalent to adding dependencies on all of the library modules
|
||||
individually.
|
||||
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 multiple extension modules that
|
||||
depend on external libraries to provide additional functionality. Some
|
||||
extensions are available from JCenter, whereas others must be built manually.
|
||||
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 from
|
||||
JCenter can be found on [Bintray][].
|
||||
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/
|
||||
[Bintray]: https://bintray.com/google/exoplayer
|
||||
[Google Maven ExoPlayer page]: https://maven.google.com/web/index.html#com.google.android.exoplayer
|
||||
|
||||
#### 3. Turn on Java 8 support ####
|
||||
#### 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
|
||||
|
|
@ -95,7 +89,13 @@ compileOptions {
|
|||
}
|
||||
```
|
||||
|
||||
### Locally ###
|
||||
#### 3. Enable multidex
|
||||
|
||||
If your Gradle `minSdkVersion` is 20 or lower, you should
|
||||
[enable multidex](https://developer.android.com/studio/build/multidex) in order
|
||||
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
|
||||
|
|
@ -107,6 +107,7 @@ branch:
|
|||
|
||||
```sh
|
||||
git clone https://github.com/google/ExoPlayer.git
|
||||
cd ExoPlayer
|
||||
git checkout release-v2
|
||||
```
|
||||
|
||||
|
|
@ -114,9 +115,8 @@ Next, add the following to your project's `settings.gradle` file, replacing
|
|||
`path/to/exoplayer` with the path to your local copy:
|
||||
|
||||
```gradle
|
||||
gradle.ext.exoplayerRoot = 'path/to/exoplayer'
|
||||
gradle.ext.exoplayerModulePrefix = 'exoplayer-'
|
||||
apply from: new File(gradle.ext.exoplayerRoot, 'core_settings.gradle')
|
||||
apply from: file("path/to/exoplayer/core_settings.gradle")
|
||||
```
|
||||
|
||||
You should now see the ExoPlayer modules appear as part of your project. You can
|
||||
|
|
@ -128,15 +128,15 @@ implementation project(':exoplayer-library-dash')
|
|||
implementation project(':exoplayer-library-ui')
|
||||
```
|
||||
|
||||
## Developing ExoPlayer ##
|
||||
## Developing ExoPlayer
|
||||
|
||||
#### Project branches ####
|
||||
#### 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.
|
||||
|
||||
#### Using Android Studio ####
|
||||
#### Using Android Studio
|
||||
|
||||
To develop ExoPlayer using Android Studio, simply open the ExoPlayer project in
|
||||
the root directory of the repository.
|
||||
|
|
|
|||
5638
RELEASENOTES.md
8
SECURITY.md
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
# Security policy #
|
||||
|
||||
To report a security issue, please email exoplayer-support+security@google.com
|
||||
with a description of the issue, the steps you took to create the issue,
|
||||
affected versions, and, if known, mitigations for the issue. Our vulnerability
|
||||
management team will respond within 3 working days of your email. If the issue
|
||||
is confirmed as a vulnerability, we will open a Security Advisory. This project
|
||||
follows a 90 day disclosure timeline.
|
||||
11
build.gradle
|
|
@ -14,22 +14,19 @@
|
|||
buildscript {
|
||||
repositories {
|
||||
google()
|
||||
jcenter()
|
||||
mavenCentral()
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:3.4.0'
|
||||
classpath 'com.novoda:bintray-release:0.9'
|
||||
classpath 'com.google.android.gms:strict-version-matcher-plugin:1.1.0'
|
||||
classpath 'com.android.tools.build:gradle:7.0.0'
|
||||
classpath 'com.google.android.gms:strict-version-matcher-plugin:1.2.2'
|
||||
}
|
||||
}
|
||||
allprojects {
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
jcenter()
|
||||
}
|
||||
project.ext {
|
||||
exoplayerPublishEnabled = false
|
||||
}
|
||||
if (it.hasProperty('externalBuildDir')) {
|
||||
if (!new File(externalBuildDir).isAbsolute()) {
|
||||
externalBuildDir = new File(rootDir, externalBuildDir)
|
||||
|
|
|
|||
34
common_library_config.gradle
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
// Copyright (C) 2020 The Android Open Source Project
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
apply from: "$gradle.ext.exoplayerSettingsDir/constants.gradle"
|
||||
apply plugin: 'com.android.library'
|
||||
|
||||
android {
|
||||
compileSdkVersion project.ext.compileSdkVersion
|
||||
|
||||
defaultConfig {
|
||||
minSdkVersion project.ext.minSdkVersion
|
||||
targetSdkVersion project.ext.targetSdkVersion
|
||||
consumerProguardFiles 'proguard-rules.txt'
|
||||
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
|
||||
testOptions.unitTests.includeAndroidResources = true
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
// Copyright (C) 2017 The Android Open Source Project
|
||||
// Copyright 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.
|
||||
|
|
@ -13,20 +13,46 @@
|
|||
// limitations under the License.
|
||||
project.ext {
|
||||
// ExoPlayer version and version code.
|
||||
releaseVersion = '2.10.4'
|
||||
releaseVersionCode = 2010004
|
||||
releaseVersion = '2.15.1'
|
||||
releaseVersionCode = 2015001
|
||||
minSdkVersion = 16
|
||||
targetSdkVersion = 28
|
||||
compileSdkVersion = 29
|
||||
dexmakerVersion = '2.21.0'
|
||||
mockitoVersion = '2.25.0'
|
||||
robolectricVersion = '4.3'
|
||||
autoValueVersion = '1.6'
|
||||
autoServiceVersion = '1.0-rc4'
|
||||
checkerframeworkVersion = '2.5.0'
|
||||
appTargetSdkVersion = 29
|
||||
// Upgrading this requires [Internal ref: b/193254928] to be fixed, or some
|
||||
// additional robolectric config.
|
||||
targetSdkVersion = 30
|
||||
compileSdkVersion = 31
|
||||
dexmakerVersion = '2.28.1'
|
||||
junitVersion = '4.13.2'
|
||||
// Use the same Guava version as the Android repo:
|
||||
// https://cs.android.com/android/platform/superproject/+/master:external/guava/METADATA
|
||||
guavaVersion = '27.1-android'
|
||||
mockitoVersion = '3.4.0'
|
||||
mockWebServerVersion = '3.12.0'
|
||||
robolectricVersion = '4.6.1'
|
||||
// Keep this in sync with Google's internal Checker Framework version.
|
||||
checkerframeworkVersion = '3.5.0'
|
||||
checkerframeworkCompatVersion = '2.5.0'
|
||||
errorProneVersion = '2.9.0'
|
||||
jsr305Version = '3.0.2'
|
||||
androidXTestVersion = '1.1.0'
|
||||
truthVersion = '0.44'
|
||||
kotlinAnnotationsVersion = '1.5.20'
|
||||
androidxAnnotationVersion = '1.2.0'
|
||||
androidxAppCompatVersion = '1.3.0'
|
||||
androidxCollectionVersion = '1.1.0'
|
||||
androidxCoreVersion = '1.6.0'
|
||||
androidxFuturesVersion = '1.1.0'
|
||||
androidxMediaVersion = '1.4.3'
|
||||
androidxMedia2Version = '1.1.3'
|
||||
androidxMultidexVersion = '2.0.1'
|
||||
androidxRecyclerViewVersion = '1.2.1'
|
||||
androidxMaterialVersion = '1.3.0'
|
||||
androidxTestCoreVersion = '1.3.0'
|
||||
androidxTestJUnitVersion = '1.1.2'
|
||||
androidxTestRunnerVersion = '1.3.0'
|
||||
androidxTestRulesVersion = '1.3.0'
|
||||
androidxTestServicesStorageVersion = '1.3.0'
|
||||
androidxTestTruthVersion = '1.3.0'
|
||||
truthVersion = '1.1.3'
|
||||
okhttpVersion = '4.9.1'
|
||||
modulePrefix = ':'
|
||||
if (gradle.ext.has('exoplayerModulePrefix')) {
|
||||
modulePrefix += gradle.ext.exoplayerModulePrefix
|
||||
|
|
|
|||
|
|
@ -11,52 +11,78 @@
|
|||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
def rootDir = gradle.ext.exoplayerRoot
|
||||
def rootDir = file(".")
|
||||
if (!gradle.ext.has('exoplayerSettingsDir')) {
|
||||
gradle.ext.exoplayerSettingsDir = rootDir.getCanonicalPath()
|
||||
}
|
||||
|
||||
def modulePrefix = ':'
|
||||
if (gradle.ext.has('exoplayerModulePrefix')) {
|
||||
modulePrefix += gradle.ext.exoplayerModulePrefix
|
||||
}
|
||||
|
||||
include modulePrefix + 'library'
|
||||
include modulePrefix + 'library-core'
|
||||
include modulePrefix + 'library-dash'
|
||||
include modulePrefix + 'library-hls'
|
||||
include modulePrefix + 'library-smoothstreaming'
|
||||
include modulePrefix + 'library-ui'
|
||||
include modulePrefix + 'testutils'
|
||||
include modulePrefix + 'extension-ffmpeg'
|
||||
include modulePrefix + 'extension-flac'
|
||||
include modulePrefix + 'extension-gvr'
|
||||
include modulePrefix + 'extension-ima'
|
||||
include modulePrefix + 'extension-cast'
|
||||
include modulePrefix + 'extension-cronet'
|
||||
include modulePrefix + 'extension-mediasession'
|
||||
include modulePrefix + 'extension-okhttp'
|
||||
include modulePrefix + 'extension-opus'
|
||||
include modulePrefix + 'extension-vp9'
|
||||
include modulePrefix + 'extension-rtmp'
|
||||
include modulePrefix + 'extension-leanback'
|
||||
include modulePrefix + 'extension-jobdispatcher'
|
||||
include modulePrefix + 'extension-workmanager'
|
||||
include modulePrefix + 'library-common'
|
||||
project(modulePrefix + 'library-common').projectDir = new File(rootDir, 'library/common')
|
||||
|
||||
project(modulePrefix + 'library').projectDir = new File(rootDir, 'library/all')
|
||||
project(modulePrefix + 'library-core').projectDir = new File(rootDir, 'library/core')
|
||||
project(modulePrefix + 'library-dash').projectDir = new File(rootDir, 'library/dash')
|
||||
project(modulePrefix + 'library-hls').projectDir = new File(rootDir, 'library/hls')
|
||||
project(modulePrefix + 'library-smoothstreaming').projectDir = new File(rootDir, 'library/smoothstreaming')
|
||||
project(modulePrefix + 'library-ui').projectDir = new File(rootDir, 'library/ui')
|
||||
project(modulePrefix + 'testutils').projectDir = new File(rootDir, 'testutils')
|
||||
project(modulePrefix + 'extension-ffmpeg').projectDir = new File(rootDir, 'extensions/ffmpeg')
|
||||
project(modulePrefix + 'extension-flac').projectDir = new File(rootDir, 'extensions/flac')
|
||||
project(modulePrefix + 'extension-gvr').projectDir = new File(rootDir, 'extensions/gvr')
|
||||
project(modulePrefix + 'extension-ima').projectDir = new File(rootDir, 'extensions/ima')
|
||||
project(modulePrefix + 'extension-cast').projectDir = new File(rootDir, 'extensions/cast')
|
||||
project(modulePrefix + 'extension-cronet').projectDir = new File(rootDir, 'extensions/cronet')
|
||||
include modulePrefix + 'extension-mediasession'
|
||||
project(modulePrefix + 'extension-mediasession').projectDir = new File(rootDir, 'extensions/mediasession')
|
||||
project(modulePrefix + 'extension-okhttp').projectDir = new File(rootDir, 'extensions/okhttp')
|
||||
project(modulePrefix + 'extension-opus').projectDir = new File(rootDir, 'extensions/opus')
|
||||
project(modulePrefix + 'extension-vp9').projectDir = new File(rootDir, 'extensions/vp9')
|
||||
project(modulePrefix + 'extension-rtmp').projectDir = new File(rootDir, 'extensions/rtmp')
|
||||
project(modulePrefix + 'extension-leanback').projectDir = new File(rootDir, 'extensions/leanback')
|
||||
project(modulePrefix + 'extension-jobdispatcher').projectDir = new File(rootDir, 'extensions/jobdispatcher')
|
||||
include modulePrefix + 'extension-media2'
|
||||
project(modulePrefix + 'extension-media2').projectDir = new File(rootDir, 'extensions/media2')
|
||||
|
||||
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 + '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 + '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 + '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 + 'library-extractor'
|
||||
project(modulePrefix + 'library-extractor').projectDir = new File(rootDir, 'library/extractor')
|
||||
|
||||
include modulePrefix + 'extension-cast'
|
||||
project(modulePrefix + 'extension-cast').projectDir = new File(rootDir, 'extensions/cast')
|
||||
|
||||
include modulePrefix + 'library-transformer'
|
||||
project(modulePrefix + 'library-transformer').projectDir = new File(rootDir, 'library/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')
|
||||
|
|
|
|||
|
|
@ -2,3 +2,24 @@
|
|||
|
||||
This directory contains applications that demonstrate how to use ExoPlayer.
|
||||
Browse the individual demos and their READMEs to learn more.
|
||||
|
||||
## Running a demo ##
|
||||
|
||||
### From Android Studio ###
|
||||
|
||||
* File -> New -> Import Project -> Specify the root ExoPlayer folder.
|
||||
* Choose the demo from the run configuration dropdown list.
|
||||
* Click Run.
|
||||
|
||||
### Using gradle from the command line: ###
|
||||
|
||||
* Open a Terminal window at the root ExoPlayer 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:installNoExtensionsDebug` installs the main ExoPlayer demo app
|
||||
in debug mode with no extensions.
|
||||
|
|
|
|||
|
|
@ -1,4 +1,7 @@
|
|||
# Cast demo application #
|
||||
# Cast demo
|
||||
|
||||
This folder contains a demo application that showcases ExoPlayer integration
|
||||
with Google Cast.
|
||||
This app demonstrates integration with Google Cast, as well as switching between
|
||||
Google Cast and local playback using ExoPlayer.
|
||||
|
||||
See the [demos README](../README.md) for instructions on how to build and run
|
||||
this demo.
|
||||
|
|
|
|||
|
|
@ -26,7 +26,8 @@ android {
|
|||
versionName project.ext.releaseVersion
|
||||
versionCode project.ext.releaseVersionCode
|
||||
minSdkVersion project.ext.minSdkVersion
|
||||
targetSdkVersion project.ext.targetSdkVersion
|
||||
targetSdkVersion project.ext.appTargetSdkVersion
|
||||
multiDexEnabled true
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
|
|
@ -37,6 +38,7 @@ android {
|
|||
"proguard-rules.txt",
|
||||
getDefaultProguardFile('proguard-android.txt')
|
||||
]
|
||||
signingConfig signingConfigs.debug
|
||||
}
|
||||
debug {
|
||||
jniDebuggable = true
|
||||
|
|
@ -53,13 +55,14 @@ 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 'com.google.android.material:material:1.0.0'
|
||||
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
|
||||
implementation 'androidx.appcompat:appcompat:1.0.2'
|
||||
implementation 'androidx.recyclerview:recyclerview:1.0.0'
|
||||
implementation 'androidx.appcompat:appcompat:' + androidxAppCompatVersion
|
||||
implementation 'androidx.multidex:multidex:' + androidxMultidexVersion
|
||||
implementation 'androidx.recyclerview:recyclerview:' + androidxRecyclerViewVersion
|
||||
implementation 'com.google.android.material:material:' + androidxMaterialVersion
|
||||
}
|
||||
|
||||
apply plugin: 'com.google.android.gms.strict-version-matcher-plugin'
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@
|
|||
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
|
||||
|
||||
<uses-sdk/>
|
||||
|
||||
|
|
@ -27,10 +28,11 @@
|
|||
<meta-data android:name="com.google.android.gms.cast.framework.OPTIONS_PROVIDER_CLASS_NAME"
|
||||
android:value="com.google.android.exoplayer2.ext.cast.DefaultCastOptionsProvider"/>
|
||||
|
||||
<activity android:name="com.google.android.exoplayer2.castdemo.MainActivity"
|
||||
<activity android:name=".MainActivity"
|
||||
android:configChanges="keyboard|keyboardHidden|orientation|screenSize|screenLayout|smallestScreenSize|uiMode"
|
||||
android:launchMode="singleTop" android:label="@string/application_name"
|
||||
android:theme="@style/Theme.AppCompat">
|
||||
android:theme="@style/Theme.AppCompat"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN"/>
|
||||
<category android:name="android.intent.category.LAUNCHER"/>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,23 @@
|
|||
/*
|
||||
* Copyright 2020 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.google.android.exoplayer2.castdemo;
|
||||
|
||||
import androidx.multidex.MultiDexApplication;
|
||||
|
||||
// Note: Multidex is enabled in code not AndroidManifest.xml because the internal build system
|
||||
// doesn't dejetify MultiDexApplication in AndroidManifest.xml.
|
||||
/** Application for multidex support. */
|
||||
public final class DemoApplication extends MultiDexApplication {}
|
||||
|
|
@ -17,8 +17,8 @@ package com.google.android.exoplayer2.castdemo;
|
|||
|
||||
import android.net.Uri;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.ext.cast.MediaItem;
|
||||
import com.google.android.exoplayer2.ext.cast.MediaItem.DrmConfiguration;
|
||||
import com.google.android.exoplayer2.MediaItem;
|
||||
import com.google.android.exoplayer2.MediaMetadata;
|
||||
import com.google.android.exoplayer2.util.MimeTypes;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
|
|
@ -42,19 +42,19 @@ import java.util.List;
|
|||
samples.add(
|
||||
new MediaItem.Builder()
|
||||
.setUri("https://storage.googleapis.com/wvmedia/clear/h264/tears/tears.mpd")
|
||||
.setTitle("Clear DASH: Tears")
|
||||
.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")
|
||||
.setTitle("Clear HLS: Angel one")
|
||||
.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")
|
||||
.setTitle("Clear MP4: Dizzy")
|
||||
.setMediaMetadata(new MediaMetadata.Builder().setTitle("Clear MP4: Dizzy").build())
|
||||
.setMimeType(MIME_TYPE_VIDEO_MP4)
|
||||
.build());
|
||||
|
||||
|
|
@ -62,39 +62,35 @@ import java.util.List;
|
|||
samples.add(
|
||||
new MediaItem.Builder()
|
||||
.setUri(Uri.parse("https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd"))
|
||||
.setTitle("Widevine DASH cenc: Tears")
|
||||
.setMediaMetadata(
|
||||
new MediaMetadata.Builder().setTitle("Widevine DASH cenc: Tears").build())
|
||||
.setMimeType(MIME_TYPE_DASH)
|
||||
.setDrmConfiguration(
|
||||
new DrmConfiguration(
|
||||
C.WIDEVINE_UUID,
|
||||
Uri.parse("https://proxy.uat.widevine.com/proxy?provider=widevine_test"),
|
||||
Collections.emptyMap()))
|
||||
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(
|
||||
Uri.parse(
|
||||
"https://storage.googleapis.com/wvmedia/cbc1/h264/tears/tears_aes_cbc1.mpd"))
|
||||
.setTitle("Widevine DASH cbc1: Tears")
|
||||
.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 DrmConfiguration(
|
||||
C.WIDEVINE_UUID,
|
||||
Uri.parse("https://proxy.uat.widevine.com/proxy?provider=widevine_test"),
|
||||
Collections.emptyMap()))
|
||||
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(
|
||||
Uri.parse(
|
||||
"https://storage.googleapis.com/wvmedia/cbcs/h264/tears/tears_aes_cbcs.mpd"))
|
||||
.setTitle("Widevine DASH cbcs: Tears")
|
||||
.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 DrmConfiguration(
|
||||
C.WIDEVINE_UUID,
|
||||
Uri.parse("https://proxy.uat.widevine.com/proxy?provider=widevine_test"),
|
||||
Collections.emptyMap()))
|
||||
new MediaItem.DrmConfiguration.Builder(C.WIDEVINE_UUID)
|
||||
.setLicenseUri("https://proxy.uat.widevine.com/proxy?provider=widevine_test")
|
||||
.build())
|
||||
.build());
|
||||
|
||||
SAMPLES = Collections.unmodifiableList(samples);
|
||||
|
|
|
|||
|
|
@ -17,15 +17,6 @@ package com.google.android.exoplayer2.castdemo;
|
|||
|
||||
import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.graphics.ColorUtils;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import androidx.recyclerview.widget.RecyclerView.ViewHolder;
|
||||
import androidx.recyclerview.widget.ItemTouchHelper;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
|
|
@ -36,18 +27,29 @@ import android.widget.ArrayAdapter;
|
|||
import android.widget.ListView;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.core.graphics.ColorUtils;
|
||||
import androidx.recyclerview.widget.ItemTouchHelper;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import androidx.recyclerview.widget.RecyclerView.ViewHolder;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.SimpleExoPlayer;
|
||||
import com.google.android.exoplayer2.ext.cast.MediaItem;
|
||||
import com.google.android.exoplayer2.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 SimpleExoPlayer} and supports casting using ExoPlayer's
|
||||
* Cast extension.
|
||||
* 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 {
|
||||
|
|
@ -171,8 +173,6 @@ public class MainActivity extends AppCompatActivity
|
|||
showToast(R.string.error_unsupported_audio);
|
||||
} else if (trackType == C.TRACK_TYPE_VIDEO) {
|
||||
showToast(R.string.error_unsupported_video);
|
||||
} else {
|
||||
// Do nothing.
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -199,17 +199,21 @@ public class MainActivity extends AppCompatActivity
|
|||
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);
|
||||
TextView v =
|
||||
(TextView)
|
||||
LayoutInflater.from(parent.getContext())
|
||||
.inflate(android.R.layout.simple_list_item_1, parent, false);
|
||||
return new QueueItemViewHolder(v);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(QueueItemViewHolder holder, int position) {
|
||||
holder.item = playerManager.getItem(position);
|
||||
holder.item = Assertions.checkNotNull(playerManager.getItem(position));
|
||||
|
||||
TextView view = holder.textView;
|
||||
view.setText(holder.item.title);
|
||||
view.setText(holder.item.mediaMetadata.title);
|
||||
// TODO: Solve coloring using the theme's ColorStateList.
|
||||
view.setTextColor(
|
||||
ColorUtils.setAlphaComponent(
|
||||
|
|
@ -221,7 +225,6 @@ public class MainActivity extends AppCompatActivity
|
|||
public int getItemCount() {
|
||||
return playerManager.getMediaQueueSize();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private class RecyclerViewCallback extends ItemTouchHelper.SimpleCallback {
|
||||
|
|
@ -236,7 +239,9 @@ public class MainActivity extends AppCompatActivity
|
|||
}
|
||||
|
||||
@Override
|
||||
public boolean onMove(RecyclerView list, RecyclerView.ViewHolder origin,
|
||||
public boolean onMove(
|
||||
@NonNull RecyclerView list,
|
||||
RecyclerView.ViewHolder origin,
|
||||
RecyclerView.ViewHolder target) {
|
||||
int fromPosition = origin.getAdapterPosition();
|
||||
int toPosition = target.getAdapterPosition();
|
||||
|
|
@ -261,7 +266,7 @@ public class MainActivity extends AppCompatActivity
|
|||
}
|
||||
|
||||
@Override
|
||||
public void clearView(RecyclerView recyclerView, ViewHolder viewHolder) {
|
||||
public void clearView(@NonNull RecyclerView recyclerView, @NonNull ViewHolder viewHolder) {
|
||||
super.clearView(recyclerView, viewHolder);
|
||||
if (draggingFromPosition != C.INDEX_UNSET) {
|
||||
QueueItemViewHolder queueItemHolder = (QueueItemViewHolder) viewHolder;
|
||||
|
|
@ -300,11 +305,11 @@ public class MainActivity extends AppCompatActivity
|
|||
super(context, android.R.layout.simple_list_item_1, DemoUtil.SAMPLES);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
@NonNull
|
||||
public View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) {
|
||||
View view = super.getView(position, convertView, parent);
|
||||
((TextView) view).setText(getItem(position).title);
|
||||
((TextView) view).setText(Util.castNonNull(getItem(position)).mediaMetadata.title);
|
||||
return view;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,50 +16,26 @@
|
|||
package com.google.android.exoplayer2.castdemo;
|
||||
|
||||
import android.content.Context;
|
||||
import android.net.Uri;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.View;
|
||||
import androidx.annotation.NonNull;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.ExoPlayerFactory;
|
||||
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.EventListener;
|
||||
import com.google.android.exoplayer2.Player.TimelineChangeReason;
|
||||
import com.google.android.exoplayer2.SimpleExoPlayer;
|
||||
import com.google.android.exoplayer2.Timeline;
|
||||
import com.google.android.exoplayer2.Timeline.Period;
|
||||
import com.google.android.exoplayer2.drm.DefaultDrmSessionManager;
|
||||
import com.google.android.exoplayer2.drm.DrmSessionManager;
|
||||
import com.google.android.exoplayer2.drm.FrameworkMediaCrypto;
|
||||
import com.google.android.exoplayer2.drm.FrameworkMediaDrm;
|
||||
import com.google.android.exoplayer2.drm.HttpMediaDrmCallback;
|
||||
import com.google.android.exoplayer2.drm.UnsupportedDrmException;
|
||||
import com.google.android.exoplayer2.TracksInfo;
|
||||
import com.google.android.exoplayer2.ext.cast.CastPlayer;
|
||||
import com.google.android.exoplayer2.ext.cast.DefaultMediaItemConverter;
|
||||
import com.google.android.exoplayer2.ext.cast.MediaItem;
|
||||
import com.google.android.exoplayer2.ext.cast.MediaItemConverter;
|
||||
import com.google.android.exoplayer2.ext.cast.SessionAvailabilityListener;
|
||||
import com.google.android.exoplayer2.source.ConcatenatingMediaSource;
|
||||
import com.google.android.exoplayer2.source.MediaSource;
|
||||
import com.google.android.exoplayer2.source.ProgressiveMediaSource;
|
||||
import com.google.android.exoplayer2.source.TrackGroupArray;
|
||||
import com.google.android.exoplayer2.source.dash.DashMediaSource;
|
||||
import com.google.android.exoplayer2.source.hls.HlsMediaSource;
|
||||
import com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource;
|
||||
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
|
||||
import com.google.android.exoplayer2.trackselection.MappingTrackSelector;
|
||||
import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
|
||||
import com.google.android.exoplayer2.ui.PlayerControlView;
|
||||
import com.google.android.exoplayer2.ui.PlayerView;
|
||||
import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory;
|
||||
import com.google.android.gms.cast.MediaQueueItem;
|
||||
import com.google.android.gms.cast.framework.CastContext;
|
||||
import java.util.ArrayList;
|
||||
import java.util.IdentityHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/** Manages players and an internal media queue for the demo app. */
|
||||
/* package */ class PlayerManager implements EventListener, SessionAvailabilityListener {
|
||||
/* package */ class PlayerManager implements Player.Listener, SessionAvailabilityListener {
|
||||
|
||||
/** Listener for events. */
|
||||
interface Listener {
|
||||
|
|
@ -75,27 +51,19 @@ import java.util.Map;
|
|||
void onUnsupportedTrack(int trackType);
|
||||
}
|
||||
|
||||
private static final String USER_AGENT = "ExoCastDemoPlayer";
|
||||
private static final DefaultHttpDataSourceFactory DATA_SOURCE_FACTORY =
|
||||
new DefaultHttpDataSourceFactory(USER_AGENT);
|
||||
|
||||
private final PlayerView localPlayerView;
|
||||
private final PlayerControlView castControlView;
|
||||
private final DefaultTrackSelector trackSelector;
|
||||
private final SimpleExoPlayer exoPlayer;
|
||||
private final Player localPlayer;
|
||||
private final CastPlayer castPlayer;
|
||||
private final ArrayList<MediaItem> mediaQueue;
|
||||
private final Listener listener;
|
||||
private final ConcatenatingMediaSource concatenatingMediaSource;
|
||||
private final MediaItemConverter mediaItemConverter;
|
||||
private final IdentityHashMap<MediaSource, FrameworkMediaDrm> mediaDrms;
|
||||
|
||||
private TrackGroupArray lastSeenTrackGroupArray;
|
||||
private TracksInfo lastSeenTrackGroupInfo;
|
||||
private int currentItemIndex;
|
||||
private Player currentPlayer;
|
||||
|
||||
/**
|
||||
* Creates a new manager for {@link SimpleExoPlayer} and {@link CastPlayer}.
|
||||
* Creates a new manager for {@link ExoPlayer} and {@link CastPlayer}.
|
||||
*
|
||||
* @param listener A {@link Listener} for queue position changes.
|
||||
* @param localPlayerView The {@link PlayerView} for local playback.
|
||||
|
|
@ -114,21 +82,17 @@ import java.util.Map;
|
|||
this.castControlView = castControlView;
|
||||
mediaQueue = new ArrayList<>();
|
||||
currentItemIndex = C.INDEX_UNSET;
|
||||
concatenatingMediaSource = new ConcatenatingMediaSource();
|
||||
mediaItemConverter = new DefaultMediaItemConverter();
|
||||
mediaDrms = new IdentityHashMap<>();
|
||||
|
||||
trackSelector = new DefaultTrackSelector(context);
|
||||
exoPlayer = ExoPlayerFactory.newSimpleInstance(context, trackSelector);
|
||||
exoPlayer.addListener(this);
|
||||
localPlayerView.setPlayer(exoPlayer);
|
||||
localPlayer = new ExoPlayer.Builder(context).build();
|
||||
localPlayer.addListener(this);
|
||||
localPlayerView.setPlayer(localPlayer);
|
||||
|
||||
castPlayer = new CastPlayer(castContext);
|
||||
castPlayer.addListener(this);
|
||||
castPlayer.setSessionAvailabilityListener(this);
|
||||
castControlView.setPlayer(castPlayer);
|
||||
|
||||
setCurrentPlayer(castPlayer.isCastSessionAvailable() ? castPlayer : exoPlayer);
|
||||
setCurrentPlayer(castPlayer.isCastSessionAvailable() ? castPlayer : localPlayer);
|
||||
}
|
||||
|
||||
// Queue manipulation methods.
|
||||
|
|
@ -139,7 +103,7 @@ import java.util.Map;
|
|||
* @param itemIndex The index of the item to play.
|
||||
*/
|
||||
public void selectQueueItem(int itemIndex) {
|
||||
setCurrentItem(itemIndex, C.TIME_UNSET, true);
|
||||
setCurrentItem(itemIndex);
|
||||
}
|
||||
|
||||
/** Returns the index of the currently played item. */
|
||||
|
|
@ -154,10 +118,7 @@ import java.util.Map;
|
|||
*/
|
||||
public void addItem(MediaItem item) {
|
||||
mediaQueue.add(item);
|
||||
concatenatingMediaSource.addMediaSource(buildMediaSource(item));
|
||||
if (currentPlayer == castPlayer) {
|
||||
castPlayer.addItems(mediaItemConverter.toMediaQueueItem(item));
|
||||
}
|
||||
currentPlayer.addMediaItem(item);
|
||||
}
|
||||
|
||||
/** Returns the size of the media queue. */
|
||||
|
|
@ -186,17 +147,7 @@ import java.util.Map;
|
|||
if (itemIndex == -1) {
|
||||
return false;
|
||||
}
|
||||
MediaSource removedMediaSource = concatenatingMediaSource.removeMediaSource(itemIndex);
|
||||
releaseMediaDrmOfMediaSource(removedMediaSource);
|
||||
if (currentPlayer == castPlayer) {
|
||||
if (castPlayer.getPlaybackState() != Player.STATE_IDLE) {
|
||||
Timeline castTimeline = castPlayer.getCurrentTimeline();
|
||||
if (castTimeline.getPeriodCount() <= itemIndex) {
|
||||
return false;
|
||||
}
|
||||
castPlayer.removeItem((int) castTimeline.getPeriod(itemIndex, new Period()).id);
|
||||
}
|
||||
}
|
||||
currentPlayer.removeMediaItem(itemIndex);
|
||||
mediaQueue.remove(itemIndex);
|
||||
if (itemIndex == currentItemIndex && itemIndex == mediaQueue.size()) {
|
||||
maybeSetCurrentItemAndNotify(C.INDEX_UNSET);
|
||||
|
|
@ -210,34 +161,25 @@ import java.util.Map;
|
|||
* Moves an item within the queue.
|
||||
*
|
||||
* @param item The item to move.
|
||||
* @param toIndex The target index of the item in the queue.
|
||||
* @param newIndex The target index of the item in the queue.
|
||||
* @return Whether the item move was successful.
|
||||
*/
|
||||
public boolean moveItem(MediaItem item, int toIndex) {
|
||||
public boolean moveItem(MediaItem item, int newIndex) {
|
||||
int fromIndex = mediaQueue.indexOf(item);
|
||||
if (fromIndex == -1) {
|
||||
return false;
|
||||
}
|
||||
// Player update.
|
||||
concatenatingMediaSource.moveMediaSource(fromIndex, toIndex);
|
||||
if (currentPlayer == castPlayer && castPlayer.getPlaybackState() != Player.STATE_IDLE) {
|
||||
Timeline castTimeline = castPlayer.getCurrentTimeline();
|
||||
int periodCount = castTimeline.getPeriodCount();
|
||||
if (periodCount <= fromIndex || periodCount <= toIndex) {
|
||||
return false;
|
||||
}
|
||||
int elementId = (int) castTimeline.getPeriod(fromIndex, new Period()).id;
|
||||
castPlayer.moveItem(elementId, toIndex);
|
||||
}
|
||||
|
||||
mediaQueue.add(toIndex, mediaQueue.remove(fromIndex));
|
||||
// Player update.
|
||||
currentPlayer.moveMediaItem(fromIndex, newIndex);
|
||||
mediaQueue.add(newIndex, mediaQueue.remove(fromIndex));
|
||||
|
||||
// Index update.
|
||||
if (fromIndex == currentItemIndex) {
|
||||
maybeSetCurrentItemAndNotify(toIndex);
|
||||
} else if (fromIndex < currentItemIndex && toIndex >= currentItemIndex) {
|
||||
maybeSetCurrentItemAndNotify(newIndex);
|
||||
} else if (fromIndex < currentItemIndex && newIndex >= currentItemIndex) {
|
||||
maybeSetCurrentItemAndNotify(currentItemIndex - 1);
|
||||
} else if (fromIndex > currentItemIndex && toIndex <= currentItemIndex) {
|
||||
} else if (fromIndex > currentItemIndex && newIndex <= currentItemIndex) {
|
||||
maybeSetCurrentItemAndNotify(currentItemIndex + 1);
|
||||
}
|
||||
|
||||
|
|
@ -251,7 +193,7 @@ import java.util.Map;
|
|||
* @return Whether the event was handled by the target view.
|
||||
*/
|
||||
public boolean dispatchKeyEvent(KeyEvent event) {
|
||||
if (currentPlayer == exoPlayer) {
|
||||
if (currentPlayer == localPlayer) {
|
||||
return localPlayerView.dispatchKeyEvent(event);
|
||||
} else /* currentPlayer == castPlayer */ {
|
||||
return castControlView.dispatchKeyEvent(event);
|
||||
|
|
@ -262,50 +204,44 @@ import java.util.Map;
|
|||
public void release() {
|
||||
currentItemIndex = C.INDEX_UNSET;
|
||||
mediaQueue.clear();
|
||||
concatenatingMediaSource.clear();
|
||||
for (FrameworkMediaDrm mediaDrm : mediaDrms.values()) {
|
||||
mediaDrm.release();
|
||||
}
|
||||
castPlayer.setSessionAvailabilityListener(null);
|
||||
castPlayer.release();
|
||||
localPlayerView.setPlayer(null);
|
||||
exoPlayer.release();
|
||||
localPlayer.release();
|
||||
}
|
||||
|
||||
// Player.EventListener implementation.
|
||||
// Player.Listener implementation.
|
||||
|
||||
@Override
|
||||
public void onPlayerStateChanged(boolean playWhenReady, @Player.State int playbackState) {
|
||||
public void onPlaybackStateChanged(@Player.State int playbackState) {
|
||||
updateCurrentItemIndex();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPositionDiscontinuity(@DiscontinuityReason int reason) {
|
||||
public void onPositionDiscontinuity(
|
||||
Player.PositionInfo oldPosition,
|
||||
Player.PositionInfo newPosition,
|
||||
@DiscontinuityReason int reason) {
|
||||
updateCurrentItemIndex();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTimelineChanged(Timeline timeline, @TimelineChangeReason int reason) {
|
||||
public void onTimelineChanged(@NonNull Timeline timeline, @TimelineChangeReason int reason) {
|
||||
updateCurrentItemIndex();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray trackSelections) {
|
||||
if (currentPlayer == exoPlayer && trackGroups != lastSeenTrackGroupArray) {
|
||||
MappingTrackSelector.MappedTrackInfo mappedTrackInfo =
|
||||
trackSelector.getCurrentMappedTrackInfo();
|
||||
if (mappedTrackInfo != null) {
|
||||
if (mappedTrackInfo.getTypeSupport(C.TRACK_TYPE_VIDEO)
|
||||
== MappingTrackSelector.MappedTrackInfo.RENDERER_SUPPORT_UNSUPPORTED_TRACKS) {
|
||||
listener.onUnsupportedTrack(C.TRACK_TYPE_VIDEO);
|
||||
}
|
||||
if (mappedTrackInfo.getTypeSupport(C.TRACK_TYPE_AUDIO)
|
||||
== MappingTrackSelector.MappedTrackInfo.RENDERER_SUPPORT_UNSUPPORTED_TRACKS) {
|
||||
listener.onUnsupportedTrack(C.TRACK_TYPE_AUDIO);
|
||||
}
|
||||
}
|
||||
lastSeenTrackGroupArray = trackGroups;
|
||||
public void onTracksInfoChanged(TracksInfo tracksInfo) {
|
||||
if (currentPlayer != localPlayer || tracksInfo == lastSeenTrackGroupInfo) {
|
||||
return;
|
||||
}
|
||||
if (!tracksInfo.isTypeSupportedOrEmpty(C.TRACK_TYPE_VIDEO)) {
|
||||
listener.onUnsupportedTrack(C.TRACK_TYPE_VIDEO);
|
||||
}
|
||||
if (!tracksInfo.isTypeSupportedOrEmpty(C.TRACK_TYPE_AUDIO)) {
|
||||
listener.onUnsupportedTrack(C.TRACK_TYPE_AUDIO);
|
||||
}
|
||||
lastSeenTrackGroupInfo = tracksInfo;
|
||||
}
|
||||
|
||||
// CastPlayer.SessionAvailabilityListener implementation.
|
||||
|
|
@ -317,7 +253,7 @@ import java.util.Map;
|
|||
|
||||
@Override
|
||||
public void onCastSessionUnavailable() {
|
||||
setCurrentPlayer(exoPlayer);
|
||||
setCurrentPlayer(localPlayer);
|
||||
}
|
||||
|
||||
// Internal methods.
|
||||
|
|
@ -336,7 +272,7 @@ import java.util.Map;
|
|||
}
|
||||
|
||||
// View management.
|
||||
if (currentPlayer == exoPlayer) {
|
||||
if (currentPlayer == localPlayer) {
|
||||
localPlayerView.setVisibility(View.VISIBLE);
|
||||
castControlView.hide();
|
||||
} else /* currentPlayer == castPlayer */ {
|
||||
|
|
@ -362,41 +298,33 @@ import java.util.Map;
|
|||
windowIndex = currentItemIndex;
|
||||
}
|
||||
}
|
||||
previousPlayer.stop(true);
|
||||
previousPlayer.stop();
|
||||
previousPlayer.clearMediaItems();
|
||||
}
|
||||
|
||||
this.currentPlayer = currentPlayer;
|
||||
|
||||
// Media queue management.
|
||||
if (currentPlayer == exoPlayer) {
|
||||
exoPlayer.prepare(concatenatingMediaSource);
|
||||
}
|
||||
|
||||
// Playback transition.
|
||||
if (windowIndex != C.INDEX_UNSET) {
|
||||
setCurrentItem(windowIndex, playbackPositionMs, playWhenReady);
|
||||
}
|
||||
currentPlayer.setMediaItems(mediaQueue, windowIndex, playbackPositionMs);
|
||||
currentPlayer.setPlayWhenReady(playWhenReady);
|
||||
currentPlayer.prepare();
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts playback of the item at the given position.
|
||||
* Starts playback of the item at the given index.
|
||||
*
|
||||
* @param itemIndex The index of the item to play.
|
||||
* @param positionMs The position at which playback should start.
|
||||
* @param playWhenReady Whether the player should proceed when ready to do so.
|
||||
*/
|
||||
private void setCurrentItem(int itemIndex, long positionMs, boolean playWhenReady) {
|
||||
private void setCurrentItem(int itemIndex) {
|
||||
maybeSetCurrentItemAndNotify(itemIndex);
|
||||
if (currentPlayer == castPlayer && castPlayer.getCurrentTimeline().isEmpty()) {
|
||||
MediaQueueItem[] items = new MediaQueueItem[mediaQueue.size()];
|
||||
for (int i = 0; i < items.length; i++) {
|
||||
items[i] = mediaItemConverter.toMediaQueueItem(mediaQueue.get(i));
|
||||
}
|
||||
castPlayer.loadItems(items, itemIndex, positionMs, Player.REPEAT_MODE_OFF);
|
||||
if (currentPlayer.getCurrentTimeline().getWindowCount() != mediaQueue.size()) {
|
||||
// This only happens with the cast player. The receiver app in the cast device clears the
|
||||
// timeline when the last item of the timeline has been played to end.
|
||||
currentPlayer.setMediaItems(mediaQueue, itemIndex, C.TIME_UNSET);
|
||||
} else {
|
||||
currentPlayer.seekTo(itemIndex, positionMs);
|
||||
currentPlayer.setPlayWhenReady(playWhenReady);
|
||||
currentPlayer.seekTo(itemIndex, C.TIME_UNSET);
|
||||
}
|
||||
currentPlayer.setPlayWhenReady(true);
|
||||
}
|
||||
|
||||
private void maybeSetCurrentItemAndNotify(int currentItemIndex) {
|
||||
|
|
@ -406,79 +334,4 @@ import java.util.Map;
|
|||
listener.onQueuePositionChanged(oldIndex, currentItemIndex);
|
||||
}
|
||||
}
|
||||
|
||||
private MediaSource buildMediaSource(MediaItem item) {
|
||||
Uri uri = item.uri;
|
||||
String mimeType = item.mimeType;
|
||||
if (mimeType == null) {
|
||||
throw new IllegalArgumentException("mimeType is required");
|
||||
}
|
||||
|
||||
FrameworkMediaDrm mediaDrm = null;
|
||||
DrmSessionManager<FrameworkMediaCrypto> drmSessionManager =
|
||||
DrmSessionManager.getDummyDrmSessionManager();
|
||||
MediaItem.DrmConfiguration drmConfiguration = item.drmConfiguration;
|
||||
if (drmConfiguration != null) {
|
||||
String licenseServerUrl =
|
||||
drmConfiguration.licenseUri != null ? drmConfiguration.licenseUri.toString() : "";
|
||||
HttpMediaDrmCallback drmCallback =
|
||||
new HttpMediaDrmCallback(licenseServerUrl, DATA_SOURCE_FACTORY);
|
||||
for (Map.Entry<String, String> requestHeader : drmConfiguration.requestHeaders.entrySet()) {
|
||||
drmCallback.setKeyRequestProperty(requestHeader.getKey(), requestHeader.getValue());
|
||||
}
|
||||
try {
|
||||
mediaDrm = FrameworkMediaDrm.newInstance(drmConfiguration.uuid);
|
||||
drmSessionManager =
|
||||
new DefaultDrmSessionManager<>(
|
||||
drmConfiguration.uuid,
|
||||
mediaDrm,
|
||||
drmCallback,
|
||||
/* optionalKeyRequestParameters= */ null,
|
||||
/* multiSession= */ true);
|
||||
} catch (UnsupportedDrmException e) {
|
||||
// Do nothing. The track selector will avoid selecting the DRM protected tracks.
|
||||
}
|
||||
}
|
||||
|
||||
MediaSource createdMediaSource;
|
||||
switch (mimeType) {
|
||||
case DemoUtil.MIME_TYPE_SS:
|
||||
createdMediaSource =
|
||||
new SsMediaSource.Factory(DATA_SOURCE_FACTORY)
|
||||
.setDrmSessionManager(drmSessionManager)
|
||||
.createMediaSource(uri);
|
||||
break;
|
||||
case DemoUtil.MIME_TYPE_DASH:
|
||||
createdMediaSource =
|
||||
new DashMediaSource.Factory(DATA_SOURCE_FACTORY)
|
||||
.setDrmSessionManager(drmSessionManager)
|
||||
.createMediaSource(uri);
|
||||
break;
|
||||
case DemoUtil.MIME_TYPE_HLS:
|
||||
createdMediaSource =
|
||||
new HlsMediaSource.Factory(DATA_SOURCE_FACTORY)
|
||||
.setDrmSessionManager(drmSessionManager)
|
||||
.createMediaSource(uri);
|
||||
break;
|
||||
case DemoUtil.MIME_TYPE_VIDEO_MP4:
|
||||
createdMediaSource =
|
||||
new ProgressiveMediaSource.Factory(DATA_SOURCE_FACTORY)
|
||||
.setDrmSessionManager(drmSessionManager)
|
||||
.createMediaSource(uri);
|
||||
break;
|
||||
default:
|
||||
throw new IllegalArgumentException("mimeType is unsupported: " + mimeType);
|
||||
}
|
||||
if (mediaDrm != null) {
|
||||
mediaDrms.put(createdMediaSource, mediaDrm);
|
||||
}
|
||||
return createdMediaSource;
|
||||
}
|
||||
|
||||
private void releaseMediaDrmOfMediaSource(MediaSource mediaSource) {
|
||||
FrameworkMediaDrm mediaDrmToRelease = mediaDrms.remove(mediaSource);
|
||||
if (mediaDrmToRelease != null) {
|
||||
mediaDrmToRelease.release();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 com.google.android.exoplayer2.castdemo;
|
||||
|
||||
import com.google.android.exoplayer2.util.NonNullApi;
|
||||
14
demos/gl/README.md
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
# ExoPlayer GL demo
|
||||
|
||||
This app demonstrates how to render video to a [GLSurfaceView][] while applying
|
||||
a GL shader.
|
||||
|
||||
The shader shows an overlap bitmap on top of the video. The overlay bitmap is
|
||||
drawn using an Android canvas, and includes the current frame's presentation
|
||||
timestamp, to show how to get the timestamp of the frame currently in the
|
||||
off-screen surface texture.
|
||||
|
||||
Please see the [demos README](../README.md) for instructions on how to build and
|
||||
run this demo.
|
||||
|
||||
[GLSurfaceView]: https://developer.android.com/reference/android/opengl/GLSurfaceView
|
||||
57
demos/gl/build.gradle
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
// Copyright (C) 2020 The Android Open Source Project
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
apply from: '../../constants.gradle'
|
||||
apply plugin: 'com.android.application'
|
||||
|
||||
android {
|
||||
compileSdkVersion project.ext.compileSdkVersion
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
versionName project.ext.releaseVersion
|
||||
versionCode project.ext.releaseVersionCode
|
||||
minSdkVersion project.ext.minSdkVersion
|
||||
targetSdkVersion project.ext.appTargetSdkVersion
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
shrinkResources true
|
||||
minifyEnabled true
|
||||
proguardFiles getDefaultProguardFile('proguard-android.txt')
|
||||
signingConfig signingConfigs.debug
|
||||
}
|
||||
}
|
||||
|
||||
lintOptions {
|
||||
// This demo app does not have translations.
|
||||
disable 'MissingTranslation'
|
||||
}
|
||||
}
|
||||
|
||||
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 'androidx.annotation:annotation:' + androidxAnnotationVersion
|
||||
compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion
|
||||
compileOnly 'org.checkerframework:checker-compat-qual:' + checkerframeworkCompatVersion
|
||||
}
|
||||
50
demos/gl/src/main/AndroidManifest.xml
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Copyright (C) 2020 The Android Open Source Project
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.google.android.exoplayer2.gldemo">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
|
||||
|
||||
<uses-sdk/>
|
||||
|
||||
<application
|
||||
android:allowBackup="false"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/application_name">
|
||||
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:configChanges="keyboard|keyboardHidden|orientation|screenSize|screenLayout|smallestScreenSize|uiMode"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN"/>
|
||||
<category android:name="android.intent.category.LAUNCHER"/>
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="com.google.android.exoplayer.gldemo.action.VIEW"/>
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
<data android:scheme="http"/>
|
||||
<data android:scheme="https"/>
|
||||
<data android:scheme="content"/>
|
||||
<data android:scheme="asset"/>
|
||||
<data android:scheme="file"/>
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
// Copyright 2020 The Android Open Source Project
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
#extension GL_OES_EGL_image_external : require
|
||||
precision mediump float;
|
||||
// External texture containing video decoder output.
|
||||
uniform samplerExternalOES tex_sampler_0;
|
||||
// Texture containing the overlap bitmap.
|
||||
uniform sampler2D tex_sampler_1;
|
||||
// Horizontal scaling factor for the overlap bitmap.
|
||||
uniform float scaleX;
|
||||
// Vertical scaling factory for the overlap bitmap.
|
||||
uniform float scaleY;
|
||||
varying vec2 v_texcoord;
|
||||
void main() {
|
||||
vec4 videoColor = texture2D(tex_sampler_0, v_texcoord);
|
||||
vec4 overlayColor = texture2D(tex_sampler_1,
|
||||
vec2(v_texcoord.x * scaleX,
|
||||
v_texcoord.y * scaleY));
|
||||
// Blend the video decoder output and the overlay bitmap.
|
||||
gl_FragColor = videoColor * (1.0 - overlayColor.a)
|
||||
+ overlayColor * overlayColor.a;
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
// Copyright 2020 The Android Open Source Project
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
attribute vec4 a_position;
|
||||
attribute vec4 a_texcoord;
|
||||
uniform mat4 tex_transform;
|
||||
varying vec2 v_texcoord;
|
||||
void main() {
|
||||
gl_Position = a_position;
|
||||
v_texcoord = (tex_transform * a_texcoord).xy;
|
||||
}
|
||||
|
|
@ -0,0 +1,162 @@
|
|||
/*
|
||||
* Copyright (C) 2020 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.google.android.exoplayer2.gldemo;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.drawable.BitmapDrawable;
|
||||
import android.opengl.GLES20;
|
||||
import android.opengl.GLUtils;
|
||||
import androidx.annotation.Nullable;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.util.Assertions;
|
||||
import com.google.android.exoplayer2.util.GlUtil;
|
||||
import java.io.IOException;
|
||||
import java.util.Locale;
|
||||
import javax.microedition.khronos.opengles.GL10;
|
||||
|
||||
/**
|
||||
* Video processor that demonstrates how to overlay a bitmap on video output using a GL shader. The
|
||||
* bitmap is drawn using an Android {@link Canvas}.
|
||||
*/
|
||||
/* package */ final class BitmapOverlayVideoProcessor
|
||||
implements VideoProcessingGLSurfaceView.VideoProcessor {
|
||||
|
||||
private static final int OVERLAY_WIDTH = 512;
|
||||
private static final int OVERLAY_HEIGHT = 256;
|
||||
|
||||
private final Context context;
|
||||
private final Paint paint;
|
||||
private final int[] textures;
|
||||
private final Bitmap overlayBitmap;
|
||||
private final Bitmap logoBitmap;
|
||||
private final Canvas overlayCanvas;
|
||||
|
||||
private int program;
|
||||
@Nullable private GlUtil.Attribute[] attributes;
|
||||
@Nullable private GlUtil.Uniform[] uniforms;
|
||||
|
||||
private float bitmapScaleX;
|
||||
private float bitmapScaleY;
|
||||
|
||||
public BitmapOverlayVideoProcessor(Context context) {
|
||||
this.context = context.getApplicationContext();
|
||||
paint = new Paint();
|
||||
paint.setTextSize(64);
|
||||
paint.setAntiAlias(true);
|
||||
paint.setARGB(0xFF, 0xFF, 0xFF, 0xFF);
|
||||
textures = new int[1];
|
||||
overlayBitmap = Bitmap.createBitmap(OVERLAY_WIDTH, OVERLAY_HEIGHT, Bitmap.Config.ARGB_8888);
|
||||
overlayCanvas = new Canvas(overlayBitmap);
|
||||
try {
|
||||
logoBitmap =
|
||||
((BitmapDrawable)
|
||||
context.getPackageManager().getApplicationIcon(context.getPackageName()))
|
||||
.getBitmap();
|
||||
} catch (PackageManager.NameNotFoundException e) {
|
||||
throw new IllegalStateException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initialize() {
|
||||
String vertexShaderCode;
|
||||
String fragmentShaderCode;
|
||||
try {
|
||||
vertexShaderCode = GlUtil.loadAsset(context, "bitmap_overlay_video_processor_vertex.glsl");
|
||||
fragmentShaderCode =
|
||||
GlUtil.loadAsset(context, "bitmap_overlay_video_processor_fragment.glsl");
|
||||
} catch (IOException e) {
|
||||
throw new IllegalStateException(e);
|
||||
}
|
||||
program = GlUtil.compileProgram(vertexShaderCode, fragmentShaderCode);
|
||||
GlUtil.Attribute[] attributes = GlUtil.getAttributes(program);
|
||||
GlUtil.Uniform[] uniforms = GlUtil.getUniforms(program);
|
||||
for (GlUtil.Attribute attribute : attributes) {
|
||||
if (attribute.name.equals("a_position")) {
|
||||
attribute.setBuffer(new float[] {-1, -1, 0, 1, 1, -1, 0, 1, -1, 1, 0, 1, 1, 1, 0, 1}, 4);
|
||||
} else if (attribute.name.equals("a_texcoord")) {
|
||||
attribute.setBuffer(new float[] {0, 0, 0, 1, 1, 0, 0, 1, 0, 1, 0, 1, 1, 1, 0, 1}, 4);
|
||||
}
|
||||
}
|
||||
this.attributes = attributes;
|
||||
this.uniforms = uniforms;
|
||||
GLES20.glGenTextures(1, textures, 0);
|
||||
GLES20.glBindTexture(GL10.GL_TEXTURE_2D, textures[0]);
|
||||
GLES20.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_MIN_FILTER, GL10.GL_NEAREST);
|
||||
GLES20.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_MAG_FILTER, GL10.GL_LINEAR);
|
||||
GLES20.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_WRAP_S, GL10.GL_REPEAT);
|
||||
GLES20.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_WRAP_T, GL10.GL_REPEAT);
|
||||
GLUtils.texImage2D(GL10.GL_TEXTURE_2D, /* level= */ 0, overlayBitmap, /* border= */ 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setSurfaceSize(int width, int height) {
|
||||
bitmapScaleX = (float) width / OVERLAY_WIDTH;
|
||||
bitmapScaleY = (float) height / OVERLAY_HEIGHT;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void draw(int frameTexture, long frameTimestampUs, float[] transformMatrix) {
|
||||
// Draw to the canvas and store it in a texture.
|
||||
String text = String.format(Locale.US, "%.02f", frameTimestampUs / (float) C.MICROS_PER_SECOND);
|
||||
overlayBitmap.eraseColor(Color.TRANSPARENT);
|
||||
overlayCanvas.drawBitmap(logoBitmap, /* left= */ 32, /* top= */ 32, paint);
|
||||
overlayCanvas.drawText(text, /* x= */ 200, /* y= */ 130, paint);
|
||||
GLES20.glBindTexture(GL10.GL_TEXTURE_2D, textures[0]);
|
||||
GLUtils.texSubImage2D(
|
||||
GL10.GL_TEXTURE_2D, /* level= */ 0, /* xoffset= */ 0, /* yoffset= */ 0, overlayBitmap);
|
||||
GlUtil.checkGlError();
|
||||
|
||||
// Run the shader program.
|
||||
GlUtil.Uniform[] uniforms = Assertions.checkNotNull(this.uniforms);
|
||||
GlUtil.Attribute[] attributes = Assertions.checkNotNull(this.attributes);
|
||||
GLES20.glUseProgram(program);
|
||||
for (GlUtil.Uniform uniform : uniforms) {
|
||||
switch (uniform.name) {
|
||||
case "tex_sampler_0":
|
||||
uniform.setSamplerTexId(frameTexture, /* unit= */ 0);
|
||||
break;
|
||||
case "tex_sampler_1":
|
||||
uniform.setSamplerTexId(textures[0], /* unit= */ 1);
|
||||
break;
|
||||
case "scaleX":
|
||||
uniform.setFloat(bitmapScaleX);
|
||||
break;
|
||||
case "scaleY":
|
||||
uniform.setFloat(bitmapScaleY);
|
||||
break;
|
||||
case "tex_transform":
|
||||
uniform.setFloats(transformMatrix);
|
||||
break;
|
||||
default: // fall out
|
||||
}
|
||||
}
|
||||
for (GlUtil.Attribute copyExternalAttribute : attributes) {
|
||||
copyExternalAttribute.bind();
|
||||
}
|
||||
for (GlUtil.Uniform copyExternalUniform : uniforms) {
|
||||
copyExternalUniform.bind();
|
||||
}
|
||||
GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);
|
||||
GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, /* first= */ 0, /* count= */ 4);
|
||||
GlUtil.checkGlError();
|
||||
}
|
||||
}
|
||||
|
|
@ -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 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,298 @@
|
|||
/*
|
||||
* Copyright (C) 2020 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.google.android.exoplayer2.gldemo;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.SurfaceTexture;
|
||||
import android.media.MediaFormat;
|
||||
import android.opengl.EGL14;
|
||||
import android.opengl.GLES20;
|
||||
import android.opengl.GLSurfaceView;
|
||||
import android.os.Handler;
|
||||
import android.view.Surface;
|
||||
import androidx.annotation.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 java.util.concurrent.atomic.AtomicBoolean;
|
||||
import javax.microedition.khronos.egl.EGL10;
|
||||
import javax.microedition.khronos.egl.EGLConfig;
|
||||
import javax.microedition.khronos.egl.EGLContext;
|
||||
import javax.microedition.khronos.egl.EGLDisplay;
|
||||
import javax.microedition.khronos.egl.EGLSurface;
|
||||
import javax.microedition.khronos.opengles.GL10;
|
||||
|
||||
/**
|
||||
* {@link GLSurfaceView} that creates a GL context (optionally for protected content) and passes
|
||||
* video frames to a {@link VideoProcessor} for drawing to the view.
|
||||
*
|
||||
* <p>This view must be created programmatically, as it is necessary to specify whether a context
|
||||
* supporting protected content should be created at construction time.
|
||||
*/
|
||||
public final class VideoProcessingGLSurfaceView extends GLSurfaceView {
|
||||
|
||||
/** Processes video frames, provided via a GL texture. */
|
||||
public interface VideoProcessor {
|
||||
/** Performs any required GL initialization. */
|
||||
void initialize();
|
||||
|
||||
/** Sets the size of the output surface in pixels. */
|
||||
void setSurfaceSize(int width, int height);
|
||||
|
||||
/**
|
||||
* Draws using GL operations.
|
||||
*
|
||||
* @param frameTexture The ID of a GL texture containing a video frame.
|
||||
* @param frameTimestampUs The presentation timestamp of the frame, in microseconds.
|
||||
* @param transformMatrix The 4 * 4 transform matrix to be applied to the texture.
|
||||
*/
|
||||
void draw(int frameTexture, long frameTimestampUs, float[] transformMatrix);
|
||||
}
|
||||
|
||||
private static final int EGL_PROTECTED_CONTENT_EXT = 0x32C0;
|
||||
|
||||
private final VideoRenderer renderer;
|
||||
private final Handler mainHandler;
|
||||
|
||||
@Nullable private SurfaceTexture surfaceTexture;
|
||||
@Nullable private Surface surface;
|
||||
@Nullable private ExoPlayer.VideoComponent videoComponent;
|
||||
|
||||
/**
|
||||
* Creates a new instance. Pass {@code true} for {@code requireSecureContext} if the {@link
|
||||
* GLSurfaceView GLSurfaceView's} associated GL context should handle secure content (if the
|
||||
* device supports it).
|
||||
*
|
||||
* @param context The {@link Context}.
|
||||
* @param requireSecureContext Whether a GL context supporting protected content should be
|
||||
* created, if supported by the device.
|
||||
* @param videoProcessor Processor that draws to the view.
|
||||
*/
|
||||
@SuppressWarnings("InlinedApi")
|
||||
public VideoProcessingGLSurfaceView(
|
||||
Context context, boolean requireSecureContext, VideoProcessor videoProcessor) {
|
||||
super(context);
|
||||
renderer = new VideoRenderer(videoProcessor);
|
||||
mainHandler = new Handler();
|
||||
setEGLContextClientVersion(2);
|
||||
setEGLConfigChooser(
|
||||
/* redSize= */ 8,
|
||||
/* greenSize= */ 8,
|
||||
/* blueSize= */ 8,
|
||||
/* alphaSize= */ 8,
|
||||
/* depthSize= */ 0,
|
||||
/* stencilSize= */ 0);
|
||||
setEGLContextFactory(
|
||||
new EGLContextFactory() {
|
||||
@Override
|
||||
public EGLContext createContext(EGL10 egl, EGLDisplay display, EGLConfig eglConfig) {
|
||||
int[] glAttributes;
|
||||
if (requireSecureContext) {
|
||||
glAttributes =
|
||||
new int[] {
|
||||
EGL14.EGL_CONTEXT_CLIENT_VERSION,
|
||||
2,
|
||||
EGL_PROTECTED_CONTENT_EXT,
|
||||
EGL14.EGL_TRUE,
|
||||
EGL14.EGL_NONE
|
||||
};
|
||||
} else {
|
||||
glAttributes = new int[] {EGL14.EGL_CONTEXT_CLIENT_VERSION, 2, EGL14.EGL_NONE};
|
||||
}
|
||||
return egl.eglCreateContext(
|
||||
display, eglConfig, /* share_context= */ EGL10.EGL_NO_CONTEXT, glAttributes);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void destroyContext(EGL10 egl, EGLDisplay display, EGLContext context) {
|
||||
egl.eglDestroyContext(display, context);
|
||||
}
|
||||
});
|
||||
setEGLWindowSurfaceFactory(
|
||||
new EGLWindowSurfaceFactory() {
|
||||
@Override
|
||||
public EGLSurface createWindowSurface(
|
||||
EGL10 egl, EGLDisplay display, EGLConfig config, Object nativeWindow) {
|
||||
int[] attribsList =
|
||||
requireSecureContext
|
||||
? new int[] {EGL_PROTECTED_CONTENT_EXT, EGL14.EGL_TRUE, EGL10.EGL_NONE}
|
||||
: new int[] {EGL10.EGL_NONE};
|
||||
return egl.eglCreateWindowSurface(display, config, nativeWindow, attribsList);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void destroySurface(EGL10 egl, EGLDisplay display, EGLSurface surface) {
|
||||
egl.eglDestroySurface(display, surface);
|
||||
}
|
||||
});
|
||||
setRenderer(renderer);
|
||||
setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY);
|
||||
}
|
||||
|
||||
/**
|
||||
* Attaches or detaches (if {@code newVideoComponent} is {@code null}) this view from the video
|
||||
* component of the player.
|
||||
*
|
||||
* @param newVideoComponent The new video component, or {@code null} to detach this view.
|
||||
*/
|
||||
public void setVideoComponent(@Nullable ExoPlayer.VideoComponent newVideoComponent) {
|
||||
if (newVideoComponent == videoComponent) {
|
||||
return;
|
||||
}
|
||||
if (videoComponent != null) {
|
||||
if (surface != null) {
|
||||
videoComponent.clearVideoSurface(surface);
|
||||
}
|
||||
videoComponent.clearVideoFrameMetadataListener(renderer);
|
||||
}
|
||||
videoComponent = newVideoComponent;
|
||||
if (videoComponent != null) {
|
||||
videoComponent.setVideoFrameMetadataListener(renderer);
|
||||
videoComponent.setVideoSurface(surface);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDetachedFromWindow() {
|
||||
super.onDetachedFromWindow();
|
||||
// Post to make sure we occur in order with any onSurfaceTextureAvailable calls.
|
||||
mainHandler.post(
|
||||
() -> {
|
||||
if (surface != null) {
|
||||
if (videoComponent != null) {
|
||||
videoComponent.setVideoSurface(null);
|
||||
}
|
||||
releaseSurface(surfaceTexture, surface);
|
||||
surfaceTexture = null;
|
||||
surface = null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void onSurfaceTextureAvailable(SurfaceTexture surfaceTexture) {
|
||||
mainHandler.post(
|
||||
() -> {
|
||||
SurfaceTexture oldSurfaceTexture = this.surfaceTexture;
|
||||
Surface oldSurface = VideoProcessingGLSurfaceView.this.surface;
|
||||
this.surfaceTexture = surfaceTexture;
|
||||
this.surface = new Surface(surfaceTexture);
|
||||
releaseSurface(oldSurfaceTexture, oldSurface);
|
||||
if (videoComponent != null) {
|
||||
videoComponent.setVideoSurface(surface);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static void releaseSurface(
|
||||
@Nullable SurfaceTexture oldSurfaceTexture, @Nullable Surface oldSurface) {
|
||||
if (oldSurfaceTexture != null) {
|
||||
oldSurfaceTexture.release();
|
||||
}
|
||||
if (oldSurface != null) {
|
||||
oldSurface.release();
|
||||
}
|
||||
}
|
||||
|
||||
private final class VideoRenderer implements GLSurfaceView.Renderer, VideoFrameMetadataListener {
|
||||
|
||||
private final VideoProcessor videoProcessor;
|
||||
private final AtomicBoolean frameAvailable;
|
||||
private final TimedValueQueue<Long> sampleTimestampQueue;
|
||||
private final float[] transformMatrix;
|
||||
|
||||
private int texture;
|
||||
@Nullable private SurfaceTexture surfaceTexture;
|
||||
|
||||
private boolean initialized;
|
||||
private int width;
|
||||
private int height;
|
||||
private long frameTimestampUs;
|
||||
|
||||
public VideoRenderer(VideoProcessor videoProcessor) {
|
||||
this.videoProcessor = videoProcessor;
|
||||
frameAvailable = new AtomicBoolean();
|
||||
sampleTimestampQueue = new TimedValueQueue<>();
|
||||
width = -1;
|
||||
height = -1;
|
||||
frameTimestampUs = C.TIME_UNSET;
|
||||
transformMatrix = new float[16];
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void onSurfaceCreated(GL10 gl, EGLConfig config) {
|
||||
texture = GlUtil.createExternalTexture();
|
||||
surfaceTexture = new SurfaceTexture(texture);
|
||||
surfaceTexture.setOnFrameAvailableListener(
|
||||
surfaceTexture -> {
|
||||
frameAvailable.set(true);
|
||||
requestRender();
|
||||
});
|
||||
onSurfaceTextureAvailable(surfaceTexture);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSurfaceChanged(GL10 gl, int width, int height) {
|
||||
GLES20.glViewport(0, 0, width, height);
|
||||
this.width = width;
|
||||
this.height = height;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDrawFrame(GL10 gl) {
|
||||
if (videoProcessor == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!initialized) {
|
||||
videoProcessor.initialize();
|
||||
initialized = true;
|
||||
}
|
||||
|
||||
if (width != -1 && height != -1) {
|
||||
videoProcessor.setSurfaceSize(width, height);
|
||||
width = -1;
|
||||
height = -1;
|
||||
}
|
||||
|
||||
if (frameAvailable.compareAndSet(true, false)) {
|
||||
SurfaceTexture surfaceTexture = Assertions.checkNotNull(this.surfaceTexture);
|
||||
surfaceTexture.updateTexImage();
|
||||
long lastFrameTimestampNs = surfaceTexture.getTimestamp();
|
||||
@Nullable Long frameTimestampUs = sampleTimestampQueue.poll(lastFrameTimestampNs);
|
||||
if (frameTimestampUs != null) {
|
||||
this.frameTimestampUs = frameTimestampUs;
|
||||
}
|
||||
surfaceTexture.getTransformMatrix(transformMatrix);
|
||||
}
|
||||
|
||||
videoProcessor.draw(texture, frameTimestampUs, transformMatrix);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onVideoFrameAboutToBeRendered(
|
||||
long presentationTimeUs,
|
||||
long releaseTimeNs,
|
||||
@NonNull Format format,
|
||||
@Nullable MediaFormat mediaFormat) {
|
||||
sampleTimestampQueue.add(releaseTimeNs, presentationTimeUs);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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 com.google.android.exoplayer2.gldemo;
|
||||
|
||||
import com.google.android.exoplayer2.util.NonNullApi;
|
||||
29
demos/gl/src/main/res/layout/main_activity.xml
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Copyright (C) 2020 The Android Open Source Project
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
<FrameLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:keepScreenOn="true">
|
||||
|
||||
<com.google.android.exoplayer2.ui.PlayerView
|
||||
android:id="@+id/player_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:surface_type="none"/>
|
||||
|
||||
</FrameLayout>
|
||||
|
Before Width: | Height: | Size: 3.3 KiB After Width: | Height: | Size: 3.3 KiB |
|
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 4.8 KiB After Width: | Height: | Size: 4.8 KiB |
|
Before Width: | Height: | Size: 7.3 KiB After Width: | Height: | Size: 7.3 KiB |
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 10 KiB |
22
demos/gl/src/main/res/values/strings.xml
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Copyright (C) 2020 The Android Open Source Project
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
<resources>
|
||||
|
||||
<string name="application_name">ExoPlayer GL demo</string>
|
||||
|
||||
<string name="error_protected_content_extension_not_supported">The GL protected content extension is not supported.</string>
|
||||
|
||||
</resources>
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
# ExoPlayer VR player demo #
|
||||
|
||||
This folder contains a demo application that showcases 360 video playback using
|
||||
ExoPlayer GVR extension.
|
||||
|
|
@ -1,59 +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.
|
||||
apply from: '../../constants.gradle'
|
||||
apply plugin: 'com.android.application'
|
||||
|
||||
android {
|
||||
compileSdkVersion project.ext.compileSdkVersion
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
versionName project.ext.releaseVersion
|
||||
versionCode project.ext.releaseVersionCode
|
||||
minSdkVersion 19
|
||||
targetSdkVersion project.ext.targetSdkVersion
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
shrinkResources true
|
||||
minifyEnabled true
|
||||
proguardFiles getDefaultProguardFile('proguard-android.txt')
|
||||
}
|
||||
debug {
|
||||
jniDebuggable = true
|
||||
}
|
||||
}
|
||||
|
||||
lintOptions {
|
||||
// The demo app isn't indexed and doesn't have translations.
|
||||
disable 'GoogleAppIndexingWarning','MissingTranslation'
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation project(modulePrefix + 'library-core')
|
||||
implementation project(modulePrefix + 'library-ui')
|
||||
implementation project(modulePrefix + 'library-dash')
|
||||
implementation project(modulePrefix + 'library-hls')
|
||||
implementation project(modulePrefix + 'library-smoothstreaming')
|
||||
implementation project(modulePrefix + 'extension-gvr')
|
||||
implementation 'androidx.annotation:annotation:1.1.0'
|
||||
}
|
||||
|
||||
apply plugin: 'com.google.android.gms.strict-version-matcher-plugin'
|
||||
|
|
@ -1,74 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Copyright (C) 2019 The Android Open Source Project
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.google.android.exoplayer2.gvrdemo">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
|
||||
|
||||
<uses-sdk/>
|
||||
|
||||
<application
|
||||
android:allowBackup="false"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/application_name"
|
||||
android:largeHeap="true">
|
||||
|
||||
<activity
|
||||
android:name="com.google.android.exoplayer2.gvrdemo.SampleChooserActivity"
|
||||
android:configChanges="keyboardHidden"
|
||||
android:exported="true"
|
||||
android:label="@string/application_name">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN"/>
|
||||
<category android:name="android.intent.category.LAUNCHER"/>
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW"/>
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
<category android:name="android.intent.category.BROWSABLE"/>
|
||||
|
||||
<data android:scheme="http"/>
|
||||
<data android:scheme="https"/>
|
||||
<data android:scheme="content"/>
|
||||
<data android:scheme="asset"/>
|
||||
<data android:scheme="file"/>
|
||||
<data android:host="*"/>
|
||||
<data android:pathPattern=".*\\.exolist\\.json"/>
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name="com.google.android.exoplayer2.gvrdemo.PlayerActivity"
|
||||
android:configChanges="density|keyboardHidden|navigation|orientation|screenSize|uiMode"
|
||||
android:enableVrMode="@string/gvr_vr_mode_component"
|
||||
android:exported="false"
|
||||
android:label="@string/application_name"
|
||||
android:launchMode="singleTask"
|
||||
android:resizeableActivity="false"
|
||||
android:screenOrientation="landscape"
|
||||
android:theme="@style/VrActivityTheme">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN"/>
|
||||
<category android:name="com.google.intent.category.CARDBOARD"/> <!-- copybara:strip(development-only) -->
|
||||
<category android:name="com.google.intent.category.DAYDREAM"/>
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
|
|
@ -1,242 +0,0 @@
|
|||
/*
|
||||
* Copyright (C) 2018 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.google.android.exoplayer2.gvrdemo;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import androidx.annotation.Nullable;
|
||||
import android.widget.Toast;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.C.ContentType;
|
||||
import com.google.android.exoplayer2.DefaultRenderersFactory;
|
||||
import com.google.android.exoplayer2.ExoPlaybackException;
|
||||
import com.google.android.exoplayer2.ExoPlayerFactory;
|
||||
import com.google.android.exoplayer2.PlaybackPreparer;
|
||||
import com.google.android.exoplayer2.Player;
|
||||
import com.google.android.exoplayer2.SimpleExoPlayer;
|
||||
import com.google.android.exoplayer2.ext.gvr.GvrPlayerActivity;
|
||||
import com.google.android.exoplayer2.source.MediaSource;
|
||||
import com.google.android.exoplayer2.source.ProgressiveMediaSource;
|
||||
import com.google.android.exoplayer2.source.TrackGroupArray;
|
||||
import com.google.android.exoplayer2.source.dash.DashMediaSource;
|
||||
import com.google.android.exoplayer2.source.hls.HlsMediaSource;
|
||||
import com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource;
|
||||
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
|
||||
import com.google.android.exoplayer2.trackselection.MappingTrackSelector.MappedTrackInfo;
|
||||
import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
|
||||
import com.google.android.exoplayer2.upstream.DataSource;
|
||||
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
|
||||
import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory;
|
||||
import com.google.android.exoplayer2.util.EventLogger;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
|
||||
/** An activity that plays media using {@link SimpleExoPlayer}. */
|
||||
public class PlayerActivity extends GvrPlayerActivity implements PlaybackPreparer {
|
||||
|
||||
public static final String EXTENSION_EXTRA = "extension";
|
||||
|
||||
public static final String SPHERICAL_STEREO_MODE_EXTRA = "spherical_stereo_mode";
|
||||
public static final String SPHERICAL_STEREO_MODE_MONO = "mono";
|
||||
public static final String SPHERICAL_STEREO_MODE_TOP_BOTTOM = "top_bottom";
|
||||
public static final String SPHERICAL_STEREO_MODE_LEFT_RIGHT = "left_right";
|
||||
|
||||
private DataSource.Factory dataSourceFactory;
|
||||
private SimpleExoPlayer player;
|
||||
private MediaSource mediaSource;
|
||||
private DefaultTrackSelector trackSelector;
|
||||
private TrackGroupArray lastSeenTrackGroupArray;
|
||||
|
||||
private boolean startAutoPlay;
|
||||
private int startWindow;
|
||||
private long startPosition;
|
||||
|
||||
// Activity lifecycle
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
String userAgent = Util.getUserAgent(this, "ExoPlayerDemo");
|
||||
dataSourceFactory =
|
||||
new DefaultDataSourceFactory(this, new DefaultHttpDataSourceFactory(userAgent));
|
||||
|
||||
String sphericalStereoMode = getIntent().getStringExtra(SPHERICAL_STEREO_MODE_EXTRA);
|
||||
if (sphericalStereoMode != null) {
|
||||
int stereoMode;
|
||||
if (SPHERICAL_STEREO_MODE_MONO.equals(sphericalStereoMode)) {
|
||||
stereoMode = C.STEREO_MODE_MONO;
|
||||
} else if (SPHERICAL_STEREO_MODE_TOP_BOTTOM.equals(sphericalStereoMode)) {
|
||||
stereoMode = C.STEREO_MODE_TOP_BOTTOM;
|
||||
} else if (SPHERICAL_STEREO_MODE_LEFT_RIGHT.equals(sphericalStereoMode)) {
|
||||
stereoMode = C.STEREO_MODE_LEFT_RIGHT;
|
||||
} else {
|
||||
showToast(R.string.error_unrecognized_stereo_mode);
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
setDefaultStereoMode(stereoMode);
|
||||
}
|
||||
|
||||
clearStartPosition();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
if (Util.SDK_INT <= 23 || player == null) {
|
||||
initializePlayer();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPause() {
|
||||
super.onPause();
|
||||
if (Util.SDK_INT <= 23) {
|
||||
releasePlayer();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
}
|
||||
|
||||
// PlaybackControlView.PlaybackPreparer implementation
|
||||
|
||||
@Override
|
||||
public void preparePlayback() {
|
||||
initializePlayer();
|
||||
}
|
||||
|
||||
// Internal methods
|
||||
|
||||
private void initializePlayer() {
|
||||
if (player == null) {
|
||||
Intent intent = getIntent();
|
||||
Uri uri = intent.getData();
|
||||
if (!Util.checkCleartextTrafficPermitted(uri)) {
|
||||
showToast(R.string.error_cleartext_not_permitted);
|
||||
return;
|
||||
}
|
||||
|
||||
DefaultRenderersFactory renderersFactory = new DefaultRenderersFactory(this);
|
||||
|
||||
trackSelector = new DefaultTrackSelector(/* context= */ this);
|
||||
lastSeenTrackGroupArray = null;
|
||||
|
||||
player =
|
||||
ExoPlayerFactory.newSimpleInstance(/* context= */ this, renderersFactory, trackSelector);
|
||||
player.addListener(new PlayerEventListener());
|
||||
player.setPlayWhenReady(startAutoPlay);
|
||||
player.addAnalyticsListener(new EventLogger(trackSelector));
|
||||
setPlayer(player);
|
||||
|
||||
mediaSource = buildMediaSource(uri, intent.getStringExtra(EXTENSION_EXTRA));
|
||||
}
|
||||
boolean haveStartPosition = startWindow != C.INDEX_UNSET;
|
||||
if (haveStartPosition) {
|
||||
player.seekTo(startWindow, startPosition);
|
||||
}
|
||||
player.prepare(mediaSource, !haveStartPosition, false);
|
||||
}
|
||||
|
||||
private MediaSource buildMediaSource(Uri uri, @Nullable String overrideExtension) {
|
||||
@ContentType int type = Util.inferContentType(uri, overrideExtension);
|
||||
switch (type) {
|
||||
case C.TYPE_DASH:
|
||||
return new DashMediaSource.Factory(dataSourceFactory).createMediaSource(uri);
|
||||
case C.TYPE_SS:
|
||||
return new SsMediaSource.Factory(dataSourceFactory).createMediaSource(uri);
|
||||
case C.TYPE_HLS:
|
||||
return new HlsMediaSource.Factory(dataSourceFactory).createMediaSource(uri);
|
||||
case C.TYPE_OTHER:
|
||||
return new ProgressiveMediaSource.Factory(dataSourceFactory).createMediaSource(uri);
|
||||
default:
|
||||
throw new IllegalStateException("Unsupported type: " + type);
|
||||
}
|
||||
}
|
||||
|
||||
private void releasePlayer() {
|
||||
if (player != null) {
|
||||
updateStartPosition();
|
||||
player.release();
|
||||
player = null;
|
||||
mediaSource = null;
|
||||
trackSelector = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void updateStartPosition() {
|
||||
if (player != null) {
|
||||
startAutoPlay = player.getPlayWhenReady();
|
||||
startWindow = player.getCurrentWindowIndex();
|
||||
startPosition = Math.max(0, player.getContentPosition());
|
||||
}
|
||||
}
|
||||
|
||||
private void clearStartPosition() {
|
||||
startAutoPlay = true;
|
||||
startWindow = C.INDEX_UNSET;
|
||||
startPosition = C.TIME_UNSET;
|
||||
}
|
||||
|
||||
private void showToast(int messageId) {
|
||||
showToast(getString(messageId));
|
||||
}
|
||||
|
||||
private void showToast(String message) {
|
||||
Toast.makeText(getApplicationContext(), message, Toast.LENGTH_LONG).show();
|
||||
}
|
||||
|
||||
private class PlayerEventListener implements Player.EventListener {
|
||||
|
||||
@Override
|
||||
public void onPlayerStateChanged(boolean playWhenReady, int playbackState) {}
|
||||
|
||||
@Override
|
||||
public void onPositionDiscontinuity(@Player.DiscontinuityReason int reason) {
|
||||
if (player.getPlaybackError() != null) {
|
||||
// The user has performed a seek whilst in the error state. Update the resume position so
|
||||
// that if the user then retries, playback resumes from the position to which they seeked.
|
||||
updateStartPosition();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPlayerError(ExoPlaybackException e) {
|
||||
updateStartPosition();
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("ReferenceEquality")
|
||||
public void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray trackSelections) {
|
||||
if (trackGroups != lastSeenTrackGroupArray) {
|
||||
MappedTrackInfo mappedTrackInfo = trackSelector.getCurrentMappedTrackInfo();
|
||||
if (mappedTrackInfo != null) {
|
||||
if (mappedTrackInfo.getTypeSupport(C.TRACK_TYPE_VIDEO)
|
||||
== MappedTrackInfo.RENDERER_SUPPORT_UNSUPPORTED_TRACKS) {
|
||||
showToast(R.string.error_unsupported_video);
|
||||
}
|
||||
if (mappedTrackInfo.getTypeSupport(C.TRACK_TYPE_AUDIO)
|
||||
== MappedTrackInfo.RENDERER_SUPPORT_UNSUPPORTED_TRACKS) {
|
||||
showToast(R.string.error_unsupported_audio);
|
||||
}
|
||||
}
|
||||
lastSeenTrackGroupArray = trackGroups;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,133 +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.gvrdemo;
|
||||
|
||||
import static com.google.android.exoplayer2.gvrdemo.PlayerActivity.SPHERICAL_STEREO_MODE_LEFT_RIGHT;
|
||||
import static com.google.android.exoplayer2.gvrdemo.PlayerActivity.SPHERICAL_STEREO_MODE_MONO;
|
||||
import static com.google.android.exoplayer2.gvrdemo.PlayerActivity.SPHERICAL_STEREO_MODE_TOP_BOTTOM;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.widget.ArrayAdapter;
|
||||
import android.widget.ListView;
|
||||
|
||||
/** An activity for selecting from a list of media samples. */
|
||||
public class SampleChooserActivity extends Activity {
|
||||
|
||||
private final Sample[] samples =
|
||||
new Sample[] {
|
||||
new Sample(
|
||||
"Congo (360 top-bottom stereo)",
|
||||
"https://storage.googleapis.com/exoplayer-test-media-1/360/congo.mp4",
|
||||
SPHERICAL_STEREO_MODE_TOP_BOTTOM),
|
||||
new Sample(
|
||||
"Sphericalv2 (180 top-bottom stereo)",
|
||||
"https://storage.googleapis.com/exoplayer-test-media-1/360/sphericalv2.mp4",
|
||||
SPHERICAL_STEREO_MODE_TOP_BOTTOM),
|
||||
new Sample(
|
||||
"Iceland (360 top-bottom stereo ts)",
|
||||
"https://storage.googleapis.com/exoplayer-test-media-1/360/iceland0.ts",
|
||||
SPHERICAL_STEREO_MODE_TOP_BOTTOM),
|
||||
new Sample(
|
||||
"Camera motion metadata test",
|
||||
"https://storage.googleapis.com/exoplayer-test-media-internal-"
|
||||
+ "63834241aced7884c2544af1a3452e01/vr180/synthetic_with_camm.mp4",
|
||||
SPHERICAL_STEREO_MODE_TOP_BOTTOM),
|
||||
new Sample(
|
||||
"actual_camera_cat",
|
||||
"https://storage.googleapis.com/exoplayer-test-media-internal-"
|
||||
+ "63834241aced7884c2544af1a3452e01/vr180/actual_camera_cat.mp4",
|
||||
SPHERICAL_STEREO_MODE_TOP_BOTTOM),
|
||||
new Sample(
|
||||
"johnny_stitched",
|
||||
"https://storage.googleapis.com/exoplayer-test-media-internal-"
|
||||
+ "63834241aced7884c2544af1a3452e01/vr180/johnny_stitched.mp4",
|
||||
SPHERICAL_STEREO_MODE_TOP_BOTTOM),
|
||||
new Sample(
|
||||
"lenovo_birds.vr",
|
||||
"https://storage.googleapis.com/exoplayer-test-media-internal-"
|
||||
+ "63834241aced7884c2544af1a3452e01/vr180/lenovo_birds.vr.mp4",
|
||||
SPHERICAL_STEREO_MODE_TOP_BOTTOM),
|
||||
new Sample(
|
||||
"mono_v1_sample",
|
||||
"https://storage.googleapis.com/exoplayer-test-media-internal-"
|
||||
+ "63834241aced7884c2544af1a3452e01/vr180/mono_v1_sample.mp4",
|
||||
SPHERICAL_STEREO_MODE_MONO),
|
||||
new Sample(
|
||||
"not_vr180_actually_shot_with_moto_mod",
|
||||
"https://storage.googleapis.com/exoplayer-test-media-internal-"
|
||||
+ "63834241aced7884c2544af1a3452e01/vr180/"
|
||||
+ "not_vr180_actually_shot_with_moto_mod.mp4",
|
||||
SPHERICAL_STEREO_MODE_TOP_BOTTOM),
|
||||
new Sample(
|
||||
"stereo_v1_sample",
|
||||
"https://storage.googleapis.com/exoplayer-test-media-internal-"
|
||||
+ "63834241aced7884c2544af1a3452e01/vr180/stereo_v1_sample.mp4",
|
||||
SPHERICAL_STEREO_MODE_TOP_BOTTOM),
|
||||
new Sample(
|
||||
"yi_giraffes.vr",
|
||||
"https://storage.googleapis.com/exoplayer-test-media-internal-"
|
||||
+ "63834241aced7884c2544af1a3452e01/vr180/yi_giraffes.vr.mp4",
|
||||
SPHERICAL_STEREO_MODE_TOP_BOTTOM),
|
||||
};
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.sample_chooser_activity);
|
||||
ListView sampleListView = findViewById(R.id.sample_list);
|
||||
sampleListView.setAdapter(
|
||||
new ArrayAdapter<>(this, android.R.layout.simple_list_item_1, samples));
|
||||
sampleListView.setOnItemClickListener(
|
||||
(parent, view, position, id) ->
|
||||
startActivity(
|
||||
samples[position].buildIntent(/* context= */ SampleChooserActivity.this)));
|
||||
}
|
||||
|
||||
private static final class Sample {
|
||||
public final String name;
|
||||
public final String uri;
|
||||
public final String extension;
|
||||
public final String sphericalStereoMode;
|
||||
|
||||
public Sample(String name, String uri, String sphericalStereoMode) {
|
||||
this(name, uri, sphericalStereoMode, null);
|
||||
}
|
||||
|
||||
public Sample(String name, String uri, String sphericalStereoMode, String extension) {
|
||||
this.name = name;
|
||||
this.uri = uri;
|
||||
this.extension = extension;
|
||||
this.sphericalStereoMode = sphericalStereoMode;
|
||||
}
|
||||
|
||||
public Intent buildIntent(Context context) {
|
||||
Intent intent = new Intent(context, PlayerActivity.class);
|
||||
return intent
|
||||
.setData(Uri.parse(uri))
|
||||
.putExtra(PlayerActivity.EXTENSION_EXTRA, extension)
|
||||
.putExtra(PlayerActivity.SPHERICAL_STEREO_MODE_EXTRA, sphericalStereoMode);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return name;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,25 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- 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.
|
||||
-->
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical">
|
||||
|
||||
<ListView android:id="@+id/sample_list"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Copyright (C) 2019 The Android Open Source Project
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
|
||||
<string name="application_name">ExoPlayer VR Demo</string>
|
||||
|
||||
<string name="error_cleartext_not_permitted">Cleartext traffic not permitted</string>
|
||||
|
||||
<string name="error_unrecognized_stereo_mode">Unrecognized stereo mode</string>
|
||||
|
||||
<string name="error_unsupported_video">Media includes video tracks, but none are playable by this device</string>
|
||||
|
||||
<string name="error_unsupported_audio">Media includes audio tracks, but none are playable by this device</string>
|
||||
|
||||
</resources>
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
# IMA demo application #
|
||||
|
||||
This folder contains a demo application that showcases ExoPlayer integration
|
||||
with the IMA SDK.
|
||||
|
|
@ -1,59 +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.
|
||||
apply from: '../../constants.gradle'
|
||||
apply plugin: 'com.android.application'
|
||||
|
||||
android {
|
||||
compileSdkVersion project.ext.compileSdkVersion
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
versionName project.ext.releaseVersion
|
||||
versionCode project.ext.releaseVersionCode
|
||||
minSdkVersion project.ext.minSdkVersion
|
||||
targetSdkVersion project.ext.targetSdkVersion
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
shrinkResources true
|
||||
minifyEnabled true
|
||||
proguardFiles getDefaultProguardFile('proguard-android.txt')
|
||||
}
|
||||
debug {
|
||||
jniDebuggable = true
|
||||
}
|
||||
}
|
||||
|
||||
lintOptions {
|
||||
// The demo app isn't indexed and doesn't have translations.
|
||||
disable 'GoogleAppIndexingWarning','MissingTranslation'
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation project(modulePrefix + 'library-core')
|
||||
implementation project(modulePrefix + 'library-ui')
|
||||
implementation project(modulePrefix + 'library-dash')
|
||||
implementation project(modulePrefix + 'library-hls')
|
||||
implementation project(modulePrefix + 'library-smoothstreaming')
|
||||
implementation project(modulePrefix + 'extension-ima')
|
||||
implementation 'androidx.annotation:annotation:1.1.0'
|
||||
}
|
||||
|
||||
apply plugin: 'com.google.android.gms.strict-version-matcher-plugin'
|
||||
|
|
@ -1,38 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Copyright (C) 2017 The Android Open Source Project
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.google.android.exoplayer2.imademo">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
|
||||
<uses-sdk/>
|
||||
|
||||
<application android:label="@string/application_name" android:icon="@mipmap/ic_launcher"
|
||||
android:largeHeap="true" android:allowBackup="false">
|
||||
|
||||
<activity android:name="com.google.android.exoplayer2.imademo.MainActivity"
|
||||
android:configChanges="keyboard|keyboardHidden|orientation|screenSize|screenLayout|smallestScreenSize|uiMode"
|
||||
android:label="@string/application_name"
|
||||
android:theme="@style/PlayerTheme">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN"/>
|
||||
<category android:name="android.intent.category.LAUNCHER"/>
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
|
|
@ -1,58 +0,0 @@
|
|||
/*
|
||||
* Copyright (C) 2017 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.google.android.exoplayer2.imademo;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.os.Bundle;
|
||||
import com.google.android.exoplayer2.ExoPlayer;
|
||||
import com.google.android.exoplayer2.ui.PlayerView;
|
||||
|
||||
/**
|
||||
* Main Activity for the IMA plugin demo. {@link ExoPlayer} objects are created by
|
||||
* {@link PlayerManager}, which this class instantiates.
|
||||
*/
|
||||
public final class MainActivity extends Activity {
|
||||
|
||||
private PlayerView playerView;
|
||||
private PlayerManager player;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.main_activity);
|
||||
playerView = findViewById(R.id.player_view);
|
||||
player = new PlayerManager(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
player.init(this, playerView);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPause() {
|
||||
super.onPause();
|
||||
player.reset();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
player.release();
|
||||
super.onDestroy();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,124 +0,0 @@
|
|||
/*
|
||||
* Copyright (C) 2017 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.google.android.exoplayer2.imademo;
|
||||
|
||||
import android.content.Context;
|
||||
import android.net.Uri;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.C.ContentType;
|
||||
import com.google.android.exoplayer2.ExoPlayer;
|
||||
import com.google.android.exoplayer2.ExoPlayerFactory;
|
||||
import com.google.android.exoplayer2.SimpleExoPlayer;
|
||||
import com.google.android.exoplayer2.ext.ima.ImaAdsLoader;
|
||||
import com.google.android.exoplayer2.source.MediaSource;
|
||||
import com.google.android.exoplayer2.source.MediaSourceFactory;
|
||||
import com.google.android.exoplayer2.source.ProgressiveMediaSource;
|
||||
import com.google.android.exoplayer2.source.ads.AdsMediaSource;
|
||||
import com.google.android.exoplayer2.source.dash.DashMediaSource;
|
||||
import com.google.android.exoplayer2.source.hls.HlsMediaSource;
|
||||
import com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource;
|
||||
import com.google.android.exoplayer2.ui.PlayerView;
|
||||
import com.google.android.exoplayer2.upstream.DataSource;
|
||||
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
|
||||
/** Manages the {@link ExoPlayer}, the IMA plugin and all video playback. */
|
||||
/* package */ final class PlayerManager implements MediaSourceFactory {
|
||||
|
||||
private final ImaAdsLoader adsLoader;
|
||||
private final DataSource.Factory dataSourceFactory;
|
||||
|
||||
private SimpleExoPlayer player;
|
||||
private long contentPosition;
|
||||
|
||||
public PlayerManager(Context context) {
|
||||
String adTag = context.getString(R.string.ad_tag_url);
|
||||
adsLoader = new ImaAdsLoader(context, Uri.parse(adTag));
|
||||
dataSourceFactory =
|
||||
new DefaultDataSourceFactory(
|
||||
context, Util.getUserAgent(context, context.getString(R.string.application_name)));
|
||||
}
|
||||
|
||||
public void init(Context context, PlayerView playerView) {
|
||||
// Create a player instance.
|
||||
player = ExoPlayerFactory.newSimpleInstance(context);
|
||||
adsLoader.setPlayer(player);
|
||||
playerView.setPlayer(player);
|
||||
|
||||
// This is the MediaSource representing the content media (i.e. not the ad).
|
||||
String contentUrl = context.getString(R.string.content_url);
|
||||
MediaSource contentMediaSource = buildMediaSource(Uri.parse(contentUrl));
|
||||
|
||||
// Compose the content media source into a new AdsMediaSource with both ads and content.
|
||||
MediaSource mediaSourceWithAds =
|
||||
new AdsMediaSource(
|
||||
contentMediaSource, /* adMediaSourceFactory= */ this, adsLoader, playerView);
|
||||
|
||||
// Prepare the player with the source.
|
||||
player.seekTo(contentPosition);
|
||||
player.prepare(mediaSourceWithAds);
|
||||
player.setPlayWhenReady(true);
|
||||
}
|
||||
|
||||
public void reset() {
|
||||
if (player != null) {
|
||||
contentPosition = player.getContentPosition();
|
||||
player.release();
|
||||
player = null;
|
||||
adsLoader.setPlayer(null);
|
||||
}
|
||||
}
|
||||
|
||||
public void release() {
|
||||
if (player != null) {
|
||||
player.release();
|
||||
player = null;
|
||||
}
|
||||
adsLoader.release();
|
||||
}
|
||||
|
||||
// MediaSourceFactory implementation.
|
||||
|
||||
@Override
|
||||
public MediaSource createMediaSource(Uri uri) {
|
||||
return buildMediaSource(uri);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int[] getSupportedTypes() {
|
||||
// IMA does not support Smooth Streaming ads.
|
||||
return new int[] {C.TYPE_DASH, C.TYPE_HLS, C.TYPE_OTHER};
|
||||
}
|
||||
|
||||
// Internal methods.
|
||||
|
||||
private MediaSource buildMediaSource(Uri uri) {
|
||||
@ContentType int type = Util.inferContentType(uri);
|
||||
switch (type) {
|
||||
case C.TYPE_DASH:
|
||||
return new DashMediaSource.Factory(dataSourceFactory).createMediaSource(uri);
|
||||
case C.TYPE_SS:
|
||||
return new SsMediaSource.Factory(dataSourceFactory).createMediaSource(uri);
|
||||
case C.TYPE_HLS:
|
||||
return new HlsMediaSource.Factory(dataSourceFactory).createMediaSource(uri);
|
||||
case C.TYPE_OTHER:
|
||||
return new ProgressiveMediaSource.Factory(dataSourceFactory).createMediaSource(uri);
|
||||
default:
|
||||
throw new IllegalStateException("Unsupported type: " + type);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,21 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Copyright (C) 2017 The Android Open Source Project
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
<com.google.android.exoplayer2.ui.PlayerView
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@+id/player_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:keepScreenOn="true"/>
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Copyright (C) 2017 The Android Open Source Project
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
<resources>
|
||||
|
||||
<string name="application_name">Exo IMA Demo</string>
|
||||
|
||||
<string name="content_url"><![CDATA[https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv]]></string>
|
||||
|
||||
<string name="ad_tag_url"><![CDATA[https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/single_ad_samples&ciu_szs=300x250&impl=s&gdfp_req=1&env=vp&output=vast&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ct%3Dlinear&correlator=]]></string>
|
||||
|
||||
</resources>
|
||||
|
|
@ -1,23 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Copyright (C) 2017 The Android Open Source Project
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
<resources xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<style name="PlayerTheme" parent="android:Theme.Holo">
|
||||
<item name="android:windowNoTitle">true</item>
|
||||
<item name="android:windowBackground">@android:color/black</item>
|
||||
</style>
|
||||
|
||||
</resources>
|
||||
|
|
@ -1,5 +1,8 @@
|
|||
# ExoPlayer main demo #
|
||||
# ExoPlayer main demo
|
||||
|
||||
This is the main ExoPlayer demo application. It uses ExoPlayer to play a number
|
||||
of test streams. It can be used as a starting point or reference project when
|
||||
developing other applications that make use of the ExoPlayer library.
|
||||
This is the main ExoPlayer demo app. It uses ExoPlayer to play a number of test
|
||||
streams. It can be used as a starting point or reference project when developing
|
||||
other applications that make use of the ExoPlayer library.
|
||||
|
||||
See the [demos README](../README.md) for instructions on how to build and run
|
||||
this demo.
|
||||
|
|
|
|||
|
|
@ -26,7 +26,8 @@ android {
|
|||
versionName project.ext.releaseVersion
|
||||
versionCode project.ext.releaseVersionCode
|
||||
minSdkVersion project.ext.minSdkVersion
|
||||
targetSdkVersion project.ext.targetSdkVersion
|
||||
targetSdkVersion project.ext.appTargetSdkVersion
|
||||
multiDexEnabled true
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
|
|
@ -37,6 +38,7 @@ android {
|
|||
"proguard-rules.txt",
|
||||
getDefaultProguardFile('proguard-android.txt')
|
||||
]
|
||||
signingConfig signingConfigs.debug
|
||||
}
|
||||
debug {
|
||||
jniDebuggable = true
|
||||
|
|
@ -49,34 +51,40 @@ android {
|
|||
disable 'GoogleAppIndexingWarning','MissingTranslation','IconDensities'
|
||||
}
|
||||
|
||||
flavorDimensions "extensions"
|
||||
flavorDimensions "decoderExtensions"
|
||||
|
||||
productFlavors {
|
||||
noExtensions {
|
||||
dimension "extensions"
|
||||
noDecoderExtensions {
|
||||
dimension "decoderExtensions"
|
||||
buildConfigField "boolean", "USE_DECODER_EXTENSIONS", "false"
|
||||
}
|
||||
withExtensions {
|
||||
dimension "extensions"
|
||||
withDecoderExtensions {
|
||||
dimension "decoderExtensions"
|
||||
buildConfigField "boolean", "USE_DECODER_EXTENSIONS", "true"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation 'androidx.annotation:annotation:1.1.0'
|
||||
implementation 'androidx.viewpager:viewpager:1.0.0'
|
||||
implementation 'androidx.fragment:fragment:1.0.0'
|
||||
implementation 'com.google.android.material:material:1.0.0'
|
||||
compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion
|
||||
implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion
|
||||
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')
|
||||
withExtensionsImplementation project(path: modulePrefix + 'extension-ffmpeg')
|
||||
withExtensionsImplementation project(path: modulePrefix + 'extension-flac')
|
||||
withExtensionsImplementation project(path: modulePrefix + 'extension-ima')
|
||||
withExtensionsImplementation project(path: modulePrefix + 'extension-opus')
|
||||
withExtensionsImplementation project(path: modulePrefix + 'extension-vp9')
|
||||
withExtensionsImplementation project(path: modulePrefix + 'extension-rtmp')
|
||||
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')
|
||||
}
|
||||
|
||||
apply plugin: 'com.google.android.gms.strict-version-matcher-plugin'
|
||||
|
|
|
|||
|
|
@ -1,7 +1,2 @@
|
|||
# Proguard rules specific to the main demo app.
|
||||
|
||||
# Constructor accessed via reflection in PlayerActivity
|
||||
-dontnote com.google.android.exoplayer2.ext.ima.ImaAdsLoader
|
||||
-keepclassmembers class com.google.android.exoplayer2.ext.ima.ImaAdsLoader {
|
||||
<init>(android.content.Context, android.net.Uri);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -34,13 +34,15 @@
|
|||
android:banner="@drawable/ic_banner"
|
||||
android:largeHeap="true"
|
||||
android:allowBackup="false"
|
||||
android:name="com.google.android.exoplayer2.demo.DemoApplication"
|
||||
tools:ignore="UnusedAttribute">
|
||||
android:requestLegacyExternalStorage="true"
|
||||
android:name="androidx.multidex.MultiDexApplication"
|
||||
tools:targetApi="29">
|
||||
|
||||
<activity android:name="com.google.android.exoplayer2.demo.SampleChooserActivity"
|
||||
<activity android:name=".SampleChooserActivity"
|
||||
android:configChanges="keyboardHidden"
|
||||
android:label="@string/application_name"
|
||||
android:theme="@style/Theme.AppCompat">
|
||||
android:theme="@style/Theme.AppCompat"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN"/>
|
||||
<category android:name="android.intent.category.LAUNCHER"/>
|
||||
|
|
@ -60,11 +62,12 @@
|
|||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity android:name="com.google.android.exoplayer2.demo.PlayerActivity"
|
||||
<activity android:name=".PlayerActivity"
|
||||
android:configChanges="keyboard|keyboardHidden|orientation|screenSize|screenLayout|smallestScreenSize|uiMode"
|
||||
android:launchMode="singleTop"
|
||||
android:label="@string/application_name"
|
||||
android:theme="@style/PlayerTheme">
|
||||
android:theme="@style/PlayerTheme"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="com.google.android.exoplayer.demo.action.VIEW"/>
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
|
|
@ -80,7 +83,7 @@
|
|||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<service android:name="com.google.android.exoplayer2.demo.DemoDownloadService"
|
||||
<service android:name=".DemoDownloadService"
|
||||
android:exported="false">
|
||||
<intent-filter>
|
||||
<action android:name="com.google.android.exoplayer.downloadService.action.RESTART"/>
|
||||
|
|
|
|||
|
|
@ -1,341 +1,227 @@
|
|||
[
|
||||
{
|
||||
"name": "YouTube DASH",
|
||||
"name": "Clear DASH",
|
||||
"samples": [
|
||||
{
|
||||
"name": "Google Glass (MP4,H264)",
|
||||
"uri": "https://www.youtube.com/api/manifest/dash/id/bf5bb2419360daf1/source/youtube?as=fmp4_audio_clear,fmp4_sd_hd_clear&sparams=ip,ipbits,expire,source,id,as&ip=0.0.0.0&ipbits=0&expire=19000000000&signature=51AF5F39AB0CEC3E5497CD9C900EBFEAECCCB5C7.8506521BFC350652163895D4C26DEE124209AA9E&key=ik0",
|
||||
"extension": "mpd"
|
||||
},
|
||||
{
|
||||
"name": "Google Play (MP4,H264)",
|
||||
"uri": "https://www.youtube.com/api/manifest/dash/id/3aa39fa2cc27967f/source/youtube?as=fmp4_audio_clear,fmp4_sd_hd_clear&sparams=ip,ipbits,expire,source,id,as&ip=0.0.0.0&ipbits=0&expire=19000000000&signature=A2716F75795F5D2AF0E88962FFCD10DB79384F29.84308FF04844498CE6FBCE4731507882B8307798&key=ik0",
|
||||
"extension": "mpd"
|
||||
},
|
||||
{
|
||||
"name": "Google Glass (WebM,VP9)",
|
||||
"uri": "https://www.youtube.com/api/manifest/dash/id/bf5bb2419360daf1/source/youtube?as=fmp4_audio_clear,webm2_sd_hd_clear&sparams=ip,ipbits,expire,source,id,as&ip=0.0.0.0&ipbits=0&expire=19000000000&signature=249B04F79E984D7F86B4D8DB48AE6FAF41C17AB3.7B9F0EC0505E1566E59B8E488E9419F253DDF413&key=ik0",
|
||||
"extension": "mpd"
|
||||
},
|
||||
{
|
||||
"name": "Google Play (WebM,VP9)",
|
||||
"uri": "https://www.youtube.com/api/manifest/dash/id/3aa39fa2cc27967f/source/youtube?as=fmp4_audio_clear,webm2_sd_hd_clear&sparams=ip,ipbits,expire,source,id,as&ip=0.0.0.0&ipbits=0&expire=19000000000&signature=B1C2A74783AC1CC4865EB312D7DD2D48230CC9FD.BD153B9882175F1F94BFE5141A5482313EA38E8D&key=ik0",
|
||||
"extension": "mpd"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Widevine DASH Policy Tests (GTS)",
|
||||
"samples": [
|
||||
{
|
||||
"name": "WV: HDCP not specified",
|
||||
"uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd",
|
||||
"drm_scheme": "widevine",
|
||||
"drm_license_url": "https://proxy.uat.widevine.com/proxy?video_id=d286538032258a1c&provider=widevine_test"
|
||||
},
|
||||
{
|
||||
"name": "WV: HDCP not required",
|
||||
"uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd",
|
||||
"drm_scheme": "widevine",
|
||||
"drm_license_url": "https://proxy.uat.widevine.com/proxy?video_id=48fcc369939ac96c&provider=widevine_test"
|
||||
},
|
||||
{
|
||||
"name": "WV: HDCP required",
|
||||
"uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd",
|
||||
"drm_scheme": "widevine",
|
||||
"drm_license_url": "https://proxy.uat.widevine.com/proxy?video_id=e06c39f1151da3df&provider=widevine_test"
|
||||
},
|
||||
{
|
||||
"name": "WV: Secure video path required (MP4,H264)",
|
||||
"uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd",
|
||||
"drm_scheme": "widevine",
|
||||
"drm_license_url": "https://proxy.uat.widevine.com/proxy?video_id=0894c7c8719b28a0&provider=widevine_test"
|
||||
},
|
||||
{
|
||||
"name": "WV: Secure video path required (WebM,VP9)",
|
||||
"uri": "https://storage.googleapis.com/wvmedia/cenc/vp9/tears/tears.mpd",
|
||||
"drm_scheme": "widevine",
|
||||
"drm_license_url": "https://proxy.uat.widevine.com/proxy?video_id=0894c7c8719b28a0&provider=widevine_test"
|
||||
},
|
||||
{
|
||||
"name": "WV: Secure video path required (MP4,H265)",
|
||||
"uri": "https://storage.googleapis.com/wvmedia/cenc/hevc/tears/tears.mpd",
|
||||
"drm_scheme": "widevine",
|
||||
"drm_license_url": "https://proxy.uat.widevine.com/proxy?video_id=0894c7c8719b28a0&provider=widevine_test"
|
||||
},
|
||||
{
|
||||
"name": "WV: HDCP + secure video path required",
|
||||
"uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd",
|
||||
"drm_scheme": "widevine",
|
||||
"drm_license_url": "https://proxy.uat.widevine.com/proxy?video_id=efd045b1eb61888a&provider=widevine_test"
|
||||
},
|
||||
{
|
||||
"name": "WV: 30s license duration (fails at ~30s)",
|
||||
"uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd",
|
||||
"drm_scheme": "widevine",
|
||||
"drm_license_url": "https://proxy.uat.widevine.com/proxy?video_id=f9a34cab7b05881a&provider=widevine_test"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Widevine HDCP Capabilities Tests",
|
||||
"samples": [
|
||||
{
|
||||
"name": "WV: HDCP: None (not required)",
|
||||
"uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd",
|
||||
"drm_scheme": "widevine",
|
||||
"drm_license_url": "https://proxy.uat.widevine.com/proxy?video_id=HDCP_None&provider=widevine_test"
|
||||
},
|
||||
{
|
||||
"name": "WV: HDCP: 1.0 required",
|
||||
"uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd",
|
||||
"drm_scheme": "widevine",
|
||||
"drm_license_url": "https://proxy.uat.widevine.com/proxy?video_id=HDCP_V1&provider=widevine_test"
|
||||
},
|
||||
{
|
||||
"name": "WV: HDCP: 2.0 required",
|
||||
"uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd",
|
||||
"drm_scheme": "widevine",
|
||||
"drm_license_url": "https://proxy.uat.widevine.com/proxy?video_id=HDCP_V2&provider=widevine_test"
|
||||
},
|
||||
{
|
||||
"name": "WV: HDCP: 2.1 required",
|
||||
"uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd",
|
||||
"drm_scheme": "widevine",
|
||||
"drm_license_url": "https://proxy.uat.widevine.com/proxy?video_id=HDCP_V2_1&provider=widevine_test"
|
||||
},
|
||||
{
|
||||
"name": "WV: HDCP: 2.2 required",
|
||||
"uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd",
|
||||
"drm_scheme": "widevine",
|
||||
"drm_license_url": "https://proxy.uat.widevine.com/proxy?video_id=HDCP_V2_2&provider=widevine_test"
|
||||
},
|
||||
{
|
||||
"name": "WV: HDCP: No digital output",
|
||||
"uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd",
|
||||
"drm_scheme": "widevine",
|
||||
"drm_license_url": "https://proxy.uat.widevine.com/proxy?video_id=HDCP_NO_DIGTAL_OUTPUT&provider=widevine_test"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Widevine DASH: MP4,H264",
|
||||
"samples": [
|
||||
{
|
||||
"name": "WV: Clear SD & HD (MP4,H264)",
|
||||
"name": "HD (MP4, H264)",
|
||||
"uri": "https://storage.googleapis.com/wvmedia/clear/h264/tears/tears.mpd"
|
||||
},
|
||||
{
|
||||
"name": "WV: Clear SD (MP4,H264)",
|
||||
"uri": "https://storage.googleapis.com/wvmedia/clear/h264/tears/tears_sd.mpd"
|
||||
},
|
||||
{
|
||||
"name": "WV: Clear HD (MP4,H264)",
|
||||
"uri": "https://storage.googleapis.com/wvmedia/clear/h264/tears/tears_hd.mpd"
|
||||
},
|
||||
{
|
||||
"name": "WV: Clear UHD (MP4,H264)",
|
||||
"name": "UHD (MP4, H264)",
|
||||
"uri": "https://storage.googleapis.com/wvmedia/clear/h264/tears/tears_uhd.mpd"
|
||||
},
|
||||
{
|
||||
"name": "WV: Secure SD & HD (cenc,MP4,H264)",
|
||||
"uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd",
|
||||
"drm_scheme": "widevine",
|
||||
"drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
|
||||
},
|
||||
{
|
||||
"name": "WV: Secure SD (cenc,MP4,H264)",
|
||||
"uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears_sd.mpd",
|
||||
"drm_scheme": "widevine",
|
||||
"drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
|
||||
},
|
||||
{
|
||||
"name": "WV: Secure HD (cenc,MP4,H264)",
|
||||
"uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears_hd.mpd",
|
||||
"drm_scheme": "widevine",
|
||||
"drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
|
||||
},
|
||||
{
|
||||
"name": "WV: Secure UHD (cenc,MP4,H264)",
|
||||
"uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears_uhd.mpd",
|
||||
"drm_scheme": "widevine",
|
||||
"drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
|
||||
},
|
||||
{
|
||||
"name": "WV: Secure SD & HD (cbc1,MP4,H264)",
|
||||
"uri": "https://storage.googleapis.com/wvmedia/cbc1/h264/tears/tears_aes_cbc1.mpd",
|
||||
"drm_scheme": "widevine",
|
||||
"drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
|
||||
},
|
||||
{
|
||||
"name": "WV: Secure SD (cbc1,MP4,H264)",
|
||||
"uri": "https://storage.googleapis.com/wvmedia/cbc1/h264/tears/tears_aes_cbc1_sd.mpd",
|
||||
"drm_scheme": "widevine",
|
||||
"drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
|
||||
},
|
||||
{
|
||||
"name": "WV: Secure HD (cbc1,MP4,H264)",
|
||||
"uri": "https://storage.googleapis.com/wvmedia/cbc1/h264/tears/tears_aes_cbc1_hd.mpd",
|
||||
"drm_scheme": "widevine",
|
||||
"drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
|
||||
},
|
||||
{
|
||||
"name": "WV: Secure UHD (cbc1,MP4,H264)",
|
||||
"uri": "https://storage.googleapis.com/wvmedia/cbc1/h264/tears/tears_aes_cbc1_uhd.mpd",
|
||||
"drm_scheme": "widevine",
|
||||
"drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
|
||||
},
|
||||
{
|
||||
"name": "WV: Secure SD & HD (cbcs,MP4,H264)",
|
||||
"uri": "https://storage.googleapis.com/wvmedia/cbcs/h264/tears/tears_aes_cbcs.mpd",
|
||||
"drm_scheme": "widevine",
|
||||
"drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
|
||||
},
|
||||
{
|
||||
"name": "WV: Secure SD (cbcs,MP4,H264)",
|
||||
"uri": "https://storage.googleapis.com/wvmedia/cbcs/h264/tears/tears_aes_cbcs_sd.mpd",
|
||||
"drm_scheme": "widevine",
|
||||
"drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
|
||||
},
|
||||
{
|
||||
"name": "WV: Secure HD (cbcs,MP4,H264)",
|
||||
"uri": "https://storage.googleapis.com/wvmedia/cbcs/h264/tears/tears_aes_cbcs_hd.mpd",
|
||||
"drm_scheme": "widevine",
|
||||
"drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
|
||||
},
|
||||
{
|
||||
"name": "WV: Secure UHD (cbcs,MP4,H264)",
|
||||
"uri": "https://storage.googleapis.com/wvmedia/cbcs/h264/tears/tears_aes_cbcs_uhd.mpd",
|
||||
"drm_scheme": "widevine",
|
||||
"drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Widevine DASH: WebM,VP9",
|
||||
"samples": [
|
||||
{
|
||||
"name": "WV: Clear SD & HD (WebM,VP9)",
|
||||
"uri": "https://storage.googleapis.com/wvmedia/clear/vp9/tears/tears.mpd"
|
||||
},
|
||||
{
|
||||
"name": "WV: Clear SD (WebM,VP9)",
|
||||
"uri": "https://storage.googleapis.com/wvmedia/clear/vp9/tears/tears_sd.mpd"
|
||||
},
|
||||
{
|
||||
"name": "WV: Clear HD (WebM,VP9)",
|
||||
"uri": "https://storage.googleapis.com/wvmedia/clear/vp9/tears/tears_hd.mpd"
|
||||
},
|
||||
{
|
||||
"name": "WV: Clear UHD (WebM,VP9)",
|
||||
"uri": "https://storage.googleapis.com/wvmedia/clear/vp9/tears/tears_uhd.mpd"
|
||||
},
|
||||
{
|
||||
"name": "WV: Secure Fullsample SD & HD (WebM,VP9)",
|
||||
"uri": "https://storage.googleapis.com/wvmedia/cenc/vp9/tears/tears.mpd",
|
||||
"drm_scheme": "widevine",
|
||||
"drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
|
||||
},
|
||||
{
|
||||
"name": "WV: Secure Fullsample SD (WebM,VP9)",
|
||||
"uri": "https://storage.googleapis.com/wvmedia/cenc/vp9/tears/tears_sd.mpd",
|
||||
"drm_scheme": "widevine",
|
||||
"drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
|
||||
},
|
||||
{
|
||||
"name": "WV: Secure Fullsample HD (WebM,VP9)",
|
||||
"uri": "https://storage.googleapis.com/wvmedia/cenc/vp9/tears/tears_hd.mpd",
|
||||
"drm_scheme": "widevine",
|
||||
"drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
|
||||
},
|
||||
{
|
||||
"name": "WV: Secure Fullsample UHD (WebM,VP9)",
|
||||
"uri": "https://storage.googleapis.com/wvmedia/cenc/vp9/tears/tears_uhd.mpd",
|
||||
"drm_scheme": "widevine",
|
||||
"drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
|
||||
},
|
||||
{
|
||||
"name": "WV: Secure Subsample SD & HD (WebM,VP9)",
|
||||
"uri": "https://storage.googleapis.com/wvmedia/cenc/vp9/subsample/24fps/tears/tears.mpd",
|
||||
"drm_scheme": "widevine",
|
||||
"drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
|
||||
},
|
||||
{
|
||||
"name": "WV: Secure Subsample SD (WebM,VP9)",
|
||||
"uri": "https://storage.googleapis.com/wvmedia/cenc/vp9/subsample/24fps/tears/tears_sd.mpd",
|
||||
"drm_scheme": "widevine",
|
||||
"drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
|
||||
},
|
||||
{
|
||||
"name": "WV: Secure Subsample HD (WebM,VP9)",
|
||||
"uri": "https://storage.googleapis.com/wvmedia/cenc/vp9/subsample/24fps/tears/tears_hd.mpd",
|
||||
"drm_scheme": "widevine",
|
||||
"drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
|
||||
},
|
||||
{
|
||||
"name": "WV: Secure Subsample UHD (WebM,VP9)",
|
||||
"uri": "https://storage.googleapis.com/wvmedia/cenc/vp9/subsample/24fps/tears/tears_uhd.mpd",
|
||||
"drm_scheme": "widevine",
|
||||
"drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Widevine DASH: MP4,H265",
|
||||
"samples": [
|
||||
{
|
||||
"name": "WV: Clear SD & HD (MP4,H265)",
|
||||
"name": "HD (MP4, H265)",
|
||||
"uri": "https://storage.googleapis.com/wvmedia/clear/hevc/tears/tears.mpd"
|
||||
},
|
||||
{
|
||||
"name": "WV: Clear SD (MP4,H265)",
|
||||
"uri": "https://storage.googleapis.com/wvmedia/clear/hevc/tears/tears_sd.mpd"
|
||||
},
|
||||
{
|
||||
"name": "WV: Clear HD (MP4,H265)",
|
||||
"uri": "https://storage.googleapis.com/wvmedia/clear/hevc/tears/tears_hd.mpd"
|
||||
},
|
||||
{
|
||||
"name": "WV: Clear UHD (MP4,H265)",
|
||||
"name": "UHD (MP4, H265)",
|
||||
"uri": "https://storage.googleapis.com/wvmedia/clear/hevc/tears/tears_uhd.mpd"
|
||||
},
|
||||
{
|
||||
"name": "WV: Secure SD & HD (MP4,H265)",
|
||||
"uri": "https://storage.googleapis.com/wvmedia/cenc/hevc/tears/tears.mpd",
|
||||
"drm_scheme": "widevine",
|
||||
"drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
|
||||
"name": "HD (WebM, VP9)",
|
||||
"uri": "https://storage.googleapis.com/wvmedia/clear/vp9/tears/tears.mpd"
|
||||
},
|
||||
{
|
||||
"name": "WV: Secure SD (MP4,H265)",
|
||||
"uri": "https://storage.googleapis.com/wvmedia/cenc/hevc/tears/tears_sd.mpd",
|
||||
"drm_scheme": "widevine",
|
||||
"drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
|
||||
},
|
||||
{
|
||||
"name": "WV: Secure HD (MP4,H265)",
|
||||
"uri": "https://storage.googleapis.com/wvmedia/cenc/hevc/tears/tears_hd.mpd",
|
||||
"drm_scheme": "widevine",
|
||||
"drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
|
||||
},
|
||||
{
|
||||
"name": "WV: Secure UHD (MP4,H265)",
|
||||
"uri": "https://storage.googleapis.com/wvmedia/cenc/hevc/tears/tears_uhd.mpd",
|
||||
"drm_scheme": "widevine",
|
||||
"drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
|
||||
"name": "UHD (WebM, VP9)",
|
||||
"uri": "https://storage.googleapis.com/wvmedia/clear/vp9/tears/tears_uhd.mpd"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "SmoothStreaming",
|
||||
"name": "Widevine DASH (MP4, H264)",
|
||||
"samples": [
|
||||
{
|
||||
"name": "Super speed",
|
||||
"uri": "https://playready.directtaps.net/smoothstreaming/SSWSS720H264/SuperSpeedway_720.ism/Manifest"
|
||||
"name": "HD (cenc)",
|
||||
"uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd",
|
||||
"drm_scheme": "widevine",
|
||||
"drm_license_uri": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
|
||||
},
|
||||
{
|
||||
"name": "Super speed (PlayReady)",
|
||||
"uri": "https://playready.directtaps.net/smoothstreaming/SSWSS720H264PR/SuperSpeedway_720.ism/Manifest",
|
||||
"drm_scheme": "playready"
|
||||
"name": "UHD (cenc)",
|
||||
"uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears_uhd.mpd",
|
||||
"drm_scheme": "widevine",
|
||||
"drm_license_uri": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
|
||||
},
|
||||
{
|
||||
"name": "HD (cbcs)",
|
||||
"uri": "https://storage.googleapis.com/wvmedia/cbcs/h264/tears/tears_aes_cbcs.mpd",
|
||||
"drm_scheme": "widevine",
|
||||
"drm_license_uri": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
|
||||
},
|
||||
{
|
||||
"name": "UHD (cbcs)",
|
||||
"uri": "https://storage.googleapis.com/wvmedia/cbcs/h264/tears/tears_aes_cbcs_uhd.mpd",
|
||||
"drm_scheme": "widevine",
|
||||
"drm_license_uri": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
|
||||
},
|
||||
{
|
||||
"name": "Secure -> Clear -> Secure (cenc)",
|
||||
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/widevine/tears_enc_clear_enc.mpd",
|
||||
"drm_scheme": "widevine",
|
||||
"drm_license_uri": "https://proxy.uat.widevine.com/proxy?provider=widevine_test",
|
||||
"drm_session_for_clear_content": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Widevine DASH (WebM, VP9)",
|
||||
"samples": [
|
||||
{
|
||||
"name": "HD (cenc, full-sample)",
|
||||
"uri": "https://storage.googleapis.com/wvmedia/cenc/vp9/tears/tears.mpd",
|
||||
"drm_scheme": "widevine",
|
||||
"drm_license_uri": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
|
||||
},
|
||||
{
|
||||
"name": "UHD (cenc, full-sample)",
|
||||
"uri": "https://storage.googleapis.com/wvmedia/cenc/vp9/tears/tears_uhd.mpd",
|
||||
"drm_scheme": "widevine",
|
||||
"drm_license_uri": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
|
||||
},
|
||||
{
|
||||
"name": "HD (cenc, sub-sample)",
|
||||
"uri": "https://storage.googleapis.com/wvmedia/cenc/vp9/subsample/24fps/tears/tears.mpd",
|
||||
"drm_scheme": "widevine",
|
||||
"drm_license_uri": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
|
||||
},
|
||||
{
|
||||
"name": "UHD (cenc, sub-sample)",
|
||||
"uri": "https://storage.googleapis.com/wvmedia/cenc/vp9/subsample/24fps/tears/tears_uhd.mpd",
|
||||
"drm_scheme": "widevine",
|
||||
"drm_license_uri": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Widevine DASH (MP4, H265)",
|
||||
"samples": [
|
||||
{
|
||||
"name": "HD (cenc)",
|
||||
"uri": "https://storage.googleapis.com/wvmedia/cenc/hevc/tears/tears.mpd",
|
||||
"drm_scheme": "widevine",
|
||||
"drm_license_uri": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
|
||||
},
|
||||
{
|
||||
"name": "UHD (cenc)",
|
||||
"uri": "https://storage.googleapis.com/wvmedia/cenc/hevc/tears/tears_uhd.mpd",
|
||||
"drm_scheme": "widevine",
|
||||
"drm_license_uri": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Widevine DASH (policy tests)",
|
||||
"samples": [
|
||||
{
|
||||
"name": "SW secure crypto (L3)",
|
||||
"uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd",
|
||||
"drm_scheme": "widevine",
|
||||
"drm_license_uri": "https://proxy.uat.widevine.com/proxy?video_id=GTS_SW_SECURE_CRYPTO&provider=widevine_test"
|
||||
},
|
||||
{
|
||||
"name": "SW secure decode",
|
||||
"uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd",
|
||||
"drm_scheme": "widevine",
|
||||
"drm_license_uri": "https://proxy.uat.widevine.com/proxy?video_id=GTS_SW_SECURE_DECODE&provider=widevine_test"
|
||||
},
|
||||
{
|
||||
"name": "HW secure crypto",
|
||||
"uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd",
|
||||
"drm_scheme": "widevine",
|
||||
"drm_license_uri": "https://proxy.uat.widevine.com/proxy?video_id=GTS_HW_SECURE_CRYPTO&provider=widevine_test"
|
||||
},
|
||||
{
|
||||
"name": "HW secure decode",
|
||||
"uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd",
|
||||
"drm_scheme": "widevine",
|
||||
"drm_license_uri": "https://proxy.uat.widevine.com/proxy?video_id=GTS_HW_SECURE_DECODE&provider=widevine_test"
|
||||
},
|
||||
{
|
||||
"name": "HW secure all (L1)",
|
||||
"uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd",
|
||||
"drm_scheme": "widevine",
|
||||
"drm_license_uri": "https://proxy.uat.widevine.com/proxy?video_id=GTS_HW_SECURE_ALL&provider=widevine_test"
|
||||
},
|
||||
{
|
||||
"name": "30s license (fails at ~30s)",
|
||||
"uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd",
|
||||
"drm_scheme": "widevine",
|
||||
"drm_license_uri": "https://proxy.uat.widevine.com/proxy?video_id=GTS_CAN_RENEW_FALSE_LICENSE_30S_PLAYBACK_30S&provider=widevine_test"
|
||||
},
|
||||
{
|
||||
"name": "HDCP not required",
|
||||
"uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd",
|
||||
"drm_scheme": "widevine",
|
||||
"drm_license_uri": "https://proxy.uat.widevine.com/proxy?video_id=GTS_SW_SECURE_CRYPTO_HDCP_NONE&provider=widevine_test"
|
||||
},
|
||||
{
|
||||
"name": "HDCP 1.0 required",
|
||||
"uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd",
|
||||
"drm_scheme": "widevine",
|
||||
"drm_license_uri": "https://proxy.uat.widevine.com/proxy?video_id=GTS_SW_SECURE_CRYPTO_HDCP_V1&provider=widevine_test"
|
||||
},
|
||||
{
|
||||
"name": "HDCP 2.0 required",
|
||||
"uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd",
|
||||
"drm_scheme": "widevine",
|
||||
"drm_license_uri": "https://proxy.uat.widevine.com/proxy?video_id=GTS_SW_SECURE_CRYPTO_HDCP_V2&provider=widevine_test"
|
||||
},
|
||||
{
|
||||
"name": "HDCP 2.1 required",
|
||||
"uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd",
|
||||
"drm_scheme": "widevine",
|
||||
"drm_license_uri": "https://proxy.uat.widevine.com/proxy?video_id=GTS_SW_SECURE_CRYPTO_HDCP_V2_1&provider=widevine_test"
|
||||
},
|
||||
{
|
||||
"name": "HDCP 2.2 required",
|
||||
"uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd",
|
||||
"drm_scheme": "widevine",
|
||||
"drm_license_uri": "https://proxy.uat.widevine.com/proxy?video_id=GTS_SW_SECURE_CRYPTO_HDCP_V2_2&provider=widevine_test"
|
||||
},
|
||||
{
|
||||
"name": "HDCP no digital output",
|
||||
"uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd",
|
||||
"drm_scheme": "widevine",
|
||||
"drm_license_uri": "https://proxy.uat.widevine.com/proxy?video_id=GTS_SW_SECURE_CRYPTO_HDCP_NO_DIGITAL_OUTPUT&provider=widevine_test"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "60fps DASH",
|
||||
"samples": [
|
||||
{
|
||||
"name": "HD (MP4, H264, Clear)",
|
||||
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/60fps/bbb-clear-1080/manifest.mpd"
|
||||
},
|
||||
{
|
||||
"name": "4K (MP4, H264, Clear)",
|
||||
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/60fps/bbb-clear-2160/manifest.mpd"
|
||||
},
|
||||
{
|
||||
"name": "HD (MP4, H264, Widevine cenc)",
|
||||
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/60fps/bbb-wv-1080/manifest.mpd",
|
||||
"drm_scheme": "widevine",
|
||||
"drm_license_uri": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
|
||||
},
|
||||
{
|
||||
"name": "4K (MP4, H264, Widevine cenc)",
|
||||
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/60fps/bbb-wv-2160/manifest.mpd",
|
||||
"drm_scheme": "widevine",
|
||||
"drm_license_uri": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "DASH - Multiple base URLs",
|
||||
"samples": [
|
||||
{
|
||||
"name": "DASH - Multiple base URLs",
|
||||
"uri": "https://storage.googleapis.com/exoplayer-test-media-0/dash-multiple-base-urls/manifest.mpd"
|
||||
},
|
||||
{
|
||||
"name": "DASH - Multiple base URLs (fail over)",
|
||||
"uri": "https://storage.googleapis.com/exoplayer-test-media-0/dash-multiple-base-urls/manifest-failover.mpd"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
|
@ -343,11 +229,11 @@
|
|||
"name": "HLS",
|
||||
"samples": [
|
||||
{
|
||||
"name": "Apple 4x3 basic stream",
|
||||
"name": "Apple 4x3 basic stream (TS)",
|
||||
"uri": "https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_4x3/bipbop_4x3_variant.m3u8"
|
||||
},
|
||||
{
|
||||
"name": "Apple 16x9 basic stream",
|
||||
"name": "Apple 16x9 basic stream (TS)",
|
||||
"uri": "https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_16x9/bipbop_16x9_variant.m3u8"
|
||||
},
|
||||
{
|
||||
|
|
@ -355,117 +241,32 @@
|
|||
"uri": "https://devstreaming-cdn.apple.com/videos/streaming/examples/img_bipbop_adv_example_ts/master.m3u8"
|
||||
},
|
||||
{
|
||||
"name": "Apple master playlist advanced (fMP4)",
|
||||
"name": "Apple master playlist advanced (FMP4)",
|
||||
"uri": "https://devstreaming-cdn.apple.com/videos/streaming/examples/img_bipbop_adv_example_fmp4/master.m3u8"
|
||||
},
|
||||
{
|
||||
"name": "Apple TS media playlist",
|
||||
"name": "Apple media playlist (TS)",
|
||||
"uri": "https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_4x3/gear1/prog_index.m3u8"
|
||||
},
|
||||
{
|
||||
"name": "Apple AAC media playlist",
|
||||
"name": "Apple media playlist (AAC)",
|
||||
"uri": "https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_4x3/gear0/prog_index.m3u8"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Misc",
|
||||
"name": "SmoothStreaming",
|
||||
"samples": [
|
||||
{
|
||||
"name": "Dizzy (MP4)",
|
||||
"uri": "https://html5demos.com/assets/dizzy.mp4"
|
||||
"name": "Super speed (MP4, H264, Clear)",
|
||||
"uri": "https://playready.directtaps.net/smoothstreaming/SSWSS720H264/SuperSpeedway_720.ism/Manifest"
|
||||
},
|
||||
{
|
||||
"name": "Apple AAC 10s",
|
||||
"uri": "https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_4x3/gear0/fileSequence0.aac"
|
||||
},
|
||||
{
|
||||
"name": "Apple TS 10s",
|
||||
"uri": "https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_4x3/gear1/fileSequence0.ts"
|
||||
},
|
||||
{
|
||||
"name": "Android screens (Matroska)",
|
||||
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv"
|
||||
},
|
||||
{
|
||||
"name": "Screens 360P (WebM,VP9,No Audio)",
|
||||
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/gen-3/screens/dash-vod-single-segment/video-vp9-360.webm"
|
||||
},
|
||||
{
|
||||
"name": "Screens 480p (FMP4,H264,No Audio)",
|
||||
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/gen-3/screens/dash-vod-single-segment/video-avc-baseline-480.mp4"
|
||||
},
|
||||
{
|
||||
"name": "Screens 1080p (FMP4,H264, No Audio)",
|
||||
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/gen-3/screens/dash-vod-single-segment/video-137.mp4"
|
||||
},
|
||||
{
|
||||
"name": "Screens (FMP4,AAC Audio)",
|
||||
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/gen-3/screens/dash-vod-single-segment/audio-141.mp4"
|
||||
},
|
||||
{
|
||||
"name": "Google Play (MP3 Audio)",
|
||||
"uri": "https://storage.googleapis.com/exoplayer-test-media-0/play.mp3"
|
||||
},
|
||||
{
|
||||
"name": "Google Play (Ogg/Vorbis Audio)",
|
||||
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/ogg/play.ogg"
|
||||
},
|
||||
{
|
||||
"name": "Big Buck Bunny (FLV Video)",
|
||||
"uri": "https://vod.leasewebcdn.com/bbb.flv?ri=1024&rs=150&start=0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Playlists",
|
||||
"samples": [
|
||||
{
|
||||
"name": "Cats -> Dogs",
|
||||
"playlist": [
|
||||
{
|
||||
"uri": "https://html5demos.com/assets/dizzy.mp4"
|
||||
},
|
||||
{
|
||||
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Audio -> Video -> Audio",
|
||||
"playlist": [
|
||||
{
|
||||
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/gen-3/screens/dash-vod-single-segment/audio-141.mp4"
|
||||
},
|
||||
{
|
||||
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv"
|
||||
},
|
||||
{
|
||||
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/gen-3/screens/dash-vod-single-segment/audio-141.mp4"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Clear -> Enc -> Clear -> Enc -> Enc",
|
||||
"drm_scheme": "widevine",
|
||||
"drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test",
|
||||
"playlist": [
|
||||
{
|
||||
"uri": "https://html5demos.com/assets/dizzy.mp4"
|
||||
},
|
||||
{
|
||||
"uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears_sd.mpd"
|
||||
},
|
||||
{
|
||||
"uri": "https://html5demos.com/assets/dizzy.mp4"
|
||||
},
|
||||
{
|
||||
"uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears_sd.mpd"
|
||||
},
|
||||
{
|
||||
"uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears_sd.mpd"
|
||||
}
|
||||
]
|
||||
"name": "Super speed (MP4, H264, PlayReady)",
|
||||
"uri": "https://playready.directtaps.net/smoothstreaming/SSWSS720H264PR/SuperSpeedway_720.ism/Manifest",
|
||||
"drm_scheme": "playready",
|
||||
"drm_license_uri": "https://playready.directtaps.net/pr/svc/rightsmanager.asmx",
|
||||
"drm_force_default_license_uri": true
|
||||
}
|
||||
]
|
||||
},
|
||||
|
|
@ -556,26 +357,242 @@
|
|||
"name": "VMAP full, empty, full midrolls",
|
||||
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv",
|
||||
"ad_tag_uri": "https://vastsynthesizer.appspot.com/empty-midroll-2"
|
||||
},
|
||||
{
|
||||
"name": "VMAP midroll at 1765 s",
|
||||
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/mp4/frame-counter-one-hour.mp4",
|
||||
"ad_tag_uri": "https://vastsynthesizer.appspot.com/midroll-large"
|
||||
},
|
||||
{
|
||||
"name": "VMAP midroll ad pod at 5 s with 10 skippable ads",
|
||||
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/mp4/frame-counter-one-hour.mp4",
|
||||
"ad_tag_uri": "https://vastsynthesizer.appspot.com/midroll-10-skippable-ads"
|
||||
},
|
||||
{
|
||||
"name": "Playlist with three ad tags",
|
||||
"playlist": [
|
||||
{
|
||||
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/mp4/android-screens-10s.mp4",
|
||||
"ad_tag_uri": "https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/single_ad_samples&ciu_szs=300x250&impl=s&gdfp_req=1&env=vp&output=vast&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ct%3Dlinear&correlator="
|
||||
},
|
||||
{
|
||||
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/mp4/android-screens-25s.mp4",
|
||||
"ad_tag_uri": "https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/ad_rule_samples&ciu_szs=300x250&ad_rule=1&impl=s&gdfp_req=1&env=vp&output=vmap&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ar%3Dpremidpost&cmsid=496&vid=short_onecue&correlator="
|
||||
},
|
||||
{
|
||||
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/mp4/frame-counter-one-hour.mp4",
|
||||
"ad_tag_uri": "https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/ad_rule_samples&ciu_szs=300x250&ad_rule=1&impl=s&gdfp_req=1&env=vp&output=vmap&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ar%3Dpremidpostlongpod&cmsid=496&vid=short_tencue&correlator="
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "360",
|
||||
"name": "Playlists",
|
||||
"samples": [
|
||||
{
|
||||
"name": "Congo (360 top-bottom stereo)",
|
||||
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/360/congo.mp4",
|
||||
"spherical_stereo_mode": "top_bottom"
|
||||
"name": "Cats -> Dogs",
|
||||
"playlist": [
|
||||
{
|
||||
"uri": "https://html5demos.com/assets/dizzy.mp4"
|
||||
},
|
||||
{
|
||||
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Sphericalv2 (180 top-bottom stereo)",
|
||||
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/360/sphericalv2.mp4",
|
||||
"spherical_stereo_mode": "top_bottom"
|
||||
"name": "Audio -> Video -> Audio",
|
||||
"playlist": [
|
||||
{
|
||||
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/gen-3/screens/dash-vod-single-segment/audio-141.mp4"
|
||||
},
|
||||
{
|
||||
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv"
|
||||
},
|
||||
{
|
||||
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/gen-3/screens/dash-vod-single-segment/audio-141.mp4"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Iceland (360 top-bottom stereo ts)",
|
||||
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/360/iceland0.ts",
|
||||
"spherical_stereo_mode": "top_bottom"
|
||||
"name": "Clear -> Enc -> Clear -> Enc -> Enc",
|
||||
"playlist": [
|
||||
{
|
||||
"uri": "https://html5demos.com/assets/dizzy.mp4"
|
||||
},
|
||||
{
|
||||
"uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears_sd.mpd",
|
||||
"drm_scheme": "widevine",
|
||||
"drm_license_uri": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
|
||||
},
|
||||
{
|
||||
"uri": "https://html5demos.com/assets/dizzy.mp4"
|
||||
},
|
||||
{
|
||||
"uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears_sd.mpd",
|
||||
"drm_scheme": "widevine",
|
||||
"drm_license_uri": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
|
||||
},
|
||||
{
|
||||
"uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears_sd.mpd",
|
||||
"drm_scheme": "widevine",
|
||||
"drm_license_uri": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Manual ad insertion",
|
||||
"playlist": [
|
||||
{
|
||||
"uri": "https://html5demos.com/assets/dizzy.mp4",
|
||||
"clip_end_position_ms": 10000
|
||||
},
|
||||
{
|
||||
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/mp4/frame-counter-one-hour.mp4",
|
||||
"clip_end_position_ms": 5000
|
||||
},
|
||||
{
|
||||
"uri": "https://html5demos.com/assets/dizzy.mp4",
|
||||
"clip_start_position_ms": 10000
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "AV1",
|
||||
"samples": [
|
||||
{
|
||||
"name": "SD (WebM, Clear)",
|
||||
"uri": "https://storage.googleapis.com/wvmedia/2019/clear/av1/24/webm/llama_av1_480p_400.webm"
|
||||
},
|
||||
{
|
||||
"name": "SD (WebM, Widevine cenc, L3)",
|
||||
"uri": "https://storage.googleapis.com/wvmedia/2019/cenc/av1/24/webm/llama_av1_480p_400.webm",
|
||||
"drm_scheme": "widevine",
|
||||
"drm_license_uri": "https://proxy.uat.widevine.com/proxy?video_id=GTS_SW_SECURE_CRYPTO&provider=widevine_test"
|
||||
},
|
||||
{
|
||||
"name": "SD (WebM, Widevine cenc, L1)",
|
||||
"uri": "https://storage.googleapis.com/wvmedia/2019/cenc/av1/24/webm/llama_av1_480p_400.webm",
|
||||
"drm_scheme": "widevine",
|
||||
"drm_license_uri": "https://proxy.uat.widevine.com/proxy?video_id=GTS_HW_SECURE_ALL&provider=widevine_test"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Subtitles",
|
||||
"samples": [
|
||||
{
|
||||
"name": "TTML positioning",
|
||||
"uri": "https://html5demos.com/assets/dizzy.mp4",
|
||||
"subtitle_uri": "https://storage.googleapis.com/exoplayer-test-media-1/ttml/netflix_ttml_sample.xml",
|
||||
"subtitle_mime_type": "application/ttml+xml",
|
||||
"subtitle_language": "en"
|
||||
},
|
||||
{
|
||||
"name": "TTML Japanese features",
|
||||
"uri": "https://html5demos.com/assets/dizzy.mp4",
|
||||
"subtitle_uri": "https://storage.googleapis.com/exoplayer-test-media-1/ttml/japanese-ttml.xml",
|
||||
"subtitle_mime_type": "application/ttml+xml",
|
||||
"subtitle_language": "ja"
|
||||
},
|
||||
{
|
||||
"name": "TTML Netflix Japanese examples (IMSC1.1)",
|
||||
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/gen-3/screens/dash-vod-single-segment/video-avc-baseline-480.mp4",
|
||||
"subtitle_uri": "https://storage.googleapis.com/exoplayer-test-media-1/ttml/netflix_japanese_ttml.xml",
|
||||
"subtitle_mime_type": "application/ttml+xml",
|
||||
"subtitle_language": "ja"
|
||||
},
|
||||
{
|
||||
"name": "WebVTT positioning",
|
||||
"uri": "https://html5demos.com/assets/dizzy.mp4",
|
||||
"subtitle_uri": "https://storage.googleapis.com/exoplayer-test-media-1/webvtt/numeric-lines.vtt",
|
||||
"subtitle_mime_type": "text/vtt",
|
||||
"subtitle_language": "en"
|
||||
},
|
||||
{
|
||||
"name": "WebVTT Japanese features",
|
||||
"uri": "https://html5demos.com/assets/dizzy.mp4",
|
||||
"subtitle_uri": "https://storage.googleapis.com/exoplayer-test-media-1/webvtt/japanese.vtt",
|
||||
"subtitle_mime_type": "text/vtt",
|
||||
"subtitle_language": "ja"
|
||||
},
|
||||
{
|
||||
"name": "SubStation Alpha positioning",
|
||||
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/gen-3/screens/dash-vod-single-segment/video-avc-baseline-480.mp4",
|
||||
"subtitle_uri": "https://storage.googleapis.com/exoplayer-test-media-1/ssa/test-subs-position.ass",
|
||||
"subtitle_mime_type": "text/x-ssa",
|
||||
"subtitle_language": "en"
|
||||
},
|
||||
{
|
||||
"name": "SubStation Alpha styling",
|
||||
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/gen-3/screens/dash-vod-single-segment/video-avc-baseline-480.mp4",
|
||||
"subtitle_uri": "https://storage.googleapis.com/exoplayer-test-media-1/ssa/test-subs-styling.ass",
|
||||
"subtitle_mime_type": "text/x-ssa",
|
||||
"subtitle_language": "en"
|
||||
},
|
||||
{
|
||||
"name": "MPEG-4 Timed Text",
|
||||
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/mp4/dizzy-with-tx3g.mp4"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Misc",
|
||||
"samples": [
|
||||
{
|
||||
"name": "Dizzy (MP4)",
|
||||
"uri": "https://html5demos.com/assets/dizzy.mp4"
|
||||
},
|
||||
{
|
||||
"name": "Apple 10s (AAC)",
|
||||
"uri": "https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_4x3/gear0/fileSequence0.aac"
|
||||
},
|
||||
{
|
||||
"name": "Apple 10s (TS)",
|
||||
"uri": "https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_4x3/gear1/fileSequence0.ts"
|
||||
},
|
||||
{
|
||||
"name": "Android screens (MKV)",
|
||||
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv"
|
||||
},
|
||||
{
|
||||
"name": "Screens 360p (WebM, VP9)",
|
||||
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/gen-3/screens/dash-vod-single-segment/video-vp9-360.webm"
|
||||
},
|
||||
{
|
||||
"name": "Screens 480p (FMP4, H264)",
|
||||
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/gen-3/screens/dash-vod-single-segment/video-avc-baseline-480.mp4"
|
||||
},
|
||||
{
|
||||
"name": "Screens 1080p (FMP4, H264)",
|
||||
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/gen-3/screens/dash-vod-single-segment/video-137.mp4"
|
||||
},
|
||||
{
|
||||
"name": "Screens audio (FMP4)",
|
||||
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/gen-3/screens/dash-vod-single-segment/audio-141.mp4"
|
||||
},
|
||||
{
|
||||
"name": "Google Play (MP3)",
|
||||
"uri": "https://storage.googleapis.com/exoplayer-test-media-0/play.mp3"
|
||||
},
|
||||
{
|
||||
"name": "Google Play (Ogg, Vorbis)",
|
||||
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/ogg/play.ogg"
|
||||
},
|
||||
{
|
||||
"name": "Google Play (Flac)",
|
||||
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/flac/play.flac"
|
||||
},
|
||||
{
|
||||
"name": "Big Buck Bunny 480p (MP4, AV1)",
|
||||
"uri": "https://storage.googleapis.com/downloads.webmproject.org/av1/exoplayer/bbb-av1-480p.mp4"
|
||||
},
|
||||
{
|
||||
"name": "One hour frame counter (MP4)",
|
||||
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/mp4/frame-counter-one-hour.mp4"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,173 +0,0 @@
|
|||
/*
|
||||
* Copyright (C) 2016 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.google.android.exoplayer2.demo;
|
||||
|
||||
import android.app.Application;
|
||||
import com.google.android.exoplayer2.DefaultRenderersFactory;
|
||||
import com.google.android.exoplayer2.RenderersFactory;
|
||||
import com.google.android.exoplayer2.database.DatabaseProvider;
|
||||
import com.google.android.exoplayer2.database.ExoDatabaseProvider;
|
||||
import com.google.android.exoplayer2.offline.ActionFileUpgradeUtil;
|
||||
import com.google.android.exoplayer2.offline.DefaultDownloadIndex;
|
||||
import com.google.android.exoplayer2.offline.DefaultDownloaderFactory;
|
||||
import com.google.android.exoplayer2.offline.DownloadManager;
|
||||
import com.google.android.exoplayer2.offline.DownloaderConstructorHelper;
|
||||
import com.google.android.exoplayer2.upstream.DataSource;
|
||||
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
|
||||
import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory;
|
||||
import com.google.android.exoplayer2.upstream.FileDataSourceFactory;
|
||||
import com.google.android.exoplayer2.upstream.HttpDataSource;
|
||||
import com.google.android.exoplayer2.upstream.cache.Cache;
|
||||
import com.google.android.exoplayer2.upstream.cache.CacheDataSource;
|
||||
import com.google.android.exoplayer2.upstream.cache.CacheDataSourceFactory;
|
||||
import com.google.android.exoplayer2.upstream.cache.NoOpCacheEvictor;
|
||||
import com.google.android.exoplayer2.upstream.cache.SimpleCache;
|
||||
import com.google.android.exoplayer2.util.Log;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* Placeholder application to facilitate overriding Application methods for debugging and testing.
|
||||
*/
|
||||
public class DemoApplication extends Application {
|
||||
|
||||
private static final String TAG = "DemoApplication";
|
||||
private static final String DOWNLOAD_ACTION_FILE = "actions";
|
||||
private static final String DOWNLOAD_TRACKER_ACTION_FILE = "tracked_actions";
|
||||
private static final String DOWNLOAD_CONTENT_DIRECTORY = "downloads";
|
||||
|
||||
protected String userAgent;
|
||||
|
||||
private DatabaseProvider databaseProvider;
|
||||
private File downloadDirectory;
|
||||
private Cache downloadCache;
|
||||
private DownloadManager downloadManager;
|
||||
private DownloadTracker downloadTracker;
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
userAgent = Util.getUserAgent(this, "ExoPlayerDemo");
|
||||
}
|
||||
|
||||
/** Returns a {@link DataSource.Factory}. */
|
||||
public DataSource.Factory buildDataSourceFactory() {
|
||||
DefaultDataSourceFactory upstreamFactory =
|
||||
new DefaultDataSourceFactory(this, buildHttpDataSourceFactory());
|
||||
return buildReadOnlyCacheDataSource(upstreamFactory, getDownloadCache());
|
||||
}
|
||||
|
||||
/** Returns a {@link HttpDataSource.Factory}. */
|
||||
public HttpDataSource.Factory buildHttpDataSourceFactory() {
|
||||
return new DefaultHttpDataSourceFactory(userAgent);
|
||||
}
|
||||
|
||||
/** Returns whether extension renderers should be used. */
|
||||
public boolean useExtensionRenderers() {
|
||||
return "withExtensions".equals(BuildConfig.FLAVOR);
|
||||
}
|
||||
|
||||
public RenderersFactory buildRenderersFactory(boolean preferExtensionRenderer) {
|
||||
@DefaultRenderersFactory.ExtensionRendererMode
|
||||
int extensionRendererMode =
|
||||
useExtensionRenderers()
|
||||
? (preferExtensionRenderer
|
||||
? DefaultRenderersFactory.EXTENSION_RENDERER_MODE_PREFER
|
||||
: DefaultRenderersFactory.EXTENSION_RENDERER_MODE_ON)
|
||||
: DefaultRenderersFactory.EXTENSION_RENDERER_MODE_OFF;
|
||||
return new DefaultRenderersFactory(/* context= */ this)
|
||||
.setExtensionRendererMode(extensionRendererMode);
|
||||
}
|
||||
|
||||
public DownloadManager getDownloadManager() {
|
||||
initDownloadManager();
|
||||
return downloadManager;
|
||||
}
|
||||
|
||||
public DownloadTracker getDownloadTracker() {
|
||||
initDownloadManager();
|
||||
return downloadTracker;
|
||||
}
|
||||
|
||||
protected synchronized Cache getDownloadCache() {
|
||||
if (downloadCache == null) {
|
||||
File downloadContentDirectory = new File(getDownloadDirectory(), DOWNLOAD_CONTENT_DIRECTORY);
|
||||
downloadCache =
|
||||
new SimpleCache(downloadContentDirectory, new NoOpCacheEvictor(), getDatabaseProvider());
|
||||
}
|
||||
return downloadCache;
|
||||
}
|
||||
|
||||
private synchronized void initDownloadManager() {
|
||||
if (downloadManager == null) {
|
||||
DefaultDownloadIndex downloadIndex = new DefaultDownloadIndex(getDatabaseProvider());
|
||||
upgradeActionFile(
|
||||
DOWNLOAD_ACTION_FILE, downloadIndex, /* addNewDownloadsAsCompleted= */ false);
|
||||
upgradeActionFile(
|
||||
DOWNLOAD_TRACKER_ACTION_FILE, downloadIndex, /* addNewDownloadsAsCompleted= */ true);
|
||||
DownloaderConstructorHelper downloaderConstructorHelper =
|
||||
new DownloaderConstructorHelper(getDownloadCache(), buildHttpDataSourceFactory());
|
||||
downloadManager =
|
||||
new DownloadManager(
|
||||
this, downloadIndex, new DefaultDownloaderFactory(downloaderConstructorHelper));
|
||||
downloadTracker =
|
||||
new DownloadTracker(/* context= */ this, buildDataSourceFactory(), downloadManager);
|
||||
}
|
||||
}
|
||||
|
||||
private void upgradeActionFile(
|
||||
String fileName, DefaultDownloadIndex downloadIndex, boolean addNewDownloadsAsCompleted) {
|
||||
try {
|
||||
ActionFileUpgradeUtil.upgradeAndDelete(
|
||||
new File(getDownloadDirectory(), fileName),
|
||||
/* downloadIdProvider= */ null,
|
||||
downloadIndex,
|
||||
/* deleteOnFailure= */ true,
|
||||
addNewDownloadsAsCompleted);
|
||||
} catch (IOException e) {
|
||||
Log.e(TAG, "Failed to upgrade action file: " + fileName, e);
|
||||
}
|
||||
}
|
||||
|
||||
private DatabaseProvider getDatabaseProvider() {
|
||||
if (databaseProvider == null) {
|
||||
databaseProvider = new ExoDatabaseProvider(this);
|
||||
}
|
||||
return databaseProvider;
|
||||
}
|
||||
|
||||
private File getDownloadDirectory() {
|
||||
if (downloadDirectory == null) {
|
||||
downloadDirectory = getExternalFilesDir(null);
|
||||
if (downloadDirectory == null) {
|
||||
downloadDirectory = getFilesDir();
|
||||
}
|
||||
}
|
||||
return downloadDirectory;
|
||||
}
|
||||
|
||||
protected static CacheDataSourceFactory buildReadOnlyCacheDataSource(
|
||||
DataSource.Factory upstreamFactory, Cache cache) {
|
||||
return new CacheDataSourceFactory(
|
||||
cache,
|
||||
upstreamFactory,
|
||||
new FileDataSourceFactory(),
|
||||
/* cacheWriteDataSinkFactory= */ null,
|
||||
CacheDataSource.FLAG_IGNORE_CACHE_ON_ERROR,
|
||||
/* eventListener= */ null);
|
||||
}
|
||||
}
|
||||
|
|
@ -15,11 +15,18 @@
|
|||
*/
|
||||
package com.google.android.exoplayer2.demo;
|
||||
|
||||
import static com.google.android.exoplayer2.demo.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;
|
||||
|
|
@ -28,64 +35,93 @@ import java.util.List;
|
|||
/** A service for downloading media. */
|
||||
public class DemoDownloadService extends DownloadService {
|
||||
|
||||
private static final String CHANNEL_ID = "download_channel";
|
||||
private static final int JOB_ID = 1;
|
||||
private static final int FOREGROUND_NOTIFICATION_ID = 1;
|
||||
|
||||
private static int nextNotificationId = FOREGROUND_NOTIFICATION_ID + 1;
|
||||
|
||||
private DownloadNotificationHelper notificationHelper;
|
||||
|
||||
public DemoDownloadService() {
|
||||
super(
|
||||
FOREGROUND_NOTIFICATION_ID,
|
||||
DEFAULT_FOREGROUND_NOTIFICATION_UPDATE_INTERVAL,
|
||||
CHANNEL_ID,
|
||||
DOWNLOAD_NOTIFICATION_CHANNEL_ID,
|
||||
R.string.exo_download_notification_channel_name,
|
||||
/* channelDescriptionResourceId= */ 0);
|
||||
nextNotificationId = FOREGROUND_NOTIFICATION_ID + 1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
notificationHelper = new DownloadNotificationHelper(this, CHANNEL_ID);
|
||||
}
|
||||
|
||||
@Override
|
||||
@NonNull
|
||||
protected DownloadManager getDownloadManager() {
|
||||
return ((DemoApplication) getApplication()).getDownloadManager();
|
||||
// This will only happen once, because getDownloadManager is guaranteed to be called only once
|
||||
// in the life cycle of the process.
|
||||
DownloadManager downloadManager = DemoUtil.getDownloadManager(/* context= */ this);
|
||||
DownloadNotificationHelper downloadNotificationHelper =
|
||||
DemoUtil.getDownloadNotificationHelper(/* context= */ this);
|
||||
downloadManager.addListener(
|
||||
new TerminalStateNotificationHelper(
|
||||
this, downloadNotificationHelper, FOREGROUND_NOTIFICATION_ID + 1));
|
||||
return downloadManager;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected PlatformScheduler getScheduler() {
|
||||
protected Scheduler getScheduler() {
|
||||
return Util.SDK_INT >= 21 ? new PlatformScheduler(this, JOB_ID) : null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Notification getForegroundNotification(List<Download> downloads) {
|
||||
return notificationHelper.buildProgressNotification(
|
||||
R.drawable.ic_download, /* contentIntent= */ null, /* message= */ null, downloads);
|
||||
@NonNull
|
||||
protected Notification getForegroundNotification(
|
||||
@NonNull List<Download> downloads, @Requirements.RequirementFlags int notMetRequirements) {
|
||||
return DemoUtil.getDownloadNotificationHelper(/* context= */ this)
|
||||
.buildProgressNotification(
|
||||
/* context= */ this,
|
||||
R.drawable.ic_download,
|
||||
/* contentIntent= */ null,
|
||||
/* message= */ null,
|
||||
downloads,
|
||||
notMetRequirements);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDownloadChanged(Download download) {
|
||||
Notification notification;
|
||||
if (download.state == Download.STATE_COMPLETED) {
|
||||
notification =
|
||||
notificationHelper.buildDownloadCompletedNotification(
|
||||
R.drawable.ic_download_done,
|
||||
/* contentIntent= */ null,
|
||||
Util.fromUtf8Bytes(download.request.data));
|
||||
} else if (download.state == Download.STATE_FAILED) {
|
||||
notification =
|
||||
notificationHelper.buildDownloadFailedNotification(
|
||||
R.drawable.ic_download_done,
|
||||
/* contentIntent= */ null,
|
||||
Util.fromUtf8Bytes(download.request.data));
|
||||
} else {
|
||||
return;
|
||||
/**
|
||||
* Creates and displays notifications for downloads when they complete or fail.
|
||||
*
|
||||
* <p>This helper will outlive the lifespan of a single instance of {@link DemoDownloadService}.
|
||||
* It is static to avoid leaking the first {@link DemoDownloadService} instance.
|
||||
*/
|
||||
private static final class TerminalStateNotificationHelper implements DownloadManager.Listener {
|
||||
|
||||
private final Context context;
|
||||
private final DownloadNotificationHelper notificationHelper;
|
||||
|
||||
private int nextNotificationId;
|
||||
|
||||
public TerminalStateNotificationHelper(
|
||||
Context context, DownloadNotificationHelper notificationHelper, int firstNotificationId) {
|
||||
this.context = context.getApplicationContext();
|
||||
this.notificationHelper = notificationHelper;
|
||||
nextNotificationId = firstNotificationId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDownloadChanged(
|
||||
DownloadManager downloadManager, Download download, @Nullable Exception finalException) {
|
||||
Notification notification;
|
||||
if (download.state == Download.STATE_COMPLETED) {
|
||||
notification =
|
||||
notificationHelper.buildDownloadCompletedNotification(
|
||||
context,
|
||||
R.drawable.ic_download_done,
|
||||
/* contentIntent= */ null,
|
||||
Util.fromUtf8Bytes(download.request.data));
|
||||
} else if (download.state == Download.STATE_FAILED) {
|
||||
notification =
|
||||
notificationHelper.buildDownloadFailedNotification(
|
||||
context,
|
||||
R.drawable.ic_download_done,
|
||||
/* contentIntent= */ null,
|
||||
Util.fromUtf8Bytes(download.request.data));
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
NotificationUtil.setNotification(context, nextNotificationId++, notification);
|
||||
}
|
||||
NotificationUtil.setNotification(this, nextNotificationId++, notification);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 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.ExoDatabaseProvider;
|
||||
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 ExoDatabaseProvider(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() {}
|
||||
}
|
||||
|
|
@ -15,24 +15,38 @@
|
|||
*/
|
||||
package com.google.android.exoplayer2.demo;
|
||||
|
||||
import static com.google.android.exoplayer2.util.Assertions.checkNotNull;
|
||||
import static com.google.android.exoplayer2.util.Assertions.checkStateNotNull;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.net.Uri;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
import android.os.AsyncTask;
|
||||
import android.widget.Toast;
|
||||
import com.google.android.exoplayer2.C;
|
||||
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.DataSource;
|
||||
import com.google.android.exoplayer2.upstream.HttpDataSource;
|
||||
import com.google.android.exoplayer2.util.Log;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
import java.io.IOException;
|
||||
|
|
@ -52,7 +66,7 @@ public class DownloadTracker {
|
|||
private static final String TAG = "DownloadTracker";
|
||||
|
||||
private final Context context;
|
||||
private final DataSource.Factory dataSourceFactory;
|
||||
private final HttpDataSource.Factory httpDataSourceFactory;
|
||||
private final CopyOnWriteArraySet<Listener> listeners;
|
||||
private final HashMap<Uri, Download> downloads;
|
||||
private final DownloadIndex downloadIndex;
|
||||
|
|
@ -61,9 +75,11 @@ public class DownloadTracker {
|
|||
@Nullable private StartDownloadDialogHelper startDownloadDialogHelper;
|
||||
|
||||
public DownloadTracker(
|
||||
Context context, DataSource.Factory dataSourceFactory, DownloadManager downloadManager) {
|
||||
Context context,
|
||||
HttpDataSource.Factory httpDataSourceFactory,
|
||||
DownloadManager downloadManager) {
|
||||
this.context = context.getApplicationContext();
|
||||
this.dataSourceFactory = dataSourceFactory;
|
||||
this.httpDataSourceFactory = httpDataSourceFactory;
|
||||
listeners = new CopyOnWriteArraySet<>();
|
||||
downloads = new HashMap<>();
|
||||
downloadIndex = downloadManager.getDownloadIndex();
|
||||
|
|
@ -73,6 +89,7 @@ public class DownloadTracker {
|
|||
}
|
||||
|
||||
public void addListener(Listener listener) {
|
||||
checkNotNull(listener);
|
||||
listeners.add(listener);
|
||||
}
|
||||
|
||||
|
|
@ -80,24 +97,21 @@ public class DownloadTracker {
|
|||
listeners.remove(listener);
|
||||
}
|
||||
|
||||
public boolean isDownloaded(Uri uri) {
|
||||
Download download = downloads.get(uri);
|
||||
public boolean isDownloaded(MediaItem mediaItem) {
|
||||
@Nullable Download download = downloads.get(checkNotNull(mediaItem.localConfiguration).uri);
|
||||
return download != null && download.state != Download.STATE_FAILED;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public DownloadRequest getDownloadRequest(Uri uri) {
|
||||
Download download = downloads.get(uri);
|
||||
@Nullable Download download = downloads.get(uri);
|
||||
return download != null && download.state != Download.STATE_FAILED ? download.request : null;
|
||||
}
|
||||
|
||||
public void toggleDownload(
|
||||
FragmentManager fragmentManager,
|
||||
String name,
|
||||
Uri uri,
|
||||
String extension,
|
||||
RenderersFactory renderersFactory) {
|
||||
Download download = downloads.get(uri);
|
||||
if (download != null) {
|
||||
FragmentManager fragmentManager, MediaItem mediaItem, RenderersFactory renderersFactory) {
|
||||
@Nullable Download download = downloads.get(checkNotNull(mediaItem.localConfiguration).uri);
|
||||
if (download != null && download.state != Download.STATE_FAILED) {
|
||||
DownloadService.sendRemoveDownload(
|
||||
context, DemoDownloadService.class, download.request.id, /* foreground= */ false);
|
||||
} else {
|
||||
|
|
@ -106,7 +120,10 @@ public class DownloadTracker {
|
|||
}
|
||||
startDownloadDialogHelper =
|
||||
new StartDownloadDialogHelper(
|
||||
fragmentManager, getDownloadHelper(uri, extension, renderersFactory), name);
|
||||
fragmentManager,
|
||||
DownloadHelper.forMediaItem(
|
||||
context, mediaItem, renderersFactory, httpDataSourceFactory),
|
||||
mediaItem);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -121,27 +138,13 @@ public class DownloadTracker {
|
|||
}
|
||||
}
|
||||
|
||||
private DownloadHelper getDownloadHelper(
|
||||
Uri uri, String extension, RenderersFactory renderersFactory) {
|
||||
int type = Util.inferContentType(uri, extension);
|
||||
switch (type) {
|
||||
case C.TYPE_DASH:
|
||||
return DownloadHelper.forDash(context, uri, dataSourceFactory, renderersFactory);
|
||||
case C.TYPE_SS:
|
||||
return DownloadHelper.forSmoothStreaming(context, uri, dataSourceFactory, renderersFactory);
|
||||
case C.TYPE_HLS:
|
||||
return DownloadHelper.forHls(context, uri, dataSourceFactory, renderersFactory);
|
||||
case C.TYPE_OTHER:
|
||||
return DownloadHelper.forProgressive(context, uri);
|
||||
default:
|
||||
throw new IllegalStateException("Unsupported type: " + type);
|
||||
}
|
||||
}
|
||||
|
||||
private class DownloadManagerListener implements DownloadManager.Listener {
|
||||
|
||||
@Override
|
||||
public void onDownloadChanged(DownloadManager downloadManager, Download download) {
|
||||
public void onDownloadChanged(
|
||||
@NonNull DownloadManager downloadManager,
|
||||
@NonNull Download download,
|
||||
@Nullable Exception finalException) {
|
||||
downloads.put(download.request.uri, download);
|
||||
for (Listener listener : listeners) {
|
||||
listener.onDownloadsChanged();
|
||||
|
|
@ -149,7 +152,8 @@ public class DownloadTracker {
|
|||
}
|
||||
|
||||
@Override
|
||||
public void onDownloadRemoved(DownloadManager downloadManager, Download download) {
|
||||
public void onDownloadRemoved(
|
||||
@NonNull DownloadManager downloadManager, @NonNull Download download) {
|
||||
downloads.remove(download.request.uri);
|
||||
for (Listener listener : listeners) {
|
||||
listener.onDownloadsChanged();
|
||||
|
|
@ -164,16 +168,18 @@ public class DownloadTracker {
|
|||
|
||||
private final FragmentManager fragmentManager;
|
||||
private final DownloadHelper downloadHelper;
|
||||
private final String name;
|
||||
private final MediaItem mediaItem;
|
||||
|
||||
private TrackSelectionDialog trackSelectionDialog;
|
||||
private MappedTrackInfo mappedTrackInfo;
|
||||
private WidevineOfflineLicenseFetchTask widevineOfflineLicenseFetchTask;
|
||||
@Nullable private byte[] keySetId;
|
||||
|
||||
public StartDownloadDialogHelper(
|
||||
FragmentManager fragmentManager, DownloadHelper downloadHelper, String name) {
|
||||
FragmentManager fragmentManager, DownloadHelper downloadHelper, MediaItem mediaItem) {
|
||||
this.fragmentManager = fragmentManager;
|
||||
this.downloadHelper = downloadHelper;
|
||||
this.name = name;
|
||||
this.mediaItem = mediaItem;
|
||||
downloadHelper.prepare(this);
|
||||
}
|
||||
|
||||
|
|
@ -182,41 +188,57 @@ public class DownloadTracker {
|
|||
if (trackSelectionDialog != null) {
|
||||
trackSelectionDialog.dismiss();
|
||||
}
|
||||
if (widevineOfflineLicenseFetchTask != null) {
|
||||
widevineOfflineLicenseFetchTask.cancel(false);
|
||||
}
|
||||
}
|
||||
|
||||
// DownloadHelper.Callback implementation.
|
||||
|
||||
@Override
|
||||
public void onPrepared(DownloadHelper helper) {
|
||||
if (helper.getPeriodCount() == 0) {
|
||||
Log.d(TAG, "No periods found. Downloading entire stream.");
|
||||
startDownload();
|
||||
downloadHelper.release();
|
||||
public void onPrepared(@NonNull DownloadHelper helper) {
|
||||
@Nullable Format format = getFirstFormatWithDrmInitData(helper);
|
||||
if (format == null) {
|
||||
onDownloadPrepared(helper);
|
||||
return;
|
||||
}
|
||||
mappedTrackInfo = downloadHelper.getMappedTrackInfo(/* periodIndex= */ 0);
|
||||
if (!TrackSelectionDialog.willHaveContent(mappedTrackInfo)) {
|
||||
Log.d(TAG, "No dialog content. Downloading entire stream.");
|
||||
startDownload();
|
||||
downloadHelper.release();
|
||||
|
||||
// The content is DRM protected. We need to acquire an offline license.
|
||||
if (Util.SDK_INT < 18) {
|
||||
Toast.makeText(context, R.string.error_drm_unsupported_before_api_18, Toast.LENGTH_LONG)
|
||||
.show();
|
||||
Log.e(TAG, "Downloading DRM protected content is not supported on API versions below 18");
|
||||
return;
|
||||
}
|
||||
trackSelectionDialog =
|
||||
TrackSelectionDialog.createForMappedTrackInfoAndParameters(
|
||||
/* titleId= */ R.string.exo_download_description,
|
||||
mappedTrackInfo,
|
||||
trackSelectorParameters,
|
||||
/* allowAdaptiveSelections =*/ false,
|
||||
/* allowMultipleOverrides= */ true,
|
||||
/* onClickListener= */ this,
|
||||
/* onDismissListener= */ this);
|
||||
trackSelectionDialog.show(fragmentManager, /* tag= */ null);
|
||||
// TODO(internal b/163107948): Support cases where DrmInitData are not in the manifest.
|
||||
if (!hasSchemaData(format.drmInitData)) {
|
||||
Toast.makeText(context, R.string.download_start_error_offline_license, Toast.LENGTH_LONG)
|
||||
.show();
|
||||
Log.e(
|
||||
TAG,
|
||||
"Downloading content where DRM scheme data is not located in the manifest is not"
|
||||
+ " supported");
|
||||
return;
|
||||
}
|
||||
widevineOfflineLicenseFetchTask =
|
||||
new WidevineOfflineLicenseFetchTask(
|
||||
format,
|
||||
mediaItem.localConfiguration.drmConfiguration,
|
||||
httpDataSourceFactory,
|
||||
/* dialogHelper= */ this,
|
||||
helper);
|
||||
widevineOfflineLicenseFetchTask.execute();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPrepareError(DownloadHelper helper, IOException e) {
|
||||
Toast.makeText(context, R.string.download_start_error, Toast.LENGTH_LONG).show();
|
||||
Log.e(TAG, "Failed to start download", e);
|
||||
public void onPrepareError(@NonNull DownloadHelper helper, @NonNull IOException e) {
|
||||
boolean isLiveContent = e instanceof LiveContentUnsupportedException;
|
||||
int toastStringId =
|
||||
isLiveContent ? R.string.download_live_unsupported : R.string.download_start_error;
|
||||
String logMessage =
|
||||
isLiveContent ? "Downloading live content unsupported" : "Failed to start download";
|
||||
Toast.makeText(context, toastStringId, Toast.LENGTH_LONG).show();
|
||||
Log.e(TAG, logMessage, e);
|
||||
}
|
||||
|
||||
// DialogInterface.OnClickListener implementation.
|
||||
|
|
@ -253,6 +275,83 @@ public class DownloadTracker {
|
|||
|
||||
// Internal methods.
|
||||
|
||||
/**
|
||||
* Returns the first {@link Format} with a non-null {@link Format#drmInitData} found in the
|
||||
* content's tracks, or null if none is found.
|
||||
*/
|
||||
@Nullable
|
||||
private Format getFirstFormatWithDrmInitData(DownloadHelper helper) {
|
||||
for (int periodIndex = 0; periodIndex < helper.getPeriodCount(); periodIndex++) {
|
||||
MappedTrackInfo mappedTrackInfo = helper.getMappedTrackInfo(periodIndex);
|
||||
for (int rendererIndex = 0;
|
||||
rendererIndex < mappedTrackInfo.getRendererCount();
|
||||
rendererIndex++) {
|
||||
TrackGroupArray trackGroups = mappedTrackInfo.getTrackGroups(rendererIndex);
|
||||
for (int trackGroupIndex = 0; trackGroupIndex < trackGroups.length; trackGroupIndex++) {
|
||||
TrackGroup trackGroup = trackGroups.get(trackGroupIndex);
|
||||
for (int formatIndex = 0; formatIndex < trackGroup.length; formatIndex++) {
|
||||
Format format = trackGroup.getFormat(formatIndex);
|
||||
if (format.drmInitData != null) {
|
||||
return format;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private void onOfflineLicenseFetched(DownloadHelper helper, byte[] keySetId) {
|
||||
this.keySetId = keySetId;
|
||||
onDownloadPrepared(helper);
|
||||
}
|
||||
|
||||
private void onOfflineLicenseFetchedError(DrmSession.DrmSessionException e) {
|
||||
Toast.makeText(context, R.string.download_start_error_offline_license, Toast.LENGTH_LONG)
|
||||
.show();
|
||||
Log.e(TAG, "Failed to fetch offline DRM license", e);
|
||||
}
|
||||
|
||||
private void onDownloadPrepared(DownloadHelper helper) {
|
||||
if (helper.getPeriodCount() == 0) {
|
||||
Log.d(TAG, "No periods found. Downloading entire stream.");
|
||||
startDownload();
|
||||
downloadHelper.release();
|
||||
return;
|
||||
}
|
||||
|
||||
mappedTrackInfo = downloadHelper.getMappedTrackInfo(/* periodIndex= */ 0);
|
||||
if (!TrackSelectionDialog.willHaveContent(mappedTrackInfo)) {
|
||||
Log.d(TAG, "No dialog content. Downloading entire stream.");
|
||||
startDownload();
|
||||
downloadHelper.release();
|
||||
return;
|
||||
}
|
||||
trackSelectionDialog =
|
||||
TrackSelectionDialog.createForMappedTrackInfoAndParameters(
|
||||
/* titleId= */ R.string.exo_download_description,
|
||||
mappedTrackInfo,
|
||||
trackSelectorParameters,
|
||||
/* allowAdaptiveSelections= */ false,
|
||||
/* allowMultipleOverrides= */ true,
|
||||
/* onClickListener= */ this,
|
||||
/* onDismissListener= */ this);
|
||||
trackSelectionDialog.show(fragmentManager, /* tag= */ null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether any the {@link DrmInitData.SchemeData} contained in {@code drmInitData} has
|
||||
* non-null {@link DrmInitData.SchemeData#data}.
|
||||
*/
|
||||
private boolean hasSchemaData(DrmInitData drmInitData) {
|
||||
for (int i = 0; i < drmInitData.schemeDataCount; i++) {
|
||||
if (drmInitData.get(i).hasData()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private void startDownload() {
|
||||
startDownload(buildDownloadRequest());
|
||||
}
|
||||
|
|
@ -263,7 +362,65 @@ public class DownloadTracker {
|
|||
}
|
||||
|
||||
private DownloadRequest buildDownloadRequest() {
|
||||
return downloadHelper.getDownloadRequest(Util.getUtf8Bytes(name));
|
||||
return downloadHelper
|
||||
.getDownloadRequest(
|
||||
Util.getUtf8Bytes(checkNotNull(mediaItem.mediaMetadata.title.toString())))
|
||||
.copyWithKeySetId(keySetId);
|
||||
}
|
||||
}
|
||||
|
||||
/** Downloads a Widevine offline license in a background thread. */
|
||||
@RequiresApi(18)
|
||||
private static final class WidevineOfflineLicenseFetchTask extends AsyncTask<Void, Void, Void> {
|
||||
|
||||
private final Format format;
|
||||
private final MediaItem.DrmConfiguration drmConfiguration;
|
||||
private final HttpDataSource.Factory httpDataSourceFactory;
|
||||
private final StartDownloadDialogHelper dialogHelper;
|
||||
private final DownloadHelper downloadHelper;
|
||||
|
||||
@Nullable private byte[] keySetId;
|
||||
@Nullable private DrmSession.DrmSessionException drmSessionException;
|
||||
|
||||
public WidevineOfflineLicenseFetchTask(
|
||||
Format format,
|
||||
MediaItem.DrmConfiguration drmConfiguration,
|
||||
HttpDataSource.Factory httpDataSourceFactory,
|
||||
StartDownloadDialogHelper dialogHelper,
|
||||
DownloadHelper downloadHelper) {
|
||||
this.format = format;
|
||||
this.drmConfiguration = drmConfiguration;
|
||||
this.httpDataSourceFactory = httpDataSourceFactory;
|
||||
this.dialogHelper = dialogHelper;
|
||||
this.downloadHelper = downloadHelper;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Void doInBackground(Void... voids) {
|
||||
OfflineLicenseHelper offlineLicenseHelper =
|
||||
OfflineLicenseHelper.newWidevineInstance(
|
||||
drmConfiguration.licenseUri.toString(),
|
||||
drmConfiguration.forceDefaultLicenseUri,
|
||||
httpDataSourceFactory,
|
||||
drmConfiguration.licenseRequestHeaders,
|
||||
new DrmSessionEventListener.EventDispatcher());
|
||||
try {
|
||||
keySetId = offlineLicenseHelper.downloadLicense(format);
|
||||
} catch (DrmSession.DrmSessionException e) {
|
||||
drmSessionException = e;
|
||||
} finally {
|
||||
offlineLicenseHelper.release();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(Void aVoid) {
|
||||
if (drmSessionException != null) {
|
||||
dialogHelper.onOfflineLicenseFetchedError(drmSessionException);
|
||||
} else {
|
||||
dialogHelper.onOfflineLicenseFetched(downloadHelper, checkStateNotNull(keySetId));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,254 @@
|
|||
/*
|
||||
* Copyright 2020 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.google.android.exoplayer2.demo;
|
||||
|
||||
import static com.google.android.exoplayer2.util.Assertions.checkNotNull;
|
||||
import static com.google.android.exoplayer2.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 java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
/** Util to read from and populate an intent. */
|
||||
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";
|
||||
|
||||
// Activity extras.
|
||||
public static final String PREFER_EXTENSION_DECODERS_EXTRA = "prefer_extension_decoders";
|
||||
|
||||
// Media item configuration extras.
|
||||
|
||||
public static final String URI_EXTRA = "uri";
|
||||
public static final String TITLE_EXTRA = "title";
|
||||
public static final String MIME_TYPE_EXTRA = "mime_type";
|
||||
public static final String CLIP_START_POSITION_MS_EXTRA = "clip_start_position_ms";
|
||||
public static final String CLIP_END_POSITION_MS_EXTRA = "clip_end_position_ms";
|
||||
|
||||
public static final String AD_TAG_URI_EXTRA = "ad_tag_uri";
|
||||
|
||||
public static final String DRM_SCHEME_EXTRA = "drm_scheme";
|
||||
public static final String DRM_LICENSE_URI_EXTRA = "drm_license_uri";
|
||||
public static final String DRM_KEY_REQUEST_PROPERTIES_EXTRA = "drm_key_request_properties";
|
||||
public static final String DRM_SESSION_FOR_CLEAR_CONTENT = "drm_session_for_clear_content";
|
||||
public static final String DRM_MULTI_SESSION_EXTRA = "drm_multi_session";
|
||||
public static final String DRM_FORCE_DEFAULT_LICENSE_URI_EXTRA = "drm_force_default_license_uri";
|
||||
|
||||
public static final String SUBTITLE_URI_EXTRA = "subtitle_uri";
|
||||
public static final String SUBTITLE_MIME_TYPE_EXTRA = "subtitle_mime_type";
|
||||
public static final String SUBTITLE_LANGUAGE_EXTRA = "subtitle_language";
|
||||
|
||||
/** Creates a list of {@link MediaItem media items} from an {@link Intent}. */
|
||||
public static List<MediaItem> createMediaItemsFromIntent(Intent intent) {
|
||||
List<MediaItem> mediaItems = new ArrayList<>();
|
||||
if (ACTION_VIEW_LIST.equals(intent.getAction())) {
|
||||
int index = 0;
|
||||
while (intent.hasExtra(URI_EXTRA + "_" + index)) {
|
||||
Uri uri = Uri.parse(intent.getStringExtra(URI_EXTRA + "_" + index));
|
||||
mediaItems.add(createMediaItemFromIntent(uri, intent, /* extrasKeySuffix= */ "_" + index));
|
||||
index++;
|
||||
}
|
||||
} else {
|
||||
Uri uri = intent.getData();
|
||||
mediaItems.add(createMediaItemFromIntent(uri, intent, /* extrasKeySuffix= */ ""));
|
||||
}
|
||||
return mediaItems;
|
||||
}
|
||||
|
||||
/** Populates the intent with the given list of {@link MediaItem media items}. */
|
||||
public static void addToIntent(List<MediaItem> mediaItems, Intent intent) {
|
||||
Assertions.checkArgument(!mediaItems.isEmpty());
|
||||
if (mediaItems.size() == 1) {
|
||||
MediaItem mediaItem = mediaItems.get(0);
|
||||
MediaItem.LocalConfiguration localConfiguration = checkNotNull(mediaItem.localConfiguration);
|
||||
intent.setAction(ACTION_VIEW).setData(mediaItem.localConfiguration.uri);
|
||||
if (mediaItem.mediaMetadata.title != null) {
|
||||
intent.putExtra(TITLE_EXTRA, mediaItem.mediaMetadata.title);
|
||||
}
|
||||
addPlaybackPropertiesToIntent(localConfiguration, intent, /* extrasKeySuffix= */ "");
|
||||
addClippingConfigurationToIntent(
|
||||
mediaItem.clippingConfiguration, intent, /* extrasKeySuffix= */ "");
|
||||
} else {
|
||||
intent.setAction(ACTION_VIEW_LIST);
|
||||
for (int i = 0; i < mediaItems.size(); i++) {
|
||||
MediaItem mediaItem = mediaItems.get(i);
|
||||
MediaItem.LocalConfiguration localConfiguration =
|
||||
checkNotNull(mediaItem.localConfiguration);
|
||||
intent.putExtra(URI_EXTRA + ("_" + i), localConfiguration.uri.toString());
|
||||
addPlaybackPropertiesToIntent(localConfiguration, intent, /* extrasKeySuffix= */ "_" + i);
|
||||
addClippingConfigurationToIntent(
|
||||
mediaItem.clippingConfiguration, intent, /* extrasKeySuffix= */ "_" + i);
|
||||
if (mediaItem.mediaMetadata.title != null) {
|
||||
intent.putExtra(TITLE_EXTRA + ("_" + i), mediaItem.mediaMetadata.title);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static MediaItem createMediaItemFromIntent(
|
||||
Uri uri, Intent intent, String extrasKeySuffix) {
|
||||
@Nullable String mimeType = intent.getStringExtra(MIME_TYPE_EXTRA + extrasKeySuffix);
|
||||
@Nullable String title = intent.getStringExtra(TITLE_EXTRA + extrasKeySuffix);
|
||||
@Nullable String adTagUri = intent.getStringExtra(AD_TAG_URI_EXTRA + extrasKeySuffix);
|
||||
MediaItem.Builder builder =
|
||||
new MediaItem.Builder()
|
||||
.setUri(uri)
|
||||
.setMimeType(mimeType)
|
||||
.setMediaMetadata(new MediaMetadata.Builder().setTitle(title).build())
|
||||
.setSubtitles(createSubtitlesFromIntent(intent, extrasKeySuffix))
|
||||
.setClipStartPositionMs(
|
||||
intent.getLongExtra(CLIP_START_POSITION_MS_EXTRA + extrasKeySuffix, 0))
|
||||
.setClipEndPositionMs(
|
||||
intent.getLongExtra(
|
||||
CLIP_END_POSITION_MS_EXTRA + extrasKeySuffix, C.TIME_END_OF_SOURCE));
|
||||
if (adTagUri != null) {
|
||||
builder.setAdsConfiguration(
|
||||
new MediaItem.AdsConfiguration.Builder(Uri.parse(adTagUri)).build());
|
||||
}
|
||||
|
||||
return populateDrmPropertiesFromIntent(builder, intent, extrasKeySuffix).build();
|
||||
}
|
||||
|
||||
private static List<MediaItem.Subtitle> createSubtitlesFromIntent(
|
||||
Intent intent, String extrasKeySuffix) {
|
||||
if (!intent.hasExtra(SUBTITLE_URI_EXTRA + extrasKeySuffix)) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
return Collections.singletonList(
|
||||
new MediaItem.Subtitle(
|
||||
Uri.parse(intent.getStringExtra(SUBTITLE_URI_EXTRA + extrasKeySuffix)),
|
||||
checkNotNull(intent.getStringExtra(SUBTITLE_MIME_TYPE_EXTRA + extrasKeySuffix)),
|
||||
intent.getStringExtra(SUBTITLE_LANGUAGE_EXTRA + extrasKeySuffix),
|
||||
C.SELECTION_FLAG_DEFAULT));
|
||||
}
|
||||
|
||||
private static MediaItem.Builder populateDrmPropertiesFromIntent(
|
||||
MediaItem.Builder builder, Intent intent, String extrasKeySuffix) {
|
||||
String schemeKey = DRM_SCHEME_EXTRA + extrasKeySuffix;
|
||||
@Nullable String drmSchemeExtra = intent.getStringExtra(schemeKey);
|
||||
if (drmSchemeExtra == null) {
|
||||
return builder;
|
||||
}
|
||||
Map<String, String> headers = new HashMap<>();
|
||||
@Nullable
|
||||
String[] keyRequestPropertiesArray =
|
||||
intent.getStringArrayExtra(DRM_KEY_REQUEST_PROPERTIES_EXTRA + extrasKeySuffix);
|
||||
if (keyRequestPropertiesArray != null) {
|
||||
for (int i = 0; i < keyRequestPropertiesArray.length; i += 2) {
|
||||
headers.put(keyRequestPropertiesArray[i], keyRequestPropertiesArray[i + 1]);
|
||||
}
|
||||
}
|
||||
@Nullable UUID drmUuid = Util.getDrmUuid(Util.castNonNull(drmSchemeExtra));
|
||||
if (drmUuid != null) {
|
||||
builder.setDrmConfiguration(
|
||||
new MediaItem.DrmConfiguration.Builder(drmUuid)
|
||||
.setLicenseUri(intent.getStringExtra(DRM_LICENSE_URI_EXTRA + extrasKeySuffix))
|
||||
.setMultiSession(
|
||||
intent.getBooleanExtra(DRM_MULTI_SESSION_EXTRA + extrasKeySuffix, false))
|
||||
.setForceDefaultLicenseUri(
|
||||
intent.getBooleanExtra(
|
||||
DRM_FORCE_DEFAULT_LICENSE_URI_EXTRA + extrasKeySuffix, false))
|
||||
.setLicenseRequestHeaders(headers)
|
||||
.forceSessionsForAudioAndVideoTracks(
|
||||
intent.getBooleanExtra(DRM_SESSION_FOR_CLEAR_CONTENT + extrasKeySuffix, false))
|
||||
.build());
|
||||
}
|
||||
return builder;
|
||||
}
|
||||
|
||||
private static void addPlaybackPropertiesToIntent(
|
||||
MediaItem.LocalConfiguration localConfiguration, Intent intent, String extrasKeySuffix) {
|
||||
intent
|
||||
.putExtra(MIME_TYPE_EXTRA + extrasKeySuffix, localConfiguration.mimeType)
|
||||
.putExtra(
|
||||
AD_TAG_URI_EXTRA + extrasKeySuffix,
|
||||
localConfiguration.adsConfiguration != null
|
||||
? localConfiguration.adsConfiguration.adTagUri.toString()
|
||||
: null);
|
||||
if (localConfiguration.drmConfiguration != null) {
|
||||
addDrmConfigurationToIntent(localConfiguration.drmConfiguration, intent, extrasKeySuffix);
|
||||
}
|
||||
if (!localConfiguration.subtitleConfigurations.isEmpty()) {
|
||||
checkState(localConfiguration.subtitleConfigurations.size() == 1);
|
||||
MediaItem.SubtitleConfiguration subtitleConfiguration =
|
||||
localConfiguration.subtitleConfigurations.get(0);
|
||||
intent.putExtra(SUBTITLE_URI_EXTRA + extrasKeySuffix, subtitleConfiguration.uri.toString());
|
||||
intent.putExtra(SUBTITLE_MIME_TYPE_EXTRA + extrasKeySuffix, subtitleConfiguration.mimeType);
|
||||
intent.putExtra(SUBTITLE_LANGUAGE_EXTRA + extrasKeySuffix, subtitleConfiguration.language);
|
||||
}
|
||||
}
|
||||
|
||||
private static void addDrmConfigurationToIntent(
|
||||
MediaItem.DrmConfiguration drmConfiguration, Intent intent, String extrasKeySuffix) {
|
||||
intent.putExtra(DRM_SCHEME_EXTRA + extrasKeySuffix, drmConfiguration.scheme.toString());
|
||||
intent.putExtra(
|
||||
DRM_LICENSE_URI_EXTRA + extrasKeySuffix,
|
||||
drmConfiguration.licenseUri != null ? drmConfiguration.licenseUri.toString() : null);
|
||||
intent.putExtra(DRM_MULTI_SESSION_EXTRA + extrasKeySuffix, drmConfiguration.multiSession);
|
||||
intent.putExtra(
|
||||
DRM_FORCE_DEFAULT_LICENSE_URI_EXTRA + extrasKeySuffix,
|
||||
drmConfiguration.forceDefaultLicenseUri);
|
||||
|
||||
String[] drmKeyRequestProperties =
|
||||
new String[drmConfiguration.licenseRequestHeaders.size() * 2];
|
||||
int index = 0;
|
||||
for (Map.Entry<String, String> entry : drmConfiguration.licenseRequestHeaders.entrySet()) {
|
||||
drmKeyRequestProperties[index++] = entry.getKey();
|
||||
drmKeyRequestProperties[index++] = entry.getValue();
|
||||
}
|
||||
intent.putExtra(DRM_KEY_REQUEST_PROPERTIES_EXTRA + extrasKeySuffix, drmKeyRequestProperties);
|
||||
|
||||
List<@C.TrackType Integer> forcedDrmSessionTrackTypes =
|
||||
drmConfiguration.forcedSessionTrackTypes;
|
||||
if (!forcedDrmSessionTrackTypes.isEmpty()) {
|
||||
// Only video and audio together are supported.
|
||||
Assertions.checkState(
|
||||
forcedDrmSessionTrackTypes.size() == 2
|
||||
&& forcedDrmSessionTrackTypes.contains(C.TRACK_TYPE_VIDEO)
|
||||
&& forcedDrmSessionTrackTypes.contains(C.TRACK_TYPE_AUDIO));
|
||||
intent.putExtra(DRM_SESSION_FOR_CLEAR_CONTENT + extrasKeySuffix, true);
|
||||
}
|
||||
}
|
||||
|
||||
private static void addClippingConfigurationToIntent(
|
||||
MediaItem.ClippingConfiguration clippingConfiguration,
|
||||
Intent intent,
|
||||
String extrasKeySuffix) {
|
||||
if (clippingConfiguration.startPositionMs != 0) {
|
||||
intent.putExtra(
|
||||
CLIP_START_POSITION_MS_EXTRA + extrasKeySuffix, clippingConfiguration.startPositionMs);
|
||||
}
|
||||
if (clippingConfiguration.endPositionMs != C.TIME_END_OF_SOURCE) {
|
||||
intent.putExtra(
|
||||
CLIP_END_POSITION_MS_EXTRA + extrasKeySuffix, clippingConfiguration.endPositionMs);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -15,13 +15,11 @@
|
|||
*/
|
||||
package com.google.android.exoplayer2.demo;
|
||||
|
||||
import static com.google.android.exoplayer2.util.Assertions.checkNotNull;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import android.util.Pair;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.View;
|
||||
|
|
@ -30,153 +28,77 @@ import android.widget.Button;
|
|||
import android.widget.LinearLayout;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.C.ContentType;
|
||||
import com.google.android.exoplayer2.ExoPlaybackException;
|
||||
import com.google.android.exoplayer2.ExoPlayerFactory;
|
||||
import com.google.android.exoplayer2.PlaybackPreparer;
|
||||
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.SimpleExoPlayer;
|
||||
import com.google.android.exoplayer2.demo.Sample.UriSample;
|
||||
import com.google.android.exoplayer2.drm.DefaultDrmSessionManager;
|
||||
import com.google.android.exoplayer2.drm.DrmSessionManager;
|
||||
import com.google.android.exoplayer2.drm.FrameworkMediaCrypto;
|
||||
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.drm.HttpMediaDrmCallback;
|
||||
import com.google.android.exoplayer2.drm.UnsupportedDrmException;
|
||||
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.DownloadHelper;
|
||||
import com.google.android.exoplayer2.offline.DownloadRequest;
|
||||
import com.google.android.exoplayer2.source.BehindLiveWindowException;
|
||||
import com.google.android.exoplayer2.source.ConcatenatingMediaSource;
|
||||
import com.google.android.exoplayer2.source.MediaSource;
|
||||
import com.google.android.exoplayer2.source.DefaultMediaSourceFactory;
|
||||
import com.google.android.exoplayer2.source.MediaSourceFactory;
|
||||
import com.google.android.exoplayer2.source.ProgressiveMediaSource;
|
||||
import com.google.android.exoplayer2.source.TrackGroupArray;
|
||||
import com.google.android.exoplayer2.source.ads.AdsLoader;
|
||||
import com.google.android.exoplayer2.source.ads.AdsMediaSource;
|
||||
import com.google.android.exoplayer2.source.dash.DashMediaSource;
|
||||
import com.google.android.exoplayer2.source.hls.HlsMediaSource;
|
||||
import com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource;
|
||||
import com.google.android.exoplayer2.trackselection.AdaptiveTrackSelection;
|
||||
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
|
||||
import com.google.android.exoplayer2.trackselection.MappingTrackSelector.MappedTrackInfo;
|
||||
import com.google.android.exoplayer2.trackselection.RandomTrackSelection;
|
||||
import com.google.android.exoplayer2.trackselection.TrackSelection;
|
||||
import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
|
||||
import com.google.android.exoplayer2.ui.DebugTextViewHelper;
|
||||
import com.google.android.exoplayer2.ui.PlayerControlView;
|
||||
import com.google.android.exoplayer2.ui.PlayerView;
|
||||
import com.google.android.exoplayer2.ui.spherical.SphericalSurfaceView;
|
||||
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.upstream.HttpDataSource;
|
||||
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 java.lang.reflect.Constructor;
|
||||
import java.net.CookieHandler;
|
||||
import java.net.CookieManager;
|
||||
import java.net.CookiePolicy;
|
||||
import java.util.ArrayList;
|
||||
import java.util.UUID;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
/** An activity that plays media using {@link SimpleExoPlayer}. */
|
||||
/** An activity that plays media using {@link ExoPlayer}. */
|
||||
public class PlayerActivity extends AppCompatActivity
|
||||
implements OnClickListener, PlaybackPreparer, PlayerControlView.VisibilityListener {
|
||||
|
||||
// Activity extras.
|
||||
|
||||
public static final String SPHERICAL_STEREO_MODE_EXTRA = "spherical_stereo_mode";
|
||||
public static final String SPHERICAL_STEREO_MODE_MONO = "mono";
|
||||
public static final String SPHERICAL_STEREO_MODE_TOP_BOTTOM = "top_bottom";
|
||||
public static final String SPHERICAL_STEREO_MODE_LEFT_RIGHT = "left_right";
|
||||
|
||||
// Actions.
|
||||
|
||||
public static final String ACTION_VIEW = "com.google.android.exoplayer.demo.action.VIEW";
|
||||
public static final String ACTION_VIEW_LIST =
|
||||
"com.google.android.exoplayer.demo.action.VIEW_LIST";
|
||||
|
||||
// Player configuration extras.
|
||||
|
||||
public static final String ABR_ALGORITHM_EXTRA = "abr_algorithm";
|
||||
public static final String ABR_ALGORITHM_DEFAULT = "default";
|
||||
public static final String ABR_ALGORITHM_RANDOM = "random";
|
||||
|
||||
// Media item configuration extras.
|
||||
|
||||
public static final String URI_EXTRA = "uri";
|
||||
public static final String EXTENSION_EXTRA = "extension";
|
||||
|
||||
public static final String DRM_SCHEME_EXTRA = "drm_scheme";
|
||||
public static final String DRM_LICENSE_URL_EXTRA = "drm_license_url";
|
||||
public static final String DRM_KEY_REQUEST_PROPERTIES_EXTRA = "drm_key_request_properties";
|
||||
public static final String DRM_MULTI_SESSION_EXTRA = "drm_multi_session";
|
||||
public static final String PREFER_EXTENSION_DECODERS_EXTRA = "prefer_extension_decoders";
|
||||
public static final String AD_TAG_URI_EXTRA = "ad_tag_uri";
|
||||
// For backwards compatibility only.
|
||||
public static final String DRM_SCHEME_UUID_EXTRA = "drm_scheme_uuid";
|
||||
implements OnClickListener, StyledPlayerControlView.VisibilityListener {
|
||||
|
||||
// Saved instance state keys.
|
||||
|
||||
private static final String KEY_TRACK_SELECTOR_PARAMETERS = "track_selector_parameters";
|
||||
private static final String KEY_TRACK_SELECTION_PARAMETERS = "track_selection_parameters";
|
||||
private static final String KEY_WINDOW = "window";
|
||||
private static final String KEY_POSITION = "position";
|
||||
private static final String KEY_AUTO_PLAY = "auto_play";
|
||||
|
||||
private static final CookieManager DEFAULT_COOKIE_MANAGER;
|
||||
static {
|
||||
DEFAULT_COOKIE_MANAGER = new CookieManager();
|
||||
DEFAULT_COOKIE_MANAGER.setCookiePolicy(CookiePolicy.ACCEPT_ORIGINAL_SERVER);
|
||||
}
|
||||
protected StyledPlayerView playerView;
|
||||
protected LinearLayout debugRootView;
|
||||
protected TextView debugTextView;
|
||||
protected @Nullable ExoPlayer player;
|
||||
|
||||
private final ArrayList<FrameworkMediaDrm> mediaDrms;
|
||||
|
||||
private PlayerView playerView;
|
||||
private LinearLayout debugRootView;
|
||||
private Button selectTracksButton;
|
||||
private TextView debugTextView;
|
||||
private boolean isShowingTrackSelectionDialog;
|
||||
|
||||
private Button selectTracksButton;
|
||||
private DataSource.Factory dataSourceFactory;
|
||||
private SimpleExoPlayer player;
|
||||
private MediaSource mediaSource;
|
||||
private List<MediaItem> mediaItems;
|
||||
private DefaultTrackSelector trackSelector;
|
||||
private DefaultTrackSelector.Parameters trackSelectorParameters;
|
||||
private DefaultTrackSelector.Parameters trackSelectionParameters;
|
||||
private DebugTextViewHelper debugViewHelper;
|
||||
private TrackGroupArray lastSeenTrackGroupArray;
|
||||
|
||||
private TracksInfo lastSeenTracksInfo;
|
||||
private boolean startAutoPlay;
|
||||
private int startWindow;
|
||||
private long startPosition;
|
||||
|
||||
// Fields used only for ad playback. The ads loader is loaded via reflection.
|
||||
// For ad playback only.
|
||||
|
||||
private AdsLoader adsLoader;
|
||||
private Uri loadedAdTagUri;
|
||||
|
||||
public PlayerActivity() {
|
||||
mediaDrms = new ArrayList<>();
|
||||
}
|
||||
|
||||
// Activity lifecycle
|
||||
// Activity lifecycle.
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
Intent intent = getIntent();
|
||||
String sphericalStereoMode = intent.getStringExtra(SPHERICAL_STEREO_MODE_EXTRA);
|
||||
if (sphericalStereoMode != null) {
|
||||
setTheme(R.style.PlayerTheme_Spherical);
|
||||
}
|
||||
public void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
dataSourceFactory = buildDataSourceFactory();
|
||||
if (CookieHandler.getDefault() != DEFAULT_COOKIE_MANAGER) {
|
||||
CookieHandler.setDefault(DEFAULT_COOKIE_MANAGER);
|
||||
}
|
||||
dataSourceFactory = DemoUtil.getDataSourceFactory(/* context= */ this);
|
||||
|
||||
setContentView(R.layout.player_activity);
|
||||
setContentView();
|
||||
debugRootView = findViewById(R.id.controls_root);
|
||||
debugTextView = findViewById(R.id.debug_text_view);
|
||||
selectTracksButton = findViewById(R.id.select_tracks_button);
|
||||
|
|
@ -186,29 +108,18 @@ public class PlayerActivity extends AppCompatActivity
|
|||
playerView.setControllerVisibilityListener(this);
|
||||
playerView.setErrorMessageProvider(new PlayerErrorMessageProvider());
|
||||
playerView.requestFocus();
|
||||
if (sphericalStereoMode != null) {
|
||||
int stereoMode;
|
||||
if (SPHERICAL_STEREO_MODE_MONO.equals(sphericalStereoMode)) {
|
||||
stereoMode = C.STEREO_MODE_MONO;
|
||||
} else if (SPHERICAL_STEREO_MODE_TOP_BOTTOM.equals(sphericalStereoMode)) {
|
||||
stereoMode = C.STEREO_MODE_TOP_BOTTOM;
|
||||
} else if (SPHERICAL_STEREO_MODE_LEFT_RIGHT.equals(sphericalStereoMode)) {
|
||||
stereoMode = C.STEREO_MODE_LEFT_RIGHT;
|
||||
} else {
|
||||
showToast(R.string.error_unrecognized_stereo_mode);
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
((SphericalSurfaceView) playerView.getVideoSurfaceView()).setDefaultStereoMode(stereoMode);
|
||||
}
|
||||
|
||||
if (savedInstanceState != null) {
|
||||
trackSelectorParameters = savedInstanceState.getParcelable(KEY_TRACK_SELECTOR_PARAMETERS);
|
||||
// Restore as DefaultTrackSelector.Parameters in case ExoPlayer specific parameters were set.
|
||||
trackSelectionParameters =
|
||||
DefaultTrackSelector.Parameters.CREATOR.fromBundle(
|
||||
savedInstanceState.getBundle(KEY_TRACK_SELECTION_PARAMETERS));
|
||||
startAutoPlay = savedInstanceState.getBoolean(KEY_AUTO_PLAY);
|
||||
startWindow = savedInstanceState.getInt(KEY_WINDOW);
|
||||
startPosition = savedInstanceState.getLong(KEY_POSITION);
|
||||
} else {
|
||||
trackSelectorParameters = DefaultTrackSelector.Parameters.getDefaults(/* context= */ this);
|
||||
trackSelectionParameters =
|
||||
new DefaultTrackSelector.ParametersBuilder(/* context= */ this).build();
|
||||
clearStartPosition();
|
||||
}
|
||||
}
|
||||
|
|
@ -273,8 +184,9 @@ public class PlayerActivity extends AppCompatActivity
|
|||
}
|
||||
|
||||
@Override
|
||||
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions,
|
||||
@NonNull int[] grantResults) {
|
||||
public void onRequestPermissionsResult(
|
||||
int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
|
||||
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
|
||||
if (grantResults.length == 0) {
|
||||
// Empty results are triggered if a permission is requested while another request was already
|
||||
// pending and can be safely ignored in this case.
|
||||
|
|
@ -289,11 +201,11 @@ public class PlayerActivity extends AppCompatActivity
|
|||
}
|
||||
|
||||
@Override
|
||||
public void onSaveInstanceState(Bundle outState) {
|
||||
public void onSaveInstanceState(@NonNull Bundle outState) {
|
||||
super.onSaveInstanceState(outState);
|
||||
updateTrackSelectorParameters();
|
||||
updateStartPosition();
|
||||
outState.putParcelable(KEY_TRACK_SELECTOR_PARAMETERS, trackSelectorParameters);
|
||||
outState.putBundle(KEY_TRACK_SELECTION_PARAMETERS, trackSelectionParameters.toBundle());
|
||||
outState.putBoolean(KEY_AUTO_PLAY, startAutoPlay);
|
||||
outState.putInt(KEY_WINDOW, startWindow);
|
||||
outState.putLong(KEY_POSITION, startPosition);
|
||||
|
|
@ -323,14 +235,7 @@ public class PlayerActivity extends AppCompatActivity
|
|||
}
|
||||
}
|
||||
|
||||
// PlaybackControlView.PlaybackPreparer implementation
|
||||
|
||||
@Override
|
||||
public void preparePlayback() {
|
||||
player.retry();
|
||||
}
|
||||
|
||||
// PlaybackControlView.VisibilityListener implementation
|
||||
// PlayerControlView.VisibilityListener implementation
|
||||
|
||||
@Override
|
||||
public void onVisibilityChange(int visibility) {
|
||||
|
|
@ -339,207 +244,111 @@ public class PlayerActivity extends AppCompatActivity
|
|||
|
||||
// Internal methods
|
||||
|
||||
private void initializePlayer() {
|
||||
protected void setContentView() {
|
||||
setContentView(R.layout.player_activity);
|
||||
}
|
||||
|
||||
/** @return Whether initialization was successful. */
|
||||
protected boolean initializePlayer() {
|
||||
if (player == null) {
|
||||
Intent intent = getIntent();
|
||||
|
||||
releaseMediaDrms();
|
||||
mediaSource = createTopLevelMediaSource(intent);
|
||||
if (mediaSource == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
TrackSelection.Factory trackSelectionFactory;
|
||||
String abrAlgorithm = intent.getStringExtra(ABR_ALGORITHM_EXTRA);
|
||||
if (abrAlgorithm == null || ABR_ALGORITHM_DEFAULT.equals(abrAlgorithm)) {
|
||||
trackSelectionFactory = new AdaptiveTrackSelection.Factory();
|
||||
} else if (ABR_ALGORITHM_RANDOM.equals(abrAlgorithm)) {
|
||||
trackSelectionFactory = new RandomTrackSelection.Factory();
|
||||
} else {
|
||||
showToast(R.string.error_unrecognized_abr_algorithm);
|
||||
finish();
|
||||
return;
|
||||
mediaItems = createMediaItems(intent);
|
||||
if (mediaItems.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
boolean preferExtensionDecoders =
|
||||
intent.getBooleanExtra(PREFER_EXTENSION_DECODERS_EXTRA, false);
|
||||
intent.getBooleanExtra(IntentUtil.PREFER_EXTENSION_DECODERS_EXTRA, false);
|
||||
RenderersFactory renderersFactory =
|
||||
((DemoApplication) getApplication()).buildRenderersFactory(preferExtensionDecoders);
|
||||
|
||||
trackSelector = new DefaultTrackSelector(/* context= */ this, trackSelectionFactory);
|
||||
trackSelector.setParameters(trackSelectorParameters);
|
||||
lastSeenTrackGroupArray = null;
|
||||
DemoUtil.buildRenderersFactory(/* context= */ this, preferExtensionDecoders);
|
||||
MediaSourceFactory mediaSourceFactory =
|
||||
new DefaultMediaSourceFactory(dataSourceFactory)
|
||||
.setAdsLoaderProvider(this::getAdsLoader)
|
||||
.setAdViewProvider(playerView);
|
||||
|
||||
trackSelector = new DefaultTrackSelector(/* context= */ this);
|
||||
lastSeenTracksInfo = TracksInfo.EMPTY;
|
||||
player =
|
||||
ExoPlayerFactory.newSimpleInstance(/* context= */ this, renderersFactory, trackSelector);
|
||||
new ExoPlayer.Builder(/* context= */ this, renderersFactory)
|
||||
.setMediaSourceFactory(mediaSourceFactory)
|
||||
.setTrackSelector(trackSelector)
|
||||
.build();
|
||||
player.setTrackSelectionParameters(trackSelectionParameters);
|
||||
player.addListener(new PlayerEventListener());
|
||||
player.setPlayWhenReady(startAutoPlay);
|
||||
player.addAnalyticsListener(new EventLogger(trackSelector));
|
||||
player.setAudioAttributes(AudioAttributes.DEFAULT, /* handleAudioFocus= */ true);
|
||||
player.setPlayWhenReady(startAutoPlay);
|
||||
playerView.setPlayer(player);
|
||||
playerView.setPlaybackPreparer(this);
|
||||
debugViewHelper = new DebugTextViewHelper(player, debugTextView);
|
||||
debugViewHelper.start();
|
||||
if (adsLoader != null) {
|
||||
adsLoader.setPlayer(player);
|
||||
}
|
||||
}
|
||||
boolean haveStartPosition = startWindow != C.INDEX_UNSET;
|
||||
if (haveStartPosition) {
|
||||
player.seekTo(startWindow, startPosition);
|
||||
}
|
||||
player.prepare(mediaSource, !haveStartPosition, false);
|
||||
player.setMediaItems(mediaItems, /* resetPosition= */ !haveStartPosition);
|
||||
player.prepare();
|
||||
updateButtonVisibility();
|
||||
return true;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private MediaSource createTopLevelMediaSource(Intent intent) {
|
||||
private List<MediaItem> createMediaItems(Intent intent) {
|
||||
String action = intent.getAction();
|
||||
boolean actionIsListView = ACTION_VIEW_LIST.equals(action);
|
||||
if (!actionIsListView && !ACTION_VIEW.equals(action)) {
|
||||
boolean actionIsListView = IntentUtil.ACTION_VIEW_LIST.equals(action);
|
||||
if (!actionIsListView && !IntentUtil.ACTION_VIEW.equals(action)) {
|
||||
showToast(getString(R.string.unexpected_intent_action, action));
|
||||
finish();
|
||||
return null;
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
Sample intentAsSample = Sample.createFromIntent(intent);
|
||||
UriSample[] samples =
|
||||
intentAsSample instanceof Sample.PlaylistSample
|
||||
? ((Sample.PlaylistSample) intentAsSample).children
|
||||
: new UriSample[] {(UriSample) intentAsSample};
|
||||
List<MediaItem> mediaItems =
|
||||
createMediaItems(intent, DemoUtil.getDownloadTracker(/* context= */ this));
|
||||
boolean hasAds = false;
|
||||
for (int i = 0; i < mediaItems.size(); i++) {
|
||||
MediaItem mediaItem = mediaItems.get(i);
|
||||
|
||||
boolean seenAdsTagUri = false;
|
||||
for (UriSample sample : samples) {
|
||||
seenAdsTagUri |= sample.adTagUri != null;
|
||||
if (!Util.checkCleartextTrafficPermitted(sample.uri)) {
|
||||
if (!Util.checkCleartextTrafficPermitted(mediaItem)) {
|
||||
showToast(R.string.error_cleartext_not_permitted);
|
||||
return null;
|
||||
finish();
|
||||
return Collections.emptyList();
|
||||
}
|
||||
if (Util.maybeRequestReadExternalStoragePermission(/* activity= */ this, sample.uri)) {
|
||||
if (Util.maybeRequestReadExternalStoragePermission(/* activity= */ this, mediaItem)) {
|
||||
// The player will be reinitialized if the permission is granted.
|
||||
return null;
|
||||
return Collections.emptyList();
|
||||
}
|
||||
}
|
||||
|
||||
MediaSource[] mediaSources = new MediaSource[samples.length];
|
||||
for (int i = 0; i < samples.length; i++) {
|
||||
mediaSources[i] = createLeafMediaSource(samples[i]);
|
||||
}
|
||||
MediaSource mediaSource =
|
||||
mediaSources.length == 1 ? mediaSources[0] : new ConcatenatingMediaSource(mediaSources);
|
||||
|
||||
if (seenAdsTagUri) {
|
||||
Uri adTagUri = samples[0].adTagUri;
|
||||
if (actionIsListView) {
|
||||
showToast(R.string.unsupported_ads_in_concatenation);
|
||||
} else {
|
||||
if (!adTagUri.equals(loadedAdTagUri)) {
|
||||
releaseAdsLoader();
|
||||
loadedAdTagUri = adTagUri;
|
||||
}
|
||||
MediaSource adsMediaSource = createAdsMediaSource(mediaSource, adTagUri);
|
||||
if (adsMediaSource != null) {
|
||||
mediaSource = adsMediaSource;
|
||||
} else {
|
||||
showToast(R.string.ima_not_loaded);
|
||||
MediaItem.DrmConfiguration drmConfiguration =
|
||||
checkNotNull(mediaItem.localConfiguration).drmConfiguration;
|
||||
if (drmConfiguration != null) {
|
||||
if (Util.SDK_INT < 18) {
|
||||
showToast(R.string.error_drm_unsupported_before_api_18);
|
||||
finish();
|
||||
return Collections.emptyList();
|
||||
} else if (!FrameworkMediaDrm.isCryptoSchemeSupported(drmConfiguration.scheme)) {
|
||||
showToast(R.string.error_drm_unsupported_scheme);
|
||||
finish();
|
||||
return Collections.emptyList();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
hasAds |= mediaItem.localConfiguration.adsConfiguration != null;
|
||||
}
|
||||
if (!hasAds) {
|
||||
releaseAdsLoader();
|
||||
}
|
||||
|
||||
return mediaSource;
|
||||
return mediaItems;
|
||||
}
|
||||
|
||||
private MediaSource createLeafMediaSource(UriSample parameters) {
|
||||
DrmSessionManager<FrameworkMediaCrypto> drmSessionManager = null;
|
||||
Sample.DrmInfo drmInfo = parameters.drmInfo;
|
||||
if (drmInfo != null) {
|
||||
int errorStringId = R.string.error_drm_unknown;
|
||||
if (Util.SDK_INT < 18) {
|
||||
errorStringId = R.string.error_drm_not_supported;
|
||||
} else {
|
||||
try {
|
||||
if (drmInfo.drmScheme == null) {
|
||||
errorStringId = R.string.error_drm_unsupported_scheme;
|
||||
} else {
|
||||
drmSessionManager =
|
||||
buildDrmSessionManagerV18(
|
||||
drmInfo.drmScheme,
|
||||
drmInfo.drmLicenseUrl,
|
||||
drmInfo.drmKeyRequestProperties,
|
||||
drmInfo.drmMultiSession);
|
||||
}
|
||||
} catch (UnsupportedDrmException e) {
|
||||
errorStringId =
|
||||
e.reason == UnsupportedDrmException.REASON_UNSUPPORTED_SCHEME
|
||||
? R.string.error_drm_unsupported_scheme
|
||||
: R.string.error_drm_unknown;
|
||||
}
|
||||
}
|
||||
if (drmSessionManager == null) {
|
||||
showToast(errorStringId);
|
||||
finish();
|
||||
return null;
|
||||
}
|
||||
} else {
|
||||
drmSessionManager = DrmSessionManager.getDummyDrmSessionManager();
|
||||
private AdsLoader getAdsLoader(MediaItem.AdsConfiguration adsConfiguration) {
|
||||
// The ads loader is reused for multiple playbacks, so that ad playback can resume.
|
||||
if (adsLoader == null) {
|
||||
adsLoader = new ImaAdsLoader.Builder(/* context= */ this).build();
|
||||
}
|
||||
|
||||
DownloadRequest downloadRequest =
|
||||
((DemoApplication) getApplication())
|
||||
.getDownloadTracker()
|
||||
.getDownloadRequest(parameters.uri);
|
||||
if (downloadRequest != null) {
|
||||
return DownloadHelper.createMediaSource(downloadRequest, dataSourceFactory);
|
||||
}
|
||||
return createLeafMediaSource(parameters.uri, parameters.extension, drmSessionManager);
|
||||
adsLoader.setPlayer(player);
|
||||
return adsLoader;
|
||||
}
|
||||
|
||||
private MediaSource createLeafMediaSource(
|
||||
Uri uri, String extension, DrmSessionManager<FrameworkMediaCrypto> drmSessionManager) {
|
||||
@ContentType int type = Util.inferContentType(uri, extension);
|
||||
switch (type) {
|
||||
case C.TYPE_DASH:
|
||||
return new DashMediaSource.Factory(dataSourceFactory)
|
||||
.setDrmSessionManager(drmSessionManager)
|
||||
.createMediaSource(uri);
|
||||
case C.TYPE_SS:
|
||||
return new SsMediaSource.Factory(dataSourceFactory)
|
||||
.setDrmSessionManager(drmSessionManager)
|
||||
.createMediaSource(uri);
|
||||
case C.TYPE_HLS:
|
||||
return new HlsMediaSource.Factory(dataSourceFactory)
|
||||
.setDrmSessionManager(drmSessionManager)
|
||||
.createMediaSource(uri);
|
||||
case C.TYPE_OTHER:
|
||||
return new ProgressiveMediaSource.Factory(dataSourceFactory)
|
||||
.setDrmSessionManager(drmSessionManager)
|
||||
.createMediaSource(uri);
|
||||
default:
|
||||
throw new IllegalStateException("Unsupported type: " + type);
|
||||
}
|
||||
}
|
||||
|
||||
private DefaultDrmSessionManager<FrameworkMediaCrypto> buildDrmSessionManagerV18(
|
||||
UUID uuid, String licenseUrl, String[] keyRequestPropertiesArray, boolean multiSession)
|
||||
throws UnsupportedDrmException {
|
||||
HttpDataSource.Factory licenseDataSourceFactory =
|
||||
((DemoApplication) getApplication()).buildHttpDataSourceFactory();
|
||||
HttpMediaDrmCallback drmCallback =
|
||||
new HttpMediaDrmCallback(licenseUrl, licenseDataSourceFactory);
|
||||
if (keyRequestPropertiesArray != null) {
|
||||
for (int i = 0; i < keyRequestPropertiesArray.length - 1; i += 2) {
|
||||
drmCallback.setKeyRequestProperty(keyRequestPropertiesArray[i],
|
||||
keyRequestPropertiesArray[i + 1]);
|
||||
}
|
||||
}
|
||||
|
||||
FrameworkMediaDrm mediaDrm = FrameworkMediaDrm.newInstance(uuid);
|
||||
mediaDrms.add(mediaDrm);
|
||||
return new DefaultDrmSessionManager<>(uuid, mediaDrm, drmCallback, null, multiSession);
|
||||
}
|
||||
|
||||
private void releasePlayer() {
|
||||
protected void releasePlayer() {
|
||||
if (player != null) {
|
||||
updateTrackSelectorParameters();
|
||||
updateStartPosition();
|
||||
|
|
@ -547,34 +356,27 @@ public class PlayerActivity extends AppCompatActivity
|
|||
debugViewHelper = null;
|
||||
player.release();
|
||||
player = null;
|
||||
mediaSource = null;
|
||||
trackSelector = null;
|
||||
mediaItems = Collections.emptyList();
|
||||
}
|
||||
if (adsLoader != null) {
|
||||
adsLoader.setPlayer(null);
|
||||
}
|
||||
releaseMediaDrms();
|
||||
}
|
||||
|
||||
private void releaseMediaDrms() {
|
||||
for (FrameworkMediaDrm mediaDrm : mediaDrms) {
|
||||
mediaDrm.release();
|
||||
}
|
||||
mediaDrms.clear();
|
||||
}
|
||||
|
||||
private void releaseAdsLoader() {
|
||||
if (adsLoader != null) {
|
||||
adsLoader.release();
|
||||
adsLoader = null;
|
||||
loadedAdTagUri = null;
|
||||
playerView.getOverlayFrameLayout().removeAllViews();
|
||||
}
|
||||
}
|
||||
|
||||
private void updateTrackSelectorParameters() {
|
||||
if (trackSelector != null) {
|
||||
trackSelectorParameters = trackSelector.getParameters();
|
||||
if (player != null) {
|
||||
// Until the demo app is fully migrated to TrackSelectionParameters, rely on ExoPlayer to use
|
||||
// DefaultTrackSelector by default.
|
||||
trackSelectionParameters =
|
||||
(DefaultTrackSelector.Parameters) player.getTrackSelectionParameters();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -586,56 +388,12 @@ public class PlayerActivity extends AppCompatActivity
|
|||
}
|
||||
}
|
||||
|
||||
private void clearStartPosition() {
|
||||
protected void clearStartPosition() {
|
||||
startAutoPlay = true;
|
||||
startWindow = C.INDEX_UNSET;
|
||||
startPosition = C.TIME_UNSET;
|
||||
}
|
||||
|
||||
/** Returns a new DataSource factory. */
|
||||
private DataSource.Factory buildDataSourceFactory() {
|
||||
return ((DemoApplication) getApplication()).buildDataSourceFactory();
|
||||
}
|
||||
|
||||
/** Returns an ads media source, reusing the ads loader if one exists. */
|
||||
@Nullable
|
||||
private MediaSource createAdsMediaSource(MediaSource mediaSource, Uri adTagUri) {
|
||||
// Load the extension source using reflection so the demo app doesn't have to depend on it.
|
||||
// The ads loader is reused for multiple playbacks, so that ad playback can resume.
|
||||
try {
|
||||
Class<?> loaderClass = Class.forName("com.google.android.exoplayer2.ext.ima.ImaAdsLoader");
|
||||
if (adsLoader == null) {
|
||||
// Full class names used so the LINT.IfChange rule triggers should any of the classes move.
|
||||
// LINT.IfChange
|
||||
Constructor<? extends AdsLoader> loaderConstructor =
|
||||
loaderClass
|
||||
.asSubclass(AdsLoader.class)
|
||||
.getConstructor(android.content.Context.class, android.net.Uri.class);
|
||||
// LINT.ThenChange(../../../../../../../../proguard-rules.txt)
|
||||
adsLoader = loaderConstructor.newInstance(this, adTagUri);
|
||||
}
|
||||
MediaSourceFactory adMediaSourceFactory =
|
||||
new MediaSourceFactory() {
|
||||
@Override
|
||||
public MediaSource createMediaSource(Uri uri) {
|
||||
return PlayerActivity.this.createLeafMediaSource(
|
||||
uri, /* extension=*/ null, DrmSessionManager.getDummyDrmSessionManager());
|
||||
}
|
||||
|
||||
@Override
|
||||
public int[] getSupportedTypes() {
|
||||
return new int[] {C.TYPE_DASH, C.TYPE_SS, C.TYPE_HLS, C.TYPE_OTHER};
|
||||
}
|
||||
};
|
||||
return new AdsMediaSource(mediaSource, adMediaSourceFactory, adsLoader, playerView);
|
||||
} catch (ClassNotFoundException e) {
|
||||
// IMA extension not loaded.
|
||||
return null;
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
// User controls
|
||||
|
||||
private void updateButtonVisibility() {
|
||||
|
|
@ -655,24 +413,10 @@ public class PlayerActivity extends AppCompatActivity
|
|||
Toast.makeText(getApplicationContext(), message, Toast.LENGTH_LONG).show();
|
||||
}
|
||||
|
||||
private static boolean isBehindLiveWindow(ExoPlaybackException e) {
|
||||
if (e.type != ExoPlaybackException.TYPE_SOURCE) {
|
||||
return false;
|
||||
}
|
||||
Throwable cause = e.getSourceException();
|
||||
while (cause != null) {
|
||||
if (cause instanceof BehindLiveWindowException) {
|
||||
return true;
|
||||
}
|
||||
cause = cause.getCause();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private class PlayerEventListener implements Player.EventListener {
|
||||
private class PlayerEventListener implements Player.Listener {
|
||||
|
||||
@Override
|
||||
public void onPlayerStateChanged(boolean playWhenReady, @Player.State int playbackState) {
|
||||
public void onPlaybackStateChanged(@Player.State int playbackState) {
|
||||
if (playbackState == Player.STATE_ENDED) {
|
||||
showControls();
|
||||
}
|
||||
|
|
@ -680,10 +424,10 @@ public class PlayerActivity extends AppCompatActivity
|
|||
}
|
||||
|
||||
@Override
|
||||
public void onPlayerError(ExoPlaybackException e) {
|
||||
if (isBehindLiveWindow(e)) {
|
||||
clearStartPosition();
|
||||
initializePlayer();
|
||||
public void onPlayerError(@NonNull PlaybackException error) {
|
||||
if (error.errorCode == PlaybackException.ERROR_CODE_BEHIND_LIVE_WINDOW) {
|
||||
player.seekToDefaultPosition();
|
||||
player.prepare();
|
||||
} else {
|
||||
updateButtonVisibility();
|
||||
showControls();
|
||||
|
|
@ -692,56 +436,80 @@ public class PlayerActivity extends AppCompatActivity
|
|||
|
||||
@Override
|
||||
@SuppressWarnings("ReferenceEquality")
|
||||
public void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray trackSelections) {
|
||||
public void onTracksInfoChanged(TracksInfo tracksInfo) {
|
||||
updateButtonVisibility();
|
||||
if (trackGroups != lastSeenTrackGroupArray) {
|
||||
MappedTrackInfo mappedTrackInfo = trackSelector.getCurrentMappedTrackInfo();
|
||||
if (mappedTrackInfo != null) {
|
||||
if (mappedTrackInfo.getTypeSupport(C.TRACK_TYPE_VIDEO)
|
||||
== MappedTrackInfo.RENDERER_SUPPORT_UNSUPPORTED_TRACKS) {
|
||||
showToast(R.string.error_unsupported_video);
|
||||
}
|
||||
if (mappedTrackInfo.getTypeSupport(C.TRACK_TYPE_AUDIO)
|
||||
== MappedTrackInfo.RENDERER_SUPPORT_UNSUPPORTED_TRACKS) {
|
||||
showToast(R.string.error_unsupported_audio);
|
||||
}
|
||||
}
|
||||
lastSeenTrackGroupArray = trackGroups;
|
||||
if (tracksInfo == lastSeenTracksInfo) {
|
||||
return;
|
||||
}
|
||||
if (!tracksInfo.isTypeSupportedOrEmpty(C.TRACK_TYPE_VIDEO)) {
|
||||
showToast(R.string.error_unsupported_video);
|
||||
}
|
||||
if (!tracksInfo.isTypeSupportedOrEmpty(C.TRACK_TYPE_AUDIO)) {
|
||||
showToast(R.string.error_unsupported_audio);
|
||||
}
|
||||
lastSeenTracksInfo = tracksInfo;
|
||||
}
|
||||
}
|
||||
|
||||
private class PlayerErrorMessageProvider implements ErrorMessageProvider<ExoPlaybackException> {
|
||||
private class PlayerErrorMessageProvider implements ErrorMessageProvider<PlaybackException> {
|
||||
|
||||
@Override
|
||||
public Pair<Integer, String> getErrorMessage(ExoPlaybackException e) {
|
||||
@NonNull
|
||||
public Pair<Integer, String> getErrorMessage(@NonNull PlaybackException e) {
|
||||
String errorString = getString(R.string.error_generic);
|
||||
if (e.type == ExoPlaybackException.TYPE_RENDERER) {
|
||||
Exception cause = e.getRendererException();
|
||||
if (cause instanceof DecoderInitializationException) {
|
||||
// Special case for decoder initialization failures.
|
||||
DecoderInitializationException decoderInitializationException =
|
||||
(DecoderInitializationException) cause;
|
||||
if (decoderInitializationException.codecInfo == null) {
|
||||
if (decoderInitializationException.getCause() instanceof DecoderQueryException) {
|
||||
errorString = getString(R.string.error_querying_decoders);
|
||||
} else if (decoderInitializationException.secureDecoderRequired) {
|
||||
errorString =
|
||||
getString(
|
||||
R.string.error_no_secure_decoder, decoderInitializationException.mimeType);
|
||||
} else {
|
||||
errorString =
|
||||
getString(R.string.error_no_decoder, decoderInitializationException.mimeType);
|
||||
}
|
||||
} else {
|
||||
Throwable cause = e.getCause();
|
||||
if (cause instanceof DecoderInitializationException) {
|
||||
// Special case for decoder initialization failures.
|
||||
DecoderInitializationException decoderInitializationException =
|
||||
(DecoderInitializationException) cause;
|
||||
if (decoderInitializationException.codecInfo == null) {
|
||||
if (decoderInitializationException.getCause() instanceof DecoderQueryException) {
|
||||
errorString = getString(R.string.error_querying_decoders);
|
||||
} else if (decoderInitializationException.secureDecoderRequired) {
|
||||
errorString =
|
||||
getString(
|
||||
R.string.error_instantiating_decoder,
|
||||
decoderInitializationException.codecInfo.name);
|
||||
R.string.error_no_secure_decoder, decoderInitializationException.mimeType);
|
||||
} else {
|
||||
errorString =
|
||||
getString(R.string.error_no_decoder, decoderInitializationException.mimeType);
|
||||
}
|
||||
} else {
|
||||
errorString =
|
||||
getString(
|
||||
R.string.error_instantiating_decoder,
|
||||
decoderInitializationException.codecInfo.name);
|
||||
}
|
||||
}
|
||||
return Pair.create(0, errorString);
|
||||
}
|
||||
}
|
||||
|
||||
private static List<MediaItem> createMediaItems(Intent intent, DownloadTracker downloadTracker) {
|
||||
List<MediaItem> mediaItems = new ArrayList<>();
|
||||
for (MediaItem item : IntentUtil.createMediaItemsFromIntent(intent)) {
|
||||
@Nullable
|
||||
DownloadRequest downloadRequest =
|
||||
downloadTracker.getDownloadRequest(checkNotNull(item.localConfiguration).uri);
|
||||
if (downloadRequest != null) {
|
||||
MediaItem.Builder builder = item.buildUpon();
|
||||
builder
|
||||
.setMediaId(downloadRequest.id)
|
||||
.setUri(downloadRequest.uri)
|
||||
.setCustomCacheKey(downloadRequest.customCacheKey)
|
||||
.setMimeType(downloadRequest.mimeType)
|
||||
.setStreamKeys(downloadRequest.streamKeys);
|
||||
@Nullable
|
||||
MediaItem.DrmConfiguration drmConfiguration = item.localConfiguration.drmConfiguration;
|
||||
if (drmConfiguration != null) {
|
||||
builder.setDrmConfiguration(
|
||||
drmConfiguration.buildUpon().setKeySetId(downloadRequest.keySetId).build());
|
||||
}
|
||||
|
||||
mediaItems.add(builder.build());
|
||||
} else {
|
||||
mediaItems.add(item);
|
||||
}
|
||||
}
|
||||
return mediaItems;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,187 +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.demo;
|
||||
|
||||
import static com.google.android.exoplayer2.demo.PlayerActivity.ACTION_VIEW_LIST;
|
||||
import static com.google.android.exoplayer2.demo.PlayerActivity.AD_TAG_URI_EXTRA;
|
||||
import static com.google.android.exoplayer2.demo.PlayerActivity.DRM_KEY_REQUEST_PROPERTIES_EXTRA;
|
||||
import static com.google.android.exoplayer2.demo.PlayerActivity.DRM_LICENSE_URL_EXTRA;
|
||||
import static com.google.android.exoplayer2.demo.PlayerActivity.DRM_MULTI_SESSION_EXTRA;
|
||||
import static com.google.android.exoplayer2.demo.PlayerActivity.DRM_SCHEME_EXTRA;
|
||||
import static com.google.android.exoplayer2.demo.PlayerActivity.DRM_SCHEME_UUID_EXTRA;
|
||||
import static com.google.android.exoplayer2.demo.PlayerActivity.EXTENSION_EXTRA;
|
||||
import static com.google.android.exoplayer2.demo.PlayerActivity.URI_EXTRA;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import androidx.annotation.Nullable;
|
||||
import com.google.android.exoplayer2.util.Assertions;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
import java.util.ArrayList;
|
||||
import java.util.UUID;
|
||||
|
||||
/* package */ abstract class Sample {
|
||||
|
||||
public static final class UriSample extends Sample {
|
||||
|
||||
public static UriSample createFromIntent(Uri uri, Intent intent, String extrasKeySuffix) {
|
||||
String extension = intent.getStringExtra(EXTENSION_EXTRA + extrasKeySuffix);
|
||||
String adsTagUriString = intent.getStringExtra(AD_TAG_URI_EXTRA + extrasKeySuffix);
|
||||
Uri adTagUri = adsTagUriString != null ? Uri.parse(adsTagUriString) : null;
|
||||
return new UriSample(
|
||||
/* name= */ null,
|
||||
DrmInfo.createFromIntent(intent, extrasKeySuffix),
|
||||
uri,
|
||||
extension,
|
||||
adTagUri,
|
||||
/* sphericalStereoMode= */ null);
|
||||
}
|
||||
|
||||
public final Uri uri;
|
||||
public final String extension;
|
||||
public final DrmInfo drmInfo;
|
||||
public final Uri adTagUri;
|
||||
public final String sphericalStereoMode;
|
||||
|
||||
public UriSample(
|
||||
String name,
|
||||
DrmInfo drmInfo,
|
||||
Uri uri,
|
||||
String extension,
|
||||
Uri adTagUri,
|
||||
String sphericalStereoMode) {
|
||||
super(name);
|
||||
this.uri = uri;
|
||||
this.extension = extension;
|
||||
this.drmInfo = drmInfo;
|
||||
this.adTagUri = adTagUri;
|
||||
this.sphericalStereoMode = sphericalStereoMode;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addToIntent(Intent intent) {
|
||||
intent.setAction(PlayerActivity.ACTION_VIEW).setData(uri);
|
||||
intent.putExtra(PlayerActivity.SPHERICAL_STEREO_MODE_EXTRA, sphericalStereoMode);
|
||||
addPlayerConfigToIntent(intent, /* extrasKeySuffix= */ "");
|
||||
}
|
||||
|
||||
public void addToPlaylistIntent(Intent intent, String extrasKeySuffix) {
|
||||
intent.putExtra(PlayerActivity.URI_EXTRA + extrasKeySuffix, uri.toString());
|
||||
addPlayerConfigToIntent(intent, extrasKeySuffix);
|
||||
}
|
||||
|
||||
private void addPlayerConfigToIntent(Intent intent, String extrasKeySuffix) {
|
||||
intent
|
||||
.putExtra(EXTENSION_EXTRA + extrasKeySuffix, extension)
|
||||
.putExtra(
|
||||
AD_TAG_URI_EXTRA + extrasKeySuffix, adTagUri != null ? adTagUri.toString() : null);
|
||||
if (drmInfo != null) {
|
||||
drmInfo.addToIntent(intent, extrasKeySuffix);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static final class PlaylistSample extends Sample {
|
||||
|
||||
public final UriSample[] children;
|
||||
|
||||
public PlaylistSample(String name, UriSample... children) {
|
||||
super(name);
|
||||
this.children = children;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addToIntent(Intent intent) {
|
||||
intent.setAction(PlayerActivity.ACTION_VIEW_LIST);
|
||||
for (int i = 0; i < children.length; i++) {
|
||||
children[i].addToPlaylistIntent(intent, /* extrasKeySuffix= */ "_" + i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static final class DrmInfo {
|
||||
|
||||
public static DrmInfo createFromIntent(Intent intent, String extrasKeySuffix) {
|
||||
String schemeKey = DRM_SCHEME_EXTRA + extrasKeySuffix;
|
||||
String schemeUuidKey = DRM_SCHEME_UUID_EXTRA + extrasKeySuffix;
|
||||
if (!intent.hasExtra(schemeKey) && !intent.hasExtra(schemeUuidKey)) {
|
||||
return null;
|
||||
}
|
||||
String drmSchemeExtra =
|
||||
intent.hasExtra(schemeKey)
|
||||
? intent.getStringExtra(schemeKey)
|
||||
: intent.getStringExtra(schemeUuidKey);
|
||||
UUID drmScheme = Util.getDrmUuid(drmSchemeExtra);
|
||||
String drmLicenseUrl = intent.getStringExtra(DRM_LICENSE_URL_EXTRA + extrasKeySuffix);
|
||||
String[] keyRequestPropertiesArray =
|
||||
intent.getStringArrayExtra(DRM_KEY_REQUEST_PROPERTIES_EXTRA + extrasKeySuffix);
|
||||
boolean drmMultiSession =
|
||||
intent.getBooleanExtra(DRM_MULTI_SESSION_EXTRA + extrasKeySuffix, false);
|
||||
return new DrmInfo(drmScheme, drmLicenseUrl, keyRequestPropertiesArray, drmMultiSession);
|
||||
}
|
||||
|
||||
public final UUID drmScheme;
|
||||
public final String drmLicenseUrl;
|
||||
public final String[] drmKeyRequestProperties;
|
||||
public final boolean drmMultiSession;
|
||||
|
||||
public DrmInfo(
|
||||
UUID drmScheme,
|
||||
String drmLicenseUrl,
|
||||
String[] drmKeyRequestProperties,
|
||||
boolean drmMultiSession) {
|
||||
this.drmScheme = drmScheme;
|
||||
this.drmLicenseUrl = drmLicenseUrl;
|
||||
this.drmKeyRequestProperties = drmKeyRequestProperties;
|
||||
this.drmMultiSession = drmMultiSession;
|
||||
}
|
||||
|
||||
public void addToIntent(Intent intent, String extrasKeySuffix) {
|
||||
Assertions.checkNotNull(intent);
|
||||
intent.putExtra(DRM_SCHEME_EXTRA + extrasKeySuffix, drmScheme.toString());
|
||||
intent.putExtra(DRM_LICENSE_URL_EXTRA + extrasKeySuffix, drmLicenseUrl);
|
||||
intent.putExtra(DRM_KEY_REQUEST_PROPERTIES_EXTRA + extrasKeySuffix, drmKeyRequestProperties);
|
||||
intent.putExtra(DRM_MULTI_SESSION_EXTRA + extrasKeySuffix, drmMultiSession);
|
||||
}
|
||||
}
|
||||
|
||||
public static Sample createFromIntent(Intent intent) {
|
||||
if (ACTION_VIEW_LIST.equals(intent.getAction())) {
|
||||
ArrayList<String> intentUris = new ArrayList<>();
|
||||
int index = 0;
|
||||
while (intent.hasExtra(URI_EXTRA + "_" + index)) {
|
||||
intentUris.add(intent.getStringExtra(URI_EXTRA + "_" + index));
|
||||
index++;
|
||||
}
|
||||
UriSample[] children = new UriSample[intentUris.size()];
|
||||
for (int i = 0; i < children.length; i++) {
|
||||
Uri uri = Uri.parse(intentUris.get(i));
|
||||
children[i] = UriSample.createFromIntent(uri, intent, /* extrasKeySuffix= */ "_" + i);
|
||||
}
|
||||
return new PlaylistSample(/* name= */ null, children);
|
||||
} else {
|
||||
return UriSample.createFromIntent(intent.getData(), intent, /* extrasKeySuffix= */ "");
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable public final String name;
|
||||
|
||||
public Sample(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public abstract void addToIntent(Intent intent);
|
||||
}
|
||||
|
|
@ -15,14 +15,18 @@
|
|||
*/
|
||||
package com.google.android.exoplayer2.demo;
|
||||
|
||||
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 android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.content.res.AssetManager;
|
||||
import android.net.Uri;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Bundle;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import android.util.JsonReader;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
|
|
@ -36,51 +40,59 @@ import android.widget.ExpandableListView.OnChildClickListener;
|
|||
import android.widget.ImageButton;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import com.google.android.exoplayer2.MediaItem;
|
||||
import com.google.android.exoplayer2.MediaMetadata;
|
||||
import com.google.android.exoplayer2.ParserException;
|
||||
import com.google.android.exoplayer2.RenderersFactory;
|
||||
import com.google.android.exoplayer2.demo.Sample.DrmInfo;
|
||||
import com.google.android.exoplayer2.demo.Sample.PlaylistSample;
|
||||
import com.google.android.exoplayer2.demo.Sample.UriSample;
|
||||
import com.google.android.exoplayer2.offline.DownloadService;
|
||||
import com.google.android.exoplayer2.upstream.DataSource;
|
||||
import com.google.android.exoplayer2.upstream.DataSourceInputStream;
|
||||
import com.google.android.exoplayer2.upstream.DataSourceUtil;
|
||||
import com.google.android.exoplayer2.upstream.DataSpec;
|
||||
import com.google.android.exoplayer2.upstream.DefaultDataSource;
|
||||
import com.google.android.exoplayer2.util.Assertions;
|
||||
import com.google.android.exoplayer2.util.Log;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
/** An activity for selecting from a list of media samples. */
|
||||
public class SampleChooserActivity extends AppCompatActivity
|
||||
implements DownloadTracker.Listener, OnChildClickListener {
|
||||
|
||||
private static final String TAG = "SampleChooserActivity";
|
||||
private static final String GROUP_POSITION_PREFERENCE_KEY = "sample_chooser_group_position";
|
||||
private static final String CHILD_POSITION_PREFERENCE_KEY = "sample_chooser_child_position";
|
||||
|
||||
private String[] uris;
|
||||
private boolean useExtensionRenderers;
|
||||
private DownloadTracker downloadTracker;
|
||||
private SampleAdapter sampleAdapter;
|
||||
private MenuItem preferExtensionDecodersMenuItem;
|
||||
private MenuItem randomAbrMenuItem;
|
||||
private ExpandableListView sampleListView;
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.sample_chooser_activity);
|
||||
sampleAdapter = new SampleAdapter();
|
||||
ExpandableListView sampleListView = findViewById(R.id.sample_list);
|
||||
sampleListView = findViewById(R.id.sample_list);
|
||||
|
||||
sampleListView.setAdapter(sampleAdapter);
|
||||
sampleListView.setOnChildClickListener(this);
|
||||
|
||||
Intent intent = getIntent();
|
||||
String dataUri = intent.getDataString();
|
||||
String[] uris;
|
||||
if (dataUri != null) {
|
||||
uris = new String[] {dataUri};
|
||||
} else {
|
||||
|
|
@ -101,11 +113,9 @@ public class SampleChooserActivity extends AppCompatActivity
|
|||
Arrays.sort(uris);
|
||||
}
|
||||
|
||||
DemoApplication application = (DemoApplication) getApplication();
|
||||
useExtensionRenderers = application.useExtensionRenderers();
|
||||
downloadTracker = application.getDownloadTracker();
|
||||
SampleListLoader loaderTask = new SampleListLoader();
|
||||
loaderTask.execute(uris);
|
||||
useExtensionRenderers = DemoUtil.useExtensionRenderers();
|
||||
downloadTracker = DemoUtil.getDownloadTracker(/* context= */ this);
|
||||
loadSample();
|
||||
|
||||
// Start the download service if it should be running but it's not currently.
|
||||
// Starting the service in the foreground causes notification flicker if there is no scheduled
|
||||
|
|
@ -124,7 +134,6 @@ public class SampleChooserActivity extends AppCompatActivity
|
|||
inflater.inflate(R.menu.sample_chooser_menu, menu);
|
||||
preferExtensionDecodersMenuItem = menu.findItem(R.id.prefer_extension_decoders);
|
||||
preferExtensionDecodersMenuItem.setVisible(useExtensionRenderers);
|
||||
randomAbrMenuItem = menu.findItem(R.id.random_abr);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
@ -152,63 +161,101 @@ public class SampleChooserActivity extends AppCompatActivity
|
|||
sampleAdapter.notifyDataSetChanged();
|
||||
}
|
||||
|
||||
private void onSampleGroups(final List<SampleGroup> groups, boolean sawError) {
|
||||
@Override
|
||||
public void onRequestPermissionsResult(
|
||||
int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
|
||||
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
|
||||
if (grantResults.length == 0) {
|
||||
// Empty results are triggered if a permission is requested while another request was already
|
||||
// pending and can be safely ignored in this case.
|
||||
return;
|
||||
}
|
||||
if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
||||
loadSample();
|
||||
} else {
|
||||
Toast.makeText(getApplicationContext(), R.string.sample_list_load_error, Toast.LENGTH_LONG)
|
||||
.show();
|
||||
finish();
|
||||
}
|
||||
}
|
||||
|
||||
private void loadSample() {
|
||||
checkNotNull(uris);
|
||||
|
||||
for (int i = 0; i < uris.length; i++) {
|
||||
Uri uri = Uri.parse(uris[i]);
|
||||
if (Util.maybeRequestReadExternalStoragePermission(this, uri)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
SampleListLoader loaderTask = new SampleListLoader();
|
||||
loaderTask.execute(uris);
|
||||
}
|
||||
|
||||
private void onPlaylistGroups(final List<PlaylistGroup> groups, boolean sawError) {
|
||||
if (sawError) {
|
||||
Toast.makeText(getApplicationContext(), R.string.sample_list_load_error, Toast.LENGTH_LONG)
|
||||
.show();
|
||||
}
|
||||
sampleAdapter.setSampleGroups(groups);
|
||||
sampleAdapter.setPlaylistGroups(groups);
|
||||
|
||||
SharedPreferences preferences = getPreferences(MODE_PRIVATE);
|
||||
int groupPosition = preferences.getInt(GROUP_POSITION_PREFERENCE_KEY, /* defValue= */ -1);
|
||||
int childPosition = preferences.getInt(CHILD_POSITION_PREFERENCE_KEY, /* defValue= */ -1);
|
||||
// Clear the group and child position if either are unset or if either are out of bounds.
|
||||
if (groupPosition != -1
|
||||
&& childPosition != -1
|
||||
&& groupPosition < groups.size()
|
||||
&& childPosition < groups.get(groupPosition).playlists.size()) {
|
||||
sampleListView.expandGroup(groupPosition); // shouldExpandGroup does not work without this.
|
||||
sampleListView.setSelectedChild(groupPosition, childPosition, /* shouldExpandGroup= */ true);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onChildClick(
|
||||
ExpandableListView parent, View view, int groupPosition, int childPosition, long id) {
|
||||
Sample sample = (Sample) view.getTag();
|
||||
// Save the selected item first to be able to restore it if the tested code crashes.
|
||||
SharedPreferences.Editor prefEditor = getPreferences(MODE_PRIVATE).edit();
|
||||
prefEditor.putInt(GROUP_POSITION_PREFERENCE_KEY, groupPosition);
|
||||
prefEditor.putInt(CHILD_POSITION_PREFERENCE_KEY, childPosition);
|
||||
prefEditor.apply();
|
||||
|
||||
PlaylistHolder playlistHolder = (PlaylistHolder) view.getTag();
|
||||
Intent intent = new Intent(this, PlayerActivity.class);
|
||||
intent.putExtra(
|
||||
PlayerActivity.PREFER_EXTENSION_DECODERS_EXTRA,
|
||||
IntentUtil.PREFER_EXTENSION_DECODERS_EXTRA,
|
||||
isNonNullAndChecked(preferExtensionDecodersMenuItem));
|
||||
String abrAlgorithm =
|
||||
isNonNullAndChecked(randomAbrMenuItem)
|
||||
? PlayerActivity.ABR_ALGORITHM_RANDOM
|
||||
: PlayerActivity.ABR_ALGORITHM_DEFAULT;
|
||||
intent.putExtra(PlayerActivity.ABR_ALGORITHM_EXTRA, abrAlgorithm);
|
||||
sample.addToIntent(intent);
|
||||
IntentUtil.addToIntent(playlistHolder.mediaItems, intent);
|
||||
startActivity(intent);
|
||||
return true;
|
||||
}
|
||||
|
||||
private void onSampleDownloadButtonClicked(Sample sample) {
|
||||
int downloadUnsupportedStringId = getDownloadUnsupportedStringId(sample);
|
||||
private void onSampleDownloadButtonClicked(PlaylistHolder playlistHolder) {
|
||||
int downloadUnsupportedStringId = getDownloadUnsupportedStringId(playlistHolder);
|
||||
if (downloadUnsupportedStringId != 0) {
|
||||
Toast.makeText(getApplicationContext(), downloadUnsupportedStringId, Toast.LENGTH_LONG)
|
||||
.show();
|
||||
} else {
|
||||
UriSample uriSample = (UriSample) sample;
|
||||
RenderersFactory renderersFactory =
|
||||
((DemoApplication) getApplication())
|
||||
.buildRenderersFactory(isNonNullAndChecked(preferExtensionDecodersMenuItem));
|
||||
DemoUtil.buildRenderersFactory(
|
||||
/* context= */ this, isNonNullAndChecked(preferExtensionDecodersMenuItem));
|
||||
downloadTracker.toggleDownload(
|
||||
getSupportFragmentManager(),
|
||||
sample.name,
|
||||
uriSample.uri,
|
||||
uriSample.extension,
|
||||
renderersFactory);
|
||||
getSupportFragmentManager(), playlistHolder.mediaItems.get(0), renderersFactory);
|
||||
}
|
||||
}
|
||||
|
||||
private int getDownloadUnsupportedStringId(Sample sample) {
|
||||
if (sample instanceof PlaylistSample) {
|
||||
private int getDownloadUnsupportedStringId(PlaylistHolder playlistHolder) {
|
||||
if (playlistHolder.mediaItems.size() > 1) {
|
||||
return R.string.download_playlist_unsupported;
|
||||
}
|
||||
UriSample uriSample = (UriSample) sample;
|
||||
if (uriSample.drmInfo != null) {
|
||||
return R.string.download_drm_unsupported;
|
||||
}
|
||||
if (uriSample.adTagUri != null) {
|
||||
MediaItem.LocalConfiguration localConfiguration =
|
||||
checkNotNull(playlistHolder.mediaItems.get(0).localConfiguration);
|
||||
if (localConfiguration.adsConfiguration != null) {
|
||||
return R.string.download_ads_unsupported;
|
||||
}
|
||||
String scheme = uriSample.uri.getScheme();
|
||||
String scheme = localConfiguration.uri.getScheme();
|
||||
if (!("http".equals(scheme) || "https".equals(scheme))) {
|
||||
return R.string.download_scheme_unsupported;
|
||||
}
|
||||
|
|
@ -220,48 +267,48 @@ public class SampleChooserActivity extends AppCompatActivity
|
|||
return menuItem != null && menuItem.isChecked();
|
||||
}
|
||||
|
||||
private final class SampleListLoader extends AsyncTask<String, Void, List<SampleGroup>> {
|
||||
private final class SampleListLoader extends AsyncTask<String, Void, List<PlaylistGroup>> {
|
||||
|
||||
private boolean sawError;
|
||||
|
||||
@Override
|
||||
protected List<SampleGroup> doInBackground(String... uris) {
|
||||
List<SampleGroup> result = new ArrayList<>();
|
||||
protected List<PlaylistGroup> doInBackground(String... uris) {
|
||||
List<PlaylistGroup> result = new ArrayList<>();
|
||||
Context context = getApplicationContext();
|
||||
String userAgent = Util.getUserAgent(context, "ExoPlayerDemo");
|
||||
DataSource dataSource =
|
||||
new DefaultDataSource(context, userAgent, /* allowCrossProtocolRedirects= */ false);
|
||||
DataSource dataSource = DemoUtil.getDataSourceFactory(context).createDataSource();
|
||||
for (String uri : uris) {
|
||||
DataSpec dataSpec = new DataSpec(Uri.parse(uri));
|
||||
InputStream inputStream = new DataSourceInputStream(dataSource, dataSpec);
|
||||
try {
|
||||
readSampleGroups(new JsonReader(new InputStreamReader(inputStream, "UTF-8")), result);
|
||||
readPlaylistGroups(new JsonReader(new InputStreamReader(inputStream, "UTF-8")), result);
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "Error loading sample list: " + uri, e);
|
||||
sawError = true;
|
||||
} finally {
|
||||
Util.closeQuietly(dataSource);
|
||||
DataSourceUtil.closeQuietly(dataSource);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(List<SampleGroup> result) {
|
||||
onSampleGroups(result, sawError);
|
||||
protected void onPostExecute(List<PlaylistGroup> result) {
|
||||
onPlaylistGroups(result, sawError);
|
||||
}
|
||||
|
||||
private void readSampleGroups(JsonReader reader, List<SampleGroup> groups) throws IOException {
|
||||
private void readPlaylistGroups(JsonReader reader, List<PlaylistGroup> groups)
|
||||
throws IOException {
|
||||
reader.beginArray();
|
||||
while (reader.hasNext()) {
|
||||
readSampleGroup(reader, groups);
|
||||
readPlaylistGroup(reader, groups);
|
||||
}
|
||||
reader.endArray();
|
||||
}
|
||||
|
||||
private void readSampleGroup(JsonReader reader, List<SampleGroup> groups) throws IOException {
|
||||
private void readPlaylistGroup(JsonReader reader, List<PlaylistGroup> groups)
|
||||
throws IOException {
|
||||
String groupName = "";
|
||||
ArrayList<Sample> samples = new ArrayList<>();
|
||||
ArrayList<PlaylistHolder> playlistHolders = new ArrayList<>();
|
||||
|
||||
reader.beginObject();
|
||||
while (reader.hasNext()) {
|
||||
|
|
@ -273,7 +320,7 @@ public class SampleChooserActivity extends AppCompatActivity
|
|||
case "samples":
|
||||
reader.beginArray();
|
||||
while (reader.hasNext()) {
|
||||
samples.add(readEntry(reader, false));
|
||||
playlistHolders.add(readEntry(reader, false));
|
||||
}
|
||||
reader.endArray();
|
||||
break;
|
||||
|
|
@ -281,33 +328,38 @@ public class SampleChooserActivity extends AppCompatActivity
|
|||
reader.nextString(); // Ignore.
|
||||
break;
|
||||
default:
|
||||
throw new ParserException("Unsupported name: " + name);
|
||||
throw ParserException.createForMalformedManifest(
|
||||
"Unsupported name: " + name, /* cause= */ null);
|
||||
}
|
||||
}
|
||||
reader.endObject();
|
||||
|
||||
SampleGroup group = getGroup(groupName, groups);
|
||||
group.samples.addAll(samples);
|
||||
PlaylistGroup group = getGroup(groupName, groups);
|
||||
group.playlists.addAll(playlistHolders);
|
||||
}
|
||||
|
||||
private Sample readEntry(JsonReader reader, boolean insidePlaylist) throws IOException {
|
||||
String sampleName = null;
|
||||
private PlaylistHolder readEntry(JsonReader reader, boolean insidePlaylist) throws IOException {
|
||||
Uri uri = null;
|
||||
String extension = null;
|
||||
String drmScheme = null;
|
||||
String drmLicenseUrl = null;
|
||||
String[] drmKeyRequestProperties = null;
|
||||
String title = null;
|
||||
ArrayList<PlaylistHolder> children = null;
|
||||
Uri subtitleUri = null;
|
||||
String subtitleMimeType = null;
|
||||
String subtitleLanguage = null;
|
||||
UUID drmUuid = null;
|
||||
String drmLicenseUri = null;
|
||||
ImmutableMap<String, String> drmLicenseRequestHeaders = ImmutableMap.of();
|
||||
boolean drmSessionForClearContent = false;
|
||||
boolean drmMultiSession = false;
|
||||
ArrayList<UriSample> playlistSamples = null;
|
||||
String adTagUri = null;
|
||||
String sphericalStereoMode = null;
|
||||
boolean drmForceDefaultLicenseUri = false;
|
||||
|
||||
MediaItem.Builder mediaItem = new MediaItem.Builder();
|
||||
reader.beginObject();
|
||||
while (reader.hasNext()) {
|
||||
String name = reader.nextName();
|
||||
switch (name) {
|
||||
case "name":
|
||||
sampleName = reader.nextString();
|
||||
title = reader.nextString();
|
||||
break;
|
||||
case "uri":
|
||||
uri = Uri.parse(reader.nextString());
|
||||
|
|
@ -315,98 +367,143 @@ public class SampleChooserActivity extends AppCompatActivity
|
|||
case "extension":
|
||||
extension = reader.nextString();
|
||||
break;
|
||||
case "drm_scheme":
|
||||
drmScheme = reader.nextString();
|
||||
case "clip_start_position_ms":
|
||||
mediaItem.setClipStartPositionMs(reader.nextLong());
|
||||
break;
|
||||
case "drm_license_url":
|
||||
drmLicenseUrl = reader.nextString();
|
||||
case "clip_end_position_ms":
|
||||
mediaItem.setClipEndPositionMs(reader.nextLong());
|
||||
break;
|
||||
case "ad_tag_uri":
|
||||
mediaItem.setAdsConfiguration(
|
||||
new MediaItem.AdsConfiguration.Builder(Uri.parse(reader.nextString())).build());
|
||||
break;
|
||||
case "drm_scheme":
|
||||
drmUuid = Util.getDrmUuid(reader.nextString());
|
||||
break;
|
||||
case "drm_license_uri":
|
||||
case "drm_license_url": // For backward compatibility only.
|
||||
drmLicenseUri = reader.nextString();
|
||||
break;
|
||||
case "drm_key_request_properties":
|
||||
ArrayList<String> drmKeyRequestPropertiesList = new ArrayList<>();
|
||||
Map<String, String> requestHeaders = new HashMap<>();
|
||||
reader.beginObject();
|
||||
while (reader.hasNext()) {
|
||||
drmKeyRequestPropertiesList.add(reader.nextName());
|
||||
drmKeyRequestPropertiesList.add(reader.nextString());
|
||||
requestHeaders.put(reader.nextName(), reader.nextString());
|
||||
}
|
||||
reader.endObject();
|
||||
drmKeyRequestProperties = drmKeyRequestPropertiesList.toArray(new String[0]);
|
||||
drmLicenseRequestHeaders = ImmutableMap.copyOf(requestHeaders);
|
||||
break;
|
||||
case "drm_session_for_clear_content":
|
||||
drmSessionForClearContent = reader.nextBoolean();
|
||||
break;
|
||||
case "drm_multi_session":
|
||||
drmMultiSession = reader.nextBoolean();
|
||||
break;
|
||||
case "drm_force_default_license_uri":
|
||||
drmForceDefaultLicenseUri = reader.nextBoolean();
|
||||
break;
|
||||
case "subtitle_uri":
|
||||
subtitleUri = Uri.parse(reader.nextString());
|
||||
break;
|
||||
case "subtitle_mime_type":
|
||||
subtitleMimeType = reader.nextString();
|
||||
break;
|
||||
case "subtitle_language":
|
||||
subtitleLanguage = reader.nextString();
|
||||
break;
|
||||
case "playlist":
|
||||
Assertions.checkState(!insidePlaylist, "Invalid nesting of playlists");
|
||||
playlistSamples = new ArrayList<>();
|
||||
checkState(!insidePlaylist, "Invalid nesting of playlists");
|
||||
children = new ArrayList<>();
|
||||
reader.beginArray();
|
||||
while (reader.hasNext()) {
|
||||
playlistSamples.add((UriSample) readEntry(reader, true));
|
||||
children.add(readEntry(reader, /* insidePlaylist= */ true));
|
||||
}
|
||||
reader.endArray();
|
||||
break;
|
||||
case "ad_tag_uri":
|
||||
adTagUri = reader.nextString();
|
||||
break;
|
||||
case "spherical_stereo_mode":
|
||||
Assertions.checkState(
|
||||
!insidePlaylist, "Invalid attribute on nested item: spherical_stereo_mode");
|
||||
sphericalStereoMode = reader.nextString();
|
||||
break;
|
||||
default:
|
||||
throw new ParserException("Unsupported attribute name: " + name);
|
||||
throw ParserException.createForMalformedManifest(
|
||||
"Unsupported attribute name: " + name, /* cause= */ null);
|
||||
}
|
||||
}
|
||||
reader.endObject();
|
||||
DrmInfo drmInfo =
|
||||
drmScheme == null
|
||||
? null
|
||||
: new DrmInfo(
|
||||
Util.getDrmUuid(drmScheme),
|
||||
drmLicenseUrl,
|
||||
drmKeyRequestProperties,
|
||||
drmMultiSession);
|
||||
if (playlistSamples != null) {
|
||||
UriSample[] playlistSamplesArray = playlistSamples.toArray(new UriSample[0]);
|
||||
return new PlaylistSample(sampleName, playlistSamplesArray);
|
||||
|
||||
if (children != null) {
|
||||
List<MediaItem> mediaItems = new ArrayList<>();
|
||||
for (int i = 0; i < children.size(); i++) {
|
||||
mediaItems.addAll(children.get(i).mediaItems);
|
||||
}
|
||||
return new PlaylistHolder(title, mediaItems);
|
||||
} else {
|
||||
return new UriSample(
|
||||
sampleName,
|
||||
drmInfo,
|
||||
uri,
|
||||
extension,
|
||||
adTagUri != null ? Uri.parse(adTagUri) : null,
|
||||
sphericalStereoMode);
|
||||
@Nullable
|
||||
String adaptiveMimeType =
|
||||
Util.getAdaptiveMimeTypeForContentType(Util.inferContentType(uri, extension));
|
||||
mediaItem
|
||||
.setUri(uri)
|
||||
.setMediaMetadata(new MediaMetadata.Builder().setTitle(title).build())
|
||||
.setMimeType(adaptiveMimeType);
|
||||
if (drmUuid != null) {
|
||||
mediaItem.setDrmConfiguration(
|
||||
new MediaItem.DrmConfiguration.Builder(drmUuid)
|
||||
.setLicenseUri(drmLicenseUri)
|
||||
.setLicenseRequestHeaders(drmLicenseRequestHeaders)
|
||||
.forceSessionsForAudioAndVideoTracks(drmSessionForClearContent)
|
||||
.setMultiSession(drmMultiSession)
|
||||
.setForceDefaultLicenseUri(drmForceDefaultLicenseUri)
|
||||
.build());
|
||||
} else {
|
||||
checkState(drmLicenseUri == null, "drm_uuid is required if drm_license_uri is set.");
|
||||
checkState(
|
||||
drmLicenseRequestHeaders.isEmpty(),
|
||||
"drm_uuid is required if drm_key_request_properties is set.");
|
||||
checkState(
|
||||
!drmSessionForClearContent,
|
||||
"drm_uuid is required if drm_session_for_clear_content is set.");
|
||||
checkState(!drmMultiSession, "drm_uuid is required if drm_multi_session is set.");
|
||||
checkState(
|
||||
!drmForceDefaultLicenseUri,
|
||||
"drm_uuid is required if drm_force_default_license_uri is set.");
|
||||
}
|
||||
if (subtitleUri != null) {
|
||||
MediaItem.Subtitle subtitle =
|
||||
new MediaItem.Subtitle(
|
||||
subtitleUri,
|
||||
checkNotNull(
|
||||
subtitleMimeType, "subtitle_mime_type is required if subtitle_uri is set."),
|
||||
subtitleLanguage);
|
||||
mediaItem.setSubtitles(Collections.singletonList(subtitle));
|
||||
}
|
||||
return new PlaylistHolder(title, Collections.singletonList(mediaItem.build()));
|
||||
}
|
||||
}
|
||||
|
||||
private SampleGroup getGroup(String groupName, List<SampleGroup> groups) {
|
||||
private PlaylistGroup getGroup(String groupName, List<PlaylistGroup> groups) {
|
||||
for (int i = 0; i < groups.size(); i++) {
|
||||
if (Util.areEqual(groupName, groups.get(i).title)) {
|
||||
return groups.get(i);
|
||||
}
|
||||
}
|
||||
SampleGroup group = new SampleGroup(groupName);
|
||||
PlaylistGroup group = new PlaylistGroup(groupName);
|
||||
groups.add(group);
|
||||
return group;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private final class SampleAdapter extends BaseExpandableListAdapter implements OnClickListener {
|
||||
|
||||
private List<SampleGroup> sampleGroups;
|
||||
private List<PlaylistGroup> playlistGroups;
|
||||
|
||||
public SampleAdapter() {
|
||||
sampleGroups = Collections.emptyList();
|
||||
playlistGroups = Collections.emptyList();
|
||||
}
|
||||
|
||||
public void setSampleGroups(List<SampleGroup> sampleGroups) {
|
||||
this.sampleGroups = sampleGroups;
|
||||
public void setPlaylistGroups(List<PlaylistGroup> playlistGroups) {
|
||||
this.playlistGroups = playlistGroups;
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Sample getChild(int groupPosition, int childPosition) {
|
||||
return getGroup(groupPosition).samples.get(childPosition);
|
||||
public PlaylistHolder getChild(int groupPosition, int childPosition) {
|
||||
return getGroup(groupPosition).playlists.get(childPosition);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -415,8 +512,12 @@ public class SampleChooserActivity extends AppCompatActivity
|
|||
}
|
||||
|
||||
@Override
|
||||
public View getChildView(int groupPosition, int childPosition, boolean isLastChild,
|
||||
View convertView, ViewGroup parent) {
|
||||
public View getChildView(
|
||||
int groupPosition,
|
||||
int childPosition,
|
||||
boolean isLastChild,
|
||||
View convertView,
|
||||
ViewGroup parent) {
|
||||
View view = convertView;
|
||||
if (view == null) {
|
||||
view = getLayoutInflater().inflate(R.layout.sample_list_item, parent, false);
|
||||
|
|
@ -430,12 +531,12 @@ public class SampleChooserActivity extends AppCompatActivity
|
|||
|
||||
@Override
|
||||
public int getChildrenCount(int groupPosition) {
|
||||
return getGroup(groupPosition).samples.size();
|
||||
return getGroup(groupPosition).playlists.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public SampleGroup getGroup(int groupPosition) {
|
||||
return sampleGroups.get(groupPosition);
|
||||
public PlaylistGroup getGroup(int groupPosition) {
|
||||
return playlistGroups.get(groupPosition);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -444,8 +545,8 @@ public class SampleChooserActivity extends AppCompatActivity
|
|||
}
|
||||
|
||||
@Override
|
||||
public View getGroupView(int groupPosition, boolean isExpanded, View convertView,
|
||||
ViewGroup parent) {
|
||||
public View getGroupView(
|
||||
int groupPosition, boolean isExpanded, View convertView, ViewGroup parent) {
|
||||
View view = convertView;
|
||||
if (view == null) {
|
||||
view =
|
||||
|
|
@ -458,7 +559,7 @@ public class SampleChooserActivity extends AppCompatActivity
|
|||
|
||||
@Override
|
||||
public int getGroupCount() {
|
||||
return sampleGroups.size();
|
||||
return playlistGroups.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -473,34 +574,46 @@ public class SampleChooserActivity extends AppCompatActivity
|
|||
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
onSampleDownloadButtonClicked((Sample) view.getTag());
|
||||
onSampleDownloadButtonClicked((PlaylistHolder) view.getTag());
|
||||
}
|
||||
|
||||
private void initializeChildView(View view, Sample sample) {
|
||||
view.setTag(sample);
|
||||
private void initializeChildView(View view, PlaylistHolder playlistHolder) {
|
||||
view.setTag(playlistHolder);
|
||||
TextView sampleTitle = view.findViewById(R.id.sample_title);
|
||||
sampleTitle.setText(sample.name);
|
||||
sampleTitle.setText(playlistHolder.title);
|
||||
|
||||
boolean canDownload = getDownloadUnsupportedStringId(sample) == 0;
|
||||
boolean isDownloaded = canDownload && downloadTracker.isDownloaded(((UriSample) sample).uri);
|
||||
boolean canDownload = getDownloadUnsupportedStringId(playlistHolder) == 0;
|
||||
boolean isDownloaded =
|
||||
canDownload && downloadTracker.isDownloaded(playlistHolder.mediaItems.get(0));
|
||||
ImageButton downloadButton = view.findViewById(R.id.download_button);
|
||||
downloadButton.setTag(sample);
|
||||
downloadButton.setTag(playlistHolder);
|
||||
downloadButton.setColorFilter(
|
||||
canDownload ? (isDownloaded ? 0xFF42A5F5 : 0xFFBDBDBD) : 0xFFEEEEEE);
|
||||
canDownload ? (isDownloaded ? 0xFF42A5F5 : 0xFFBDBDBD) : 0xFF666666);
|
||||
downloadButton.setImageResource(
|
||||
isDownloaded ? R.drawable.ic_download_done : R.drawable.ic_download);
|
||||
}
|
||||
}
|
||||
|
||||
private static final class SampleGroup {
|
||||
private static final class PlaylistHolder {
|
||||
|
||||
public final String title;
|
||||
public final List<Sample> samples;
|
||||
public final List<MediaItem> mediaItems;
|
||||
|
||||
public SampleGroup(String title) {
|
||||
private PlaylistHolder(String title, List<MediaItem> mediaItems) {
|
||||
checkArgument(!mediaItems.isEmpty());
|
||||
this.title = title;
|
||||
this.samples = new ArrayList<>();
|
||||
this.mediaItems = Collections.unmodifiableList(new ArrayList<>(mediaItems));
|
||||
}
|
||||
}
|
||||
|
||||
private static final class PlaylistGroup {
|
||||
|
||||
public final String title;
|
||||
public final List<PlaylistHolder> playlists;
|
||||
|
||||
public PlaylistGroup(String title) {
|
||||
this.title = title;
|
||||
this.playlists = new ArrayList<>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,18 +19,18 @@ import android.app.Dialog;
|
|||
import android.content.DialogInterface;
|
||||
import android.content.res.Resources;
|
||||
import android.os.Bundle;
|
||||
import androidx.annotation.Nullable;
|
||||
import com.google.android.material.tabs.TabLayout;
|
||||
import androidx.fragment.app.DialogFragment;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
import androidx.fragment.app.FragmentPagerAdapter;
|
||||
import androidx.appcompat.app.AppCompatDialog;
|
||||
import android.util.SparseArray;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.Button;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AppCompatDialog;
|
||||
import androidx.fragment.app.DialogFragment;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
import androidx.fragment.app.FragmentPagerAdapter;
|
||||
import androidx.viewpager.widget.ViewPager;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.source.TrackGroupArray;
|
||||
|
|
@ -39,6 +39,7 @@ import com.google.android.exoplayer2.trackselection.DefaultTrackSelector.Selecti
|
|||
import com.google.android.exoplayer2.trackselection.MappingTrackSelector.MappedTrackInfo;
|
||||
import com.google.android.exoplayer2.ui.TrackSelectionView;
|
||||
import com.google.android.exoplayer2.util.Assertions;
|
||||
import com.google.android.material.tabs.TabLayout;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
|
@ -93,7 +94,7 @@ public final class TrackSelectionDialog extends DialogFragment {
|
|||
/* titleId= */ R.string.track_selection_title,
|
||||
mappedTrackInfo,
|
||||
/* initialParameters = */ parameters,
|
||||
/* allowAdaptiveSelections =*/ true,
|
||||
/* allowAdaptiveSelections= */ true,
|
||||
/* allowMultipleOverrides= */ false,
|
||||
/* onClickListener= */ (dialog, which) -> {
|
||||
DefaultTrackSelector.ParametersBuilder builder = parameters.buildUpon();
|
||||
|
|
@ -212,6 +213,7 @@ public final class TrackSelectionDialog extends DialogFragment {
|
|||
}
|
||||
|
||||
@Override
|
||||
@NonNull
|
||||
public Dialog onCreateDialog(Bundle savedInstanceState) {
|
||||
// We need to own the view to let tab layout work correctly on all API levels. We can't use
|
||||
// AlertDialog because it owns the view itself, so we use AppCompatDialog instead, themed using
|
||||
|
|
@ -223,16 +225,14 @@ public final class TrackSelectionDialog extends DialogFragment {
|
|||
}
|
||||
|
||||
@Override
|
||||
public void onDismiss(DialogInterface dialog) {
|
||||
public void onDismiss(@NonNull DialogInterface dialog) {
|
||||
super.onDismiss(dialog);
|
||||
onDismissListener.onDismiss(dialog);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public View onCreateView(
|
||||
LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
|
||||
|
||||
View dialogView = inflater.inflate(R.layout.track_selection_dialog, container, false);
|
||||
TabLayout tabLayout = dialogView.findViewById(R.id.track_selection_dialog_tab_layout);
|
||||
ViewPager viewPager = dialogView.findViewById(R.id.track_selection_dialog_view_pager);
|
||||
|
|
@ -286,10 +286,11 @@ public final class TrackSelectionDialog extends DialogFragment {
|
|||
private final class FragmentAdapter extends FragmentPagerAdapter {
|
||||
|
||||
public FragmentAdapter(FragmentManager fragmentManager) {
|
||||
super(fragmentManager);
|
||||
super(fragmentManager, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT);
|
||||
}
|
||||
|
||||
@Override
|
||||
@NonNull
|
||||
public Fragment getItem(int position) {
|
||||
return tabFragments.valueAt(position);
|
||||
}
|
||||
|
|
@ -299,7 +300,6 @@ public final class TrackSelectionDialog extends DialogFragment {
|
|||
return tabFragments.size();
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public CharSequence getPageTitle(int position) {
|
||||
return getTrackTypeString(getResources(), tabTrackTypes.get(position));
|
||||
|
|
@ -341,7 +341,6 @@ public final class TrackSelectionDialog extends DialogFragment {
|
|||
this.allowMultipleOverrides = allowMultipleOverrides;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public View onCreateView(
|
||||
LayoutInflater inflater,
|
||||
|
|
@ -355,12 +354,18 @@ public final class TrackSelectionDialog extends DialogFragment {
|
|||
trackSelectionView.setAllowMultipleOverrides(allowMultipleOverrides);
|
||||
trackSelectionView.setAllowAdaptiveSelections(allowAdaptiveSelections);
|
||||
trackSelectionView.init(
|
||||
mappedTrackInfo, rendererIndex, isDisabled, overrides, /* listener= */ this);
|
||||
mappedTrackInfo,
|
||||
rendererIndex,
|
||||
isDisabled,
|
||||
overrides,
|
||||
/* trackFormatComparator= */ null,
|
||||
/* listener= */ this);
|
||||
return rootView;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTrackSelectionChanged(boolean isDisabled, List<SelectionOverride> overrides) {
|
||||
public void onTrackSelectionChanged(
|
||||
boolean isDisabled, @NonNull List<SelectionOverride> overrides) {
|
||||
this.isDisabled = isDisabled;
|
||||
this.overrides = overrides;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 com.google.android.exoplayer2.demo;
|
||||
|
||||
import com.google.android.exoplayer2.util.NonNullApi;
|
||||
|
|
@ -15,14 +15,17 @@
|
|||
-->
|
||||
<FrameLayout 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:id="@+id/root"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:keepScreenOn="true">
|
||||
|
||||
<com.google.android.exoplayer2.ui.PlayerView android:id="@+id/player_view"
|
||||
<com.google.android.exoplayer2.ui.StyledPlayerView android:id="@+id/player_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"/>
|
||||
android:layout_height="match_parent"
|
||||
app:show_shuffle_button="true"
|
||||
app:show_subtitle_button="true"/>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
|
|
|
|||
|
|
@ -19,8 +19,4 @@
|
|||
android:title="@string/prefer_extension_decoders"
|
||||
android:checkable="true"
|
||||
app:showAsAction="never"/>
|
||||
<item android:id="@+id/random_abr"
|
||||
android:title="@string/random_abr"
|
||||
android:checkable="true"
|
||||
app:showAsAction="never"/>
|
||||
</menu>
|
||||
|
|
|
|||
|
|
@ -21,20 +21,14 @@
|
|||
|
||||
<string name="unexpected_intent_action">Unexpected intent action: <xliff:g id="action">%1$s</xliff:g></string>
|
||||
|
||||
<string name="error_cleartext_not_permitted">Cleartext traffic not permitted</string>
|
||||
<string name="error_cleartext_not_permitted">Cleartext HTTP traffic not permitted. See https://exoplayer.dev/issues/cleartext-not-permitted</string>
|
||||
|
||||
<string name="error_generic">Playback failed</string>
|
||||
|
||||
<string name="error_unrecognized_abr_algorithm">Unrecognized ABR algorithm</string>
|
||||
|
||||
<string name="error_unrecognized_stereo_mode">Unrecognized stereo mode</string>
|
||||
|
||||
<string name="error_drm_not_supported">Protected content not supported on API levels below 18</string>
|
||||
<string name="error_drm_unsupported_before_api_18">DRM content not supported on API levels below 18</string>
|
||||
|
||||
<string name="error_drm_unsupported_scheme">This device does not support the required DRM scheme</string>
|
||||
|
||||
<string name="error_drm_unknown">An unknown DRM error occurred</string>
|
||||
|
||||
<string name="error_no_decoder">This device does not provide a decoder for <xliff:g id="mime_type">%1$s</xliff:g></string>
|
||||
|
||||
<string name="error_no_secure_decoder">This device does not provide a secure decoder for <xliff:g id="mime_type">%1$s</xliff:g></string>
|
||||
|
|
@ -51,22 +45,18 @@
|
|||
|
||||
<string name="sample_list_load_error">One or more sample lists failed to load</string>
|
||||
|
||||
<string name="ima_not_loaded">Playing sample without ads, as the IMA extension was not loaded</string>
|
||||
|
||||
<string name="unsupported_ads_in_concatenation">Playing sample without ads, as ads are not supported in concatenations</string>
|
||||
|
||||
<string name="download_start_error">Failed to start download</string>
|
||||
|
||||
<string name="download_start_error_offline_license">Failed to obtain offline license</string>
|
||||
|
||||
<string name="download_playlist_unsupported">This demo app does not support downloading playlists</string>
|
||||
|
||||
<string name="download_drm_unsupported">This demo app does not support downloading protected content</string>
|
||||
|
||||
<string name="download_scheme_unsupported">This demo app only supports downloading http streams</string>
|
||||
|
||||
<string name="download_live_unsupported">This demo app does not support downloading live content</string>
|
||||
|
||||
<string name="download_ads_unsupported">IMA does not support offline ads</string>
|
||||
|
||||
<string name="prefer_extension_decoders">Prefer extension decoders</string>
|
||||
|
||||
<string name="random_abr">Enable random ABR</string>
|
||||
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -23,8 +23,4 @@
|
|||
<item name="android:windowBackground">@android:color/black</item>
|
||||
</style>
|
||||
|
||||
<style name="PlayerTheme.Spherical">
|
||||
<item name="surface_type">spherical_view</item>
|
||||
</style>
|
||||
|
||||
</resources>
|
||||
|
|
|
|||
24
demos/surface/README.md
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
# ExoPlayer SurfaceControl demo
|
||||
|
||||
This app demonstrates how to use the [SurfaceControl][] API to redirect video
|
||||
output from ExoPlayer between different views or off-screen. `SurfaceControl`
|
||||
is new in Android 10, so the app requires `minSdkVersion` 29.
|
||||
|
||||
The app layout has a grid of `SurfaceViews`. Initially video is output to one
|
||||
of the views. Tap a `SurfaceView` to move video output to it. You can also tap
|
||||
the buttons at the top of the activity to move video output off-screen, to a
|
||||
full-screen `SurfaceView` or to a new activity.
|
||||
|
||||
When using `SurfaceControl`, the `MediaCodec` always has the same surface
|
||||
attached to it, which can be freely 'reparented' to any `SurfaceView` (or
|
||||
off-screen) without any interruptions to playback. This works better than
|
||||
calling `MediaCodec.setOutputSurface` to change the output surface of the codec
|
||||
because `MediaCodec` does not re-render its last frame when that method is
|
||||
called, and because you can move output off-screen easily (`setOutputSurface`
|
||||
can't take a `null` surface, so the player has to use a `DummySurface`, which
|
||||
doesn't handle protected output on all devices).
|
||||
|
||||
See the [demos README](../README.md) for instructions on how to build and run
|
||||
this demo.
|
||||
|
||||
[SurfaceControl]: https://developer.android.com/reference/android/view/SurfaceControl
|
||||
55
demos/surface/build.gradle
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
// Copyright (C) 2019 The Android Open Source Project
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
apply from: '../../constants.gradle'
|
||||
apply plugin: 'com.android.application'
|
||||
|
||||
android {
|
||||
compileSdkVersion project.ext.compileSdkVersion
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
versionName project.ext.releaseVersion
|
||||
versionCode project.ext.releaseVersionCode
|
||||
minSdkVersion 29
|
||||
targetSdkVersion project.ext.appTargetSdkVersion
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
shrinkResources true
|
||||
minifyEnabled true
|
||||
proguardFiles getDefaultProguardFile('proguard-android.txt')
|
||||
signingConfig signingConfigs.debug
|
||||
}
|
||||
}
|
||||
|
||||
lintOptions {
|
||||
// This demo app does not have translations.
|
||||
disable 'MissingTranslation'
|
||||
}
|
||||
}
|
||||
|
||||
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 'androidx.annotation:annotation:' + androidxAnnotationVersion
|
||||
}
|
||||
48
demos/surface/src/main/AndroidManifest.xml
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Copyright (C) 2019 The Android Open Source Project
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.google.android.exoplayer2.surfacedemo">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
|
||||
|
||||
<uses-sdk/>
|
||||
|
||||
<application
|
||||
android:allowBackup="false"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/application_name"
|
||||
android:exported="true">
|
||||
|
||||
<activity android:name=".MainActivity">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN"/>
|
||||
<category android:name="android.intent.category.LAUNCHER"/>
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="com.google.android.exoplayer.surfacedemo.action.VIEW"/>
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
<data android:scheme="http"/>
|
||||
<data android:scheme="https"/>
|
||||
<data android:scheme="content"/>
|
||||
<data android:scheme="asset"/>
|
||||
<data android:scheme="file"/>
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
|
|
@ -0,0 +1,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 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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 com.google.android.exoplayer2.surfacedemo;
|
||||
|
||||
import com.google.android.exoplayer2.util.NonNullApi;
|
||||
51
demos/surface/src/main/res/layout/main_activity.xml
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Copyright (C) 2019 The Android Open Source Project
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
<FrameLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:keepScreenOn="true">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical">
|
||||
|
||||
<GridLayout
|
||||
android:id="@+id/grid_layout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1"
|
||||
android:columnCount="3"/>
|
||||
|
||||
<com.google.android.exoplayer2.ui.PlayerControlView
|
||||
android:id="@+id/player_control_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="bottom"
|
||||
app:show_timeout="0"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<SurfaceView
|
||||
android:id="@+id/full_screen_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:visibility="gone"/>
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
|
Before Width: | Height: | Size: 3.3 KiB After Width: | Height: | Size: 3.3 KiB |
|
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 4.8 KiB After Width: | Height: | Size: 4.8 KiB |
|
Before Width: | Height: | Size: 7.3 KiB After Width: | Height: | Size: 7.3 KiB |
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 10 KiB |
23
demos/surface/src/main/res/values/strings.xml
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Copyright (C) 2019 The Android Open Source Project
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
<resources>
|
||||
|
||||
<string name="application_name">ExoPlayer SurfaceControl demo</string>
|
||||
<string name="no_output_label">No output</string>
|
||||
<string name="full_screen_label">Full screen</string>
|
||||
<string name="new_activity_label">New activity</string>
|
||||
|
||||
</resources>
|
||||
9
docs/.hgignore
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
# 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
|
||||
3
docs/404.html
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
---
|
||||
layout: 404
|
||||
---
|
||||
1
docs/CNAME
Normal file
|
|
@ -0,0 +1 @@
|
|||
exoplayer.dev
|
||||
6
docs/Gemfile
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
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
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
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.
|
||||
12
docs/README.md
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
# 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.
|
||||
95
docs/_config.yml
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
## => 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
|
||||
111
docs/_data/locale.yml
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
## => 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
|
||||
83
docs/_data/navigation.yml
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
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
|
||||
64
docs/_data/variables.yml
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
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'
|
||||
3
docs/_includes/analytics-providers/custom.html
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
<!-- start custom analytics snippet -->
|
||||
|
||||
<!-- end custom analytics snippet -->
|
||||
14
docs/_includes/analytics-providers/google.html
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
{%- 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 -%}
|
||||
7
docs/_includes/analytics.html
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
{%- 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 -%}
|
||||
55
docs/_includes/article-footer.html
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
{%- 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>
|
||||
49
docs/_includes/article-header.html
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
{%- 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 -%}
|
||||
96
docs/_includes/article-info.html
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
{%- 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 -%}
|
||||