diff --git a/.github/ISSUE_TEMPLATE/bug.md b/.github/ISSUE_TEMPLATE/bug.md
new file mode 100644
index 0000000000..8824c9e8d8
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/bug.md
@@ -0,0 +1,57 @@
+---
+name: Bug report
+about: Issue template for a bug report.
+title: ''
+labels: bug, needs triage
+assignees: ''
+---
+
+Before filing a bug:
+-----------------------
+- Search existing issues, including issues that are closed:
+ https://github.com/google/ExoPlayer/issues?q=is%3Aissue
+- Consult our developer website, which can be found at https://exoplayer.dev/.
+ It provides detailed information about supported formats and devices.
+- Learn how to create useful log output by using the EventLogger:
+ https://exoplayer.dev/listening-to-player-events.html#using-eventlogger
+- Rule out issues in your own code. A good way to do this is to try and
+ reproduce the issue in the ExoPlayer demo app. Information about the ExoPlayer
+ demo app can be found here:
+ http://exoplayer.dev/demo-application.html.
+
+When reporting a bug:
+-----------------------
+Fill out the sections below, leaving the headers but replacing the content. If
+you're unable to provide certain information, please explain why in the relevant
+section. We may close issues if they do not include sufficient information.
+
+### [REQUIRED] Issue description
+Describe the issue in detail, including observed and expected behavior.
+
+### [REQUIRED] Reproduction steps
+Describe how the issue can be reproduced, ideally using the ExoPlayer demo app
+or a small sample app that you’re able to share as source code on GitHub.
+
+### [REQUIRED] Link to test content
+Provide a JSON snippet for the demo app’s media.exolist.json file, or a link to
+media that reproduces the issue. If you don't wish to post it publicly, please
+submit the issue, then email the link to dev.exoplayer@gmail.com using a subject
+in the format "Issue #1234", where "#1234" should be replaced with your issue
+number. Provide all the metadata we'd need to play the content like drm license
+urls or similar. If the content is accessible only in certain countries or
+regions, please say so.
+
+### [REQUIRED] A full bug report captured from the device
+Capture a full bug report using "adb bugreport". Output from "adb logcat" or a
+log snippet is NOT sufficient. Please attach the captured bug report as a file.
+If you don't wish to post it publicly, please submit the issue, then email the
+bug report to dev.exoplayer@gmail.com using a subject in the format
+"Issue #1234", where "#1234" should be replaced with your issue number.
+
+### [REQUIRED] Version of ExoPlayer being used
+Specify the absolute version number. Avoid using terms such as "latest".
+
+### [REQUIRED] Device(s) and version(s) of Android being used
+Specify the devices and versions of Android on which the issue can be
+reproduced, and how easily it reproduces. If possible, please test on multiple
+devices and Android versions.
diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md
new file mode 100644
index 0000000000..d660d0342a
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/feature_request.md
@@ -0,0 +1,30 @@
+---
+name: Feature request
+about: Issue template for a feature request.
+title: ''
+labels: enhancement, needs triage
+assignees: ''
+---
+
+Before filing a feature request:
+-----------------------
+- Search existing open issues, specifically with the label ‘enhancement’:
+ https://github.com/google/ExoPlayer/labels/enhancement
+- Search existing pull requests: https://github.com/google/ExoPlayer/pulls
+
+When filing a feature request:
+-----------------------
+Fill out the sections below, leaving the headers but replacing the content. If
+you're unable to provide certain information, please explain why in the relevant
+section. We may close issues if they do not include sufficient information.
+
+### [REQUIRED] Use case description
+Describe the use case or problem you are trying to solve in detail. If there are
+any standards or specifications involved, please provide the relevant details.
+
+### Proposed solution
+A clear and concise description of your proposed solution, if you have one.
+
+### Alternatives considered
+A clear and concise description of any alternative solutions you considered,
+if applicable.
diff --git a/.github/ISSUE_TEMPLATE/question.md b/.github/ISSUE_TEMPLATE/question.md
new file mode 100644
index 0000000000..f3ad83b67d
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/question.md
@@ -0,0 +1,50 @@
+---
+name: Question
+about: Issue template for a question.
+title: ''
+labels: question, needs triage
+assignees: ''
+---
+
+Before filing a question:
+-----------------------
+- This issue tracker is intended ExoPlayer specific questions. If you're asking
+ a general Android development question, please do so on Stack Overflow.
+- Search existing issues, including issues that are closed. It’s often the
+ quickest way to get an answer!
+ https://github.com/google/ExoPlayer/issues?q=is%3Aissue
+- Consult our developer website, which can be found at https://exoplayer.dev/.
+ It provides detailed information about supported formats, devices as well as
+ information about how to use the ExoPlayer library.
+- The ExoPlayer library Javadoc can be found at
+ https://exoplayer.dev/doc/reference/
+
+When filing a question:
+-----------------------
+Fill out the sections below, leaving the headers but replacing the content. If
+you're unable to provide certain information, please explain why in the relevant
+section. We may close issues if they do not include sufficient information.
+
+### [REQUIRED] Searched documentation and issues
+Tell us where you’ve already looked for an answer to your question. It’s
+important for us to know this so that we can improve our documentation.
+
+### [REQUIRED] Question
+Describe your question in detail.
+
+### A full bug report captured from the device
+In case your question refers to a problem you are seeing in your app, capture a
+full bug report using "adb bugreport". Please attach the captured bug report as
+a file. If you don't wish to post it publicly, please submit the issue, then
+email the bug report to dev.exoplayer@gmail.com using a subject in the format
+"Issue #1234", where "#1234" should be replaced with your issue number.
+
+### Link to test content
+In case your question is related to a piece of media, which you are trying to
+play, please provide a JSON snippet for the demo app’s media.exolist.json file,
+or a link to media that reproduces the issue. If you don't wish to post it
+publicly, please submit the issue, then email the link to
+dev.exoplayer@gmail.com using a subject in the format "Issue #1234", where
+"#1234" should be replaced with your issue number. Provide all the metadata we'd
+need to play the content like drm license urls or similar. If the content is
+accessible only in certain countries or regions, please say so.
diff --git a/.gitignore b/.gitignore
index 1146c06456..790a44c22f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -37,16 +37,29 @@ local.properties
proguard.cfg
proguard-project.txt
+# Bazel
+bazel-bin
+bazel-genfiles
+bazel-out
+bazel-testlogs
+
# Other
.DS_Store
+cmake-build-debug
dist
tmp
+# External native builds
+.externalNativeBuild
+
# VP9 extension
extensions/vp9/src/main/jni/libvpx
extensions/vp9/src/main/jni/libvpx_android_configs
extensions/vp9/src/main/jni/libyuv
+# AV1 extension
+extensions/av1/src/main/jni/libgav1
+
# Opus extension
extensions/opus/src/main/jni/libopus
diff --git a/.hgignore b/.hgignore
new file mode 100644
index 0000000000..7819a90ac5
--- /dev/null
+++ b/.hgignore
@@ -0,0 +1,81 @@
+# Mercurial's .hgignore files can only be used in the root directory.
+# You can still apply these rules by adding
+# include:path/to/this/directory/.hgignore to the top-level .hgignore file.
+
+# Ensure same syntax as in .gitignore can be used
+syntax:glob
+
+# Android generated
+bin
+gen
+libs
+obj
+lint.xml
+
+# IntelliJ IDEA & Android Studio
+.idea
+*.iml
+*.ipr
+*.iws
+classes
+gen-external-apklibs
+*.li
+
+# Eclipse
+.project
+.classpath
+.settings
+.checkstyle
+.cproject
+
+# Gradle
+.gradle
+build
+buildout
+out
+
+# Maven
+target
+release.properties
+pom.xml.*
+
+# Ant
+ant.properties
+local.properties
+proguard.cfg
+proguard-project.txt
+
+# Bazel
+bazel-bin
+bazel-genfiles
+bazel-out
+bazel-testlogs
+
+# Other
+.DS_Store
+cmake-build-debug
+dist
+tmp
+
+# VP9 extension
+extensions/vp9/src/main/jni/libvpx
+extensions/vp9/src/main/jni/libvpx_android_configs
+extensions/vp9/src/main/jni/libyuv
+
+# AV1 extension
+extensions/av1/src/main/jni/libgav1
+
+# Opus extension
+extensions/opus/src/main/jni/libopus
+
+# FLAC extension
+extensions/flac/src/main/jni/flac
+
+# FFmpeg extension
+extensions/ffmpeg/src/main/jni/ffmpeg
+
+# Cronet extension
+extensions/cronet/jniLibs/*
+!extensions/cronet/jniLibs/README.md
+extensions/cronet/libs/*
+!extensions/cronet/libs/README.md
diff --git a/.idea/codeStyleSettings.xml b/.idea/codeStyleSettings.xml
new file mode 100644
index 0000000000..056b47a1e8
--- /dev/null
+++ b/.idea/codeStyleSettings.xml
@@ -0,0 +1,495 @@
+
+
+
+
+
+
+
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 43c4809480..94b349b217 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -16,9 +16,8 @@ all of the information requested in the issue template.
## Pull requests ##
We will also consider high quality pull requests. These should normally merge
-into the `dev-vX` branch with the highest major version number. Bug fixes may
-be suitable for merging into older `dev-vX` branches. Before a pull request can
-be accepted you must submit a Contributor License Agreement, as described below.
+into the `dev-v2` branch. Before a pull request can be accepted you must submit
+a Contributor License Agreement, as described below.
[dev]: https://github.com/google/ExoPlayer/tree/dev
diff --git a/ISSUE_TEMPLATE b/ISSUE_TEMPLATE
deleted file mode 100644
index 1b912312d1..0000000000
--- a/ISSUE_TEMPLATE
+++ /dev/null
@@ -1,44 +0,0 @@
-*** ISSUES THAT IGNORE THIS TEMPLATE WILL BE CLOSED WITHOUT INVESTIGATION ***
-
-Before filing an issue:
------------------------
-- Search existing issues, including issues that are closed.
-- Consult our FAQs, supported devices and supported formats pages. These can be
- found at https://google.github.io/ExoPlayer/.
-- Rule out issues in your own code. A good way to do this is to try and
- reproduce the issue in the ExoPlayer demo app.
-- This issue tracker is intended for bugs, feature requests and ExoPlayer
- specific questions. If you're asking a general Android development question,
- please do so on Stack Overflow.
-
-When reporting a bug:
------------------------
-Fill out the sections below, leaving the headers but replacing the content. If
-you're unable to provide certain information, please explain why in the relevant
-section. We may close issues if they do not include sufficient information.
-
-### Issue description
-Describe the issue in detail, including observed and expected behavior.
-
-### Reproduction steps
-Describe how the issue can be reproduced, ideally using the ExoPlayer demo app.
-
-### Link to test content
-Provide a link to media that reproduces the issue. If you don't wish to post it
-publicly, please submit the issue, then email the link to
-dev.exoplayer@gmail.com including the issue number in the subject line.
-
-### Version of ExoPlayer being used
-Specify the absolute version number. Avoid using terms such as "latest".
-
-### Device(s) and version(s) of Android being used
-Specify the devices and versions of Android on which the issue can be
-reproduced, and how easily it reproduces. If possible, please test on multiple
-devices and Android versions.
-
-### A full bug report captured from the device
-Capture a full bug report using "adb bugreport". Output from "adb logcat" or a
-log snippet is NOT sufficient. Please attach the captured bug report as a file.
-If you don't wish to post it publicly, please submit the issue, then email the
-bug report to dev.exoplayer@gmail.com including the issue number in the subject
-line.
diff --git a/README.md b/README.md
index 3de86d21a3..d488f4113e 100644
--- a/README.md
+++ b/README.md
@@ -9,47 +9,61 @@ and extend, and can be updated through Play Store application updates.
## Documentation ##
-* The [developer guide][] provides a wealth of information to help you get
- started.
-* The [class reference][] documents the ExoPlayer library classes.
+* The [developer guide][] provides a wealth of information.
+* The [class reference][] documents ExoPlayer classes.
* The [release notes][] document the major changes in each release.
+* Follow our [developer blog][] to keep up to date with the latest ExoPlayer
+ developments!
-[developer guide]: https://google.github.io/ExoPlayer/guide.html
-[class reference]: https://google.github.io/ExoPlayer/doc/reference
-[release notes]: https://github.com/google/ExoPlayer/blob/dev-v2/RELEASENOTES.md
+[developer guide]: https://exoplayer.dev/guide.html
+[class reference]: https://exoplayer.dev/doc/reference
+[release notes]: https://github.com/google/ExoPlayer/blob/release-v2/RELEASENOTES.md
+[developer blog]: https://medium.com/google-exoplayer
## Using ExoPlayer ##
+ExoPlayer modules can be obtained from JCenter. It's also possible to clone the
+repository and depend on the modules locally.
+
+### From JCenter ###
+
+#### 1. Add repositories ####
+
The easiest way to get started using ExoPlayer is to add it as a gradle
-dependency. You need to make sure you have the jcenter repository included in
-the `build.gradle` file in the root of your project:
+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()
}
```
-Next add a gradle compile dependency to the `build.gradle` file of your app
-module. The following will add a dependency to the full ExoPlayer library:
+#### 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:
```gradle
-compile 'com.google.android.exoplayer:exoplayer:r2.X.X'
+implementation 'com.google.android.exoplayer:exoplayer:2.X.X'
```
-where `r2.X.X` is your preferred version. Alternatively, you can depend on only
-the library modules that you actually need. For example the following will add
-dependencies on the Core, DASH and UI library modules, as might be required for
-an app that plays DASH content:
+where `2.X.X` is your preferred version.
+
+As an alternative to the full library, you can depend on only the library
+modules that you actually need. For example the following will add dependencies
+on the Core, DASH and UI library modules, as might be required for an app that
+plays DASH content:
```gradle
-compile 'com.google.android.exoplayer:exoplayer-core:r2.X.X'
-compile 'com.google.android.exoplayer:exoplayer-dash:r2.X.X'
-compile 'com.google.android.exoplayer:exoplayer-ui:r2.X.X'
+implementation 'com.google.android.exoplayer:exoplayer-core:2.X.X'
+implementation 'com.google.android.exoplayer:exoplayer-dash:2.X.X'
+implementation 'com.google.android.exoplayer:exoplayer-ui:2.X.X'
```
-The available modules are listed below. Adding a dependency to the full
-ExoPlayer library is equivalent to adding dependencies on all of the modules
+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-core`: Core functionality (required).
@@ -58,25 +72,70 @@ individually.
* `exoplayer-smoothstreaming`: Support for SmoothStreaming content.
* `exoplayer-ui`: UI components and resources for use with ExoPlayer.
-For more details, see the project on [Bintray][]. For information about the
-latest versions, see the [Release notes][].
+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.
+Browse the [extensions directory][] and their individual READMEs for details.
+More information on the library and extension modules that are available from
+JCenter can be found on [Bintray][].
+
+[extensions directory]: https://github.com/google/ExoPlayer/tree/release-v2/extensions/
[Bintray]: https://bintray.com/google/exoplayer
-[Release notes]: https://github.com/google/ExoPlayer/blob/release-v2/RELEASENOTES.md
+
+#### 3. Turn on Java 8 support ####
+
+If not enabled already, you also need to turn on Java 8 support in all
+`build.gradle` files depending on ExoPlayer, by adding the following to the
+`android` section:
+
+```gradle
+compileOptions {
+ targetCompatibility JavaVersion.VERSION_1_8
+}
+```
+
+### Locally ###
+
+Cloning the repository and depending on the modules locally is required when
+using some ExoPlayer extension modules. It's also a suitable approach if you
+want to make local changes to ExoPlayer, or if you want to use a development
+branch.
+
+First, clone the repository into a local directory and checkout the desired
+branch:
+
+```sh
+git clone https://github.com/google/ExoPlayer.git
+cd ExoPlayer
+git checkout release-v2
+```
+
+Next, add the following to your project's `settings.gradle` file, replacing
+`path/to/exoplayer` with the path to your local copy:
+
+```gradle
+gradle.ext.exoplayerRoot = 'path/to/exoplayer'
+gradle.ext.exoplayerModulePrefix = 'exoplayer-'
+apply from: new File(gradle.ext.exoplayerRoot, 'core_settings.gradle')
+```
+
+You should now see the ExoPlayer modules appear as part of your project. You can
+depend on them as you would on any other local module, for example:
+
+```gradle
+implementation project(':exoplayer-library-core')
+implementation project(':exoplayer-library-dash')
+implementation project(':exoplayer-library-ui')
+```
## Developing ExoPlayer ##
#### Project branches ####
- * The project has `dev-vX` and `release-vX` branches, where `X` is the major
- version number.
- * Most development work happens on the `dev-vX` branch with the highest major
- version number. Pull requests should normally be made to this branch.
- * Bug fixes may be submitted to older `dev-vX` branches. When doing this, the
- same (or an equivalent) fix should also be submitted to all subsequent
- `dev-vX` branches.
- * A `release-vX` branch holds the most recent stable release for major version
- `X`.
+* 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 ####
diff --git a/RELEASENOTES.md b/RELEASENOTES.md
index ff1bd42fde..2dba34486b 100644
--- a/RELEASENOTES.md
+++ b/RELEASENOTES.md
@@ -1,6 +1,1521 @@
# Release notes #
-### r2.4.4 ###
+### dev-v2 (not yet released) ###
+
+* Add Java FLAC extractor
+ ([#6406](https://github.com/google/ExoPlayer/issues/6406)).
+ If `DefaultExtractorsFactory` is used, this extractor is only used if the FLAC
+ extension is not loaded.
+* Make `MediaSourceEventListener.LoadEventInfo` and
+ `MediaSourceEventListener.MediaLoadData` top-level classes.
+* Rename `MediaCodecRenderer.onOutputFormatChanged` to
+ `MediaCodecRenderer.onOutputMediaFormatChanged`, further
+ clarifying the distinction between `Format` and `MediaFormat`.
+* Downloads: Merge downloads in `SegmentDownloader` to improve overall download
+ speed ([#5978](https://github.com/google/ExoPlayer/issues/5978)).
+* In MP4 streams, store the Android capture frame rate only in
+ `Format.metadata`. `Format.frameRate` now stores the calculated frame rate.
+* Add `play` and `pause` methods to `Player`.
+* Upgrade Truth dependency from 0.44 to 1.0.
+* Upgrade to JUnit 4.13-rc-2.
+* Add support for attaching DRM sessions to clear content in the demo app.
+* Add `SpannedSubject` to testutils, for assertions on
+ [Span-styled text]( https://developer.android.com/guide/topics/text/spans)
+ (e.g. subtitles).
+* Add `Player.getCurrentLiveOffset` to conveniently return the live offset.
+* Update `IcyDecoder` to try ISO-8859-1 decoding if UTF-8 decoding fails.
+ Also change `IcyInfo.rawMetadata` from `String` to `byte[]` to allow
+ developers to handle data that's neither UTF-8 nor ISO-8859-1
+ ([#6753](https://github.com/google/ExoPlayer/issues/6753)).
+* Add playlist API ([#6161](https://github.com/google/ExoPlayer/issues/6161)).
+* Fix handling of network transitions in `RequirementsWatcher`
+ ([#6733](https://github.com/google/ExoPlayer/issues/6733)). Incorrect handling
+ could previously cause downloads to be paused when they should have been able
+ to proceed.
+* Fix handling of E-AC-3 streams that contain AC-3 syncframes
+ ([#6602](https://github.com/google/ExoPlayer/issues/6602)).
+* Support "twos" codec (big endian PCM) in MP4
+ ([#5789](https://github.com/google/ExoPlayer/issues/5789)).
+* WAV: Support IMA ADPCM encoded data.
+
+### 2.11.1 (2019-12-20) ###
+
+* UI: Exclude `DefaultTimeBar` region from system gesture detection
+ ([#6685](https://github.com/google/ExoPlayer/issues/6685)).
+* ProGuard fixes:
+ * Ensure `Libgav1VideoRenderer` constructor is kept for use by
+ `DefaultRenderersFactory`
+ ([#6773](https://github.com/google/ExoPlayer/issues/6773)).
+ * Ensure `VideoDecoderOutputBuffer` and its members are kept for use by video
+ decoder extensions.
+ * Ensure raw resources used with `RawResourceDataSource` are kept.
+ * Suppress spurious warnings about the `javax.annotation` package, and
+ restructure use of `IntDef` annotations to remove spurious warnings about
+ `SsaStyle$SsaAlignment`
+ ([#6771](https://github.com/google/ExoPlayer/issues/6771)).
+* Fix `CacheDataSource` to correctly propagate `DataSpec.httpRequestHeaders`.
+* Fix issue with `DefaultDownloadIndex` that could result in an
+ `IllegalStateException` being thrown from
+ `DefaultDownloadIndex.getDownloadForCurrentRow`
+ ([#6785](https://github.com/google/ExoPlayer/issues/6785)).
+* Fix `IndexOutOfBoundsException` in `SinglePeriodTimeline.getWindow`
+ ([#6776](https://github.com/google/ExoPlayer/issues/6776)).
+* Add missing `@Nullable` to `MediaCodecAudioRenderer.getMediaClock` and
+ `SimpleDecoderAudioRenderer.getMediaClock`
+ ([#6792](https://github.com/google/ExoPlayer/issues/6792)).
+
+### 2.11.0 (2019-12-11) ###
+
+* Core library:
+ * Replace `ExoPlayerFactory` by `SimpleExoPlayer.Builder` and
+ `ExoPlayer.Builder`.
+ * Add automatic `WakeLock` handling to `SimpleExoPlayer`, which can be enabled
+ by calling `SimpleExoPlayer.setHandleWakeLock`
+ ([#5846](https://github.com/google/ExoPlayer/issues/5846)). To use this
+ feature, you must add the
+ [WAKE_LOCK](https://developer.android.com/reference/android/Manifest.permission.html#WAKE_LOCK)
+ permission to your application's manifest file.
+ * Add automatic "audio becoming noisy" handling to `SimpleExoPlayer`, which
+ can be enabled by calling `SimpleExoPlayer.setHandleAudioBecomingNoisy`.
+ * Wrap decoder exceptions in a new `DecoderException` class and report them as
+ renderer errors.
+ * Add `Timeline.Window.isLive` to indicate that a window is a live stream
+ ([#2668](https://github.com/google/ExoPlayer/issues/2668) and
+ [#5973](https://github.com/google/ExoPlayer/issues/5973)).
+ * Add `Timeline.Window.uid` to uniquely identify window instances.
+ * Deprecate `setTag` parameter of `Timeline.getWindow`. Tags will always be
+ set.
+ * Deprecate passing the manifest directly to
+ `Player.EventListener.onTimelineChanged`. It can be accessed through
+ `Timeline.Window.manifest` or `Player.getCurrentManifest()`
+ * Add `MediaSource.enable` and `MediaSource.disable` to improve resource
+ management in playlists.
+ * Add `MediaPeriod.isLoading` to improve `Player.isLoading` state.
+ * Fix issue where player errors are thrown too early at playlist transitions
+ ([#5407](https://github.com/google/ExoPlayer/issues/5407)).
+ * Add `Format` and renderer support flags to renderer `ExoPlaybackException`s.
+ * Where there are multiple platform decoders for a given MIME type, prefer to
+ use one that advertises support for the profile and level of the media being
+ played over one that does not, even if it does not come first in the
+ `MediaCodecList`.
+* DRM:
+ * Inject `DrmSessionManager` into the `MediaSources` instead of `Renderers`.
+ This allows each `MediaSource` in a `ConcatenatingMediaSource` to use a
+ different `DrmSessionManager`
+ ([#5619](https://github.com/google/ExoPlayer/issues/5619)).
+ * Add `DefaultDrmSessionManager.Builder`, and remove
+ `DefaultDrmSessionManager` static factory methods that leaked
+ `ExoMediaDrm` instances
+ ([#4721](https://github.com/google/ExoPlayer/issues/4721)).
+ * Add support for the use of secure decoders when playing clear content
+ ([#4867](https://github.com/google/ExoPlayer/issues/4867)). This can
+ be enabled using `DefaultDrmSessionManager.Builder`'s
+ `setUseDrmSessionsForClearContent` method.
+ * Add support for custom `LoadErrorHandlingPolicies` in key and provisioning
+ requests ([#6334](https://github.com/google/ExoPlayer/issues/6334)). Custom
+ policies can be passed via `DefaultDrmSessionManager.Builder`'s
+ `setLoadErrorHandlingPolicy` method.
+ * Use `ExoMediaDrm.Provider` in `OfflineLicenseHelper` to avoid leaking
+ `ExoMediaDrm` instances
+ ([#4721](https://github.com/google/ExoPlayer/issues/4721)).
+* Track selection:
+ * Update `DefaultTrackSelector` to set a viewport constraint for the default
+ display by default.
+ * Update `DefaultTrackSelector` to set text language and role flag
+ constraints for the device's accessibility settings by default
+ ([#5749](https://github.com/google/ExoPlayer/issues/5749)).
+ * Add option to set preferred text role flags using
+ `DefaultTrackSelector.ParametersBuilder.setPreferredTextRoleFlags`.
+* Android 10:
+ * Set `compileSdkVersion` to 29 to enable use of Android 10 APIs.
+ * Expose new `isHardwareAccelerated`, `isSoftwareOnly` and `isVendor` flags
+ in `MediaCodecInfo`
+ ([#5839](https://github.com/google/ExoPlayer/issues/5839)).
+ * Add `allowedCapturePolicy` field to `AudioAttributes` to allow to
+ configuration of the audio capture policy.
+* Video:
+ * Pass the codec output `MediaFormat` to `VideoFrameMetadataListener`.
+ * Fix byte order of HDR10+ static metadata to match CTA-861.3.
+ * Support out-of-band HDR10+ dynamic metadata for VP9 in WebM/Matroska.
+ * Assume that protected content requires a secure decoder when evaluating
+ whether `MediaCodecVideoRenderer` supports a given video format
+ ([#5568](https://github.com/google/ExoPlayer/issues/5568)).
+ * Fix Dolby Vision fallback to AVC and HEVC.
+ * Fix early end-of-stream detection when using video tunneling, on API level
+ 23 and above.
+ * Fix an issue where a keyframe was rendered rather than skipped when
+ performing an exact seek to a non-zero position close to the start of the
+ stream.
+* Audio:
+ * Fix the start of audio getting truncated when transitioning to a new
+ item in a playlist of Opus streams.
+ * Workaround broken raw audio decoding on Oppo R9
+ ([#5782](https://github.com/google/ExoPlayer/issues/5782)).
+ * Reconfigure audio sink when PCM encoding changes
+ ([#6601](https://github.com/google/ExoPlayer/issues/6601)).
+ * Allow `AdtsExtractor` to encounter EOF when calculating average frame size
+ ([#6700](https://github.com/google/ExoPlayer/issues/6700)).
+* Text:
+ * Add support for position and overlapping start/end times in SSA/ASS
+ subtitles ([#6320](https://github.com/google/ExoPlayer/issues/6320)).
+ * Require an end time or duration for SubRip (SRT) and SubStation Alpha
+ (SSA/ASS) subtitles. This applies to both sidecar files & subtitles
+ [embedded in Matroska streams](https://matroska.org/technical/specs/subtitles/index.html).
+* UI:
+ * Make showing and hiding player controls accessible to TalkBack in
+ `PlayerView`.
+ * Rename `spherical_view` surface type to `spherical_gl_surface_view`.
+ * Make it easier to override the shuffle, repeat, fullscreen, VR and small
+ notification icon assets
+ ([#6709](https://github.com/google/ExoPlayer/issues/6709)).
+* Analytics:
+ * Remove `AnalyticsCollector.Factory`. Instances should be created directly,
+ and the `Player` should be set by calling `AnalyticsCollector.setPlayer`.
+ * Add `PlaybackStatsListener` to collect `PlaybackStats` for analysis and
+ analytics reporting.
+* DataSource
+ * Add `DataSpec.httpRequestHeaders` to support setting per-request headers for
+ HTTP and HTTPS.
+ * Remove the `DataSpec.FLAG_ALLOW_ICY_METADATA` flag. Use is replaced by
+ setting the `IcyHeaders.REQUEST_HEADER_ENABLE_METADATA_NAME` header in
+ `DataSpec.httpRequestHeaders`.
+ * Fail more explicitly when local file URIs contain invalid parts (e.g. a
+ fragment) ([#6470](https://github.com/google/ExoPlayer/issues/6470)).
+* DASH: Support negative @r values in segment timelines
+ ([#1787](https://github.com/google/ExoPlayer/issues/1787)).
+* HLS:
+ * Use peak bitrate rather than average bitrate for adaptive track selection.
+ * Fix issue where streams could get stuck in an infinite buffering state
+ after a postroll ad
+ ([#6314](https://github.com/google/ExoPlayer/issues/6314)).
+* Matroska: Support lacing in Blocks
+ ([#3026](https://github.com/google/ExoPlayer/issues/3026)).
+* AV1 extension:
+ * New in this release. The AV1 extension allows use of the
+ [libgav1 software decoder](https://chromium.googlesource.com/codecs/libgav1/)
+ in ExoPlayer. You can read more about playing AV1 videos with ExoPlayer
+ [here](https://medium.com/google-exoplayer/playing-av1-videos-with-exoplayer-a7cb19bedef9).
+* VP9 extension:
+ * Update to use NDK r20.
+ * Rename `VpxVideoSurfaceView` to `VideoDecoderSurfaceView` and move it to the
+ core library.
+ * Move `LibvpxVideoRenderer.MSG_SET_OUTPUT_BUFFER_RENDERER` to
+ `C.MSG_SET_OUTPUT_BUFFER_RENDERER`.
+ * Use `VideoDecoderRenderer` as an implementation of
+ `VideoDecoderOutputBufferRenderer`, instead of `VideoDecoderSurfaceView`.
+* Flac extension: Update to use NDK r20.
+* Opus extension: Update to use NDK r20.
+* FFmpeg extension:
+ * Update to use NDK r20.
+ * Update to use FFmpeg version 4.2. It is necessary to rebuild the native part
+ of the extension after this change, following the instructions in the
+ extension's readme.
+* MediaSession extension: Add `MediaSessionConnector.setCaptionCallback` to
+ support `ACTION_SET_CAPTIONING_ENABLED` events.
+* GVR extension: This extension is now deprecated.
+* Demo apps:
+ * Add [SurfaceControl demo app](https://github.com/google/ExoPlayer/tree/r2.11.0/demos/surface)
+ to show how to use the Android 10 `SurfaceControl` API with ExoPlayer
+ ([#677](https://github.com/google/ExoPlayer/issues/677)).
+ * Add support for subtitle files to the
+ [Main demo app](https://github.com/google/ExoPlayer/tree/r2.11.0/demos/main)
+ ([#5523](https://github.com/google/ExoPlayer/issues/5523)).
+ * Remove the IMA demo app. IMA functionality is demonstrated by the
+ [main demo app](https://github.com/google/ExoPlayer/tree/r2.11.0/demos/main).
+ * Add basic DRM support to the
+ [Cast demo app](https://github.com/google/ExoPlayer/tree/r2.11.0/demos/cast).
+* TestUtils: Publish the `testutils` module to simplify unit testing with
+ ExoPlayer ([#6267](https://github.com/google/ExoPlayer/issues/6267)).
+* IMA extension: Remove `AdsManager` listeners on release to avoid leaking an
+ `AdEventListener` provided by the app
+ ([#6687](https://github.com/google/ExoPlayer/issues/6687)).
+
+### 2.10.8 (2019-11-19) ###
+
+* E-AC3 JOC
+ * Handle new signaling in DASH manifests
+ ([#6636](https://github.com/google/ExoPlayer/issues/6636)).
+ * Fix E-AC3 JOC passthrough playback failing to initialize due to incorrect
+ channel count check.
+* FLAC
+ * Fix sniffing for some FLAC streams.
+ * Fix FLAC `Format.bitrate` values.
+* Parse ALAC channel count and sample rate information from a more robust source
+ when contained in MP4
+ ([#6648](https://github.com/google/ExoPlayer/issues/6648)).
+* Fix seeking into multi-period content in the edge case that the period
+ containing the seek position has just been removed
+ ([#6641](https://github.com/google/ExoPlayer/issues/6641)).
+
+### 2.10.7 (2019-11-06) ###
+
+* HLS: Fix detection of Dolby Atmos to match the HLS authoring specification.
+* MediaSession extension: Update shuffle and repeat modes when playback state
+ is invalidated ([#6582](https://github.com/google/ExoPlayer/issues/6582)).
+* Fix the start of audio getting truncated when transitioning to a new
+ item in a playlist of Opus streams.
+
+### 2.10.6 (2019-10-17) ###
+
+* Add `Player.onPlaybackSuppressionReasonChanged` to allow listeners to
+ detect playbacks suppressions (e.g. transient audio focus loss) directly
+ ([#6203](https://github.com/google/ExoPlayer/issues/6203)).
+* DASH:
+ * Support `Label` elements
+ ([#6297](https://github.com/google/ExoPlayer/issues/6297)).
+ * Support legacy audio channel configuration
+ ([#6523](https://github.com/google/ExoPlayer/issues/6523)).
+* HLS: Add support for ID3 in EMSG when using FMP4 streams
+ ([spec](https://aomediacodec.github.io/av1-id3/)).
+* MP3: Add workaround to avoid prematurely ending playback of some SHOUTcast
+ live streams ([#6537](https://github.com/google/ExoPlayer/issues/6537),
+ [#6315](https://github.com/google/ExoPlayer/issues/6315) and
+ [#5658](https://github.com/google/ExoPlayer/issues/5658)).
+* Metadata: Expose the raw ICY metadata through `IcyInfo`
+ ([#6476](https://github.com/google/ExoPlayer/issues/6476)).
+* UI:
+ * Setting `app:played_color` on `PlayerView` and `PlayerControlView` no longer
+ adjusts the colors of the scrubber handle , buffered and unplayed parts of
+ the time bar. These can be set separately using `app:scrubber_color`,
+ `app:buffered_color` and `app_unplayed_color` respectively.
+ * Setting `app:ad_marker_color` on `PlayerView` and `PlayerControlView` no
+ longer adjusts the color of played ad markers. The color of played ad
+ markers can be set separately using `app:played_ad_marker_color`.
+
+### 2.10.5 (2019-09-20) ###
+
+* Add `Player.isPlaying` and `EventListener.onIsPlayingChanged` to check whether
+ the playback position is advancing. This helps to determine if playback is
+ suppressed due to audio focus loss. Also add
+ `Player.getPlaybackSuppressedReason` to determine the reason of the
+ suppression ([#6203](https://github.com/google/ExoPlayer/issues/6203)).
+* Track selection
+ * Add `allowAudioMixedChannelCountAdaptiveness` parameter to
+ `DefaultTrackSelector` to allow adaptive selections of audio tracks with
+ different channel counts.
+ * Improve text selection logic to always prefer the better language matches
+ over other selection parameters.
+ * Fix audio selection issue where languages are compared by bitrate
+ ([#6335](https://github.com/google/ExoPlayer/issues/6335)).
+* Performance
+ * Increase maximum video buffer size from 13MB to 32MB. The previous default
+ was too small for high quality streams.
+ * Reset `DefaultBandwidthMeter` to initial values on network change.
+ * Bypass sniffing in `ProgressiveMediaPeriod` in case a single extractor is
+ provided ([#6325](https://github.com/google/ExoPlayer/issues/6325)).
+* Metadata
+ * Support EMSG V1 boxes in FMP4.
+ * Support unwrapping of nested metadata (e.g. ID3 and SCTE-35 in EMSG).
+* Add `HttpDataSource.getResponseCode` to provide the status code associated
+ with the most recent HTTP response.
+* Fix transitions between packed audio and non-packed audio segments in HLS
+ ([#6444](https://github.com/google/ExoPlayer/issues/6444)).
+* Fix issue where a request would be retried after encountering an error, even
+ though the `LoadErrorHandlingPolicy` classified the error as fatal.
+* Fix initialization data handling for FLAC in MP4
+ ([#6396](https://github.com/google/ExoPlayer/issues/6396),
+ [#6397](https://github.com/google/ExoPlayer/issues/6397)).
+* Fix decoder selection for E-AC3 JOC streams
+ ([#6398](https://github.com/google/ExoPlayer/issues/6398)).
+* Fix `PlayerNotificationManager` to show play icon rather than pause icon when
+ playback is ended ([#6324](https://github.com/google/ExoPlayer/issues/6324)).
+* RTMP extension: Upgrade LibRtmp-Client-for-Android to fix RTMP playback issues
+ ([#4200](https://github.com/google/ExoPlayer/issues/4200),
+ [#4249](https://github.com/google/ExoPlayer/issues/4249),
+ [#4319](https://github.com/google/ExoPlayer/issues/4319),
+ [#4337](https://github.com/google/ExoPlayer/issues/4337)).
+* IMA extension: Fix crash in `ImaAdsLoader.onTimelineChanged`
+ ([#5831](https://github.com/google/ExoPlayer/issues/5831)).
+
+### 2.10.4 (2019-07-26) ###
+
+* Offline: Add `Scheduler` implementation that uses `WorkManager`.
+* Add ability to specify a description when creating notification channels via
+ ExoPlayer library classes.
+* Switch normalized BCP-47 language codes to use 2-letter ISO 639-1 language
+ tags instead of 3-letter ISO 639-2 language tags.
+* Ensure the `SilenceMediaSource` position is in range
+ ([#6229](https://github.com/google/ExoPlayer/issues/6229)).
+* WAV: Calculate correct duration for clipped streams
+ ([#6241](https://github.com/google/ExoPlayer/issues/6241)).
+* MP3: Use CBR header bitrate, not calculated bitrate. This reverts a change
+ from 2.9.3 ([#6238](https://github.com/google/ExoPlayer/issues/6238)).
+* Flac extension: Parse `VORBIS_COMMENT` and `PICTURE` metadata
+ ([#5527](https://github.com/google/ExoPlayer/issues/5527)).
+* Fix issue where initial seek positions get ignored when playing a preroll ad
+ ([#6201](https://github.com/google/ExoPlayer/issues/6201)).
+* Fix issue where invalid language tags were normalized to "und" instead of
+ keeping the original
+ ([#6153](https://github.com/google/ExoPlayer/issues/6153)).
+* Fix `DataSchemeDataSource` re-opening and range requests
+ ([#6192](https://github.com/google/ExoPlayer/issues/6192)).
+* Fix Flac and ALAC playback on some LG devices
+ ([#5938](https://github.com/google/ExoPlayer/issues/5938)).
+* Fix issue when calling `performClick` on `PlayerView` without
+ `PlayerControlView`
+ ([#6260](https://github.com/google/ExoPlayer/issues/6260)).
+* Fix issue where playback speeds are not used in adaptive track selections
+ after manual selection changes for other renderers
+ ([#6256](https://github.com/google/ExoPlayer/issues/6256)).
+
+### 2.10.3 (2019-07-09) ###
+
+* Display last frame when seeking to end of stream
+ ([#2568](https://github.com/google/ExoPlayer/issues/2568)).
+* Audio:
+ * Fix an issue where not all audio was played out when the configuration
+ for the underlying track was changing (e.g., at some period transitions).
+ * Fix an issue where playback speed was applied inaccurately in playlists
+ ([#6117](https://github.com/google/ExoPlayer/issues/6117)).
+* UI: Fix `PlayerView` incorrectly consuming touch events if no controller is
+ attached ([#6109](https://github.com/google/ExoPlayer/issues/6109)).
+* CEA608: Fix repetition of special North American characters
+ ([#6133](https://github.com/google/ExoPlayer/issues/6133)).
+* FLV: Fix bug that caused playback of some live streams to not start
+ ([#6111](https://github.com/google/ExoPlayer/issues/6111)).
+* SmoothStreaming: Parse text stream `Subtype` into `Format.roleFlags`.
+* MediaSession extension: Fix `MediaSessionConnector.play()` not resuming
+ playback ([#6093](https://github.com/google/ExoPlayer/issues/6093)).
+
+### 2.10.2 (2019-06-03) ###
+
+* Add `ResolvingDataSource` for just-in-time resolution of `DataSpec`s
+ ([#5779](https://github.com/google/ExoPlayer/issues/5779)).
+* Add `SilenceMediaSource` that can be used to play silence of a given
+ duration ([#5735](https://github.com/google/ExoPlayer/issues/5735)).
+* Offline:
+ * Prevent unexpected `DownloadHelper.Callback.onPrepared` callbacks after
+ preparation of a `DownloadHelper` fails
+ ([#5915](https://github.com/google/ExoPlayer/issues/5915)).
+ * Fix `CacheUtil.cache()` downloading too much data
+ ([#5927](https://github.com/google/ExoPlayer/issues/5927)).
+ * Fix misreporting cached bytes when caching is paused
+ ([#5573](https://github.com/google/ExoPlayer/issues/5573)).
+* UI:
+ * Allow setting `DefaultTimeBar` attributes on `PlayerView` and
+ `PlayerControlView`.
+ * Change playback controls toggle from touch down to touch up events
+ ([#5784](https://github.com/google/ExoPlayer/issues/5784)).
+ * Fix issue where playback controls were not kept visible on key presses
+ ([#5963](https://github.com/google/ExoPlayer/issues/5963)).
+* Subtitles:
+ * CEA-608: Handle XDS and TEXT modes
+ ([#5807](https://github.com/google/ExoPlayer/pull/5807)).
+ * TTML: Fix bitmap rendering
+ ([#5633](https://github.com/google/ExoPlayer/pull/5633)).
+* IMA: Fix ad pod index offset calculation without preroll
+ ([#5928](https://github.com/google/ExoPlayer/issues/5928)).
+* Add a `playWhenReady` flag to MediaSessionConnector.PlaybackPreparer methods
+ to indicate whether a controller sent a play or only a prepare command. This
+ allows to take advantage of decoder reuse with the MediaSessionConnector
+ ([#5891](https://github.com/google/ExoPlayer/issues/5891)).
+* Add `ProgressUpdateListener` to `PlayerControlView`
+ ([#5834](https://github.com/google/ExoPlayer/issues/5834)).
+* Add support for auto-detecting UDP streams in `DefaultDataSource`
+ ([#6036](https://github.com/google/ExoPlayer/pull/6036)).
+* Allow enabling decoder fallback with `DefaultRenderersFactory`
+ ([#5942](https://github.com/google/ExoPlayer/issues/5942)).
+* Gracefully handle revoked `ACCESS_NETWORK_STATE` permission
+ ([#6019](https://github.com/google/ExoPlayer/issues/6019)).
+* Fix decoding problems when seeking back after seeking beyond a mid-roll ad
+ ([#6009](https://github.com/google/ExoPlayer/issues/6009)).
+* Fix application of `maxAudioBitrate` for adaptive audio track groups
+ ([#6006](https://github.com/google/ExoPlayer/issues/6006)).
+* Fix bug caused by parallel adaptive track selection using `Format`s without
+ bitrate information
+ ([#5971](https://github.com/google/ExoPlayer/issues/5971)).
+* Fix bug in `CastPlayer.getCurrentWindowIndex()`
+ ([#5955](https://github.com/google/ExoPlayer/issues/5955)).
+
+### 2.10.1 (2019-05-16) ###
+
+* Offline: Add option to remove all downloads.
+* HLS: Fix `NullPointerException` when using HLS chunkless preparation
+ ([#5868](https://github.com/google/ExoPlayer/issues/5868)).
+* Fix handling of empty values and line terminators in SHOUTcast ICY metadata
+ ([#5876](https://github.com/google/ExoPlayer/issues/5876)).
+* Fix DVB subtitles for SDK 28
+ ([#5862](https://github.com/google/ExoPlayer/issues/5862)).
+* Add a workaround for a decoder failure on ZTE Axon7 mini devices when playing
+ 48kHz audio ([#5821](https://github.com/google/ExoPlayer/issues/5821)).
+
+### 2.10.0 (2019-04-15) ###
+
+* Core library:
+ * Improve decoder re-use between playbacks
+ ([#2826](https://github.com/google/ExoPlayer/issues/2826)). Read
+ [this blog post](https://medium.com/google-exoplayer/improved-decoder-reuse-in-exoplayer-ef4c6d99591d)
+ for more details.
+ * Rename `ExtractorMediaSource` to `ProgressiveMediaSource`.
+ * Fix issue where using `ProgressiveMediaSource.Factory` would mean that
+ `DefaultExtractorsFactory` would be kept by proguard. Custom
+ `ExtractorsFactory` instances must now be passed via the
+ `ProgressiveMediaSource.Factory` constructor, and `setExtractorsFactory` is
+ deprecated.
+ * Make the default minimum buffer size equal the maximum buffer size for video
+ playbacks ([#2083](https://github.com/google/ExoPlayer/issues/2083)).
+ * Move `PriorityTaskManager` from `DefaultLoadControl` to `SimpleExoPlayer`.
+ * Add new `ExoPlaybackException` types for remote exceptions and out-of-memory
+ errors.
+ * Use full BCP 47 language tags in `Format`.
+ * Do not retry failed loads whose error is `FileNotFoundException`.
+ * Fix issue where not resetting the position for a new `MediaSource` in calls
+ to `ExoPlayer.prepare` causes an `IndexOutOfBoundsException`
+ ([#5520](https://github.com/google/ExoPlayer/issues/5520)).
+* Offline:
+ * Improve offline support. `DownloadManager` now tracks all offline content,
+ not just tasks in progress. Read
+ [this page](https://exoplayer.dev/downloading-media.html) for more details.
+* Caching:
+ * Improve performance of `SimpleCache`
+ ([#4253](https://github.com/google/ExoPlayer/issues/4253)).
+ * Cache data with unknown length by default. The previous flag to opt in to
+ this behavior (`DataSpec.FLAG_ALLOW_CACHING_UNKNOWN_LENGTH`) has been
+ replaced with an opt out flag
+ (`DataSpec.FLAG_DONT_CACHE_IF_LENGTH_UNKNOWN`).
+* Extractors:
+ * MP4/FMP4: Add support for Dolby Vision.
+ * MP4: Fix issue handling meta atoms in some streams
+ ([#5698](https://github.com/google/ExoPlayer/issues/5698),
+ [#5694](https://github.com/google/ExoPlayer/issues/5694)).
+ * MP3: Add support for SHOUTcast ICY metadata
+ ([#3735](https://github.com/google/ExoPlayer/issues/3735)).
+ * MP3: Fix ID3 frame unsychronization
+ ([#5673](https://github.com/google/ExoPlayer/issues/5673)).
+ * MP3: Fix playback of badly clipped files
+ ([#5772](https://github.com/google/ExoPlayer/issues/5772)).
+ * MPEG-TS: Enable HDMV DTS stream detection only if a flag is set. By default
+ (i.e. if the flag is not set), the 0x82 elementary stream type is now
+ treated as an SCTE subtitle track
+ ([#5330](https://github.com/google/ExoPlayer/issues/5330)).
+* Track selection:
+ * Add options for controlling audio track selections to `DefaultTrackSelector`
+ ([#3314](https://github.com/google/ExoPlayer/issues/3314)).
+ * Update `TrackSelection.Factory` interface to support creating all track
+ selections together.
+ * Allow to specify a selection reason for a `SelectionOverride`.
+ * Select audio track based on system language if no preference is provided.
+ * When no text language preference matches, only select forced text tracks
+ whose language matches the selected audio language.
+* UI:
+ * Update `DefaultTimeBar` based on duration of media and add parameter to set
+ the minimum update interval to control the smoothness of the updates
+ ([#5040](https://github.com/google/ExoPlayer/issues/5040)).
+ * Move creation of dialogs for `TrackSelectionView`s to
+ `TrackSelectionDialogBuilder` and add option to select multiple overrides.
+ * Change signature of `PlayerNotificationManager.NotificationListener` to
+ better fit service requirements.
+ * Add option to include navigation actions in the compact mode of
+ notifications created using `PlayerNotificationManager`.
+ * Fix issues with flickering notifications on KitKat when using
+ `PlayerNotificationManager` and `DownloadNotificationUtil`. For the latter,
+ applications should switch to using `DownloadNotificationHelper`.
+ * Fix accuracy of D-pad seeking in `DefaultTimeBar`
+ ([#5767](https://github.com/google/ExoPlayer/issues/5767)).
+* Audio:
+ * Allow `AudioProcessor`s to be drained of pending output after they are
+ reconfigured.
+ * Fix an issue that caused audio to be truncated at the end of a period
+ when switching to a new period where gapless playback information was newly
+ present or newly absent.
+ * Add support for reading AC-4 streams
+ ([#5303](https://github.com/google/ExoPlayer/pull/5303)).
+* Video:
+ * Remove `MediaCodecSelector.DEFAULT_WITH_FALLBACK`. Apps should instead
+ signal that fallback should be used by passing `true` as the
+ `enableDecoderFallback` parameter when instantiating the video renderer.
+ * Support video tunneling when the decoder is not listed first for the MIME
+ type ([#3100](https://github.com/google/ExoPlayer/issues/3100)).
+ * Query `MediaCodecList.ALL_CODECS` when selecting a tunneling decoder
+ ([#5547](https://github.com/google/ExoPlayer/issues/5547)).
+* DRM:
+ * Fix black flicker when keys rotate in DRM protected content
+ ([#3561](https://github.com/google/ExoPlayer/issues/3561)).
+ * Work around lack of LA_URL attribute in PlayReady key request init data.
+* CEA-608: Improved conformance to the specification
+ ([#3860](https://github.com/google/ExoPlayer/issues/3860)).
+* DASH:
+ * Parse role and accessibility descriptors into `Format.roleFlags`.
+ * Support multiple CEA-608 channels muxed into FMP4 representations
+ ([#5656](https://github.com/google/ExoPlayer/issues/5656)).
+* HLS:
+ * Prevent unnecessary reloads of initialization segments.
+ * Form an adaptive track group out of audio renditions with matching name.
+ * Support encrypted initialization segments
+ ([#5441](https://github.com/google/ExoPlayer/issues/5441)).
+ * Parse `EXT-X-MEDIA` `CHARACTERISTICS` attribute into `Format.roleFlags`.
+ * Add metadata entry for HLS tracks to expose master playlist information.
+ * Prevent `IndexOutOfBoundsException` in some live HLS scenarios
+ ([#5816](https://github.com/google/ExoPlayer/issues/5816)).
+* Support for playing spherical videos on Daydream.
+* Cast extension: Work around Cast framework returning a limited-size queue
+ items list ([#4964](https://github.com/google/ExoPlayer/issues/4964)).
+* VP9 extension: Remove RGB output mode and libyuv dependency, and switch to
+ surface YUV output as the default. Remove constructor parameters `scaleToFit`
+ and `useSurfaceYuvOutput`.
+* MediaSession extension:
+ * Let apps intercept media button events
+ ([#5179](https://github.com/google/ExoPlayer/issues/5179)).
+ * Fix issue with `TimelineQueueNavigator` not publishing the queue in shuffled
+ order when in shuffle mode.
+ * Allow handling of custom commands via `registerCustomCommandReceiver`.
+ * Add ability to include an extras `Bundle` when reporting a custom error.
+* Log warnings when extension native libraries can't be used, to help with
+ diagnosing playback failures
+ ([#5788](https://github.com/google/ExoPlayer/issues/5788)).
+
+### 2.9.6 (2019-02-19) ###
+
+* Remove `player` and `isTopLevelSource` parameters from `MediaSource.prepare`.
+* IMA extension:
+ * Require setting the `Player` on `AdsLoader` instances before
+ playback.
+ * Remove deprecated `ImaAdsMediaSource`. Create `AdsMediaSource` with an
+ `ImaAdsLoader` instead.
+ * Remove deprecated `AdsMediaSource` constructors. Listen for media source
+ events using `AdsMediaSource.addEventListener`, and ad interaction events by
+ adding a listener when building `ImaAdsLoader`.
+ * Allow apps to register playback-related obstructing views that are on top of
+ their ad display containers via `AdsLoader.AdViewProvider`. `PlayerView`
+ implements this interface and will register its control view. This makes it
+ possible for ad loading SDKs to calculate ad viewability accurately.
+* DASH: Fix issue handling large `EventStream` presentation timestamps
+ ([#5490](https://github.com/google/ExoPlayer/issues/5490)).
+* HLS: Fix transition to STATE_ENDED when playing fragmented mp4 in chunkless
+ preparation ([#5524](https://github.com/google/ExoPlayer/issues/5524)).
+* Revert workaround for video quality problems with Amlogic decoders, as this
+ may cause problems for some devices and/or non-interlaced content
+ ([#5003](https://github.com/google/ExoPlayer/issues/5003)).
+
+### 2.9.5 (2019-01-31) ###
+
+* HLS: Parse `CHANNELS` attribute from `EXT-X-MEDIA` tag.
+* ConcatenatingMediaSource:
+ * Add `Handler` parameter to methods that take a callback `Runnable`.
+ * Fix issue with dropped messages when releasing the source
+ ([#5464](https://github.com/google/ExoPlayer/issues/5464)).
+* ExtractorMediaSource: Fix issue that could cause the player to get stuck
+ buffering at the end of the media.
+* PlayerView: Fix issue preventing `OnClickListener` from receiving events
+ ([#5433](https://github.com/google/ExoPlayer/issues/5433)).
+* IMA extension: Upgrade IMA dependency to 3.10.6.
+* Cronet extension: Upgrade Cronet dependency to 71.3578.98.
+* OkHttp extension: Upgrade OkHttp dependency to 3.12.1.
+* MP3: Wider fix for issue where streams would play twice on some Samsung
+ devices ([#4519](https://github.com/google/ExoPlayer/issues/4519)).
+
+### 2.9.4 (2019-01-15) ###
+
+* IMA extension: Clear ads loader listeners on release
+ ([#4114](https://github.com/google/ExoPlayer/issues/4114)).
+* SmoothStreaming: Fix support for subtitles in DRM protected streams
+ ([#5378](https://github.com/google/ExoPlayer/issues/5378)).
+* FFmpeg extension: Treat invalid data errors as non-fatal to match the behavior
+ of MediaCodec ([#5293](https://github.com/google/ExoPlayer/issues/5293)).
+* GVR extension: upgrade GVR SDK dependency to 1.190.0.
+* Associate fatal player errors of type SOURCE with the loading source in
+ `AnalyticsListener.EventTime`
+ ([#5407](https://github.com/google/ExoPlayer/issues/5407)).
+* Add `startPositionUs` to `MediaSource.createPeriod`. This fixes an issue where
+ using lazy preparation in `ConcatenatingMediaSource` with an
+ `ExtractorMediaSource` overrides initial seek positions
+ ([#5350](https://github.com/google/ExoPlayer/issues/5350)).
+* Add subtext to the `MediaDescriptionAdapter` of the
+ `PlayerNotificationManager`.
+* Add workaround for video quality problems with Amlogic decoders
+ ([#5003](https://github.com/google/ExoPlayer/issues/5003)).
+* Fix issue where sending callbacks for playlist changes may cause problems
+ because of parallel player access
+ ([#5240](https://github.com/google/ExoPlayer/issues/5240)).
+* Fix issue with reusing a `ClippingMediaSource` with an inner
+ `ExtractorMediaSource` and a non-zero start position
+ ([#5351](https://github.com/google/ExoPlayer/issues/5351)).
+* Fix issue where uneven track durations in MP4 streams can cause OOM problems
+ ([#3670](https://github.com/google/ExoPlayer/issues/3670)).
+
+### 2.9.3 (2018-12-20) ###
+
+* Captions: Support PNG subtitles in SMPTE-TT
+ ([#1583](https://github.com/google/ExoPlayer/issues/1583)).
+* MPEG-TS: Use random access indicators to minimize the need for
+ `FLAG_ALLOW_NON_IDR_KEYFRAMES`.
+* Downloading: Reduce time taken to remove downloads
+ ([#5136](https://github.com/google/ExoPlayer/issues/5136)).
+* MP3:
+ * Use the true bitrate for constant-bitrate MP3 seeking.
+ * Fix issue where streams would play twice on some Samsung devices
+ ([#4519](https://github.com/google/ExoPlayer/issues/4519)).
+* Fix regression where some audio formats were incorrectly marked as being
+ unplayable due to under-reporting of platform decoder capabilities
+ ([#5145](https://github.com/google/ExoPlayer/issues/5145)).
+* Fix decode-only frame skipping on Nvidia Shield TV devices.
+* Workaround for MiTV (dangal) issue when swapping output surface
+ ([#5169](https://github.com/google/ExoPlayer/issues/5169)).
+
+### 2.9.2 (2018-11-28) ###
+
+* HLS:
+ * Fix issue causing unnecessary media playlist requests when playing live
+ streams ([#5059](https://github.com/google/ExoPlayer/issues/5059)).
+ * Fix decoder re-instantiation issue for packed audio streams
+ ([#5063](https://github.com/google/ExoPlayer/issues/5063)).
+* MP4: Support Opus and FLAC in the MP4 container, and in DASH
+ ([#4883](https://github.com/google/ExoPlayer/issues/4883)).
+* DASH: Fix detecting the end of live events
+ ([#4780](https://github.com/google/ExoPlayer/issues/4780)).
+* Spherical video: Fall back to `TYPE_ROTATION_VECTOR` if
+ `TYPE_GAME_ROTATION_VECTOR` is unavailable
+ ([#5119](https://github.com/google/ExoPlayer/issues/5119)).
+* Support seeking for a wider range of MPEG-TS streams
+ ([#5097](https://github.com/google/ExoPlayer/issues/5097)).
+* Include channel count in audio capabilities check
+ ([#4690](https://github.com/google/ExoPlayer/issues/4690)).
+* Fix issue with applying the `show_buffering` attribute in `PlayerView`
+ ([#5139](https://github.com/google/ExoPlayer/issues/5139)).
+* Fix issue where null `Metadata` was output when it failed to decode
+ ([#5149](https://github.com/google/ExoPlayer/issues/5149)).
+* Fix playback of some invalid but playable MP4 streams by replacing assertions
+ with logged warnings in sample table parsing code
+ ([#5162](https://github.com/google/ExoPlayer/issues/5162)).
+* Fix UUID passed to `MediaCrypto` when using `C.CLEARKEY_UUID` before API 27.
+
+### 2.9.1 (2018-11-01) ###
+
+* Add convenience methods `Player.next`, `Player.previous`, `Player.hasNext`
+ and `Player.hasPrevious`
+ ([#4863](https://github.com/google/ExoPlayer/issues/4863)).
+* Improve initial bandwidth meter estimates using the current country and
+ network type.
+* IMA extension:
+ * For preroll to live stream transitions, project forward the loading position
+ to avoid being behind the live window.
+ * Let apps specify whether to focus the skip button on ATV
+ ([#5019](https://github.com/google/ExoPlayer/issues/5019)).
+* MP3:
+ * Support seeking based on MLLT metadata
+ ([#3241](https://github.com/google/ExoPlayer/issues/3241)).
+ * Fix handling of streams with appended data
+ ([#4954](https://github.com/google/ExoPlayer/issues/4954)).
+* DASH: Parse ProgramInformation element if present in the manifest.
+* HLS:
+ * Add constructor to `DefaultHlsExtractorFactory` for adding TS payload
+ reader factory flags
+ ([#4861](https://github.com/google/ExoPlayer/issues/4861)).
+ * Fix bug in segment sniffing
+ ([#5039](https://github.com/google/ExoPlayer/issues/5039)).
+* SubRip: Add support for alignment tags, and remove tags from the displayed
+ captions ([#4306](https://github.com/google/ExoPlayer/issues/4306)).
+* Fix issue with blind seeking to windows with non-zero offset in a
+ `ConcatenatingMediaSource`
+ ([#4873](https://github.com/google/ExoPlayer/issues/4873)).
+* Fix logic for enabling next and previous actions in `TimelineQueueNavigator`
+ ([#5065](https://github.com/google/ExoPlayer/issues/5065)).
+* Fix issue where audio focus handling could not be disabled after enabling it
+ ([#5055](https://github.com/google/ExoPlayer/issues/5055)).
+* Fix issue where subtitles were positioned incorrectly if `SubtitleView` had a
+ non-zero position offset to its parent
+ ([#4788](https://github.com/google/ExoPlayer/issues/4788)).
+* Fix issue where the buffered position was not updated correctly when
+ transitioning between periods
+ ([#4899](https://github.com/google/ExoPlayer/issues/4899)).
+* Fix issue where a `NullPointerException` is thrown when removing an unprepared
+ media source from a `ConcatenatingMediaSource` with the `useLazyPreparation`
+ option enabled ([#4986](https://github.com/google/ExoPlayer/issues/4986)).
+* Work around an issue where a non-empty end-of-stream audio buffer would be
+ output with timestamp zero, causing the player position to jump backwards
+ ([#5045](https://github.com/google/ExoPlayer/issues/5045)).
+* Suppress a spurious assertion failure on some Samsung devices
+ ([#4532](https://github.com/google/ExoPlayer/issues/4532)).
+* Suppress spurious "references unknown class member" shrinking warning
+ ([#4890](https://github.com/google/ExoPlayer/issues/4890)).
+* Swap recommended order for google() and jcenter() in gradle config
+ ([#4997](https://github.com/google/ExoPlayer/issues/4997)).
+
+### 2.9.0 (2018-09-06) ###
+
+* Turn on Java 8 compiler support for the ExoPlayer library. Apps may need to
+ add `compileOptions { targetCompatibility JavaVersion.VERSION_1_8 }` to their
+ gradle settings to ensure bytecode compatibility.
+* Set `compileSdkVersion` and `targetSdkVersion` to 28.
+* Support for automatic audio focus handling via
+ `SimpleExoPlayer.setAudioAttributes`.
+* Add `ExoPlayer.retry` convenience method.
+* Add `AudioListener` for listening to changes in audio configuration during
+ playback ([#3994](https://github.com/google/ExoPlayer/issues/3994)).
+* Add `LoadErrorHandlingPolicy` to allow configuration of load error handling
+ across `MediaSource` implementations
+ ([#3370](https://github.com/google/ExoPlayer/issues/3370)).
+* Allow passing a `Looper`, which specifies the thread that must be used to
+ access the player, when instantiating player instances using
+ `ExoPlayerFactory` ([#4278](https://github.com/google/ExoPlayer/issues/4278)).
+* Allow setting log level for ExoPlayer logcat output
+ ([#4665](https://github.com/google/ExoPlayer/issues/4665)).
+* Simplify `BandwidthMeter` injection: The `BandwidthMeter` should now be
+ passed directly to `ExoPlayerFactory`, instead of to `TrackSelection.Factory`
+ and `DataSource.Factory`. The `BandwidthMeter` is passed to the components
+ that need it internally. The `BandwidthMeter` may also be omitted, in which
+ case a default instance will be used.
+* Spherical video:
+ * Support for spherical video by setting `surface_type="spherical_view"` on
+ `PlayerView`.
+ * Support for
+ [VR180](https://github.com/google/spatial-media/blob/master/docs/vr180.md).
+* HLS:
+ * Support PlayReady.
+ * Add container format sniffing
+ ([#2025](https://github.com/google/ExoPlayer/issues/2025)).
+ * Support alternative `EXT-X-KEY` tags.
+ * Support `EXT-X-INDEPENDENT-SEGMENTS` in the master playlist.
+ * Support variable substitution
+ ([#4422](https://github.com/google/ExoPlayer/issues/4422)).
+ * Fix the bitrate being unset on primary track sample formats
+ ([#3297](https://github.com/google/ExoPlayer/issues/3297)).
+ * Make `HlsMediaSource.Factory` take a factory of trackers instead of a
+ tracker instance ([#4814](https://github.com/google/ExoPlayer/issues/4814)).
+* DASH:
+ * Support `messageData` attribute for in-manifest event streams.
+ * Clip periods to their specified durations
+ ([#4185](https://github.com/google/ExoPlayer/issues/4185)).
+* Improve seeking support for progressive streams:
+ * Support seeking in MPEG-TS
+ ([#966](https://github.com/google/ExoPlayer/issues/966)).
+ * Support seeking in MPEG-PS
+ ([#4476](https://github.com/google/ExoPlayer/issues/4476)).
+ * Support approximate seeking in ADTS using a constant bitrate assumption
+ ([#4548](https://github.com/google/ExoPlayer/issues/4548)). The
+ `FLAG_ENABLE_CONSTANT_BITRATE_SEEKING` flag must be set on the extractor to
+ enable this functionality.
+ * Support approximate seeking in AMR using a constant bitrate assumption.
+ The `FLAG_ENABLE_CONSTANT_BITRATE_SEEKING` flag must be set on the extractor
+ to enable this functionality.
+ * Add `DefaultExtractorsFactory.setConstantBitrateSeekingEnabled` to enable
+ approximate seeking using a constant bitrate assumption on all extractors
+ that support it.
+* Video:
+ * Add callback to `VideoListener` to notify of surface size changes.
+ * Improve performance when playing high frame-rate content, and when playing
+ at greater than 1x speed
+ ([#2777](https://github.com/google/ExoPlayer/issues/2777)).
+ * Scale up the initial video decoder maximum input size so playlist
+ transitions with small increases in maximum sample size do not require
+ reinitialization ([#4510](https://github.com/google/ExoPlayer/issues/4510)).
+ * Fix a bug where the player would not transition to the ended state when
+ playing video in tunneled mode.
+* Audio:
+ * Support attaching auxiliary audio effects to the `AudioTrack` via
+ `Player.setAuxEffectInfo` and `Player.clearAuxEffectInfo`.
+ * Support seamless adaptation while playing xHE-AAC streams.
+ ([#4360](https://github.com/google/ExoPlayer/issues/4360)).
+ * Increase `AudioTrack` buffer sizes to the theoretical maximum required for
+ each encoding for passthrough playbacks
+ ([#3803](https://github.com/google/ExoPlayer/issues/3803)).
+ * WAV: Fix issue where white noise would be output at the end of playback
+ ([#4724](https://github.com/google/ExoPlayer/issues/4724)).
+ * MP3: Fix issue where streams would play twice on the SM-T530
+ ([#4519](https://github.com/google/ExoPlayer/issues/4519)).
+* Analytics:
+ * Add callbacks to `DefaultDrmSessionEventListener` and `AnalyticsListener` to
+ be notified of acquired and released DRM sessions.
+ * Add uri field to `LoadEventInfo` in `MediaSourceEventListener` and
+ `AnalyticsListener` callbacks. This uri is the redirected uri if redirection
+ occurred ([#2054](https://github.com/google/ExoPlayer/issues/2054)).
+ * Add response headers field to `LoadEventInfo` in `MediaSourceEventListener`
+ and `AnalyticsListener` callbacks
+ ([#4361](https://github.com/google/ExoPlayer/issues/4361) and
+ [#4615](https://github.com/google/ExoPlayer/issues/4615)).
+* UI:
+ * Add option to `PlayerView` to show buffering view when playWhenReady is
+ false ([#4304](https://github.com/google/ExoPlayer/issues/4304)).
+ * Allow any `Drawable` to be used as `PlayerView` default artwork.
+* ConcatenatingMediaSource:
+ * Support lazy preparation of playlist media sources
+ ([#3972](https://github.com/google/ExoPlayer/issues/3972)).
+ * Support range removal with `removeMediaSourceRange` methods
+ ([#4542](https://github.com/google/ExoPlayer/issues/4542)).
+ * Support setting a new shuffle order with `setShuffleOrder`
+ ([#4791](https://github.com/google/ExoPlayer/issues/4791)).
+* MPEG-TS: Support CEA-608/708 in H262
+ ([#2565](https://github.com/google/ExoPlayer/issues/2565)).
+* Allow configuration of the back buffer in `DefaultLoadControl.Builder`
+ ([#4857](https://github.com/google/ExoPlayer/issues/4857)).
+* Allow apps to pass a `CacheKeyFactory` for setting custom cache keys when
+ creating a `CacheDataSource`.
+* Provide additional information for adaptive track selection.
+ `TrackSelection.updateSelectedTrack` has two new parameters for the current
+ queue of media chunks and iterators for information about upcoming chunks.
+* Allow `MediaCodecSelector`s to return multiple compatible decoders for
+ `MediaCodecRenderer`, and provide an (optional) `MediaCodecSelector` that
+ falls back to less preferred decoders like `MediaCodec.createDecoderByType`
+ ([#273](https://github.com/google/ExoPlayer/issues/273)).
+* Enable gzip for requests made by `SingleSampleMediaSource`
+ ([#4771](https://github.com/google/ExoPlayer/issues/4771)).
+* Fix bug reporting buffered position for multi-period windows, and add
+ convenience methods `Player.getTotalBufferedDuration` and
+ `Player.getContentBufferedDuration`
+ ([#4023](https://github.com/google/ExoPlayer/issues/4023)).
+* Fix bug where transitions to clipped media sources would happen too early
+ ([#4583](https://github.com/google/ExoPlayer/issues/4583)).
+* Fix bugs reporting events for multi-period media sources
+ ([#4492](https://github.com/google/ExoPlayer/issues/4492) and
+ [#4634](https://github.com/google/ExoPlayer/issues/4634)).
+* Fix issue where removing looping media from a playlist throws an exception
+ ([#4871](https://github.com/google/ExoPlayer/issues/4871).
+* Fix issue where the preferred audio or text track would not be selected if
+ mapped onto a secondary renderer of the corresponding type
+ ([#4711](http://github.com/google/ExoPlayer/issues/4711)).
+* Fix issue where errors of upcoming playlist items are thrown too early
+ ([#4661](https://github.com/google/ExoPlayer/issues/4661)).
+* Allow edit lists which do not start with a sync sample.
+ ([#4774](https://github.com/google/ExoPlayer/issues/4774)).
+* Fix issue with audio discontinuities at period transitions, e.g. when
+ looping ([#3829](https://github.com/google/ExoPlayer/issues/3829)).
+* Fix issue where `player.getCurrentTag()` throws an `IndexOutOfBoundsException`
+ ([#4822](https://github.com/google/ExoPlayer/issues/4822)).
+* Fix bug preventing use of multiple key session support (`multiSession=true`)
+ for non-Widevine `DefaultDrmSessionManager` instances
+ ([#4834](https://github.com/google/ExoPlayer/issues/4834)).
+* Fix issue where audio and video would desynchronize when playing
+ concatenations of gapless content
+ ([#4559](https://github.com/google/ExoPlayer/issues/4559)).
+* IMA extension:
+ * Refine the previous fix for empty ad groups to avoid discarding ad breaks
+ unnecessarily ([#4030](https://github.com/google/ExoPlayer/issues/4030) and
+ [#4280](https://github.com/google/ExoPlayer/issues/4280)).
+ * Fix handling of empty postrolls
+ ([#4681](https://github.com/google/ExoPlayer/issues/4681)).
+ * Fix handling of postrolls with multiple ads
+ ([#4710](https://github.com/google/ExoPlayer/issues/4710)).
+* MediaSession extension:
+ * Add `MediaSessionConnector.setCustomErrorMessage` to support setting custom
+ error messages.
+ * Add `MediaMetadataProvider` to support setting custom metadata
+ ([#3497](https://github.com/google/ExoPlayer/issues/3497)).
+* Cronet extension: Now distributed via jCenter.
+* FFmpeg extension: Support mu-law and A-law PCM.
+
+### 2.8.4 (2018-08-17) ###
+
+* IMA extension: Improve handling of consecutive empty ad groups
+ ([#4030](https://github.com/google/ExoPlayer/issues/4030)),
+ ([#4280](https://github.com/google/ExoPlayer/issues/4280)).
+
+### 2.8.3 (2018-07-23) ###
+
+* IMA extension:
+ * Fix behavior when creating/releasing the player then releasing
+ `ImaAdsLoader` ([#3879](https://github.com/google/ExoPlayer/issues/3879)).
+ * Add support for setting slots for companion ads.
+* Captions:
+ * TTML: Fix an issue with TTML using font size as % of cell resolution that
+ makes `SubtitleView.setApplyEmbeddedFontSizes()` not work correctly.
+ ([#4491](https://github.com/google/ExoPlayer/issues/4491)).
+ * CEA-608: Improve handling of embedded styles
+ ([#4321](https://github.com/google/ExoPlayer/issues/4321)).
+* DASH:
+ * Exclude text streams from duration calculations
+ ([#4029](https://github.com/google/ExoPlayer/issues/4029)).
+ * Fix freezing when playing multi-period manifests with `EventStream`s
+ ([#4492](https://github.com/google/ExoPlayer/issues/4492)).
+* DRM: Allow DrmInitData to carry a license server URL
+ ([#3393](https://github.com/google/ExoPlayer/issues/3393)).
+* MPEG-TS: Fix bug preventing SCTE-35 cues from being output
+ ([#4573](https://github.com/google/ExoPlayer/issues/4573)).
+* Expose all internal ID3 data stored in MP4 udta boxes, and switch from using
+ CommentFrame to InternalFrame for frames with gapless metadata in MP4.
+* Add `PlayerView.isControllerVisible`
+ ([#4385](https://github.com/google/ExoPlayer/issues/4385)).
+* Fix issue playing DRM protected streams on Asus Zenfone 2
+ ([#4403](https://github.com/google/ExoPlayer/issues/4413)).
+* Add support for multiple audio and video tracks in MPEG-PS streams
+ ([#4406](https://github.com/google/ExoPlayer/issues/4406)).
+* Add workaround for track index mismatches between trex and tkhd boxes in
+ fragmented MP4 files
+ ([#4477](https://github.com/google/ExoPlayer/issues/4477)).
+* Add workaround for track index mismatches between tfhd and tkhd boxes in
+ fragmented MP4 files
+ ([#4083](https://github.com/google/ExoPlayer/issues/4083)).
+* Ignore all MP4 edit lists if one edit list couldn't be handled
+ ([#4348](https://github.com/google/ExoPlayer/issues/4348)).
+* Fix issue when switching track selection from an embedded track to a primary
+ track in DASH ([#4477](https://github.com/google/ExoPlayer/issues/4477)).
+* Fix accessibility class name for `DefaultTimeBar`
+ ([#4611](https://github.com/google/ExoPlayer/issues/4611)).
+* Improved compatibility with FireOS devices.
+
+### 2.8.2 (2018-06-06) ###
+
+* IMA extension: Don't advertise support for video/mpeg ad media, as we don't
+ have an extractor for this
+ ([#4297](https://github.com/google/ExoPlayer/issues/4297)).
+* DASH: Fix playback getting stuck when playing representations that have both
+ sidx atoms and non-zero presentationTimeOffset values.
+* HLS:
+ * Allow injection of custom playlist trackers.
+ * Fix adaptation in live playlists with EXT-X-PROGRAM-DATE-TIME tags.
+* Mitigate memory leaks when `MediaSource` loads are slow to cancel
+ ([#4249](https://github.com/google/ExoPlayer/issues/4249)).
+* Fix inconsistent `Player.EventListener` invocations for recursive player state
+ changes ([#4276](https://github.com/google/ExoPlayer/issues/4276)).
+* Fix `MediaCodec.native_setSurface` crash on Moto C
+ ([#4315](https://github.com/google/ExoPlayer/issues/4315)).
+* Fix missing whitespace in CEA-608
+ ([#3906](https://github.com/google/ExoPlayer/issues/3906)).
+* Fix crash downloading HLS media playlists
+ ([#4396](https://github.com/google/ExoPlayer/issues/4396)).
+* Fix a bug where download cancellation was ignored
+ ([#4403](https://github.com/google/ExoPlayer/issues/4403)).
+* Set `METADATA_KEY_TITLE` on media descriptions
+ ([#4292](https://github.com/google/ExoPlayer/issues/4292)).
+* Allow apps to register custom MIME types
+ ([#4264](https://github.com/google/ExoPlayer/issues/4264)).
+
+### 2.8.1 (2018-05-22) ###
+
+* HLS:
+ * Fix playback of livestreams with EXT-X-PROGRAM-DATE-TIME tags
+ ([#4239](https://github.com/google/ExoPlayer/issues/4239)).
+ * Fix playback of clipped streams starting from non-keyframe positions
+ ([#4241](https://github.com/google/ExoPlayer/issues/4241)).
+* OkHttp extension: Fix to correctly include response headers in thrown
+ `InvalidResponseCodeException`s.
+* Add possibility to cancel `PlayerMessage`s.
+* UI:
+ * Add `PlayerView.setKeepContentOnPlayerReset` to keep the currently displayed
+ video frame or media artwork visible when the player is reset
+ ([#2843](https://github.com/google/ExoPlayer/issues/2843)).
+* Fix crash when switching surface on Moto E(4)
+ ([#4134](https://github.com/google/ExoPlayer/issues/4134)).
+* Fix a bug that could cause event listeners to be called with inconsistent
+ information if an event listener interacted with the player
+ ([#4262](https://github.com/google/ExoPlayer/issues/4262)).
+* Audio:
+ * Fix extraction of PCM in MP4/MOV
+ ([#4228](https://github.com/google/ExoPlayer/issues/4228)).
+ * FLAC: Supports seeking for FLAC files without SEEKTABLE
+ ([#1808](https://github.com/google/ExoPlayer/issues/1808)).
+* Captions:
+ * TTML:
+ * Fix a styling issue when there are multiple regions displayed at the same
+ time that can make text size of each region much smaller than defined.
+ * Fix an issue when the caption line has no text (empty line or only line
+ break), and the line's background is still displayed.
+ * Support TTML font size using % correctly (as percentage of document cell
+ resolution).
+
+### 2.8.0 (2018-05-03) ###
+
+* Downloading:
+ * Add `DownloadService`, `DownloadManager` and related classes
+ ([#2643](https://github.com/google/ExoPlayer/issues/2643)). Information on
+ using these components to download progressive formats can be found
+ [here](https://medium.com/google-exoplayer/downloading-streams-6d259eec7f95).
+ To see how to download DASH, HLS and SmoothStreaming media, take a look at
+ the app.
+ * Updated main demo app to support downloading DASH, HLS, SmoothStreaming and
+ progressive media.
+* MediaSources:
+ * Allow reusing media sources after they have been released and
+ also in parallel to allow adding them multiple times to a concatenation.
+ ([#3498](https://github.com/google/ExoPlayer/issues/3498)).
+ * Merged `DynamicConcatenatingMediaSource` into `ConcatenatingMediaSource` and
+ deprecated `DynamicConcatenatingMediaSource`.
+ * Allow clipping of child media sources where the period and window have a
+ non-zero offset with `ClippingMediaSource`.
+ * Allow adding and removing `MediaSourceEventListener`s to MediaSources after
+ they have been created. Listening to events is now supported for all
+ media sources including composite sources.
+ * Added callbacks to `MediaSourceEventListener` to get notified when media
+ periods are created, released and being read from.
+ * Support live stream clipping with `ClippingMediaSource`.
+ * Allow setting tags for all media sources in their factories. The tag of the
+ current window can be retrieved with `Player.getCurrentTag`.
+* UI:
+ * Add support for displaying error messages and a buffering spinner in
+ `PlayerView`.
+ * Add support for listening to `AspectRatioFrameLayout`'s aspect ratio update
+ ([#3736](https://github.com/google/ExoPlayer/issues/3736)).
+ * Add `PlayerNotificationManager` for displaying notifications reflecting the
+ player state.
+ * Add `TrackSelectionView` for selecting tracks with `DefaultTrackSelector`.
+ * Add `TrackNameProvider` for converting track `Format`s to textual
+ descriptions, and `DefaultTrackNameProvider` as a default implementation.
+* Track selection:
+ * Reworked `MappingTrackSelector` and `DefaultTrackSelector`.
+ * `DefaultTrackSelector.Parameters` now implements `Parcelable`.
+ * Added UI components for track selection (see above).
+* Audio:
+ * Support extracting data from AMR container formats, including both narrow
+ and wide band ([#2527](https://github.com/google/ExoPlayer/issues/2527)).
+ * FLAC:
+ * Sniff FLAC files correctly if they have ID3 headers
+ ([#4055](https://github.com/google/ExoPlayer/issues/4055)).
+ * Supports FLAC files with high sample rate (176400 and 192000)
+ ([#3769](https://github.com/google/ExoPlayer/issues/3769)).
+ * Factor out `AudioTrack` position tracking from `DefaultAudioSink`.
+ * Fix an issue where the playback position would pause just after playback
+ begins, and poll the audio timestamp less frequently once it starts
+ advancing ([#3841](https://github.com/google/ExoPlayer/issues/3841)).
+ * Add an option to skip silent audio in `PlaybackParameters`
+ ([#2635](https://github.com/google/ExoPlayer/issues/2635)).
+ * Fix an issue where playback of TrueHD streams would get stuck after seeking
+ due to not finding a syncframe
+ ([#3845](https://github.com/google/ExoPlayer/issues/3845)).
+ * Fix an issue with eac3-joc playback where a codec would fail to configure
+ ([#4165](https://github.com/google/ExoPlayer/issues/4165)).
+ * Handle non-empty end-of-stream buffers, to fix gapless playback of streams
+ with encoder padding when the decoder returns a non-empty final buffer.
+ * Allow trimming more than one sample when applying an elst audio edit via
+ gapless playback info.
+ * Allow overriding skipping/scaling with custom `AudioProcessor`s
+ ([#3142](https://github.com/google/ExoPlayer/issues/3142)).
+* Caching:
+ * Add release method to the `Cache` interface, and prevent multiple instances
+ of `SimpleCache` using the same folder at the same time.
+ * Cache redirect URLs
+ ([#2360](https://github.com/google/ExoPlayer/issues/2360)).
+* DRM:
+ * Allow multiple listeners for `DefaultDrmSessionManager`.
+ * Pass `DrmSessionManager` to `ExoPlayerFactory` instead of `RendererFactory`.
+ * Change minimum API requirement for CBC and pattern encryption from 24 to 25
+ ([#4022](https://github.com/google/ExoPlayer/issues/4022)).
+ * Fix handling of 307/308 redirects when making license requests
+ ([#4108](https://github.com/google/ExoPlayer/issues/4108)).
+* HLS:
+ * Fix playlist loading error propagation when the current selection does
+ not include all of the playlist's variants.
+ * Fix SAMPLE-AES-CENC and SAMPLE-AES-CTR EXT-X-KEY methods
+ ([#4145](https://github.com/google/ExoPlayer/issues/4145)).
+ * Preeptively declare an ID3 track in chunkless preparation
+ ([#4016](https://github.com/google/ExoPlayer/issues/4016)).
+ * Add support for multiple #EXT-X-MAP tags in a media playlist
+ ([#4164](https://github.com/google/ExoPlayer/issues/4182)).
+ * Fix seeking in live streams
+ ([#4187](https://github.com/google/ExoPlayer/issues/4187)).
+* IMA extension:
+ * Allow setting the ad media load timeout
+ ([#3691](https://github.com/google/ExoPlayer/issues/3691)).
+ * Expose ad load errors via `MediaSourceEventListener` on `AdsMediaSource`,
+ and allow setting an ad event listener on `ImaAdsLoader`. Deprecate the
+ `AdsMediaSource.EventListener`.
+* Add `AnalyticsListener` interface which can be registered in
+ `SimpleExoPlayer` to receive detailed metadata for each ExoPlayer event.
+* Optimize seeking in FMP4 by enabling seeking to the nearest sync sample within
+ a fragment. This benefits standalone FMP4 playbacks, DASH and SmoothStreaming.
+* Updated default max buffer length in `DefaultLoadControl`.
+* Fix ClearKey decryption error if the key contains a forward slash
+ ([#4075](https://github.com/google/ExoPlayer/issues/4075)).
+* Fix crash when switching surface on Huawei P9 Lite
+ ([#4084](https://github.com/google/ExoPlayer/issues/4084)), and Philips QM163E
+ ([#4104](https://github.com/google/ExoPlayer/issues/4104)).
+* Support ZLIB compressed PGS subtitles.
+* Added `getPlaybackError` to `Player` interface.
+* Moved initial bitrate estimate from `AdaptiveTrackSelection` to
+ `DefaultBandwidthMeter`.
+* Removed default renderer time offset of 60000000 from internal player. The
+ actual renderer timestamp offset can be obtained by listening to
+ `BaseRenderer.onStreamChanged`.
+* Added dependencies on checkerframework annotations for static code analysis.
+
+### 2.7.3 (2018-04-04) ###
+
+* Fix ProGuard configuration for Cast, IMA and OkHttp extensions.
+* Update OkHttp extension to depend on OkHttp 3.10.0.
+
+### 2.7.2 (2018-03-29) ###
+
+* Gradle: Upgrade Gradle version from 4.1 to 4.4 so it can work with Android
+ Studio 3.1 ([#3708](https://github.com/google/ExoPlayer/issues/3708)).
+* Match codecs starting with "mp4a" to different Audio MimeTypes
+ ([#3779](https://github.com/google/ExoPlayer/issues/3779)).
+* Fix ANR issue on Redmi 4X and Redmi Note 4
+ ([#4006](https://github.com/google/ExoPlayer/issues/4006)).
+* Fix handling of zero padded strings when parsing Matroska streams
+ ([#4010](https://github.com/google/ExoPlayer/issues/4010)).
+* Fix "Decoder input buffer too small" error when playing some FLAC streams.
+* MediaSession extension: Omit fast forward and rewind actions when media is not
+ seekable ([#4001](https://github.com/google/ExoPlayer/issues/4001)).
+
+### 2.7.1 (2018-03-09) ###
+
+* Gradle: Replaced 'compile' (deprecated) with 'implementation' and
+ 'api'. This may lead to build breakage for applications upgrading from
+ previous version that rely on indirect dependencies of certain modules. In
+ such cases, application developers need to add the missing dependency to
+ their gradle file. You can read more about the new dependency configurations
+ [here](https://developer.android.com/studio/build/gradle-plugin-3-0-0-migration.html#new_configurations).
+* HlsMediaSource: Make HLS periods start at zero instead of the epoch.
+ Applications that rely on HLS timelines having a period starting at
+ the epoch will need to update their handling of HLS timelines. The program
+ date time is still available via the informational
+ `Timeline.Window.windowStartTimeMs` field
+ ([#3865](https://github.com/google/ExoPlayer/issues/3865),
+ [#3888](https://github.com/google/ExoPlayer/issues/3888)).
+* Enable seeking in MP4 streams where duration is set incorrectly in the track
+ header ([#3926](https://github.com/google/ExoPlayer/issues/3926)).
+* Video: Force rendering a frame periodically in `MediaCodecVideoRenderer` and
+ `LibvpxVideoRenderer`, even if it is late.
+
+### 2.7.0 (2018-02-19) ###
+
+* Player interface:
+ * Add optional parameter to `stop` to reset the player when stopping.
+ * Add a reason to `EventListener.onTimelineChanged` to distinguish between
+ initial preparation, reset and dynamic updates.
+ * Add `Player.DISCONTINUITY_REASON_AD_INSERTION` to the possible reasons
+ reported in `Eventlistener.onPositionDiscontinuity` to distinguish
+ transitions to and from ads within one period from transitions between
+ periods.
+ * Replaced `ExoPlayer.sendMessages` with `ExoPlayer.createMessage` to allow
+ more customization of the message. Now supports setting a message delivery
+ playback position and/or a delivery handler
+ ([#2189](https://github.com/google/ExoPlayer/issues/2189)).
+ * Add `Player.VideoComponent`, `Player.TextComponent` and
+ `Player.MetadataComponent` interfaces that define optional video, text and
+ metadata output functionality. New `getVideoComponent`, `getTextComponent`
+ and `getMetadataComponent` methods provide access to this functionality.
+* Add `ExoPlayer.setSeekParameters` for controlling how seek operations are
+ performed. The `SeekParameters` class contains defaults for exact seeking and
+ seeking to the closest sync points before, either side or after specified seek
+ positions. `SeekParameters` are not currently supported when playing HLS
+ streams.
+* DefaultTrackSelector:
+ * Replace `DefaultTrackSelector.Parameters` copy methods with a builder.
+ * Support disabling of individual text track selection flags.
+* Buffering:
+ * Allow a back-buffer of media to be retained behind the current playback
+ position, for fast backward seeking. The back-buffer can be configured by
+ custom `LoadControl` implementations.
+ * Add ability for `SequenceableLoader` to re-evaluate its buffer and discard
+ buffered media so that it can be re-buffered in a different quality.
+ * Allow more flexible loading strategy when playing media containing multiple
+ sub-streams, by allowing injection of custom `CompositeSequenceableLoader`
+ factories through `DashMediaSource.Factory`, `HlsMediaSource.Factory`,
+ `SsMediaSource.Factory`, and `MergingMediaSource`.
+ * Play out existing buffer before retrying for progressive live streams
+ ([#1606](https://github.com/google/ExoPlayer/issues/1606)).
+* UI:
+ * Generalized player and control views to allow them to bind with any
+ `Player`, and renamed them to `PlayerView` and `PlayerControlView`
+ respectively.
+ * Made `PlayerView` automatically apply video rotation when configured to use
+ `TextureView` ([#91](https://github.com/google/ExoPlayer/issues/91)).
+ * Made `PlayerView` play button behave correctly when the player is ended
+ ([#3689](https://github.com/google/ExoPlayer/issues/3689)), and call a
+ `PlaybackPreparer` when the player is idle.
+* DRM: Optimistically attempt playback of DRM protected content that does not
+ declare scheme specific init data in the manifest. If playback of clear
+ samples without keys is allowed, delay DRM session error propagation until
+ keys are actually needed
+ ([#3630](https://github.com/google/ExoPlayer/issues/3630)).
+* DASH:
+ * Support in-band Emsg events targeting the player with scheme id
+ `urn:mpeg:dash:event:2012` and scheme values "1", "2" and "3".
+ * Support EventStream elements in DASH manifests.
+* HLS:
+ * Add opt-in support for chunkless preparation in HLS. This allows an
+ HLS source to finish preparation without downloading any chunks, which can
+ significantly reduce initial buffering time
+ ([#3149](https://github.com/google/ExoPlayer/issues/3149)). More details
+ can be found
+ [here](https://medium.com/google-exoplayer/faster-hls-preparation-f6611aa15ea6).
+ * Fail if unable to sync with the Transport Stream, rather than entering
+ stuck in an indefinite buffering state.
+ * Fix mime type propagation
+ ([#3653](https://github.com/google/ExoPlayer/issues/3653)).
+ * Fix ID3 context reuse across segment format changes
+ ([#3622](https://github.com/google/ExoPlayer/issues/3622)).
+ * Use long for media sequence numbers
+ ([#3747](https://github.com/google/ExoPlayer/issues/3747))
+ * Add initial support for the EXT-X-GAP tag.
+* Audio:
+ * Support TrueHD passthrough for rechunked samples in Matroska files
+ ([#2147](https://github.com/google/ExoPlayer/issues/2147)).
+ * Support resampling 24-bit and 32-bit integer to 32-bit float for high
+ resolution output in `DefaultAudioSink`
+ ([#3635](https://github.com/google/ExoPlayer/pull/3635)).
+* Captions:
+ * Basic support for PGS subtitles
+ ([#3008](https://github.com/google/ExoPlayer/issues/3008)).
+ * Fix handling of CEA-608 captions where multiple buffers have the same
+ presentation timestamp
+ ([#3782](https://github.com/google/ExoPlayer/issues/3782)).
+* Caching:
+ * Fix cache corruption issue
+ ([#3762](https://github.com/google/ExoPlayer/issues/3762)).
+ * Implement periodic check in `CacheDataSource` to see whether it's possible
+ to switch to reading/writing the cache having initially bypassed it.
+* IMA extension:
+ * Fix the player getting stuck when an ad group fails to load
+ ([#3584](https://github.com/google/ExoPlayer/issues/3584)).
+ * Work around loadAd not being called beore the LOADED AdEvent arrives
+ ([#3552](https://github.com/google/ExoPlayer/issues/3552)).
+ * Handle asset mismatch errors
+ ([#3801](https://github.com/google/ExoPlayer/issues/3801)).
+ * Add support for playing non-Extractor content MediaSources in
+ the IMA demo app
+ ([#3676](https://github.com/google/ExoPlayer/issues/3676)).
+ * Fix handling of ad tags where ad groups are out of order
+ ([#3716](https://github.com/google/ExoPlayer/issues/3716)).
+ * Fix handling of ad tags with only preroll/postroll ad groups
+ ([#3715](https://github.com/google/ExoPlayer/issues/3715)).
+ * Propagate ad media preparation errors to IMA so that the ads can be
+ skipped.
+ * Handle exceptions in IMA callbacks so that can be logged less verbosely.
+* New Cast extension. Simplifies toggling between local and Cast playbacks.
+* `EventLogger` moved from the demo app into the core library.
+* Fix ANR issue on the Huawei P8 Lite, Huawei Y6II, Moto C+, Meizu M5C,
+ Lenovo K4 Note and Sony Xperia E5.
+ ([#3724](https://github.com/google/ExoPlayer/issues/3724),
+ [#3835](https://github.com/google/ExoPlayer/issues/3835)).
+* Fix potential NPE when removing media sources from a
+ DynamicConcatenatingMediaSource
+ ([#3796](https://github.com/google/ExoPlayer/issues/3796)).
+* Check `sys.display-size` on Philips ATVs
+ ([#3807](https://github.com/google/ExoPlayer/issues/3807)).
+* Release `Extractor`s on the loading thread to avoid potentially leaking
+ resources when the playback thread has quit by the time the loading task has
+ completed.
+* ID3: Better handle malformed ID3 data
+ ([#3792](https://github.com/google/ExoPlayer/issues/3792).
+* Support 14-bit mode and little endianness in DTS PES packets
+ ([#3340](https://github.com/google/ExoPlayer/issues/3340)).
+* Demo app: Add ability to download not DRM protected content.
+
+### 2.6.1 (2017-12-15) ###
+
+* Add factories to `ExtractorMediaSource`, `HlsMediaSource`, `SsMediaSource`,
+ `DashMediaSource` and `SingleSampleMediaSource`.
+* Use the same listener `MediaSourceEventListener` for all MediaSource
+ implementations.
+* IMA extension:
+ * Support non-ExtractorMediaSource ads
+ ([#3302](https://github.com/google/ExoPlayer/issues/3302)).
+ * Skip ads before the ad preceding the player's initial seek position
+ ([#3527](https://github.com/google/ExoPlayer/issues/3527)).
+ * Fix ad loading when there is no preroll.
+ * Add an option to turn off hiding controls during ad playback
+ ([#3532](https://github.com/google/ExoPlayer/issues/3532)).
+ * Support specifying an ads response instead of an ad tag
+ ([#3548](https://github.com/google/ExoPlayer/issues/3548)).
+ * Support overriding the ad load timeout
+ ([#3556](https://github.com/google/ExoPlayer/issues/3556)).
+* DASH: Support time zone designators in ISO8601 UTCTiming elements
+ ([#3524](https://github.com/google/ExoPlayer/issues/3524)).
+* Audio:
+ * Support 32-bit PCM float output from `DefaultAudioSink`, and add an option
+ to use this with `FfmpegAudioRenderer`.
+ * Add support for extracting 32-bit WAVE files
+ ([#3379](https://github.com/google/ExoPlayer/issues/3379)).
+ * Support extraction and decoding of Dolby Atmos
+ ([#2465](https://github.com/google/ExoPlayer/issues/2465)).
+ * Fix handling of playback parameter changes while paused when followed by a
+ seek.
+* SimpleExoPlayer: Allow multiple audio and video debug listeners.
+* DefaultTrackSelector: Support undefined language text track selection when the
+ preferred language is not available
+ ([#2980](https://github.com/google/ExoPlayer/issues/2980)).
+* Add options to `DefaultLoadControl` to set maximum buffer size in bytes and
+ to choose whether size or time constraints are prioritized.
+* Use surfaceless context for secure `DummySurface`, if available
+ ([#3558](https://github.com/google/ExoPlayer/issues/3558)).
+* FLV: Fix playback of live streams that do not contain an audio track
+ ([#3188](https://github.com/google/ExoPlayer/issues/3188)).
+* CEA-608: Fix handling of row count changes in roll-up mode
+ ([#3513](https://github.com/google/ExoPlayer/issues/3513)).
+* Prevent period transitions when seeking to the end of a period when paused
+ ([#2439](https://github.com/google/ExoPlayer/issues/2439)).
+
+### 2.6.0 (2017-11-03) ###
+
+* Removed "r" prefix from versions. This release is "2.6.0", not "r2.6.0".
+* New `Player.DefaultEventListener` abstract class can be extended to avoid
+ having to implement all methods defined by `Player.EventListener`.
+* Added a reason to `EventListener.onPositionDiscontinuity`
+ ([#3252](https://github.com/google/ExoPlayer/issues/3252)).
+* New `setShuffleModeEnabled` method for enabling shuffled playback.
+* SimpleExoPlayer: Support for multiple video, text and metadata outputs.
+* Support for `Renderer`s that don't consume any media
+ ([#3212](https://github.com/google/ExoPlayer/issues/3212)).
+* Fix reporting of internal position discontinuities via
+ `Player.onPositionDiscontinuity`. `DISCONTINUITY_REASON_SEEK_ADJUSTMENT` is
+ added to disambiguate position adjustments during seeks from other types of
+ internal position discontinuity.
+* Fix potential `IndexOutOfBoundsException` when calling `ExoPlayer.getDuration`
+ ([#3362](https://github.com/google/ExoPlayer/issues/3362)).
+* Fix playbacks involving looping, concatenation and ads getting stuck when
+ media contains tracks with uneven durations
+ ([#1874](https://github.com/google/ExoPlayer/issues/1874)).
+* Fix issue with `ContentDataSource` when reading from certain `ContentProvider`
+ implementations ([#3426](https://github.com/google/ExoPlayer/issues/3426)).
+* Better playback experience when the video decoder cannot keep up, by skipping
+ to key-frames. This is particularly relevant for variable speed playbacks.
+* Allow `SingleSampleMediaSource` to suppress load errors
+ ([#3140](https://github.com/google/ExoPlayer/issues/3140)).
+* `DynamicConcatenatingMediaSource`: Allow specifying a callback to be invoked
+ after a dynamic playlist modification has been applied
+ ([#3407](https://github.com/google/ExoPlayer/issues/3407)).
+* Audio: New `AudioSink` interface allows customization of audio output path.
+* Offline: Added `Downloader` implementations for DASH, HLS, SmoothStreaming
+ and progressive streams.
+* Track selection:
+ * Fixed adaptive track selection logic for live playbacks
+ ([#3017](https://github.com/google/ExoPlayer/issues/3017)).
+ * Added ability to select the lowest bitrate tracks.
+* DASH:
+ * Don't crash when a malformed or unexpected manifest update occurs
+ ([#2795](https://github.com/google/ExoPlayer/issues/2795)).
+* HLS:
+ * Support for Widevine protected FMP4 variants.
+ * Support CEA-608 in FMP4 variants.
+ * Support extractor injection
+ ([#2748](https://github.com/google/ExoPlayer/issues/2748)).
+* DRM:
+ * Improved compatibility with ClearKey content
+ ([#3138](https://github.com/google/ExoPlayer/issues/3138)).
+ * Support multiple PSSH boxes of the same type.
+ * Retry initial provisioning and key requests if they fail
+ * Fix incorrect parsing of non-CENC sinf boxes.
+* IMA extension:
+ * Expose `AdsLoader` via getter
+ ([#3322](https://github.com/google/ExoPlayer/issues/3322)).
+ * Handle `setPlayWhenReady` calls during ad playbacks
+ ([#3303](https://github.com/google/ExoPlayer/issues/3303)).
+ * Ignore seeks if an ad is playing
+ ([#3309](https://github.com/google/ExoPlayer/issues/3309)).
+ * Improve robustness of `ImaAdsLoader` in case content is not paused between
+ content to ad transitions
+ ([#3430](https://github.com/google/ExoPlayer/issues/3430)).
+* UI:
+ * Allow specifying a `Drawable` for the `TimeBar` scrubber
+ ([#3337](https://github.com/google/ExoPlayer/issues/3337)).
+ * Allow multiple listeners on `TimeBar`
+ ([#3406](https://github.com/google/ExoPlayer/issues/3406)).
+* New Leanback extension: Simplifies binding Exoplayer to Leanback UI
+ components.
+* Unit tests moved to Robolectric.
+* Misc bugfixes.
+
+### r2.5.4 (2017-10-19) ###
+
+* Remove unnecessary media playlist fetches during playback of live HLS streams.
+* Add the ability to inject a HLS playlist parser through `HlsMediaSource`.
+* Fix potential `IndexOutOfBoundsException` when using `ImaMediaSource`
+ ([#3334](https://github.com/google/ExoPlayer/issues/3334)).
+* Fix an issue parsing MP4 content containing non-CENC sinf boxes.
+* Fix memory leak when seeking with repeated periods.
+* Fix playback position when `ExoPlayer.prepare` is called with `resetPosition`
+ set to false.
+* Ignore MP4 edit lists that seem invalid
+ ([#3351](https://github.com/google/ExoPlayer/issues/3351)).
+* Add extractor flag for ignoring all MP4 edit lists
+ ([#3358](https://github.com/google/ExoPlayer/issues/3358)).
+* Improve extensibility by exposing public constructors for
+ `FrameworkMediaCrypto` and by making `DefaultDashChunkSource.getNextChunk`
+ non-final.
+
+### r2.5.3 (2017-09-20) ###
+
+* IMA extension: Support skipping of skippable ads on AndroidTV and other
+ non-touch devices ([#3258](https://github.com/google/ExoPlayer/issues/3258)).
+* HLS: Fix broken WebVTT captions when PTS wraps around
+ ([#2928](https://github.com/google/ExoPlayer/issues/2928)).
+* Captions: Fix issues rendering CEA-608 captions
+ ([#3250](https://github.com/google/ExoPlayer/issues/3250)).
+* Workaround broken AAC decoders on Galaxy S6
+ ([#3249](https://github.com/google/ExoPlayer/issues/3249)).
+* Caching: Fix infinite loop when cache eviction fails
+ ([#3260](https://github.com/google/ExoPlayer/issues/3260)).
+* Caching: Force use of BouncyCastle on JellyBean to fix decryption issue
+ ([#2755](https://github.com/google/ExoPlayer/issues/2755)).
+
+### r2.5.2 (2017-09-11) ###
+
+* IMA extension: Fix issue where ad playback could end prematurely for some
+ content types ([#3180](https://github.com/google/ExoPlayer/issues/3180)).
+* RTMP extension: Fix SIGABRT on fast RTMP stream restart
+ ([#3156](https://github.com/google/ExoPlayer/issues/3156)).
+* UI: Allow app to manually specify ad markers
+ ([#3184](https://github.com/google/ExoPlayer/issues/3184)).
+* DASH: Expose segment indices to subclasses of DefaultDashChunkSource
+ ([#3037](https://github.com/google/ExoPlayer/issues/3037)).
+* Captions: Added robustness against malformed WebVTT captions
+ ([#3228](https://github.com/google/ExoPlayer/issues/3228)).
+* DRM: Support forcing a specific license URL.
+* Fix playback error when seeking in media loaded through content:// URIs
+ ([#3216](https://github.com/google/ExoPlayer/issues/3216)).
+* Fix issue playing MP4s in which the last atom specifies a size of zero
+ ([#3191](https://github.com/google/ExoPlayer/issues/3191)).
+* Workaround playback failures on some Xiaomi devices
+ ([#3171](https://github.com/google/ExoPlayer/issues/3171)).
+* Workaround SIGSEGV issue on some devices when setting and swapping surface for
+ secure playbacks ([#3215](https://github.com/google/ExoPlayer/issues/3215)).
+* Workaround for Nexus 7 issue when swapping output surface
+ ([#3236](https://github.com/google/ExoPlayer/issues/3236)).
+* Workaround for SimpleExoPlayerView's surface not being hidden properly
+ ([#3160](https://github.com/google/ExoPlayer/issues/3160)).
+
+### r2.5.1 (2017-08-08) ###
+
+* Fix an issue that could cause the reported playback position to stop advancing
+ in some cases.
+* Fix an issue where a Surface could be released whilst still in use by the
+ player.
+
+### r2.5.0 (2017-08-07) ###
+
+* IMA extension: Wraps the Google Interactive Media Ads (IMA) SDK to provide an
+ easy and seamless way of incorporating display ads into ExoPlayer playbacks.
+ You can read more about the IMA extension
+ [here](https://medium.com/google-exoplayer/playing-ads-with-exoplayer-and-ima-868dfd767ea).
+* MediaSession extension: Provides an easy way to connect ExoPlayer with
+ MediaSessionCompat in the Android Support Library.
+* RTMP extension: An extension for playing streams over RTMP.
+* Build: Made it easier for application developers to depend on a local checkout
+ of ExoPlayer. You can learn how to do this
+ [here](https://medium.com/google-exoplayer/howto-2-depend-on-a-local-checkout-of-exoplayer-bcd7f8531720).
+* Core playback improvements:
+ * Eliminated re-buffering when changing audio and text track selections during
+ playback of progressive streams
+ ([#2926](https://github.com/google/ExoPlayer/issues/2926)).
+ * New DynamicConcatenatingMediaSource class to support playback of dynamic
+ playlists.
+ * New ExoPlayer.setRepeatMode method for dynamic toggling of repeat mode
+ during playback. Use of setRepeatMode should be preferred to
+ LoopingMediaSource for most looping use cases. You can read more about
+ setRepeatMode
+ [here](https://medium.com/google-exoplayer/repeat-modes-in-exoplayer-19dd85f036d3).
+ * Eliminated jank when switching video playback from one Surface to another on
+ API level 23+ for unencrypted content, and on devices that support the
+ EGL_EXT_protected_content OpenGL extension for protected content
+ ([#677](https://github.com/google/ExoPlayer/issues/677)).
+ * Enabled ExoPlayer instantiation on background threads without Loopers.
+ Events from such players are delivered on the application's main thread.
+* HLS improvements:
+ * Optimized adaptive switches for playlists that specify the
+ EXT-X-INDEPENDENT-SEGMENTS tag.
+ * Optimized in-buffer seeking
+ ([#551](https://github.com/google/ExoPlayer/issues/551)).
+ * Eliminated re-buffering when changing audio and text track selections during
+ playback, provided the new selection does not require switching to different
+ renditions ([#2718](https://github.com/google/ExoPlayer/issues/2718)).
+ * Exposed all media playlist tags in ExoPlayer's MediaPlaylist object.
+* DASH: Support for seamless switching across streams in different AdaptationSet
+ elements ([#2431](https://github.com/google/ExoPlayer/issues/2431)).
+* DRM: Support for additional crypto schemes (cbc1, cbcs and cens) on
+ API level 24+ ([#1989](https://github.com/google/ExoPlayer/issues/1989)).
+* Captions: Initial support for SSA/ASS subtitles
+ ([#889](https://github.com/google/ExoPlayer/issues/889)).
+* AndroidTV: Fixed issue where tunneled video playback would not start on some
+ devices ([#2985](https://github.com/google/ExoPlayer/issues/2985)).
+* MPEG-TS: Fixed segmentation issue when parsing H262
+ ([#2891](https://github.com/google/ExoPlayer/issues/2891)).
+* Cronet extension: Support for a user-defined fallback if Cronet library is not
+ present.
+* Fix buffer too small IllegalStateException issue affecting some composite
+ media playbacks ([#2900](https://github.com/google/ExoPlayer/issues/2900)).
+* Misc bugfixes.
+
+### r2.4.4 (2017-07-19) ###
* HLS/MPEG-TS: Some initial optimizations of MPEG-TS extractor performance
([#3040](https://github.com/google/ExoPlayer/issues/3040)).
@@ -11,7 +1526,7 @@
* Video: Fix video dimension reporting on some devices
([#3007](https://github.com/google/ExoPlayer/issues/3007)).
-### r2.4.3 ###
+### r2.4.3 (2017-06-30) ###
* Audio: Workaround custom audio decoders misreporting their maximum supported
channel counts ([#2940](https://github.com/google/ExoPlayer/issues/2940)).
@@ -29,7 +1544,7 @@
([#2977](https://github.com/google/ExoPlayer/pull/2977)).
* Misc bugfixes.
-### r2.4.2 ###
+### r2.4.2 (2017-06-06) ###
* Stability: Work around Nexus 10 reboot when playing certain content
([#2806](https://github.com/google/ExoPlayer/issues/2806)).
@@ -43,7 +1558,7 @@
([#2871](https://github.com/google/ExoPlayer/issues/2871)).
* Misc bugfixes.
-### r2.4.1 ###
+### r2.4.1 (2017-05-23) ###
* Stability: Avoid OutOfMemoryError in extractors when parsing malformed media
([#2780](https://github.com/google/ExoPlayer/issues/2780)).
@@ -66,7 +1581,7 @@
([#2824](https://github.com/google/ExoPlayer/issues/2824)).
* Misc bugfixes.
-### r2.4.0 ###
+### r2.4.0 (2017-04-25) ###
* New modular library structure. You can read more about depending on individual
library modules
@@ -99,7 +1614,7 @@
* FLAC extension: Enabled 64 bit targets.
* Misc bugfixes.
-### r2.3.1 ###
+### r2.3.1 (2017-03-23) ###
* Fix NPE enabling WebVTT subtitles in DASH streams
([#2596](https://github.com/google/ExoPlayer/issues/2596)).
@@ -108,7 +1623,7 @@
* Minor fix for CEA-708 decoder
([#2595](https://github.com/google/ExoPlayer/issues/2595)).
-### r2.3.0 ###
+### r2.3.0 (2017-03-16) ###
* GVR extension: Wraps the Google VR Audio SDK to provide spatial audio
rendering. You can read more about the GVR extension
@@ -155,7 +1670,7 @@
([#2427](https://github.com/google/ExoPlayer/issues/2427)).
* Misc bugfixes.
-### r2.2.0 ###
+### r2.2.0 (2017-01-30) ###
* Demo app: Automatic recovery from BehindLiveWindowException, plus improved
handling of pausing and resuming live streams
@@ -218,7 +1733,7 @@
[#2264](https://github.com/google/ExoPlayer/issues/2264) and
[#2290](https://github.com/google/ExoPlayer/issues/2290).
-### r2.1.1 ###
+### r2.1.1 (2016-12-20) ###
* Fix some subtitle types (e.g. WebVTT) being displayed out of sync
([#2208](https://github.com/google/ExoPlayer/issues/2208)).
@@ -228,7 +1743,7 @@
* Fix issue where playbacks could get stuck in the initial buffering state if
over 1MB of data needs to be read to initialize the playback.
-### r2.1.0 ###
+### r2.1.0 (2016-12-14) ###
* HLS: Support for seeking in live streams
([#87](https://github.com/google/ExoPlayer/issues/87)).
@@ -249,7 +1764,7 @@
[here](https://medium.com/google-exoplayer/customizing-exoplayers-ui-components-728cf55ee07a#.9ewjg7avi).
* Robustness improvements when handling MediaSource timeline changes and
MediaPeriod transitions.
-* EIA608: Support for caption styling and positioning.
+* CEA-608: Support for caption styling and positioning.
* MPEG-TS: Improved support:
* Support injection of custom TS payload readers.
* Support injection of custom section payload readers.
@@ -282,12 +1797,12 @@
([#2145](https://github.com/google/ExoPlayer/issues/2145)).
* Misc bugfixes.
-### r2.0.4 ###
+### r2.0.4 (2016-10-20) ###
* Fix crash on Jellybean devices when using playback controls
([#1965](https://github.com/google/ExoPlayer/issues/1965)).
-### r2.0.3 ###
+### r2.0.3 (2016-10-17) ###
* Fixed NullPointerException in ExtractorMediaSource
([#1914](https://github.com/google/ExoPlayer/issues/1914)).
@@ -304,7 +1819,7 @@
* Improvements to Cronet network stack extension.
* Misc bug fixes.
-### r2.0.2 ###
+### r2.0.2 (2016-10-06) ###
* Fixes for MergingMediaSource and sideloaded subtitles.
([#1882](https://github.com/google/ExoPlayer/issues/1882),
@@ -315,7 +1830,7 @@
* Initial support for fragmented MP4 in HLS.
* Misc bug fixes and minor features.
-### r2.0.1 ###
+### r2.0.1 (2016-09-30) ###
* Fix playback of short duration content
([#1837](https://github.com/google/ExoPlayer/issues/1837)).
@@ -324,7 +1839,7 @@
* Fix live stream buffering (out of memory) issue
([#1825](https://github.com/google/ExoPlayer/issues/1825)).
-### r2.0.0 ###
+### r2.0.0 (2016-09-14) ###
ExoPlayer 2.x is a major iteration of the library. It includes significant API
and architectural changes, new features and many bug fixes. You can read about
@@ -493,8 +2008,8 @@ V2 release.
(#801).
* MP3: Fix playback of some streams when stream length is unknown.
* ID3: Support multiple frames of the same type in a single tag.
-* EIA608: Correctly handle repeated control characters, fixing an issue in which
- captions would immediately disappear.
+* CEA-608: Correctly handle repeated control characters, fixing an issue in
+ which captions would immediately disappear.
* AVC3: Fix decoder failures on some MediaTek devices in the case where the
first buffer fed to the decoder does not start with SPS/PPS NAL units.
* Misc bug fixes.
diff --git a/build.gradle b/build.gradle
index a4ae1f175e..a4823b94ee 100644
--- a/build.gradle
+++ b/build.gradle
@@ -13,43 +13,22 @@
// limitations under the License.
buildscript {
repositories {
+ google()
jcenter()
}
dependencies {
- classpath 'com.android.tools.build:gradle:2.3.1'
- classpath 'com.novoda:bintray-release:0.4.0'
- }
- // Workaround for the following test coverage issue. Remove when fixed:
- // https://code.google.com/p/android/issues/detail?id=226070
- configurations.all {
- resolutionStrategy {
- force 'org.jacoco:org.jacoco.report:0.7.4.201502262128'
- force 'org.jacoco:org.jacoco.core:0.7.4.201502262128'
- }
+ classpath 'com.android.tools.build:gradle:3.5.1'
+ classpath 'com.novoda:bintray-release:0.9.1'
+ classpath 'com.google.android.gms:strict-version-matcher-plugin:1.2.0'
}
}
allprojects {
repositories {
+ google()
jcenter()
}
project.ext {
- // Important: ExoPlayer specifies a minSdkVersion of 9 because various
- // components provided by the library may be of use on older devices.
- // However, please note that the core media playback functionality
- // provided by the library requires API level 16 or greater.
- minSdkVersion = 9
- compileSdkVersion = 25
- targetSdkVersion = 25
- buildToolsVersion = '25'
- testSupportLibraryVersion = '0.5'
- supportLibraryVersion = '25.3.1'
- dexmakerVersion = '1.2'
- mockitoVersion = '1.9.5'
- releaseRepoName = getBintrayRepo()
- releaseUserOrg = 'google'
- releaseGroupId = 'com.google.android.exoplayer'
- releaseVersion = 'r2.4.4'
- releaseWebsite = 'https://github.com/google/ExoPlayer'
+ exoplayerPublishEnabled = false
}
if (it.hasProperty('externalBuildDir')) {
if (!new File(externalBuildDir).isAbsolute()) {
@@ -57,12 +36,7 @@ allprojects {
}
buildDir = "${externalBuildDir}/${project.name}"
}
-}
-
-def getBintrayRepo() {
- boolean publicRepo = hasProperty('publicRepo') &&
- property('publicRepo').toBoolean()
- return publicRepo ? 'exoplayer' : 'exoplayer-test'
+ group = 'com.google.android.exoplayer'
}
apply from: 'javadoc_combined.gradle'
diff --git a/constants.gradle b/constants.gradle
new file mode 100644
index 0000000000..88bfe41d5a
--- /dev/null
+++ b/constants.gradle
@@ -0,0 +1,43 @@
+// Copyright (C) 2017 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+project.ext {
+ // ExoPlayer version and version code.
+ releaseVersion = '2.11.1'
+ releaseVersionCode = 2011001
+ minSdkVersion = 16
+ appTargetSdkVersion = 29
+ targetSdkVersion = 28 // TODO: Bump once b/143232359 is resolved
+ compileSdkVersion = 29
+ dexmakerVersion = '2.21.0'
+ junitVersion = '4.13-rc-2'
+ guavaVersion = '23.5-android'
+ mockitoVersion = '2.25.0'
+ robolectricVersion = '4.3.1'
+ checkerframeworkVersion = '2.5.0'
+ jsr305Version = '3.0.2'
+ kotlinAnnotationsVersion = '1.3.31'
+ androidxAnnotationVersion = '1.1.0'
+ androidxAppCompatVersion = '1.1.0'
+ androidxCollectionVersion = '1.1.0'
+ androidxMediaVersion = '1.0.1'
+ androidxTestCoreVersion = '1.2.0'
+ androidxTestJUnitVersion = '1.1.1'
+ androidxTestRunnerVersion = '1.2.0'
+ androidxTestRulesVersion = '1.2.0'
+ truthVersion = '1.0'
+ modulePrefix = ':'
+ if (gradle.ext.has('exoplayerModulePrefix')) {
+ modulePrefix += gradle.ext.exoplayerModulePrefix
+ }
+}
diff --git a/core_settings.gradle b/core_settings.gradle
new file mode 100644
index 0000000000..0f9746af96
--- /dev/null
+++ b/core_settings.gradle
@@ -0,0 +1,64 @@
+// Copyright (C) 2017 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+def rootDir = gradle.ext.exoplayerRoot
+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-av1'
+include modulePrefix + 'extension-ffmpeg'
+include modulePrefix + 'extension-flac'
+include modulePrefix + 'extension-gvr'
+include modulePrefix + 'extension-ima'
+include modulePrefix + 'extension-cast'
+include modulePrefix + 'extension-cronet'
+include modulePrefix + 'extension-mediasession'
+include modulePrefix + 'extension-okhttp'
+include modulePrefix + 'extension-opus'
+include modulePrefix + 'extension-vp9'
+include modulePrefix + 'extension-rtmp'
+include modulePrefix + 'extension-leanback'
+include modulePrefix + 'extension-jobdispatcher'
+include modulePrefix + 'extension-workmanager'
+
+project(modulePrefix + 'library').projectDir = new File(rootDir, 'library/all')
+project(modulePrefix + 'library-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-av1').projectDir = new File(rootDir, 'extensions/av1')
+project(modulePrefix + 'extension-ffmpeg').projectDir = new File(rootDir, 'extensions/ffmpeg')
+project(modulePrefix + 'extension-flac').projectDir = new File(rootDir, 'extensions/flac')
+project(modulePrefix + 'extension-gvr').projectDir = new File(rootDir, 'extensions/gvr')
+project(modulePrefix + 'extension-ima').projectDir = new File(rootDir, 'extensions/ima')
+project(modulePrefix + 'extension-cast').projectDir = new File(rootDir, 'extensions/cast')
+project(modulePrefix + 'extension-cronet').projectDir = new File(rootDir, 'extensions/cronet')
+project(modulePrefix + 'extension-mediasession').projectDir = new File(rootDir, 'extensions/mediasession')
+project(modulePrefix + 'extension-okhttp').projectDir = new File(rootDir, 'extensions/okhttp')
+project(modulePrefix + 'extension-opus').projectDir = new File(rootDir, 'extensions/opus')
+project(modulePrefix + 'extension-vp9').projectDir = new File(rootDir, 'extensions/vp9')
+project(modulePrefix + 'extension-rtmp').projectDir = new File(rootDir, 'extensions/rtmp')
+project(modulePrefix + 'extension-leanback').projectDir = new File(rootDir, 'extensions/leanback')
+project(modulePrefix + 'extension-jobdispatcher').projectDir = new File(rootDir, 'extensions/jobdispatcher')
+project(modulePrefix + 'extension-workmanager').projectDir = new File(rootDir, 'extensions/workmanager')
diff --git a/demo/src/main/assets/media.exolist.json b/demo/src/main/assets/media.exolist.json
deleted file mode 100644
index 814c89a45b..0000000000
--- a/demo/src/main/assets/media.exolist.json
+++ /dev/null
@@ -1,456 +0,0 @@
-[
- {
- "name": "YouTube DASH",
- "samples": [
- {
- "name": "Google Glass (MP4,H264)",
- "uri": "http://www.youtube.com/api/manifest/dash/id/bf5bb2419360daf1/source/youtube?as=fmp4_audio_clear,fmp4_sd_hd_clear&sparams=ip,ipbits,expire,source,id,as&ip=0.0.0.0&ipbits=0&expire=19000000000&signature=51AF5F39AB0CEC3E5497CD9C900EBFEAECCCB5C7.8506521BFC350652163895D4C26DEE124209AA9E&key=ik0",
- "extension": "mpd"
- },
- {
- "name": "Google Play (MP4,H264)",
- "uri": "http://www.youtube.com/api/manifest/dash/id/3aa39fa2cc27967f/source/youtube?as=fmp4_audio_clear,fmp4_sd_hd_clear&sparams=ip,ipbits,expire,source,id,as&ip=0.0.0.0&ipbits=0&expire=19000000000&signature=A2716F75795F5D2AF0E88962FFCD10DB79384F29.84308FF04844498CE6FBCE4731507882B8307798&key=ik0",
- "extension": "mpd"
- },
- {
- "name": "Google Glass (WebM,VP9)",
- "uri": "http://www.youtube.com/api/manifest/dash/id/bf5bb2419360daf1/source/youtube?as=fmp4_audio_clear,webm2_sd_hd_clear&sparams=ip,ipbits,expire,source,id,as&ip=0.0.0.0&ipbits=0&expire=19000000000&signature=249B04F79E984D7F86B4D8DB48AE6FAF41C17AB3.7B9F0EC0505E1566E59B8E488E9419F253DDF413&key=ik0",
- "extension": "mpd"
- },
- {
- "name": "Google Play (WebM,VP9)",
- "uri": "http://www.youtube.com/api/manifest/dash/id/3aa39fa2cc27967f/source/youtube?as=fmp4_audio_clear,webm2_sd_hd_clear&sparams=ip,ipbits,expire,source,id,as&ip=0.0.0.0&ipbits=0&expire=19000000000&signature=B1C2A74783AC1CC4865EB312D7DD2D48230CC9FD.BD153B9882175F1F94BFE5141A5482313EA38E8D&key=ik0",
- "extension": "mpd"
- }
- ]
- },
- {
- "name": "Widevine DASH Policy Tests (GTS)",
- "samples": [
- {
- "name": "WV: HDCP not specified",
- "uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd",
- "drm_scheme": "widevine",
- "drm_license_url": "https://proxy.uat.widevine.com/proxy?video_id=d286538032258a1c&provider=widevine_test"
- },
- {
- "name": "WV: HDCP not required",
- "uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd",
- "drm_scheme": "widevine",
- "drm_license_url": "https://proxy.uat.widevine.com/proxy?video_id=48fcc369939ac96c&provider=widevine_test"
- },
- {
- "name": "WV: HDCP required",
- "uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd",
- "drm_scheme": "widevine",
- "drm_license_url": "https://proxy.uat.widevine.com/proxy?video_id=e06c39f1151da3df&provider=widevine_test"
- },
- {
- "name": "WV: Secure video path required (MP4,H264)",
- "uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd",
- "drm_scheme": "widevine",
- "drm_license_url": "https://proxy.uat.widevine.com/proxy?video_id=0894c7c8719b28a0&provider=widevine_test"
- },
- {
- "name": "WV: Secure video path required (WebM,VP9)",
- "uri": "https://storage.googleapis.com/wvmedia/cenc/vp9/tears/tears.mpd",
- "drm_scheme": "widevine",
- "drm_license_url": "https://proxy.uat.widevine.com/proxy?video_id=0894c7c8719b28a0&provider=widevine_test"
- },
- {
- "name": "WV: Secure video path required (MP4,H265)",
- "uri": "https://storage.googleapis.com/wvmedia/cenc/hevc/tears/tears.mpd",
- "drm_scheme": "widevine",
- "drm_license_url": "https://proxy.uat.widevine.com/proxy?video_id=0894c7c8719b28a0&provider=widevine_test"
- },
- {
- "name": "WV: HDCP + secure video path required",
- "uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd",
- "drm_scheme": "widevine",
- "drm_license_url": "https://proxy.uat.widevine.com/proxy?video_id=efd045b1eb61888a&provider=widevine_test"
- },
- {
- "name": "WV: 30s license duration (fails at ~30s)",
- "uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd",
- "drm_scheme": "widevine",
- "drm_license_url": "https://proxy.uat.widevine.com/proxy?video_id=f9a34cab7b05881a&provider=widevine_test"
- }
- ]
- },
- {
- "name": "Widevine HDCP Capabilities Tests",
- "samples": [
- {
- "name": "WV: HDCP: None (not required)",
- "uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd",
- "drm_scheme": "widevine",
- "drm_license_url": "https://proxy.uat.widevine.com/proxy?video_id=HDCP_None&provider=widevine_test"
- },
- {
- "name": "WV: HDCP: 1.0 required",
- "uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd",
- "drm_scheme": "widevine",
- "drm_license_url": "https://proxy.uat.widevine.com/proxy?video_id=HDCP_V1&provider=widevine_test"
- },
- {
- "name": "WV: HDCP: 2.0 required",
- "uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd",
- "drm_scheme": "widevine",
- "drm_license_url": "https://proxy.uat.widevine.com/proxy?video_id=HDCP_V2&provider=widevine_test"
- },
- {
- "name": "WV: HDCP: 2.1 required",
- "uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd",
- "drm_scheme": "widevine",
- "drm_license_url": "https://proxy.uat.widevine.com/proxy?video_id=HDCP_V2_1&provider=widevine_test"
- },
- {
- "name": "WV: HDCP: 2.2 required",
- "uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd",
- "drm_scheme": "widevine",
- "drm_license_url": "https://proxy.uat.widevine.com/proxy?video_id=HDCP_V2_2&provider=widevine_test"
- },
- {
- "name": "WV: HDCP: No digital output",
- "uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd",
- "drm_scheme": "widevine",
- "drm_license_url": "https://proxy.uat.widevine.com/proxy?video_id=HDCP_NO_DIGTAL_OUTPUT&provider=widevine_test"
- }
- ]
- },
- {
- "name": "Widevine DASH: MP4,H264",
- "samples": [
- {
- "name": "WV: Clear SD & HD (MP4,H264)",
- "uri": "https://storage.googleapis.com/wvmedia/clear/h264/tears/tears.mpd"
- },
- {
- "name": "WV: Clear SD (MP4,H264)",
- "uri": "https://storage.googleapis.com/wvmedia/clear/h264/tears/tears_sd.mpd"
- },
- {
- "name": "WV: Clear HD (MP4,H264)",
- "uri": "https://storage.googleapis.com/wvmedia/clear/h264/tears/tears_hd.mpd"
- },
- {
- "name": "WV: Clear UHD (MP4,H264)",
- "uri": "https://storage.googleapis.com/wvmedia/clear/h264/tears/tears_uhd.mpd"
- },
- {
- "name": "WV: Secure SD & HD (MP4,H264)",
- "uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd",
- "drm_scheme": "widevine",
- "drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
- },
- {
- "name": "WV: Secure SD (MP4,H264)",
- "uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears_sd.mpd",
- "drm_scheme": "widevine",
- "drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
- },
- {
- "name": "WV: Secure HD (MP4,H264)",
- "uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears_hd.mpd",
- "drm_scheme": "widevine",
- "drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
- },
- {
- "name": "WV: Secure UHD (MP4,H264)",
- "uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears_uhd.mpd",
- "drm_scheme": "widevine",
- "drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
- }
- ]
- },
- {
- "name": "Widevine DASH: WebM,VP9",
- "samples": [
- {
- "name": "WV: Clear SD & HD (WebM,VP9)",
- "uri": "https://storage.googleapis.com/wvmedia/clear/vp9/tears/tears.mpd"
- },
- {
- "name": "WV: Clear SD (WebM,VP9)",
- "uri": "https://storage.googleapis.com/wvmedia/clear/vp9/tears/tears_sd.mpd"
- },
- {
- "name": "WV: Clear HD (WebM,VP9)",
- "uri": "https://storage.googleapis.com/wvmedia/clear/vp9/tears/tears_hd.mpd"
- },
- {
- "name": "WV: Clear UHD (WebM,VP9)",
- "uri": "https://storage.googleapis.com/wvmedia/clear/vp9/tears/tears_uhd.mpd"
- },
- {
- "name": "WV: Secure Fullsample SD & HD (WebM,VP9)",
- "uri": "https://storage.googleapis.com/wvmedia/cenc/vp9/tears/tears.mpd",
- "drm_scheme": "widevine",
- "drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
- },
- {
- "name": "WV: Secure Fullsample SD (WebM,VP9)",
- "uri": "https://storage.googleapis.com/wvmedia/cenc/vp9/tears/tears_sd.mpd",
- "drm_scheme": "widevine",
- "drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
- },
- {
- "name": "WV: Secure Fullsample HD (WebM,VP9)",
- "uri": "https://storage.googleapis.com/wvmedia/cenc/vp9/tears/tears_hd.mpd",
- "drm_scheme": "widevine",
- "drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
- },
- {
- "name": "WV: Secure Fullsample UHD (WebM,VP9)",
- "uri": "https://storage.googleapis.com/wvmedia/cenc/vp9/tears/tears_uhd.mpd",
- "drm_scheme": "widevine",
- "drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
- },
- {
- "name": "WV: Secure Subsample SD & HD (WebM,VP9)",
- "uri": "https://storage.googleapis.com/wvmedia/cenc/vp9/subsample/24fps/tears/tears.mpd",
- "drm_scheme": "widevine",
- "drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
- },
- {
- "name": "WV: Secure Subsample SD (WebM,VP9)",
- "uri": "https://storage.googleapis.com/wvmedia/cenc/vp9/subsample/24fps/tears/tears_sd.mpd",
- "drm_scheme": "widevine",
- "drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
- },
- {
- "name": "WV: Secure Subsample HD (WebM,VP9)",
- "uri": "https://storage.googleapis.com/wvmedia/cenc/vp9/subsample/24fps/tears/tears_hd.mpd",
- "drm_scheme": "widevine",
- "drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
- },
- {
- "name": "WV: Secure Subsample UHD (WebM,VP9)",
- "uri": "https://storage.googleapis.com/wvmedia/cenc/vp9/subsample/24fps/tears/tears_uhd.mpd",
- "drm_scheme": "widevine",
- "drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
- }
- ]
- },
- {
- "name": "Widevine DASH: MP4,H265",
- "samples": [
- {
- "name": "WV: Clear SD & HD (MP4,H265)",
- "uri": "https://storage.googleapis.com/wvmedia/clear/hevc/tears/tears.mpd"
- },
- {
- "name": "WV: Clear SD (MP4,H265)",
- "uri": "https://storage.googleapis.com/wvmedia/clear/hevc/tears/tears_sd.mpd"
- },
- {
- "name": "WV: Clear HD (MP4,H265)",
- "uri": "https://storage.googleapis.com/wvmedia/clear/hevc/tears/tears_hd.mpd"
- },
- {
- "name": "WV: Clear UHD (MP4,H265)",
- "uri": "https://storage.googleapis.com/wvmedia/clear/hevc/tears/tears_uhd.mpd"
- },
- {
- "name": "WV: Secure SD & HD (MP4,H265)",
- "uri": "https://storage.googleapis.com/wvmedia/cenc/hevc/tears/tears.mpd",
- "drm_scheme": "widevine",
- "drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
- },
- {
- "name": "WV: Secure SD (MP4,H265)",
- "uri": "https://storage.googleapis.com/wvmedia/cenc/hevc/tears/tears_sd.mpd",
- "drm_scheme": "widevine",
- "drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
- },
- {
- "name": "WV: Secure HD (MP4,H265)",
- "uri": "https://storage.googleapis.com/wvmedia/cenc/hevc/tears/tears_hd.mpd",
- "drm_scheme": "widevine",
- "drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
- },
- {
- "name": "WV: Secure UHD (MP4,H265)",
- "uri": "https://storage.googleapis.com/wvmedia/cenc/hevc/tears/tears_uhd.mpd",
- "drm_scheme": "widevine",
- "drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
- }
- ]
- },
- {
- "name": "ClearKey DASH",
- "samples": [
- {
- "name": "Big Buck Bunny (CENC ClearKey)",
- "uri": "http://html5.cablelabs.com:8100/cenc/ck/dash.mpd",
- "extension": "mpd",
- "drm_scheme": "cenc",
- "drm_license_url": "https://wasabeef.jp/demos/cenc-ck-dash.json"
- }
- ]
- },
- {
- "name": "SmoothStreaming",
- "samples": [
- {
- "name": "Super speed",
- "uri": "http://playready.directtaps.net/smoothstreaming/SSWSS720H264/SuperSpeedway_720.ism"
- },
- {
- "name": "Super speed (PlayReady)",
- "uri": "http://playready.directtaps.net/smoothstreaming/SSWSS720H264PR/SuperSpeedway_720.ism",
- "drm_scheme": "playready"
- }
- ]
- },
- {
- "name": "HLS",
- "samples": [
- {
- "name": "Apple 4x3 basic stream",
- "uri": "https://devimages.apple.com.edgekey.net/streaming/examples/bipbop_4x3/bipbop_4x3_variant.m3u8"
- },
- {
- "name": "Apple 16x9 basic stream",
- "uri": "https://devimages.apple.com.edgekey.net/streaming/examples/bipbop_16x9/bipbop_16x9_variant.m3u8"
- },
- {
- "name": "Apple master playlist advanced (TS)",
- "uri": "https://tungsten.aaplimg.com/VOD/bipbop_adv_example_v2/master.m3u8"
- },
- {
- "name": "Apple master playlist advanced (fMP4)",
- "uri": "https://tungsten.aaplimg.com/VOD/bipbop_adv_fmp4_example/master.m3u8"
- },
- {
- "name": "Apple TS media playlist",
- "uri": "https://devimages.apple.com.edgekey.net/streaming/examples/bipbop_4x3/gear1/prog_index.m3u8"
- },
- {
- "name": "Apple AAC media playlist",
- "uri": "https://devimages.apple.com.edgekey.net/streaming/examples/bipbop_4x3/gear0/prog_index.m3u8"
- },
- {
- "name": "Apple ID3 metadata",
- "uri": "http://devimages.apple.com/samplecode/adDemo/ad.m3u8"
- }
- ]
- },
- {
- "name": "Misc",
- "samples": [
- {
- "name": "Dizzy",
- "uri": "http://html5demos.com/assets/dizzy.mp4"
- },
- {
- "name": "Apple AAC 10s",
- "uri": "https://devimages.apple.com.edgekey.net/streaming/examples/bipbop_4x3/gear0/fileSequence0.aac"
- },
- {
- "name": "Apple TS 10s",
- "uri": "https://devimages.apple.com.edgekey.net/streaming/examples/bipbop_4x3/gear1/fileSequence0.ts"
- },
- {
- "name": "Android screens (Matroska)",
- "uri": "http://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv"
- },
- {
- "name": "Big Buck Bunny (MP4 Video)",
- "uri": "http://redirector.c.youtube.com/videoplayback?id=604ed5ce52eda7ee&itag=22&source=youtube&sparams=ip,ipbits,expire,source,id&ip=0.0.0.0&ipbits=0&expire=19000000000&signature=513F28C7FDCBEC60A66C86C9A393556C99DC47FB.04C88036EEE12565A1ED864A875A58F15D8B5300&key=ik0"
- },
- {
- "name": "Screens 360P (WebM,VP9,No Audio)",
- "uri": "https://storage.googleapis.com/exoplayer-test-media-1/gen-3/screens/dash-vod-single-segment/video-vp9-360.webm"
- },
- {
- "name": "Screens 480p (FMP4,H264,No Audio)",
- "uri": "https://storage.googleapis.com/exoplayer-test-media-1/gen-3/screens/dash-vod-single-segment/video-avc-baseline-480.mp4"
- },
- {
- "name": "Screens 1080p (FMP4,H264, No Audio)",
- "uri": "https://storage.googleapis.com/exoplayer-test-media-1/gen-3/screens/dash-vod-single-segment/video-137.mp4"
- },
- {
- "name": "Screens (FMP4,AAC Audio)",
- "uri": "https://storage.googleapis.com/exoplayer-test-media-1/gen-3/screens/dash-vod-single-segment/audio-141.mp4"
- },
- {
- "name": "Google Play (MP3 Audio)",
- "uri": "http://storage.googleapis.com/exoplayer-test-media-0/play.mp3"
- },
- {
- "name": "Google Play (Ogg/Vorbis Audio)",
- "uri": "https://storage.googleapis.com/exoplayer-test-media-1/ogg/play.ogg"
- },
- {
- "name": "Google Glass (WebM Video with Vorbis Audio)",
- "uri": "http://demos.webmproject.org/exoplayer/glass_vp9_vorbis.webm"
- },
- {
- "name": "Google Glass (VP9 in MP4/ISO-BMFF)",
- "uri": "http://demos.webmproject.org/exoplayer/glass.mp4"
- },
- {
- "name": "Google Glass DASH - VP9 and Opus",
- "uri": "http://demos.webmproject.org/dash/201410/vp9_glass/manifest_vp9_opus.mpd"
- },
- {
- "name": "Big Buck Bunny (FLV Video)",
- "uri": "http://vod.leasewebcdn.com/bbb.flv?ri=1024&rs=150&start=0"
- }
- ]
- },
- {
- "name": "Playlists",
- "samples": [
- {
- "name": "Cats -> Dogs",
- "playlist": [
- {
- "uri": "http://html5demos.com/assets/dizzy.mp4"
- },
- {
- "uri": "http://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv"
- }
- ]
- },
- {
- "name": "Audio -> Video -> Audio",
- "playlist": [
- {
- "uri": "https://storage.googleapis.com/exoplayer-test-media-1/gen-3/screens/dash-vod-single-segment/audio-141.mp4"
- },
- {
- "uri": "http://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv"
- },
- {
- "uri": "https://storage.googleapis.com/exoplayer-test-media-1/gen-3/screens/dash-vod-single-segment/audio-141.mp4"
- }
- ]
- },
- {
- "name": "Clear -> Enc -> Clear -> Enc -> Enc",
- "drm_scheme": "widevine",
- "drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test",
- "playlist": [
- {
- "uri": "http://html5demos.com/assets/dizzy.mp4"
- },
- {
- "uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears_sd.mpd"
- },
- {
- "uri": "http://html5demos.com/assets/dizzy.mp4"
- },
- {
- "uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears_sd.mpd"
- },
- {
- "uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears_sd.mpd"
- }
- ]
- }
- ]
- }
-]
diff --git a/demo/src/main/java/com/google/android/exoplayer2/demo/DemoApplication.java b/demo/src/main/java/com/google/android/exoplayer2/demo/DemoApplication.java
deleted file mode 100644
index b5db4c018d..0000000000
--- a/demo/src/main/java/com/google/android/exoplayer2/demo/DemoApplication.java
+++ /dev/null
@@ -1,52 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.google.android.exoplayer2.demo;
-
-import android.app.Application;
-import com.google.android.exoplayer2.upstream.DataSource;
-import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter;
-import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
-import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory;
-import com.google.android.exoplayer2.upstream.HttpDataSource;
-import com.google.android.exoplayer2.util.Util;
-
-/**
- * Placeholder application to facilitate overriding Application methods for debugging and testing.
- */
-public class DemoApplication extends Application {
-
- protected String userAgent;
-
- @Override
- public void onCreate() {
- super.onCreate();
- userAgent = Util.getUserAgent(this, "ExoPlayerDemo");
- }
-
- public DataSource.Factory buildDataSourceFactory(DefaultBandwidthMeter bandwidthMeter) {
- return new DefaultDataSourceFactory(this, bandwidthMeter,
- buildHttpDataSourceFactory(bandwidthMeter));
- }
-
- public HttpDataSource.Factory buildHttpDataSourceFactory(DefaultBandwidthMeter bandwidthMeter) {
- return new DefaultHttpDataSourceFactory(userAgent, bandwidthMeter);
- }
-
- public boolean useExtensionRenderers() {
- return BuildConfig.FLAVOR.equals("withExtensions");
- }
-
-}
diff --git a/demo/src/main/java/com/google/android/exoplayer2/demo/DemoUtil.java b/demo/src/main/java/com/google/android/exoplayer2/demo/DemoUtil.java
deleted file mode 100644
index f9e9c34158..0000000000
--- a/demo/src/main/java/com/google/android/exoplayer2/demo/DemoUtil.java
+++ /dev/null
@@ -1,86 +0,0 @@
-/*
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.google.android.exoplayer2.demo;
-
-import android.text.TextUtils;
-import com.google.android.exoplayer2.Format;
-import com.google.android.exoplayer2.util.MimeTypes;
-import java.util.Locale;
-
-/**
- * Utility methods for demo application.
- */
-/*package*/ final class DemoUtil {
-
- /**
- * Builds a track name for display.
- *
- * @param format {@link Format} of the track.
- * @return a generated name specific to the track.
- */
- public static String buildTrackName(Format format) {
- String trackName;
- if (MimeTypes.isVideo(format.sampleMimeType)) {
- trackName = joinWithSeparator(joinWithSeparator(joinWithSeparator(
- buildResolutionString(format), buildBitrateString(format)), buildTrackIdString(format)),
- buildSampleMimeTypeString(format));
- } else if (MimeTypes.isAudio(format.sampleMimeType)) {
- trackName = joinWithSeparator(joinWithSeparator(joinWithSeparator(joinWithSeparator(
- buildLanguageString(format), buildAudioPropertyString(format)),
- buildBitrateString(format)), buildTrackIdString(format)),
- buildSampleMimeTypeString(format));
- } else {
- trackName = joinWithSeparator(joinWithSeparator(joinWithSeparator(buildLanguageString(format),
- buildBitrateString(format)), buildTrackIdString(format)),
- buildSampleMimeTypeString(format));
- }
- return trackName.length() == 0 ? "unknown" : trackName;
- }
-
- private static String buildResolutionString(Format format) {
- return format.width == Format.NO_VALUE || format.height == Format.NO_VALUE
- ? "" : format.width + "x" + format.height;
- }
-
- private static String buildAudioPropertyString(Format format) {
- return format.channelCount == Format.NO_VALUE || format.sampleRate == Format.NO_VALUE
- ? "" : format.channelCount + "ch, " + format.sampleRate + "Hz";
- }
-
- private static String buildLanguageString(Format format) {
- return TextUtils.isEmpty(format.language) || "und".equals(format.language) ? ""
- : format.language;
- }
-
- private static String buildBitrateString(Format format) {
- return format.bitrate == Format.NO_VALUE ? ""
- : String.format(Locale.US, "%.2fMbit", format.bitrate / 1000000f);
- }
-
- private static String joinWithSeparator(String first, String second) {
- return first.length() == 0 ? second : (second.length() == 0 ? first : first + ", " + second);
- }
-
- private static String buildTrackIdString(Format format) {
- return format.id == null ? "" : ("id:" + format.id);
- }
-
- private static String buildSampleMimeTypeString(Format format) {
- return format.sampleMimeType == null ? "" : format.sampleMimeType;
- }
-
- private DemoUtil() {}
-}
diff --git a/demo/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java b/demo/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java
deleted file mode 100644
index 953021fe6f..0000000000
--- a/demo/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java
+++ /dev/null
@@ -1,464 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.google.android.exoplayer2.demo;
-
-import android.os.SystemClock;
-import android.util.Log;
-import android.view.Surface;
-import com.google.android.exoplayer2.C;
-import com.google.android.exoplayer2.ExoPlaybackException;
-import com.google.android.exoplayer2.ExoPlayer;
-import com.google.android.exoplayer2.Format;
-import com.google.android.exoplayer2.PlaybackParameters;
-import com.google.android.exoplayer2.RendererCapabilities;
-import com.google.android.exoplayer2.Timeline;
-import com.google.android.exoplayer2.audio.AudioRendererEventListener;
-import com.google.android.exoplayer2.decoder.DecoderCounters;
-import com.google.android.exoplayer2.drm.DefaultDrmSessionManager;
-import com.google.android.exoplayer2.metadata.Metadata;
-import com.google.android.exoplayer2.metadata.MetadataRenderer;
-import com.google.android.exoplayer2.metadata.emsg.EventMessage;
-import com.google.android.exoplayer2.metadata.id3.ApicFrame;
-import com.google.android.exoplayer2.metadata.id3.CommentFrame;
-import com.google.android.exoplayer2.metadata.id3.GeobFrame;
-import com.google.android.exoplayer2.metadata.id3.Id3Frame;
-import com.google.android.exoplayer2.metadata.id3.PrivFrame;
-import com.google.android.exoplayer2.metadata.id3.TextInformationFrame;
-import com.google.android.exoplayer2.metadata.id3.UrlLinkFrame;
-import com.google.android.exoplayer2.source.AdaptiveMediaSourceEventListener;
-import com.google.android.exoplayer2.source.ExtractorMediaSource;
-import com.google.android.exoplayer2.source.TrackGroup;
-import com.google.android.exoplayer2.source.TrackGroupArray;
-import com.google.android.exoplayer2.trackselection.MappingTrackSelector;
-import com.google.android.exoplayer2.trackselection.MappingTrackSelector.MappedTrackInfo;
-import com.google.android.exoplayer2.trackselection.TrackSelection;
-import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
-import com.google.android.exoplayer2.upstream.DataSpec;
-import com.google.android.exoplayer2.video.VideoRendererEventListener;
-import java.io.IOException;
-import java.text.NumberFormat;
-import java.util.Locale;
-
-/**
- * Logs player events using {@link Log}.
- */
-/* package */ final class EventLogger implements ExoPlayer.EventListener,
- AudioRendererEventListener, VideoRendererEventListener, AdaptiveMediaSourceEventListener,
- ExtractorMediaSource.EventListener, DefaultDrmSessionManager.EventListener,
- MetadataRenderer.Output {
-
- private static final String TAG = "EventLogger";
- private static final int MAX_TIMELINE_ITEM_LINES = 3;
- private static final NumberFormat TIME_FORMAT;
- static {
- TIME_FORMAT = NumberFormat.getInstance(Locale.US);
- TIME_FORMAT.setMinimumFractionDigits(2);
- TIME_FORMAT.setMaximumFractionDigits(2);
- TIME_FORMAT.setGroupingUsed(false);
- }
-
- private final MappingTrackSelector trackSelector;
- private final Timeline.Window window;
- private final Timeline.Period period;
- private final long startTimeMs;
-
- public EventLogger(MappingTrackSelector trackSelector) {
- this.trackSelector = trackSelector;
- window = new Timeline.Window();
- period = new Timeline.Period();
- startTimeMs = SystemClock.elapsedRealtime();
- }
-
- // ExoPlayer.EventListener
-
- @Override
- public void onLoadingChanged(boolean isLoading) {
- Log.d(TAG, "loading [" + isLoading + "]");
- }
-
- @Override
- public void onPlayerStateChanged(boolean playWhenReady, int state) {
- Log.d(TAG, "state [" + getSessionTimeString() + ", " + playWhenReady + ", "
- + getStateString(state) + "]");
- }
-
- @Override
- public void onPositionDiscontinuity() {
- Log.d(TAG, "positionDiscontinuity");
- }
-
- @Override
- public void onPlaybackParametersChanged(PlaybackParameters playbackParameters) {
- Log.d(TAG, "playbackParameters " + String.format(
- "[speed=%.2f, pitch=%.2f]", playbackParameters.speed, playbackParameters.pitch));
- }
-
- @Override
- public void onTimelineChanged(Timeline timeline, Object manifest) {
- int periodCount = timeline.getPeriodCount();
- int windowCount = timeline.getWindowCount();
- Log.d(TAG, "sourceInfo [periodCount=" + periodCount + ", windowCount=" + windowCount);
- for (int i = 0; i < Math.min(periodCount, MAX_TIMELINE_ITEM_LINES); i++) {
- timeline.getPeriod(i, period);
- Log.d(TAG, " " + "period [" + getTimeString(period.getDurationMs()) + "]");
- }
- if (periodCount > MAX_TIMELINE_ITEM_LINES) {
- Log.d(TAG, " ...");
- }
- for (int i = 0; i < Math.min(windowCount, MAX_TIMELINE_ITEM_LINES); i++) {
- timeline.getWindow(i, window);
- Log.d(TAG, " " + "window [" + getTimeString(window.getDurationMs()) + ", "
- + window.isSeekable + ", " + window.isDynamic + "]");
- }
- if (windowCount > MAX_TIMELINE_ITEM_LINES) {
- Log.d(TAG, " ...");
- }
- Log.d(TAG, "]");
- }
-
- @Override
- public void onPlayerError(ExoPlaybackException e) {
- Log.e(TAG, "playerFailed [" + getSessionTimeString() + "]", e);
- }
-
- @Override
- public void onTracksChanged(TrackGroupArray ignored, TrackSelectionArray trackSelections) {
- MappedTrackInfo mappedTrackInfo = trackSelector.getCurrentMappedTrackInfo();
- if (mappedTrackInfo == null) {
- Log.d(TAG, "Tracks []");
- return;
- }
- Log.d(TAG, "Tracks [");
- // Log tracks associated to renderers.
- for (int rendererIndex = 0; rendererIndex < mappedTrackInfo.length; rendererIndex++) {
- TrackGroupArray rendererTrackGroups = mappedTrackInfo.getTrackGroups(rendererIndex);
- TrackSelection trackSelection = trackSelections.get(rendererIndex);
- if (rendererTrackGroups.length > 0) {
- Log.d(TAG, " Renderer:" + rendererIndex + " [");
- for (int groupIndex = 0; groupIndex < rendererTrackGroups.length; groupIndex++) {
- TrackGroup trackGroup = rendererTrackGroups.get(groupIndex);
- String adaptiveSupport = getAdaptiveSupportString(trackGroup.length,
- mappedTrackInfo.getAdaptiveSupport(rendererIndex, groupIndex, false));
- Log.d(TAG, " Group:" + groupIndex + ", adaptive_supported=" + adaptiveSupport + " [");
- for (int trackIndex = 0; trackIndex < trackGroup.length; trackIndex++) {
- String status = getTrackStatusString(trackSelection, trackGroup, trackIndex);
- String formatSupport = getFormatSupportString(
- mappedTrackInfo.getTrackFormatSupport(rendererIndex, groupIndex, trackIndex));
- Log.d(TAG, " " + status + " Track:" + trackIndex + ", "
- + Format.toLogString(trackGroup.getFormat(trackIndex))
- + ", supported=" + formatSupport);
- }
- Log.d(TAG, " ]");
- }
- // Log metadata for at most one of the tracks selected for the renderer.
- if (trackSelection != null) {
- for (int selectionIndex = 0; selectionIndex < trackSelection.length(); selectionIndex++) {
- Metadata metadata = trackSelection.getFormat(selectionIndex).metadata;
- if (metadata != null) {
- Log.d(TAG, " Metadata [");
- printMetadata(metadata, " ");
- Log.d(TAG, " ]");
- break;
- }
- }
- }
- Log.d(TAG, " ]");
- }
- }
- // Log tracks not associated with a renderer.
- TrackGroupArray unassociatedTrackGroups = mappedTrackInfo.getUnassociatedTrackGroups();
- if (unassociatedTrackGroups.length > 0) {
- Log.d(TAG, " Renderer:None [");
- for (int groupIndex = 0; groupIndex < unassociatedTrackGroups.length; groupIndex++) {
- Log.d(TAG, " Group:" + groupIndex + " [");
- TrackGroup trackGroup = unassociatedTrackGroups.get(groupIndex);
- for (int trackIndex = 0; trackIndex < trackGroup.length; trackIndex++) {
- String status = getTrackStatusString(false);
- String formatSupport = getFormatSupportString(
- RendererCapabilities.FORMAT_UNSUPPORTED_TYPE);
- Log.d(TAG, " " + status + " Track:" + trackIndex + ", "
- + Format.toLogString(trackGroup.getFormat(trackIndex))
- + ", supported=" + formatSupport);
- }
- Log.d(TAG, " ]");
- }
- Log.d(TAG, " ]");
- }
- Log.d(TAG, "]");
- }
-
- // MetadataRenderer.Output
-
- @Override
- public void onMetadata(Metadata metadata) {
- Log.d(TAG, "onMetadata [");
- printMetadata(metadata, " ");
- Log.d(TAG, "]");
- }
-
- // AudioRendererEventListener
-
- @Override
- public void onAudioEnabled(DecoderCounters counters) {
- Log.d(TAG, "audioEnabled [" + getSessionTimeString() + "]");
- }
-
- @Override
- public void onAudioSessionId(int audioSessionId) {
- Log.d(TAG, "audioSessionId [" + audioSessionId + "]");
- }
-
- @Override
- public void onAudioDecoderInitialized(String decoderName, long elapsedRealtimeMs,
- long initializationDurationMs) {
- Log.d(TAG, "audioDecoderInitialized [" + getSessionTimeString() + ", " + decoderName + "]");
- }
-
- @Override
- public void onAudioInputFormatChanged(Format format) {
- Log.d(TAG, "audioFormatChanged [" + getSessionTimeString() + ", " + Format.toLogString(format)
- + "]");
- }
-
- @Override
- public void onAudioDisabled(DecoderCounters counters) {
- Log.d(TAG, "audioDisabled [" + getSessionTimeString() + "]");
- }
-
- @Override
- public void onAudioTrackUnderrun(int bufferSize, long bufferSizeMs, long elapsedSinceLastFeedMs) {
- printInternalError("audioTrackUnderrun [" + bufferSize + ", " + bufferSizeMs + ", "
- + elapsedSinceLastFeedMs + "]", null);
- }
-
- // VideoRendererEventListener
-
- @Override
- public void onVideoEnabled(DecoderCounters counters) {
- Log.d(TAG, "videoEnabled [" + getSessionTimeString() + "]");
- }
-
- @Override
- public void onVideoDecoderInitialized(String decoderName, long elapsedRealtimeMs,
- long initializationDurationMs) {
- Log.d(TAG, "videoDecoderInitialized [" + getSessionTimeString() + ", " + decoderName + "]");
- }
-
- @Override
- public void onVideoInputFormatChanged(Format format) {
- Log.d(TAG, "videoFormatChanged [" + getSessionTimeString() + ", " + Format.toLogString(format)
- + "]");
- }
-
- @Override
- public void onVideoDisabled(DecoderCounters counters) {
- Log.d(TAG, "videoDisabled [" + getSessionTimeString() + "]");
- }
-
- @Override
- public void onDroppedFrames(int count, long elapsed) {
- Log.d(TAG, "droppedFrames [" + getSessionTimeString() + ", " + count + "]");
- }
-
- @Override
- public void onVideoSizeChanged(int width, int height, int unappliedRotationDegrees,
- float pixelWidthHeightRatio) {
- // Do nothing.
- }
-
- @Override
- public void onRenderedFirstFrame(Surface surface) {
- Log.d(TAG, "renderedFirstFrame [" + surface + "]");
- }
-
- // DefaultDrmSessionManager.EventListener
-
- @Override
- public void onDrmSessionManagerError(Exception e) {
- printInternalError("drmSessionManagerError", e);
- }
-
- @Override
- public void onDrmKeysRestored() {
- Log.d(TAG, "drmKeysRestored [" + getSessionTimeString() + "]");
- }
-
- @Override
- public void onDrmKeysRemoved() {
- Log.d(TAG, "drmKeysRemoved [" + getSessionTimeString() + "]");
- }
-
- @Override
- public void onDrmKeysLoaded() {
- Log.d(TAG, "drmKeysLoaded [" + getSessionTimeString() + "]");
- }
-
- // ExtractorMediaSource.EventListener
-
- @Override
- public void onLoadError(IOException error) {
- printInternalError("loadError", error);
- }
-
- // AdaptiveMediaSourceEventListener
-
- @Override
- public void onLoadStarted(DataSpec dataSpec, int dataType, int trackType, Format trackFormat,
- int trackSelectionReason, Object trackSelectionData, long mediaStartTimeMs,
- long mediaEndTimeMs, long elapsedRealtimeMs) {
- // Do nothing.
- }
-
- @Override
- public void onLoadError(DataSpec dataSpec, int dataType, int trackType, Format trackFormat,
- int trackSelectionReason, Object trackSelectionData, long mediaStartTimeMs,
- long mediaEndTimeMs, long elapsedRealtimeMs, long loadDurationMs, long bytesLoaded,
- IOException error, boolean wasCanceled) {
- printInternalError("loadError", error);
- }
-
- @Override
- public void onLoadCanceled(DataSpec dataSpec, int dataType, int trackType, Format trackFormat,
- int trackSelectionReason, Object trackSelectionData, long mediaStartTimeMs,
- long mediaEndTimeMs, long elapsedRealtimeMs, long loadDurationMs, long bytesLoaded) {
- // Do nothing.
- }
-
- @Override
- public void onLoadCompleted(DataSpec dataSpec, int dataType, int trackType, Format trackFormat,
- int trackSelectionReason, Object trackSelectionData, long mediaStartTimeMs,
- long mediaEndTimeMs, long elapsedRealtimeMs, long loadDurationMs, long bytesLoaded) {
- // Do nothing.
- }
-
- @Override
- public void onUpstreamDiscarded(int trackType, long mediaStartTimeMs, long mediaEndTimeMs) {
- // Do nothing.
- }
-
- @Override
- public void onDownstreamFormatChanged(int trackType, Format trackFormat, int trackSelectionReason,
- Object trackSelectionData, long mediaTimeMs) {
- // Do nothing.
- }
-
- // Internal methods
-
- private void printInternalError(String type, Exception e) {
- Log.e(TAG, "internalError [" + getSessionTimeString() + ", " + type + "]", e);
- }
-
- private void printMetadata(Metadata metadata, String prefix) {
- for (int i = 0; i < metadata.length(); i++) {
- Metadata.Entry entry = metadata.get(i);
- if (entry instanceof TextInformationFrame) {
- TextInformationFrame textInformationFrame = (TextInformationFrame) entry;
- Log.d(TAG, prefix + String.format("%s: value=%s", textInformationFrame.id,
- textInformationFrame.value));
- } else if (entry instanceof UrlLinkFrame) {
- UrlLinkFrame urlLinkFrame = (UrlLinkFrame) entry;
- Log.d(TAG, prefix + String.format("%s: url=%s", urlLinkFrame.id, urlLinkFrame.url));
- } else if (entry instanceof PrivFrame) {
- PrivFrame privFrame = (PrivFrame) entry;
- Log.d(TAG, prefix + String.format("%s: owner=%s", privFrame.id, privFrame.owner));
- } else if (entry instanceof GeobFrame) {
- GeobFrame geobFrame = (GeobFrame) entry;
- Log.d(TAG, prefix + String.format("%s: mimeType=%s, filename=%s, description=%s",
- geobFrame.id, geobFrame.mimeType, geobFrame.filename, geobFrame.description));
- } else if (entry instanceof ApicFrame) {
- ApicFrame apicFrame = (ApicFrame) entry;
- Log.d(TAG, prefix + String.format("%s: mimeType=%s, description=%s",
- apicFrame.id, apicFrame.mimeType, apicFrame.description));
- } else if (entry instanceof CommentFrame) {
- CommentFrame commentFrame = (CommentFrame) entry;
- Log.d(TAG, prefix + String.format("%s: language=%s, description=%s", commentFrame.id,
- commentFrame.language, commentFrame.description));
- } else if (entry instanceof Id3Frame) {
- Id3Frame id3Frame = (Id3Frame) entry;
- Log.d(TAG, prefix + String.format("%s", id3Frame.id));
- } else if (entry instanceof EventMessage) {
- EventMessage eventMessage = (EventMessage) entry;
- Log.d(TAG, prefix + String.format("EMSG: scheme=%s, id=%d, value=%s",
- eventMessage.schemeIdUri, eventMessage.id, eventMessage.value));
- }
- }
- }
-
- private String getSessionTimeString() {
- return getTimeString(SystemClock.elapsedRealtime() - startTimeMs);
- }
-
- private static String getTimeString(long timeMs) {
- return timeMs == C.TIME_UNSET ? "?" : TIME_FORMAT.format((timeMs) / 1000f);
- }
-
- private static String getStateString(int state) {
- switch (state) {
- case ExoPlayer.STATE_BUFFERING:
- return "B";
- case ExoPlayer.STATE_ENDED:
- return "E";
- case ExoPlayer.STATE_IDLE:
- return "I";
- case ExoPlayer.STATE_READY:
- return "R";
- default:
- return "?";
- }
- }
-
- private static String getFormatSupportString(int formatSupport) {
- switch (formatSupport) {
- case RendererCapabilities.FORMAT_HANDLED:
- return "YES";
- case RendererCapabilities.FORMAT_EXCEEDS_CAPABILITIES:
- return "NO_EXCEEDS_CAPABILITIES";
- case RendererCapabilities.FORMAT_UNSUPPORTED_SUBTYPE:
- return "NO_UNSUPPORTED_TYPE";
- case RendererCapabilities.FORMAT_UNSUPPORTED_TYPE:
- return "NO";
- default:
- return "?";
- }
- }
-
- private static String getAdaptiveSupportString(int trackCount, int adaptiveSupport) {
- if (trackCount < 2) {
- return "N/A";
- }
- switch (adaptiveSupport) {
- case RendererCapabilities.ADAPTIVE_SEAMLESS:
- return "YES";
- case RendererCapabilities.ADAPTIVE_NOT_SEAMLESS:
- return "YES_NOT_SEAMLESS";
- case RendererCapabilities.ADAPTIVE_NOT_SUPPORTED:
- return "NO";
- default:
- return "?";
- }
- }
-
- private static String getTrackStatusString(TrackSelection selection, TrackGroup group,
- int trackIndex) {
- return getTrackStatusString(selection != null && selection.getTrackGroup() == group
- && selection.indexOf(trackIndex) != C.INDEX_UNSET);
- }
-
- private static String getTrackStatusString(boolean enabled) {
- return enabled ? "[X]" : "[ ]";
- }
-
-}
diff --git a/demo/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java b/demo/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java
deleted file mode 100644
index d0703f3496..0000000000
--- a/demo/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java
+++ /dev/null
@@ -1,574 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.google.android.exoplayer2.demo;
-
-import android.app.Activity;
-import android.content.Intent;
-import android.content.pm.PackageManager;
-import android.net.Uri;
-import android.os.Bundle;
-import android.os.Handler;
-import android.support.annotation.NonNull;
-import android.text.TextUtils;
-import android.view.KeyEvent;
-import android.view.View;
-import android.view.View.OnClickListener;
-import android.widget.Button;
-import android.widget.LinearLayout;
-import android.widget.TextView;
-import android.widget.Toast;
-import com.google.android.exoplayer2.C;
-import com.google.android.exoplayer2.DefaultRenderersFactory;
-import com.google.android.exoplayer2.ExoPlaybackException;
-import com.google.android.exoplayer2.ExoPlayer;
-import com.google.android.exoplayer2.ExoPlayerFactory;
-import com.google.android.exoplayer2.PlaybackParameters;
-import com.google.android.exoplayer2.SimpleExoPlayer;
-import com.google.android.exoplayer2.Timeline;
-import com.google.android.exoplayer2.drm.DefaultDrmSessionManager;
-import com.google.android.exoplayer2.drm.DrmSessionManager;
-import com.google.android.exoplayer2.drm.FrameworkMediaCrypto;
-import com.google.android.exoplayer2.drm.FrameworkMediaDrm;
-import com.google.android.exoplayer2.drm.HttpMediaDrmCallback;
-import com.google.android.exoplayer2.drm.UnsupportedDrmException;
-import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory;
-import com.google.android.exoplayer2.mediacodec.MediaCodecRenderer.DecoderInitializationException;
-import com.google.android.exoplayer2.mediacodec.MediaCodecUtil.DecoderQueryException;
-import com.google.android.exoplayer2.source.BehindLiveWindowException;
-import com.google.android.exoplayer2.source.ConcatenatingMediaSource;
-import com.google.android.exoplayer2.source.ExtractorMediaSource;
-import com.google.android.exoplayer2.source.MediaSource;
-import com.google.android.exoplayer2.source.TrackGroupArray;
-import com.google.android.exoplayer2.source.dash.DashMediaSource;
-import com.google.android.exoplayer2.source.dash.DefaultDashChunkSource;
-import com.google.android.exoplayer2.source.hls.HlsMediaSource;
-import com.google.android.exoplayer2.source.smoothstreaming.DefaultSsChunkSource;
-import com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource;
-import com.google.android.exoplayer2.trackselection.AdaptiveTrackSelection;
-import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
-import com.google.android.exoplayer2.trackselection.MappingTrackSelector.MappedTrackInfo;
-import com.google.android.exoplayer2.trackselection.TrackSelection;
-import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
-import com.google.android.exoplayer2.ui.DebugTextViewHelper;
-import com.google.android.exoplayer2.ui.PlaybackControlView;
-import com.google.android.exoplayer2.ui.SimpleExoPlayerView;
-import com.google.android.exoplayer2.upstream.DataSource;
-import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter;
-import com.google.android.exoplayer2.upstream.HttpDataSource;
-import com.google.android.exoplayer2.util.Util;
-import java.net.CookieHandler;
-import java.net.CookieManager;
-import java.net.CookiePolicy;
-import java.util.UUID;
-
-/**
- * An activity that plays media using {@link SimpleExoPlayer}.
- */
-public class PlayerActivity extends Activity implements OnClickListener, ExoPlayer.EventListener,
- PlaybackControlView.VisibilityListener {
-
- public static final String DRM_SCHEME_UUID_EXTRA = "drm_scheme_uuid";
- public static final String DRM_LICENSE_URL = "drm_license_url";
- public static final String DRM_KEY_REQUEST_PROPERTIES = "drm_key_request_properties";
- public static final String PREFER_EXTENSION_DECODERS = "prefer_extension_decoders";
-
- public static final String ACTION_VIEW = "com.google.android.exoplayer.demo.action.VIEW";
- public static final String EXTENSION_EXTRA = "extension";
-
- public static final String ACTION_VIEW_LIST =
- "com.google.android.exoplayer.demo.action.VIEW_LIST";
- public static final String URI_LIST_EXTRA = "uri_list";
- public static final String EXTENSION_LIST_EXTRA = "extension_list";
-
- private static final DefaultBandwidthMeter BANDWIDTH_METER = new DefaultBandwidthMeter();
- private static final CookieManager DEFAULT_COOKIE_MANAGER;
- static {
- DEFAULT_COOKIE_MANAGER = new CookieManager();
- DEFAULT_COOKIE_MANAGER.setCookiePolicy(CookiePolicy.ACCEPT_ORIGINAL_SERVER);
- }
-
- private Handler mainHandler;
- private EventLogger eventLogger;
- private SimpleExoPlayerView simpleExoPlayerView;
- private LinearLayout debugRootView;
- private TextView debugTextView;
- private Button retryButton;
-
- private DataSource.Factory mediaDataSourceFactory;
- private SimpleExoPlayer player;
- private DefaultTrackSelector trackSelector;
- private TrackSelectionHelper trackSelectionHelper;
- private DebugTextViewHelper debugViewHelper;
- private boolean needRetrySource;
- private TrackGroupArray lastSeenTrackGroupArray;
-
- private boolean shouldAutoPlay;
- private int resumeWindow;
- private long resumePosition;
-
- // Activity lifecycle
-
- @Override
- public void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- shouldAutoPlay = true;
- clearResumePosition();
- mediaDataSourceFactory = buildDataSourceFactory(true);
- mainHandler = new Handler();
- if (CookieHandler.getDefault() != DEFAULT_COOKIE_MANAGER) {
- CookieHandler.setDefault(DEFAULT_COOKIE_MANAGER);
- }
-
- setContentView(R.layout.player_activity);
- View rootView = findViewById(R.id.root);
- rootView.setOnClickListener(this);
- debugRootView = (LinearLayout) findViewById(R.id.controls_root);
- debugTextView = (TextView) findViewById(R.id.debug_text_view);
- retryButton = (Button) findViewById(R.id.retry_button);
- retryButton.setOnClickListener(this);
-
- simpleExoPlayerView = (SimpleExoPlayerView) findViewById(R.id.player_view);
- simpleExoPlayerView.setControllerVisibilityListener(this);
- simpleExoPlayerView.requestFocus();
- }
-
- @Override
- public void onNewIntent(Intent intent) {
- releasePlayer();
- shouldAutoPlay = true;
- clearResumePosition();
- setIntent(intent);
- }
-
- @Override
- public void onStart() {
- super.onStart();
- if (Util.SDK_INT > 23) {
- initializePlayer();
- }
- }
-
- @Override
- public void onResume() {
- super.onResume();
- if ((Util.SDK_INT <= 23 || player == null)) {
- initializePlayer();
- }
- }
-
- @Override
- public void onPause() {
- super.onPause();
- if (Util.SDK_INT <= 23) {
- releasePlayer();
- }
- }
-
- @Override
- public void onStop() {
- super.onStop();
- if (Util.SDK_INT > 23) {
- releasePlayer();
- }
- }
-
- @Override
- public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions,
- @NonNull int[] grantResults) {
- if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
- initializePlayer();
- } else {
- showToast(R.string.storage_permission_denied);
- finish();
- }
- }
-
- // Activity input
-
- @Override
- public boolean dispatchKeyEvent(KeyEvent event) {
- // Show the controls on any key event.
- simpleExoPlayerView.showController();
- // If the event was not handled then see if the player view can handle it as a media key event.
- return super.dispatchKeyEvent(event) || simpleExoPlayerView.dispatchMediaKeyEvent(event);
- }
-
- // OnClickListener methods
-
- @Override
- public void onClick(View view) {
- if (view == retryButton) {
- initializePlayer();
- } else if (view.getParent() == debugRootView) {
- MappedTrackInfo mappedTrackInfo = trackSelector.getCurrentMappedTrackInfo();
- if (mappedTrackInfo != null) {
- trackSelectionHelper.showSelectionDialog(this, ((Button) view).getText(),
- trackSelector.getCurrentMappedTrackInfo(), (int) view.getTag());
- }
- }
- }
-
- // PlaybackControlView.VisibilityListener implementation
-
- @Override
- public void onVisibilityChange(int visibility) {
- debugRootView.setVisibility(visibility);
- }
-
- // Internal methods
-
- private void initializePlayer() {
- Intent intent = getIntent();
- boolean needNewPlayer = player == null;
- if (needNewPlayer) {
- TrackSelection.Factory adaptiveTrackSelectionFactory =
- new AdaptiveTrackSelection.Factory(BANDWIDTH_METER);
- trackSelector = new DefaultTrackSelector(adaptiveTrackSelectionFactory);
- trackSelectionHelper = new TrackSelectionHelper(trackSelector, adaptiveTrackSelectionFactory);
- lastSeenTrackGroupArray = null;
- eventLogger = new EventLogger(trackSelector);
-
- UUID drmSchemeUuid = intent.hasExtra(DRM_SCHEME_UUID_EXTRA)
- ? UUID.fromString(intent.getStringExtra(DRM_SCHEME_UUID_EXTRA)) : null;
- DrmSessionManager drmSessionManager = null;
- if (drmSchemeUuid != null) {
- String drmLicenseUrl = intent.getStringExtra(DRM_LICENSE_URL);
- String[] keyRequestPropertiesArray = intent.getStringArrayExtra(DRM_KEY_REQUEST_PROPERTIES);
- try {
- drmSessionManager = buildDrmSessionManager(drmSchemeUuid, drmLicenseUrl,
- keyRequestPropertiesArray);
- } catch (UnsupportedDrmException e) {
- int errorStringId = Util.SDK_INT < 18 ? R.string.error_drm_not_supported
- : (e.reason == UnsupportedDrmException.REASON_UNSUPPORTED_SCHEME
- ? R.string.error_drm_unsupported_scheme : R.string.error_drm_unknown);
- showToast(errorStringId);
- return;
- }
- }
-
- boolean preferExtensionDecoders = intent.getBooleanExtra(PREFER_EXTENSION_DECODERS, false);
- @DefaultRenderersFactory.ExtensionRendererMode int extensionRendererMode =
- ((DemoApplication) getApplication()).useExtensionRenderers()
- ? (preferExtensionDecoders ? DefaultRenderersFactory.EXTENSION_RENDERER_MODE_PREFER
- : DefaultRenderersFactory.EXTENSION_RENDERER_MODE_ON)
- : DefaultRenderersFactory.EXTENSION_RENDERER_MODE_OFF;
- DefaultRenderersFactory renderersFactory = new DefaultRenderersFactory(this,
- drmSessionManager, extensionRendererMode);
-
- player = ExoPlayerFactory.newSimpleInstance(renderersFactory, trackSelector);
- player.addListener(this);
- player.addListener(eventLogger);
- player.setAudioDebugListener(eventLogger);
- player.setVideoDebugListener(eventLogger);
- player.setMetadataOutput(eventLogger);
-
- simpleExoPlayerView.setPlayer(player);
- player.setPlayWhenReady(shouldAutoPlay);
- debugViewHelper = new DebugTextViewHelper(player, debugTextView);
- debugViewHelper.start();
- }
- if (needNewPlayer || needRetrySource) {
- String action = intent.getAction();
- Uri[] uris;
- String[] extensions;
- if (ACTION_VIEW.equals(action)) {
- uris = new Uri[] {intent.getData()};
- extensions = new String[] {intent.getStringExtra(EXTENSION_EXTRA)};
- } else if (ACTION_VIEW_LIST.equals(action)) {
- String[] uriStrings = intent.getStringArrayExtra(URI_LIST_EXTRA);
- uris = new Uri[uriStrings.length];
- for (int i = 0; i < uriStrings.length; i++) {
- uris[i] = Uri.parse(uriStrings[i]);
- }
- extensions = intent.getStringArrayExtra(EXTENSION_LIST_EXTRA);
- if (extensions == null) {
- extensions = new String[uriStrings.length];
- }
- } else {
- showToast(getString(R.string.unexpected_intent_action, action));
- return;
- }
- if (Util.maybeRequestReadExternalStoragePermission(this, uris)) {
- // The player will be reinitialized if the permission is granted.
- return;
- }
- MediaSource[] mediaSources = new MediaSource[uris.length];
- for (int i = 0; i < uris.length; i++) {
- mediaSources[i] = buildMediaSource(uris[i], extensions[i]);
- }
- MediaSource mediaSource = mediaSources.length == 1 ? mediaSources[0]
- : new ConcatenatingMediaSource(mediaSources);
- boolean haveResumePosition = resumeWindow != C.INDEX_UNSET;
- if (haveResumePosition) {
- player.seekTo(resumeWindow, resumePosition);
- }
- player.prepare(mediaSource, !haveResumePosition, false);
- needRetrySource = false;
- updateButtonVisibilities();
- }
- }
-
- private MediaSource buildMediaSource(Uri uri, String overrideExtension) {
- int type = TextUtils.isEmpty(overrideExtension) ? Util.inferContentType(uri)
- : Util.inferContentType("." + overrideExtension);
- switch (type) {
- case C.TYPE_SS:
- return new SsMediaSource(uri, buildDataSourceFactory(false),
- new DefaultSsChunkSource.Factory(mediaDataSourceFactory), mainHandler, eventLogger);
- case C.TYPE_DASH:
- return new DashMediaSource(uri, buildDataSourceFactory(false),
- new DefaultDashChunkSource.Factory(mediaDataSourceFactory), mainHandler, eventLogger);
- case C.TYPE_HLS:
- return new HlsMediaSource(uri, mediaDataSourceFactory, mainHandler, eventLogger);
- case C.TYPE_OTHER:
- return new ExtractorMediaSource(uri, mediaDataSourceFactory, new DefaultExtractorsFactory(),
- mainHandler, eventLogger);
- default: {
- throw new IllegalStateException("Unsupported type: " + type);
- }
- }
- }
-
- private DrmSessionManager buildDrmSessionManager(UUID uuid,
- String licenseUrl, String[] keyRequestPropertiesArray) throws UnsupportedDrmException {
- if (Util.SDK_INT < 18) {
- return null;
- }
- HttpMediaDrmCallback drmCallback = new HttpMediaDrmCallback(licenseUrl,
- buildHttpDataSourceFactory(false));
- if (keyRequestPropertiesArray != null) {
- for (int i = 0; i < keyRequestPropertiesArray.length - 1; i += 2) {
- drmCallback.setKeyRequestProperty(keyRequestPropertiesArray[i],
- keyRequestPropertiesArray[i + 1]);
- }
- }
- return new DefaultDrmSessionManager<>(uuid,
- FrameworkMediaDrm.newInstance(uuid), drmCallback, null, mainHandler, eventLogger);
- }
-
- private void releasePlayer() {
- if (player != null) {
- debugViewHelper.stop();
- debugViewHelper = null;
- shouldAutoPlay = player.getPlayWhenReady();
- updateResumePosition();
- player.release();
- player = null;
- trackSelector = null;
- trackSelectionHelper = null;
- eventLogger = null;
- }
- }
-
- private void updateResumePosition() {
- resumeWindow = player.getCurrentWindowIndex();
- resumePosition = player.isCurrentWindowSeekable() ? Math.max(0, player.getCurrentPosition())
- : C.TIME_UNSET;
- }
-
- private void clearResumePosition() {
- resumeWindow = C.INDEX_UNSET;
- resumePosition = C.TIME_UNSET;
- }
-
- /**
- * Returns a new DataSource factory.
- *
- * @param useBandwidthMeter Whether to set {@link #BANDWIDTH_METER} as a listener to the new
- * DataSource factory.
- * @return A new DataSource factory.
- */
- private DataSource.Factory buildDataSourceFactory(boolean useBandwidthMeter) {
- return ((DemoApplication) getApplication())
- .buildDataSourceFactory(useBandwidthMeter ? BANDWIDTH_METER : null);
- }
-
- /**
- * Returns a new HttpDataSource factory.
- *
- * @param useBandwidthMeter Whether to set {@link #BANDWIDTH_METER} as a listener to the new
- * DataSource factory.
- * @return A new HttpDataSource factory.
- */
- private HttpDataSource.Factory buildHttpDataSourceFactory(boolean useBandwidthMeter) {
- return ((DemoApplication) getApplication())
- .buildHttpDataSourceFactory(useBandwidthMeter ? BANDWIDTH_METER : null);
- }
-
- // ExoPlayer.EventListener implementation
-
- @Override
- public void onLoadingChanged(boolean isLoading) {
- // Do nothing.
- }
-
- @Override
- public void onPlayerStateChanged(boolean playWhenReady, int playbackState) {
- if (playbackState == ExoPlayer.STATE_ENDED) {
- showControls();
- }
- updateButtonVisibilities();
- }
-
- @Override
- public void onPositionDiscontinuity() {
- if (needRetrySource) {
- // This will only occur if the user has performed a seek whilst in the error state. Update the
- // resume position so that if the user then retries, playback will resume from the position to
- // which they seeked.
- updateResumePosition();
- }
- }
-
- @Override
- public void onPlaybackParametersChanged(PlaybackParameters playbackParameters) {
- // Do nothing.
- }
-
- @Override
- public void onTimelineChanged(Timeline timeline, Object manifest) {
- // Do nothing.
- }
-
- @Override
- public void onPlayerError(ExoPlaybackException e) {
- String errorString = null;
- if (e.type == ExoPlaybackException.TYPE_RENDERER) {
- Exception cause = e.getRendererException();
- if (cause instanceof DecoderInitializationException) {
- // Special case for decoder initialization failures.
- DecoderInitializationException decoderInitializationException =
- (DecoderInitializationException) cause;
- if (decoderInitializationException.decoderName == null) {
- if (decoderInitializationException.getCause() instanceof DecoderQueryException) {
- errorString = getString(R.string.error_querying_decoders);
- } else if (decoderInitializationException.secureDecoderRequired) {
- errorString = getString(R.string.error_no_secure_decoder,
- decoderInitializationException.mimeType);
- } else {
- errorString = getString(R.string.error_no_decoder,
- decoderInitializationException.mimeType);
- }
- } else {
- errorString = getString(R.string.error_instantiating_decoder,
- decoderInitializationException.decoderName);
- }
- }
- }
- if (errorString != null) {
- showToast(errorString);
- }
- needRetrySource = true;
- if (isBehindLiveWindow(e)) {
- clearResumePosition();
- initializePlayer();
- } else {
- updateResumePosition();
- updateButtonVisibilities();
- showControls();
- }
- }
-
- @Override
- @SuppressWarnings("ReferenceEquality")
- public void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray trackSelections) {
- updateButtonVisibilities();
- if (trackGroups != lastSeenTrackGroupArray) {
- MappedTrackInfo mappedTrackInfo = trackSelector.getCurrentMappedTrackInfo();
- if (mappedTrackInfo != null) {
- if (mappedTrackInfo.getTrackTypeRendererSupport(C.TRACK_TYPE_VIDEO)
- == MappedTrackInfo.RENDERER_SUPPORT_UNSUPPORTED_TRACKS) {
- showToast(R.string.error_unsupported_video);
- }
- if (mappedTrackInfo.getTrackTypeRendererSupport(C.TRACK_TYPE_AUDIO)
- == MappedTrackInfo.RENDERER_SUPPORT_UNSUPPORTED_TRACKS) {
- showToast(R.string.error_unsupported_audio);
- }
- }
- lastSeenTrackGroupArray = trackGroups;
- }
- }
-
- // User controls
-
- private void updateButtonVisibilities() {
- debugRootView.removeAllViews();
-
- retryButton.setVisibility(needRetrySource ? View.VISIBLE : View.GONE);
- debugRootView.addView(retryButton);
-
- if (player == null) {
- return;
- }
-
- MappedTrackInfo mappedTrackInfo = trackSelector.getCurrentMappedTrackInfo();
- if (mappedTrackInfo == null) {
- return;
- }
-
- for (int i = 0; i < mappedTrackInfo.length; i++) {
- TrackGroupArray trackGroups = mappedTrackInfo.getTrackGroups(i);
- if (trackGroups.length != 0) {
- Button button = new Button(this);
- int label;
- switch (player.getRendererType(i)) {
- case C.TRACK_TYPE_AUDIO:
- label = R.string.audio;
- break;
- case C.TRACK_TYPE_VIDEO:
- label = R.string.video;
- break;
- case C.TRACK_TYPE_TEXT:
- label = R.string.text;
- break;
- default:
- continue;
- }
- button.setText(label);
- button.setTag(i);
- button.setOnClickListener(this);
- debugRootView.addView(button, debugRootView.getChildCount() - 1);
- }
- }
- }
-
- private void showControls() {
- debugRootView.setVisibility(View.VISIBLE);
- }
-
- private void showToast(int messageId) {
- showToast(getString(messageId));
- }
-
- private void showToast(String message) {
- Toast.makeText(getApplicationContext(), message, Toast.LENGTH_LONG).show();
- }
-
- private static boolean isBehindLiveWindow(ExoPlaybackException e) {
- if (e.type != ExoPlaybackException.TYPE_SOURCE) {
- return false;
- }
- Throwable cause = e.getSourceException();
- while (cause != null) {
- if (cause instanceof BehindLiveWindowException) {
- return true;
- }
- cause = cause.getCause();
- }
- return false;
- }
-
-}
diff --git a/demo/src/main/java/com/google/android/exoplayer2/demo/SampleChooserActivity.java b/demo/src/main/java/com/google/android/exoplayer2/demo/SampleChooserActivity.java
deleted file mode 100644
index 081ad190b5..0000000000
--- a/demo/src/main/java/com/google/android/exoplayer2/demo/SampleChooserActivity.java
+++ /dev/null
@@ -1,451 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.google.android.exoplayer2.demo;
-
-import android.app.Activity;
-import android.content.Context;
-import android.content.Intent;
-import android.content.res.AssetManager;
-import android.net.Uri;
-import android.os.AsyncTask;
-import android.os.Bundle;
-import android.util.JsonReader;
-import android.util.Log;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.BaseExpandableListAdapter;
-import android.widget.ExpandableListView;
-import android.widget.ExpandableListView.OnChildClickListener;
-import android.widget.TextView;
-import android.widget.Toast;
-import com.google.android.exoplayer2.C;
-import com.google.android.exoplayer2.ParserException;
-import com.google.android.exoplayer2.upstream.DataSource;
-import com.google.android.exoplayer2.upstream.DataSourceInputStream;
-import com.google.android.exoplayer2.upstream.DataSpec;
-import com.google.android.exoplayer2.upstream.DefaultDataSource;
-import com.google.android.exoplayer2.util.Assertions;
-import com.google.android.exoplayer2.util.Util;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.InputStreamReader;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.List;
-import java.util.UUID;
-
-/**
- * An activity for selecting from a list of samples.
- */
-public class SampleChooserActivity extends Activity {
-
- private static final String TAG = "SampleChooserActivity";
-
- @Override
- public void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- setContentView(R.layout.sample_chooser_activity);
- Intent intent = getIntent();
- String dataUri = intent.getDataString();
- String[] uris;
- if (dataUri != null) {
- uris = new String[] {dataUri};
- } else {
- ArrayList uriList = new ArrayList<>();
- AssetManager assetManager = getAssets();
- try {
- for (String asset : assetManager.list("")) {
- if (asset.endsWith(".exolist.json")) {
- uriList.add("asset:///" + asset);
- }
- }
- } catch (IOException e) {
- Toast.makeText(getApplicationContext(), R.string.sample_list_load_error, Toast.LENGTH_LONG)
- .show();
- }
- uris = new String[uriList.size()];
- uriList.toArray(uris);
- Arrays.sort(uris);
- }
- SampleListLoader loaderTask = new SampleListLoader();
- loaderTask.execute(uris);
- }
-
- private void onSampleGroups(final List groups, boolean sawError) {
- if (sawError) {
- Toast.makeText(getApplicationContext(), R.string.sample_list_load_error, Toast.LENGTH_LONG)
- .show();
- }
- ExpandableListView sampleList = (ExpandableListView) findViewById(R.id.sample_list);
- sampleList.setAdapter(new SampleAdapter(this, groups));
- sampleList.setOnChildClickListener(new OnChildClickListener() {
- @Override
- public boolean onChildClick(ExpandableListView parent, View view, int groupPosition,
- int childPosition, long id) {
- onSampleSelected(groups.get(groupPosition).samples.get(childPosition));
- return true;
- }
- });
- }
-
- private void onSampleSelected(Sample sample) {
- startActivity(sample.buildIntent(this));
- }
-
- private final class SampleListLoader extends AsyncTask> {
-
- private boolean sawError;
-
- @Override
- protected List doInBackground(String... uris) {
- List result = new ArrayList<>();
- Context context = getApplicationContext();
- String userAgent = Util.getUserAgent(context, "ExoPlayerDemo");
- DataSource dataSource = new DefaultDataSource(context, null, userAgent, false);
- for (String uri : uris) {
- DataSpec dataSpec = new DataSpec(Uri.parse(uri));
- InputStream inputStream = new DataSourceInputStream(dataSource, dataSpec);
- try {
- readSampleGroups(new JsonReader(new InputStreamReader(inputStream, "UTF-8")), result);
- } catch (Exception e) {
- Log.e(TAG, "Error loading sample list: " + uri, e);
- sawError = true;
- } finally {
- Util.closeQuietly(dataSource);
- }
- }
- return result;
- }
-
- @Override
- protected void onPostExecute(List result) {
- onSampleGroups(result, sawError);
- }
-
- private void readSampleGroups(JsonReader reader, List groups) throws IOException {
- reader.beginArray();
- while (reader.hasNext()) {
- readSampleGroup(reader, groups);
- }
- reader.endArray();
- }
-
- private void readSampleGroup(JsonReader reader, List groups) throws IOException {
- String groupName = "";
- ArrayList samples = new ArrayList<>();
-
- reader.beginObject();
- while (reader.hasNext()) {
- String name = reader.nextName();
- switch (name) {
- case "name":
- groupName = reader.nextString();
- break;
- case "samples":
- reader.beginArray();
- while (reader.hasNext()) {
- samples.add(readEntry(reader, false));
- }
- reader.endArray();
- break;
- case "_comment":
- reader.nextString(); // Ignore.
- break;
- default:
- throw new ParserException("Unsupported name: " + name);
- }
- }
- reader.endObject();
-
- SampleGroup group = getGroup(groupName, groups);
- group.samples.addAll(samples);
- }
-
- private Sample readEntry(JsonReader reader, boolean insidePlaylist) throws IOException {
- String sampleName = null;
- String uri = null;
- String extension = null;
- UUID drmUuid = null;
- String drmLicenseUrl = null;
- String[] drmKeyRequestProperties = null;
- boolean preferExtensionDecoders = false;
- ArrayList playlistSamples = null;
-
- reader.beginObject();
- while (reader.hasNext()) {
- String name = reader.nextName();
- switch (name) {
- case "name":
- sampleName = reader.nextString();
- break;
- case "uri":
- uri = reader.nextString();
- break;
- case "extension":
- extension = reader.nextString();
- break;
- case "drm_scheme":
- Assertions.checkState(!insidePlaylist, "Invalid attribute on nested item: drm_scheme");
- drmUuid = getDrmUuid(reader.nextString());
- break;
- case "drm_license_url":
- Assertions.checkState(!insidePlaylist,
- "Invalid attribute on nested item: drm_license_url");
- drmLicenseUrl = reader.nextString();
- break;
- case "drm_key_request_properties":
- Assertions.checkState(!insidePlaylist,
- "Invalid attribute on nested item: drm_key_request_properties");
- ArrayList drmKeyRequestPropertiesList = new ArrayList<>();
- reader.beginObject();
- while (reader.hasNext()) {
- drmKeyRequestPropertiesList.add(reader.nextName());
- drmKeyRequestPropertiesList.add(reader.nextString());
- }
- reader.endObject();
- drmKeyRequestProperties = drmKeyRequestPropertiesList.toArray(new String[0]);
- break;
- case "prefer_extension_decoders":
- Assertions.checkState(!insidePlaylist,
- "Invalid attribute on nested item: prefer_extension_decoders");
- preferExtensionDecoders = reader.nextBoolean();
- break;
- case "playlist":
- Assertions.checkState(!insidePlaylist, "Invalid nesting of playlists");
- playlistSamples = new ArrayList<>();
- reader.beginArray();
- while (reader.hasNext()) {
- playlistSamples.add((UriSample) readEntry(reader, true));
- }
- reader.endArray();
- break;
- default:
- throw new ParserException("Unsupported attribute name: " + name);
- }
- }
- reader.endObject();
-
- if (playlistSamples != null) {
- UriSample[] playlistSamplesArray = playlistSamples.toArray(
- new UriSample[playlistSamples.size()]);
- return new PlaylistSample(sampleName, drmUuid, drmLicenseUrl, drmKeyRequestProperties,
- preferExtensionDecoders, playlistSamplesArray);
- } else {
- return new UriSample(sampleName, drmUuid, drmLicenseUrl, drmKeyRequestProperties,
- preferExtensionDecoders, uri, extension);
- }
- }
-
- private SampleGroup getGroup(String groupName, List groups) {
- for (int i = 0; i < groups.size(); i++) {
- if (Util.areEqual(groupName, groups.get(i).title)) {
- return groups.get(i);
- }
- }
- SampleGroup group = new SampleGroup(groupName);
- groups.add(group);
- return group;
- }
-
- private UUID getDrmUuid(String typeString) throws ParserException {
- switch (Util.toLowerInvariant(typeString)) {
- case "widevine":
- return C.WIDEVINE_UUID;
- case "playready":
- return C.PLAYREADY_UUID;
- case "cenc":
- return C.CLEARKEY_UUID;
- default:
- try {
- return UUID.fromString(typeString);
- } catch (RuntimeException e) {
- throw new ParserException("Unsupported drm type: " + typeString);
- }
- }
- }
-
- }
-
- private static final class SampleAdapter extends BaseExpandableListAdapter {
-
- private final Context context;
- private final List sampleGroups;
-
- public SampleAdapter(Context context, List sampleGroups) {
- this.context = context;
- this.sampleGroups = sampleGroups;
- }
-
- @Override
- public Sample getChild(int groupPosition, int childPosition) {
- return getGroup(groupPosition).samples.get(childPosition);
- }
-
- @Override
- public long getChildId(int groupPosition, int childPosition) {
- return childPosition;
- }
-
- @Override
- public View getChildView(int groupPosition, int childPosition, boolean isLastChild,
- View convertView, ViewGroup parent) {
- View view = convertView;
- if (view == null) {
- view = LayoutInflater.from(context).inflate(android.R.layout.simple_list_item_1, parent,
- false);
- }
- ((TextView) view).setText(getChild(groupPosition, childPosition).name);
- return view;
- }
-
- @Override
- public int getChildrenCount(int groupPosition) {
- return getGroup(groupPosition).samples.size();
- }
-
- @Override
- public SampleGroup getGroup(int groupPosition) {
- return sampleGroups.get(groupPosition);
- }
-
- @Override
- public long getGroupId(int groupPosition) {
- return groupPosition;
- }
-
- @Override
- public View getGroupView(int groupPosition, boolean isExpanded, View convertView,
- ViewGroup parent) {
- View view = convertView;
- if (view == null) {
- view = LayoutInflater.from(context).inflate(android.R.layout.simple_expandable_list_item_1,
- parent, false);
- }
- ((TextView) view).setText(getGroup(groupPosition).title);
- return view;
- }
-
- @Override
- public int getGroupCount() {
- return sampleGroups.size();
- }
-
- @Override
- public boolean hasStableIds() {
- return false;
- }
-
- @Override
- public boolean isChildSelectable(int groupPosition, int childPosition) {
- return true;
- }
-
- }
-
- private static final class SampleGroup {
-
- public final String title;
- public final List samples;
-
- public SampleGroup(String title) {
- this.title = title;
- this.samples = new ArrayList<>();
- }
-
- }
-
- private abstract static class Sample {
-
- public final String name;
- public final boolean preferExtensionDecoders;
- public final UUID drmSchemeUuid;
- public final String drmLicenseUrl;
- public final String[] drmKeyRequestProperties;
-
- public Sample(String name, UUID drmSchemeUuid, String drmLicenseUrl,
- String[] drmKeyRequestProperties, boolean preferExtensionDecoders) {
- this.name = name;
- this.drmSchemeUuid = drmSchemeUuid;
- this.drmLicenseUrl = drmLicenseUrl;
- this.drmKeyRequestProperties = drmKeyRequestProperties;
- this.preferExtensionDecoders = preferExtensionDecoders;
- }
-
- public Intent buildIntent(Context context) {
- Intent intent = new Intent(context, PlayerActivity.class);
- intent.putExtra(PlayerActivity.PREFER_EXTENSION_DECODERS, preferExtensionDecoders);
- if (drmSchemeUuid != null) {
- intent.putExtra(PlayerActivity.DRM_SCHEME_UUID_EXTRA, drmSchemeUuid.toString());
- intent.putExtra(PlayerActivity.DRM_LICENSE_URL, drmLicenseUrl);
- intent.putExtra(PlayerActivity.DRM_KEY_REQUEST_PROPERTIES, drmKeyRequestProperties);
- }
- return intent;
- }
-
- }
-
- private static final class UriSample extends Sample {
-
- public final String uri;
- public final String extension;
-
- public UriSample(String name, UUID drmSchemeUuid, String drmLicenseUrl,
- String[] drmKeyRequestProperties, boolean preferExtensionDecoders, String uri,
- String extension) {
- super(name, drmSchemeUuid, drmLicenseUrl, drmKeyRequestProperties, preferExtensionDecoders);
- this.uri = uri;
- this.extension = extension;
- }
-
- @Override
- public Intent buildIntent(Context context) {
- return super.buildIntent(context)
- .setData(Uri.parse(uri))
- .putExtra(PlayerActivity.EXTENSION_EXTRA, extension)
- .setAction(PlayerActivity.ACTION_VIEW);
- }
-
- }
-
- private static final class PlaylistSample extends Sample {
-
- public final UriSample[] children;
-
- public PlaylistSample(String name, UUID drmSchemeUuid, String drmLicenseUrl,
- String[] drmKeyRequestProperties, boolean preferExtensionDecoders,
- UriSample... children) {
- super(name, drmSchemeUuid, drmLicenseUrl, drmKeyRequestProperties, preferExtensionDecoders);
- this.children = children;
- }
-
- @Override
- public Intent buildIntent(Context context) {
- String[] uris = new String[children.length];
- String[] extensions = new String[children.length];
- for (int i = 0; i < children.length; i++) {
- uris[i] = children[i].uri;
- extensions[i] = children[i].extension;
- }
- return super.buildIntent(context)
- .putExtra(PlayerActivity.URI_LIST_EXTRA, uris)
- .putExtra(PlayerActivity.EXTENSION_LIST_EXTRA, extensions)
- .setAction(PlayerActivity.ACTION_VIEW_LIST);
- }
-
- }
-
-}
diff --git a/demo/src/main/java/com/google/android/exoplayer2/demo/TrackSelectionHelper.java b/demo/src/main/java/com/google/android/exoplayer2/demo/TrackSelectionHelper.java
deleted file mode 100644
index fb7217f8fd..0000000000
--- a/demo/src/main/java/com/google/android/exoplayer2/demo/TrackSelectionHelper.java
+++ /dev/null
@@ -1,290 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.google.android.exoplayer2.demo;
-
-import android.annotation.SuppressLint;
-import android.app.Activity;
-import android.app.AlertDialog;
-import android.content.Context;
-import android.content.DialogInterface;
-import android.content.res.TypedArray;
-import android.util.Pair;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.CheckedTextView;
-import com.google.android.exoplayer2.RendererCapabilities;
-import com.google.android.exoplayer2.source.TrackGroup;
-import com.google.android.exoplayer2.source.TrackGroupArray;
-import com.google.android.exoplayer2.trackselection.FixedTrackSelection;
-import com.google.android.exoplayer2.trackselection.MappingTrackSelector;
-import com.google.android.exoplayer2.trackselection.MappingTrackSelector.MappedTrackInfo;
-import com.google.android.exoplayer2.trackselection.MappingTrackSelector.SelectionOverride;
-import com.google.android.exoplayer2.trackselection.RandomTrackSelection;
-import com.google.android.exoplayer2.trackselection.TrackSelection;
-import java.util.Arrays;
-
-/**
- * Helper class for displaying track selection dialogs.
- */
-/* package */ final class TrackSelectionHelper implements View.OnClickListener,
- DialogInterface.OnClickListener {
-
- private static final TrackSelection.Factory FIXED_FACTORY = new FixedTrackSelection.Factory();
- private static final TrackSelection.Factory RANDOM_FACTORY = new RandomTrackSelection.Factory();
-
- private final MappingTrackSelector selector;
- private final TrackSelection.Factory adaptiveTrackSelectionFactory;
-
- private MappedTrackInfo trackInfo;
- private int rendererIndex;
- private TrackGroupArray trackGroups;
- private boolean[] trackGroupsAdaptive;
- private boolean isDisabled;
- private SelectionOverride override;
-
- private CheckedTextView disableView;
- private CheckedTextView defaultView;
- private CheckedTextView enableRandomAdaptationView;
- private CheckedTextView[][] trackViews;
-
- /**
- * @param selector The track selector.
- * @param adaptiveTrackSelectionFactory A factory for adaptive {@link TrackSelection}s, or null
- * if the selection helper should not support adaptive tracks.
- */
- public TrackSelectionHelper(MappingTrackSelector selector,
- TrackSelection.Factory adaptiveTrackSelectionFactory) {
- this.selector = selector;
- this.adaptiveTrackSelectionFactory = adaptiveTrackSelectionFactory;
- }
-
- /**
- * Shows the selection dialog for a given renderer.
- *
- * @param activity The parent activity.
- * @param title The dialog's title.
- * @param trackInfo The current track information.
- * @param rendererIndex The index of the renderer.
- */
- public void showSelectionDialog(Activity activity, CharSequence title, MappedTrackInfo trackInfo,
- int rendererIndex) {
- this.trackInfo = trackInfo;
- this.rendererIndex = rendererIndex;
-
- trackGroups = trackInfo.getTrackGroups(rendererIndex);
- trackGroupsAdaptive = new boolean[trackGroups.length];
- for (int i = 0; i < trackGroups.length; i++) {
- trackGroupsAdaptive[i] = adaptiveTrackSelectionFactory != null
- && trackInfo.getAdaptiveSupport(rendererIndex, i, false)
- != RendererCapabilities.ADAPTIVE_NOT_SUPPORTED
- && trackGroups.get(i).length > 1;
- }
- isDisabled = selector.getRendererDisabled(rendererIndex);
- override = selector.getSelectionOverride(rendererIndex, trackGroups);
-
- AlertDialog.Builder builder = new AlertDialog.Builder(activity);
- builder.setTitle(title)
- .setView(buildView(builder.getContext()))
- .setPositiveButton(android.R.string.ok, this)
- .setNegativeButton(android.R.string.cancel, null)
- .create()
- .show();
- }
-
- @SuppressLint("InflateParams")
- private View buildView(Context context) {
- LayoutInflater inflater = LayoutInflater.from(context);
- View view = inflater.inflate(R.layout.track_selection_dialog, null);
- ViewGroup root = (ViewGroup) view.findViewById(R.id.root);
-
- TypedArray attributeArray = context.getTheme().obtainStyledAttributes(
- new int[] {android.R.attr.selectableItemBackground});
- int selectableItemBackgroundResourceId = attributeArray.getResourceId(0, 0);
- attributeArray.recycle();
-
- // View for disabling the renderer.
- disableView = (CheckedTextView) inflater.inflate(
- android.R.layout.simple_list_item_single_choice, root, false);
- disableView.setBackgroundResource(selectableItemBackgroundResourceId);
- disableView.setText(R.string.selection_disabled);
- disableView.setFocusable(true);
- disableView.setOnClickListener(this);
- root.addView(disableView);
-
- // View for clearing the override to allow the selector to use its default selection logic.
- defaultView = (CheckedTextView) inflater.inflate(
- android.R.layout.simple_list_item_single_choice, root, false);
- defaultView.setBackgroundResource(selectableItemBackgroundResourceId);
- defaultView.setText(R.string.selection_default);
- defaultView.setFocusable(true);
- defaultView.setOnClickListener(this);
- root.addView(inflater.inflate(R.layout.list_divider, root, false));
- root.addView(defaultView);
-
- // Per-track views.
- boolean haveAdaptiveTracks = false;
- trackViews = new CheckedTextView[trackGroups.length][];
- for (int groupIndex = 0; groupIndex < trackGroups.length; groupIndex++) {
- TrackGroup group = trackGroups.get(groupIndex);
- boolean groupIsAdaptive = trackGroupsAdaptive[groupIndex];
- haveAdaptiveTracks |= groupIsAdaptive;
- trackViews[groupIndex] = new CheckedTextView[group.length];
- for (int trackIndex = 0; trackIndex < group.length; trackIndex++) {
- if (trackIndex == 0) {
- root.addView(inflater.inflate(R.layout.list_divider, root, false));
- }
- int trackViewLayoutId = groupIsAdaptive ? android.R.layout.simple_list_item_multiple_choice
- : android.R.layout.simple_list_item_single_choice;
- CheckedTextView trackView = (CheckedTextView) inflater.inflate(
- trackViewLayoutId, root, false);
- trackView.setBackgroundResource(selectableItemBackgroundResourceId);
- trackView.setText(DemoUtil.buildTrackName(group.getFormat(trackIndex)));
- if (trackInfo.getTrackFormatSupport(rendererIndex, groupIndex, trackIndex)
- == RendererCapabilities.FORMAT_HANDLED) {
- trackView.setFocusable(true);
- trackView.setTag(Pair.create(groupIndex, trackIndex));
- trackView.setOnClickListener(this);
- } else {
- trackView.setFocusable(false);
- trackView.setEnabled(false);
- }
- trackViews[groupIndex][trackIndex] = trackView;
- root.addView(trackView);
- }
- }
-
- if (haveAdaptiveTracks) {
- // View for using random adaptation.
- enableRandomAdaptationView = (CheckedTextView) inflater.inflate(
- android.R.layout.simple_list_item_multiple_choice, root, false);
- enableRandomAdaptationView.setBackgroundResource(selectableItemBackgroundResourceId);
- enableRandomAdaptationView.setText(R.string.enable_random_adaptation);
- enableRandomAdaptationView.setOnClickListener(this);
- root.addView(inflater.inflate(R.layout.list_divider, root, false));
- root.addView(enableRandomAdaptationView);
- }
-
- updateViews();
- return view;
- }
-
- private void updateViews() {
- disableView.setChecked(isDisabled);
- defaultView.setChecked(!isDisabled && override == null);
- for (int i = 0; i < trackViews.length; i++) {
- for (int j = 0; j < trackViews[i].length; j++) {
- trackViews[i][j].setChecked(override != null && override.groupIndex == i
- && override.containsTrack(j));
- }
- }
- if (enableRandomAdaptationView != null) {
- boolean enableView = !isDisabled && override != null && override.length > 1;
- enableRandomAdaptationView.setEnabled(enableView);
- enableRandomAdaptationView.setFocusable(enableView);
- if (enableView) {
- enableRandomAdaptationView.setChecked(!isDisabled
- && override.factory instanceof RandomTrackSelection.Factory);
- }
- }
- }
-
- // DialogInterface.OnClickListener
-
- @Override
- public void onClick(DialogInterface dialog, int which) {
- selector.setRendererDisabled(rendererIndex, isDisabled);
- if (override != null) {
- selector.setSelectionOverride(rendererIndex, trackGroups, override);
- } else {
- selector.clearSelectionOverrides(rendererIndex);
- }
- }
-
- // View.OnClickListener
-
- @Override
- public void onClick(View view) {
- if (view == disableView) {
- isDisabled = true;
- override = null;
- } else if (view == defaultView) {
- isDisabled = false;
- override = null;
- } else if (view == enableRandomAdaptationView) {
- setOverride(override.groupIndex, override.tracks, !enableRandomAdaptationView.isChecked());
- } else {
- isDisabled = false;
- @SuppressWarnings("unchecked")
- Pair tag = (Pair) view.getTag();
- int groupIndex = tag.first;
- int trackIndex = tag.second;
- if (!trackGroupsAdaptive[groupIndex] || override == null
- || override.groupIndex != groupIndex) {
- override = new SelectionOverride(FIXED_FACTORY, groupIndex, trackIndex);
- } else {
- // The group being modified is adaptive and we already have a non-null override.
- boolean isEnabled = ((CheckedTextView) view).isChecked();
- int overrideLength = override.length;
- if (isEnabled) {
- // Remove the track from the override.
- if (overrideLength == 1) {
- // The last track is being removed, so the override becomes empty.
- override = null;
- isDisabled = true;
- } else {
- setOverride(groupIndex, getTracksRemoving(override, trackIndex),
- enableRandomAdaptationView.isChecked());
- }
- } else {
- // Add the track to the override.
- setOverride(groupIndex, getTracksAdding(override, trackIndex),
- enableRandomAdaptationView.isChecked());
- }
- }
- }
- // Update the views with the new state.
- updateViews();
- }
-
- private void setOverride(int group, int[] tracks, boolean enableRandomAdaptation) {
- TrackSelection.Factory factory = tracks.length == 1 ? FIXED_FACTORY
- : (enableRandomAdaptation ? RANDOM_FACTORY : adaptiveTrackSelectionFactory);
- override = new SelectionOverride(factory, group, tracks);
- }
-
- // Track array manipulation.
-
- private static int[] getTracksAdding(SelectionOverride override, int addedTrack) {
- int[] tracks = override.tracks;
- tracks = Arrays.copyOf(tracks, tracks.length + 1);
- tracks[tracks.length - 1] = addedTrack;
- return tracks;
- }
-
- private static int[] getTracksRemoving(SelectionOverride override, int removedTrack) {
- int[] tracks = new int[override.length - 1];
- int trackCount = 0;
- for (int i = 0; i < tracks.length + 1; i++) {
- int track = override.tracks[i];
- if (track != removedTrack) {
- tracks[trackCount++] = track;
- }
- }
- return tracks;
- }
-
-}
diff --git a/demo/src/main/res/drawable-xhdpi/ic_banner.png b/demo/src/main/res/drawable-xhdpi/ic_banner.png
deleted file mode 100644
index 520d83cc3b..0000000000
Binary files a/demo/src/main/res/drawable-xhdpi/ic_banner.png and /dev/null differ
diff --git a/demo/src/main/res/mipmap-hdpi/ic_launcher.png b/demo/src/main/res/mipmap-hdpi/ic_launcher.png
deleted file mode 100644
index 6e8b5499de..0000000000
Binary files a/demo/src/main/res/mipmap-hdpi/ic_launcher.png and /dev/null differ
diff --git a/demo/src/main/res/mipmap-mdpi/ic_launcher.png b/demo/src/main/res/mipmap-mdpi/ic_launcher.png
deleted file mode 100644
index 26fe2f0782..0000000000
Binary files a/demo/src/main/res/mipmap-mdpi/ic_launcher.png and /dev/null differ
diff --git a/demo/src/main/res/mipmap-xhdpi/ic_launcher.png b/demo/src/main/res/mipmap-xhdpi/ic_launcher.png
deleted file mode 100644
index d3251491ce..0000000000
Binary files a/demo/src/main/res/mipmap-xhdpi/ic_launcher.png and /dev/null differ
diff --git a/demo/src/main/res/mipmap-xxhdpi/ic_launcher.png b/demo/src/main/res/mipmap-xxhdpi/ic_launcher.png
deleted file mode 100644
index b5a12d35f3..0000000000
Binary files a/demo/src/main/res/mipmap-xxhdpi/ic_launcher.png and /dev/null differ
diff --git a/demo/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/demo/src/main/res/mipmap-xxxhdpi/ic_launcher.png
deleted file mode 100644
index 9c26192c32..0000000000
Binary files a/demo/src/main/res/mipmap-xxxhdpi/ic_launcher.png and /dev/null differ
diff --git a/demos/README.md b/demos/README.md
new file mode 100644
index 0000000000..7e62249db1
--- /dev/null
+++ b/demos/README.md
@@ -0,0 +1,4 @@
+# ExoPlayer demos #
+
+This directory contains applications that demonstrate how to use ExoPlayer.
+Browse the individual demos and their READMEs to learn more.
diff --git a/demos/cast/README.md b/demos/cast/README.md
new file mode 100644
index 0000000000..2c68a5277a
--- /dev/null
+++ b/demos/cast/README.md
@@ -0,0 +1,4 @@
+# Cast demo application #
+
+This folder contains a demo application that showcases ExoPlayer integration
+with Google Cast.
diff --git a/demos/cast/build.gradle b/demos/cast/build.gradle
new file mode 100644
index 0000000000..f9228e4b79
--- /dev/null
+++ b/demos/cast/build.gradle
@@ -0,0 +1,64 @@
+// Copyright (C) 2017 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+apply from: '../../constants.gradle'
+apply plugin: 'com.android.application'
+
+android {
+ compileSdkVersion project.ext.compileSdkVersion
+
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_1_8
+ targetCompatibility JavaVersion.VERSION_1_8
+ }
+
+ defaultConfig {
+ versionName project.ext.releaseVersion
+ versionCode project.ext.releaseVersionCode
+ minSdkVersion project.ext.minSdkVersion
+ targetSdkVersion project.ext.appTargetSdkVersion
+ }
+
+ buildTypes {
+ release {
+ shrinkResources true
+ minifyEnabled true
+ proguardFiles = [
+ "proguard-rules.txt",
+ getDefaultProguardFile('proguard-android.txt')
+ ]
+ }
+ debug {
+ jniDebuggable = true
+ }
+ }
+
+ lintOptions {
+ // The demo app isn't indexed and doesn't have translations.
+ disable 'GoogleAppIndexingWarning','MissingTranslation'
+ }
+}
+
+dependencies {
+ implementation project(modulePrefix + 'library-core')
+ implementation project(modulePrefix + 'library-dash')
+ implementation project(modulePrefix + 'library-hls')
+ implementation project(modulePrefix + 'library-smoothstreaming')
+ implementation project(modulePrefix + 'library-ui')
+ implementation project(modulePrefix + 'extension-cast')
+ implementation 'androidx.appcompat:appcompat:' + androidxAppCompatVersion
+ implementation 'androidx.recyclerview:recyclerview:1.0.0'
+ implementation 'com.google.android.material:material:1.0.0'
+}
+
+apply plugin: 'com.google.android.gms.strict-version-matcher-plugin'
diff --git a/demos/cast/proguard-rules.txt b/demos/cast/proguard-rules.txt
new file mode 100644
index 0000000000..e6bf2dd3bf
--- /dev/null
+++ b/demos/cast/proguard-rules.txt
@@ -0,0 +1,6 @@
+# Proguard rules specific to the Cast demo app.
+
+# Accessed via menu.xml
+-keep class androidx.mediarouter.app.MediaRouteActionProvider {
+ *;
+}
diff --git a/demos/cast/src/main/AndroidManifest.xml b/demos/cast/src/main/AndroidManifest.xml
new file mode 100644
index 0000000000..dbfdd833f6
--- /dev/null
+++ b/demos/cast/src/main/AndroidManifest.xml
@@ -0,0 +1,42 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/DemoUtil.java b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/DemoUtil.java
new file mode 100644
index 0000000000..dacdbfe616
--- /dev/null
+++ b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/DemoUtil.java
@@ -0,0 +1,104 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.castdemo;
+
+import android.net.Uri;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.ext.cast.MediaItem;
+import com.google.android.exoplayer2.ext.cast.MediaItem.DrmConfiguration;
+import com.google.android.exoplayer2.util.MimeTypes;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/** Utility methods and constants for the Cast demo application. */
+/* package */ final class DemoUtil {
+
+ public static final String MIME_TYPE_DASH = MimeTypes.APPLICATION_MPD;
+ public static final String MIME_TYPE_HLS = MimeTypes.APPLICATION_M3U8;
+ public static final String MIME_TYPE_SS = MimeTypes.APPLICATION_SS;
+ public static final String MIME_TYPE_VIDEO_MP4 = MimeTypes.VIDEO_MP4;
+
+ /** The list of samples available in the cast demo app. */
+ public static final List SAMPLES;
+
+ static {
+ ArrayList samples = new ArrayList<>();
+
+ // Clear content.
+ samples.add(
+ new MediaItem.Builder()
+ .setUri("https://storage.googleapis.com/wvmedia/clear/h264/tears/tears.mpd")
+ .setTitle("Clear DASH: Tears")
+ .setMimeType(MIME_TYPE_DASH)
+ .build());
+ samples.add(
+ new MediaItem.Builder()
+ .setUri("https://storage.googleapis.com/shaka-demo-assets/angel-one-hls/hls.m3u8")
+ .setTitle("Clear HLS: Angel one")
+ .setMimeType(MIME_TYPE_HLS)
+ .build());
+ samples.add(
+ new MediaItem.Builder()
+ .setUri("https://html5demos.com/assets/dizzy.mp4")
+ .setTitle("Clear MP4: Dizzy")
+ .setMimeType(MIME_TYPE_VIDEO_MP4)
+ .build());
+
+ // DRM content.
+ samples.add(
+ new MediaItem.Builder()
+ .setUri(Uri.parse("https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd"))
+ .setTitle("Widevine DASH cenc: Tears")
+ .setMimeType(MIME_TYPE_DASH)
+ .setDrmConfiguration(
+ new DrmConfiguration(
+ C.WIDEVINE_UUID,
+ Uri.parse("https://proxy.uat.widevine.com/proxy?provider=widevine_test"),
+ Collections.emptyMap()))
+ .build());
+ samples.add(
+ new MediaItem.Builder()
+ .setUri(
+ Uri.parse(
+ "https://storage.googleapis.com/wvmedia/cbc1/h264/tears/tears_aes_cbc1.mpd"))
+ .setTitle("Widevine DASH cbc1: Tears")
+ .setMimeType(MIME_TYPE_DASH)
+ .setDrmConfiguration(
+ new DrmConfiguration(
+ C.WIDEVINE_UUID,
+ Uri.parse("https://proxy.uat.widevine.com/proxy?provider=widevine_test"),
+ Collections.emptyMap()))
+ .build());
+ samples.add(
+ new MediaItem.Builder()
+ .setUri(
+ Uri.parse(
+ "https://storage.googleapis.com/wvmedia/cbcs/h264/tears/tears_aes_cbcs.mpd"))
+ .setTitle("Widevine DASH cbcs: Tears")
+ .setMimeType(MIME_TYPE_DASH)
+ .setDrmConfiguration(
+ new DrmConfiguration(
+ C.WIDEVINE_UUID,
+ Uri.parse("https://proxy.uat.widevine.com/proxy?provider=widevine_test"),
+ Collections.emptyMap()))
+ .build());
+
+ SAMPLES = Collections.unmodifiableList(samples);
+ }
+
+ private DemoUtil() {}
+}
diff --git a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/MainActivity.java b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/MainActivity.java
new file mode 100644
index 0000000000..0c5b5037f5
--- /dev/null
+++ b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/MainActivity.java
@@ -0,0 +1,311 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.castdemo;
+
+import android.content.Context;
+import android.os.Bundle;
+import android.view.KeyEvent;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.ViewGroup;
+import android.widget.ArrayAdapter;
+import android.widget.ListView;
+import android.widget.TextView;
+import android.widget.Toast;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.appcompat.app.AlertDialog;
+import androidx.appcompat.app.AppCompatActivity;
+import androidx.core.graphics.ColorUtils;
+import androidx.recyclerview.widget.ItemTouchHelper;
+import androidx.recyclerview.widget.LinearLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
+import androidx.recyclerview.widget.RecyclerView.ViewHolder;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.SimpleExoPlayer;
+import com.google.android.exoplayer2.ext.cast.MediaItem;
+import com.google.android.exoplayer2.ui.PlayerControlView;
+import com.google.android.exoplayer2.ui.PlayerView;
+import com.google.android.gms.cast.framework.CastButtonFactory;
+import com.google.android.gms.cast.framework.CastContext;
+import com.google.android.gms.dynamite.DynamiteModule;
+
+/**
+ * An activity that plays video using {@link SimpleExoPlayer} and supports casting using ExoPlayer's
+ * Cast extension.
+ */
+public class MainActivity extends AppCompatActivity
+ implements OnClickListener, PlayerManager.Listener {
+
+ private PlayerView localPlayerView;
+ private PlayerControlView castControlView;
+ private PlayerManager playerManager;
+ private RecyclerView mediaQueueList;
+ private MediaQueueListAdapter mediaQueueListAdapter;
+ private CastContext castContext;
+
+ // Activity lifecycle methods.
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ // Getting the cast context later than onStart can cause device discovery not to take place.
+ try {
+ castContext = CastContext.getSharedInstance(this);
+ } catch (RuntimeException e) {
+ Throwable cause = e.getCause();
+ while (cause != null) {
+ if (cause instanceof DynamiteModule.LoadingException) {
+ setContentView(R.layout.cast_context_error);
+ return;
+ }
+ cause = cause.getCause();
+ }
+ // Unknown error. We propagate it.
+ throw e;
+ }
+
+ setContentView(R.layout.main_activity);
+
+ localPlayerView = findViewById(R.id.local_player_view);
+ localPlayerView.requestFocus();
+
+ castControlView = findViewById(R.id.cast_control_view);
+
+ mediaQueueList = findViewById(R.id.sample_list);
+ ItemTouchHelper helper = new ItemTouchHelper(new RecyclerViewCallback());
+ helper.attachToRecyclerView(mediaQueueList);
+ mediaQueueList.setLayoutManager(new LinearLayoutManager(this));
+ mediaQueueList.setHasFixedSize(true);
+ mediaQueueListAdapter = new MediaQueueListAdapter();
+
+ findViewById(R.id.add_sample_button).setOnClickListener(this);
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ super.onCreateOptionsMenu(menu);
+ getMenuInflater().inflate(R.menu.menu, menu);
+ CastButtonFactory.setUpMediaRouteButton(this, menu, R.id.media_route_menu_item);
+ return true;
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ if (castContext == null) {
+ // There is no Cast context to work with. Do nothing.
+ return;
+ }
+ playerManager =
+ new PlayerManager(
+ /* listener= */ this,
+ localPlayerView,
+ castControlView,
+ /* context= */ this,
+ castContext);
+ mediaQueueList.setAdapter(mediaQueueListAdapter);
+ }
+
+ @Override
+ public void onPause() {
+ super.onPause();
+ if (castContext == null) {
+ // Nothing to release.
+ return;
+ }
+ mediaQueueListAdapter.notifyItemRangeRemoved(0, mediaQueueListAdapter.getItemCount());
+ mediaQueueList.setAdapter(null);
+ playerManager.release();
+ playerManager = null;
+ }
+
+ // Activity input.
+
+ @Override
+ public boolean dispatchKeyEvent(KeyEvent event) {
+ // If the event was not handled then see if the player view can handle it.
+ return super.dispatchKeyEvent(event) || playerManager.dispatchKeyEvent(event);
+ }
+
+ @Override
+ public void onClick(View view) {
+ new AlertDialog.Builder(this)
+ .setTitle(R.string.add_samples)
+ .setView(buildSampleListView())
+ .setPositiveButton(android.R.string.ok, null)
+ .create()
+ .show();
+ }
+
+ // PlayerManager.Listener implementation.
+
+ @Override
+ public void onQueuePositionChanged(int previousIndex, int newIndex) {
+ if (previousIndex != C.INDEX_UNSET) {
+ mediaQueueListAdapter.notifyItemChanged(previousIndex);
+ }
+ if (newIndex != C.INDEX_UNSET) {
+ mediaQueueListAdapter.notifyItemChanged(newIndex);
+ }
+ }
+
+ @Override
+ public void onUnsupportedTrack(int trackType) {
+ if (trackType == C.TRACK_TYPE_AUDIO) {
+ showToast(R.string.error_unsupported_audio);
+ } else if (trackType == C.TRACK_TYPE_VIDEO) {
+ showToast(R.string.error_unsupported_video);
+ } else {
+ // Do nothing.
+ }
+ }
+
+ // Internal methods.
+
+ private void showToast(int messageId) {
+ Toast.makeText(getApplicationContext(), messageId, Toast.LENGTH_LONG).show();
+ }
+
+ private View buildSampleListView() {
+ View dialogList = getLayoutInflater().inflate(R.layout.sample_list, null);
+ ListView sampleList = dialogList.findViewById(R.id.sample_list);
+ sampleList.setAdapter(new SampleListAdapter(this));
+ sampleList.setOnItemClickListener(
+ (parent, view, position, id) -> {
+ playerManager.addItem(DemoUtil.SAMPLES.get(position));
+ mediaQueueListAdapter.notifyItemInserted(playerManager.getMediaQueueSize() - 1);
+ });
+ return dialogList;
+ }
+
+ // Internal classes.
+
+ private class MediaQueueListAdapter extends RecyclerView.Adapter {
+
+ @Override
+ public QueueItemViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
+ TextView v = (TextView) LayoutInflater.from(parent.getContext())
+ .inflate(android.R.layout.simple_list_item_1, parent, false);
+ return new QueueItemViewHolder(v);
+ }
+
+ @Override
+ public void onBindViewHolder(QueueItemViewHolder holder, int position) {
+ holder.item = playerManager.getItem(position);
+ TextView view = holder.textView;
+ view.setText(holder.item.title);
+ // TODO: Solve coloring using the theme's ColorStateList.
+ view.setTextColor(
+ ColorUtils.setAlphaComponent(
+ view.getCurrentTextColor(),
+ position == playerManager.getCurrentItemIndex() ? 255 : 100));
+ }
+
+ @Override
+ public int getItemCount() {
+ return playerManager.getMediaQueueSize();
+ }
+
+ }
+
+ private class RecyclerViewCallback extends ItemTouchHelper.SimpleCallback {
+
+ private int draggingFromPosition;
+ private int draggingToPosition;
+
+ public RecyclerViewCallback() {
+ super(ItemTouchHelper.UP | ItemTouchHelper.DOWN, ItemTouchHelper.START | ItemTouchHelper.END);
+ draggingFromPosition = C.INDEX_UNSET;
+ draggingToPosition = C.INDEX_UNSET;
+ }
+
+ @Override
+ public boolean onMove(RecyclerView list, RecyclerView.ViewHolder origin,
+ RecyclerView.ViewHolder target) {
+ int fromPosition = origin.getAdapterPosition();
+ int toPosition = target.getAdapterPosition();
+ if (draggingFromPosition == C.INDEX_UNSET) {
+ // A drag has started, but changes to the media queue will be reflected in clearView().
+ draggingFromPosition = fromPosition;
+ }
+ draggingToPosition = toPosition;
+ mediaQueueListAdapter.notifyItemMoved(fromPosition, toPosition);
+ return true;
+ }
+
+ @Override
+ public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) {
+ int position = viewHolder.getAdapterPosition();
+ QueueItemViewHolder queueItemHolder = (QueueItemViewHolder) viewHolder;
+ if (playerManager.removeItem(queueItemHolder.item)) {
+ mediaQueueListAdapter.notifyItemRemoved(position);
+ // Update whichever item took its place, in case it became the new selected item.
+ mediaQueueListAdapter.notifyItemChanged(position);
+ }
+ }
+
+ @Override
+ public void clearView(RecyclerView recyclerView, ViewHolder viewHolder) {
+ super.clearView(recyclerView, viewHolder);
+ if (draggingFromPosition != C.INDEX_UNSET) {
+ QueueItemViewHolder queueItemHolder = (QueueItemViewHolder) viewHolder;
+ // A drag has ended. We reflect the media queue change in the player.
+ if (!playerManager.moveItem(queueItemHolder.item, draggingToPosition)) {
+ // The move failed. The entire sequence of onMove calls since the drag started needs to be
+ // invalidated.
+ mediaQueueListAdapter.notifyDataSetChanged();
+ }
+ }
+ draggingFromPosition = C.INDEX_UNSET;
+ draggingToPosition = C.INDEX_UNSET;
+ }
+ }
+
+ private class QueueItemViewHolder extends RecyclerView.ViewHolder implements OnClickListener {
+
+ public final TextView textView;
+ public MediaItem item;
+
+ public QueueItemViewHolder(TextView textView) {
+ super(textView);
+ this.textView = textView;
+ textView.setOnClickListener(this);
+ }
+
+ @Override
+ public void onClick(View v) {
+ playerManager.selectQueueItem(getAdapterPosition());
+ }
+ }
+
+ private static final class SampleListAdapter extends ArrayAdapter {
+
+ public SampleListAdapter(Context context) {
+ super(context, android.R.layout.simple_list_item_1, DemoUtil.SAMPLES);
+ }
+
+ @NonNull
+ @Override
+ public View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) {
+ View view = super.getView(position, convertView, parent);
+ ((TextView) view).setText(getItem(position).title);
+ return view;
+ }
+ }
+}
diff --git a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/PlayerManager.java b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/PlayerManager.java
new file mode 100644
index 0000000000..85104e0d18
--- /dev/null
+++ b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/PlayerManager.java
@@ -0,0 +1,459 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.castdemo;
+
+import android.content.Context;
+import android.net.Uri;
+import android.view.KeyEvent;
+import android.view.View;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.Player;
+import com.google.android.exoplayer2.Player.DiscontinuityReason;
+import com.google.android.exoplayer2.Player.EventListener;
+import com.google.android.exoplayer2.Player.TimelineChangeReason;
+import com.google.android.exoplayer2.SimpleExoPlayer;
+import com.google.android.exoplayer2.Timeline;
+import com.google.android.exoplayer2.Timeline.Period;
+import com.google.android.exoplayer2.drm.DefaultDrmSessionManager;
+import com.google.android.exoplayer2.drm.DrmSessionManager;
+import com.google.android.exoplayer2.drm.ExoMediaCrypto;
+import com.google.android.exoplayer2.drm.FrameworkMediaDrm;
+import com.google.android.exoplayer2.drm.HttpMediaDrmCallback;
+import com.google.android.exoplayer2.ext.cast.CastPlayer;
+import com.google.android.exoplayer2.ext.cast.DefaultMediaItemConverter;
+import com.google.android.exoplayer2.ext.cast.MediaItem;
+import com.google.android.exoplayer2.ext.cast.MediaItemConverter;
+import com.google.android.exoplayer2.ext.cast.SessionAvailabilityListener;
+import com.google.android.exoplayer2.source.ConcatenatingMediaSource;
+import com.google.android.exoplayer2.source.MediaSource;
+import com.google.android.exoplayer2.source.ProgressiveMediaSource;
+import com.google.android.exoplayer2.source.TrackGroupArray;
+import com.google.android.exoplayer2.source.dash.DashMediaSource;
+import com.google.android.exoplayer2.source.hls.HlsMediaSource;
+import com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource;
+import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
+import com.google.android.exoplayer2.trackselection.MappingTrackSelector;
+import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
+import com.google.android.exoplayer2.ui.PlayerControlView;
+import com.google.android.exoplayer2.ui.PlayerView;
+import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory;
+import com.google.android.exoplayer2.util.Util;
+import com.google.android.gms.cast.MediaQueueItem;
+import com.google.android.gms.cast.framework.CastContext;
+import java.util.ArrayList;
+import java.util.Map;
+
+/** Manages players and an internal media queue for the demo app. */
+/* package */ class PlayerManager implements EventListener, SessionAvailabilityListener {
+
+ /** Listener for events. */
+ interface Listener {
+
+ /** Called when the currently played item of the media queue changes. */
+ void onQueuePositionChanged(int previousIndex, int newIndex);
+
+ /**
+ * Called when a track of type {@code trackType} is not supported by the player.
+ *
+ * @param trackType One of the {@link C}{@code .TRACK_TYPE_*} constants.
+ */
+ void onUnsupportedTrack(int trackType);
+ }
+
+ private static final String USER_AGENT = "ExoCastDemoPlayer";
+ private static final DefaultHttpDataSourceFactory DATA_SOURCE_FACTORY =
+ new DefaultHttpDataSourceFactory(USER_AGENT);
+
+ private final PlayerView localPlayerView;
+ private final PlayerControlView castControlView;
+ private final DefaultTrackSelector trackSelector;
+ private final SimpleExoPlayer exoPlayer;
+ private final CastPlayer castPlayer;
+ private final ArrayList mediaQueue;
+ private final Listener listener;
+ private final ConcatenatingMediaSource concatenatingMediaSource;
+ private final MediaItemConverter mediaItemConverter;
+
+ private TrackGroupArray lastSeenTrackGroupArray;
+ private int currentItemIndex;
+ private Player currentPlayer;
+
+ /**
+ * Creates a new manager for {@link SimpleExoPlayer} and {@link CastPlayer}.
+ *
+ * @param listener A {@link Listener} for queue position changes.
+ * @param localPlayerView The {@link PlayerView} for local playback.
+ * @param castControlView The {@link PlayerControlView} to control remote playback.
+ * @param context A {@link Context}.
+ * @param castContext The {@link CastContext}.
+ */
+ public PlayerManager(
+ Listener listener,
+ PlayerView localPlayerView,
+ PlayerControlView castControlView,
+ Context context,
+ CastContext castContext) {
+ this.listener = listener;
+ this.localPlayerView = localPlayerView;
+ this.castControlView = castControlView;
+ mediaQueue = new ArrayList<>();
+ currentItemIndex = C.INDEX_UNSET;
+ concatenatingMediaSource = new ConcatenatingMediaSource();
+ mediaItemConverter = new DefaultMediaItemConverter();
+
+ trackSelector = new DefaultTrackSelector(context);
+ exoPlayer = new SimpleExoPlayer.Builder(context).setTrackSelector(trackSelector).build();
+ exoPlayer.addListener(this);
+ localPlayerView.setPlayer(exoPlayer);
+
+ castPlayer = new CastPlayer(castContext);
+ castPlayer.addListener(this);
+ castPlayer.setSessionAvailabilityListener(this);
+ castControlView.setPlayer(castPlayer);
+
+ setCurrentPlayer(castPlayer.isCastSessionAvailable() ? castPlayer : exoPlayer);
+ }
+
+ // Queue manipulation methods.
+
+ /**
+ * Plays a specified queue item in the current player.
+ *
+ * @param itemIndex The index of the item to play.
+ */
+ public void selectQueueItem(int itemIndex) {
+ setCurrentItem(itemIndex, C.TIME_UNSET, true);
+ }
+
+ /** Returns the index of the currently played item. */
+ public int getCurrentItemIndex() {
+ return currentItemIndex;
+ }
+
+ /**
+ * Appends {@code item} to the media queue.
+ *
+ * @param item The {@link MediaItem} to append.
+ */
+ public void addItem(MediaItem item) {
+ mediaQueue.add(item);
+ concatenatingMediaSource.addMediaSource(buildMediaSource(item));
+ if (currentPlayer == castPlayer) {
+ castPlayer.addItems(mediaItemConverter.toMediaQueueItem(item));
+ }
+ }
+
+ /** Returns the size of the media queue. */
+ public int getMediaQueueSize() {
+ return mediaQueue.size();
+ }
+
+ /**
+ * Returns the item at the given index in the media queue.
+ *
+ * @param position The index of the item.
+ * @return The item at the given index in the media queue.
+ */
+ public MediaItem getItem(int position) {
+ return mediaQueue.get(position);
+ }
+
+ /**
+ * Removes the item at the given index from the media queue.
+ *
+ * @param item The item to remove.
+ * @return Whether the removal was successful.
+ */
+ public boolean removeItem(MediaItem item) {
+ int itemIndex = mediaQueue.indexOf(item);
+ if (itemIndex == -1) {
+ return false;
+ }
+ concatenatingMediaSource.removeMediaSource(itemIndex);
+ if (currentPlayer == castPlayer) {
+ if (castPlayer.getPlaybackState() != Player.STATE_IDLE) {
+ Timeline castTimeline = castPlayer.getCurrentTimeline();
+ if (castTimeline.getPeriodCount() <= itemIndex) {
+ return false;
+ }
+ castPlayer.removeItem((int) castTimeline.getPeriod(itemIndex, new Period()).id);
+ }
+ }
+ mediaQueue.remove(itemIndex);
+ if (itemIndex == currentItemIndex && itemIndex == mediaQueue.size()) {
+ maybeSetCurrentItemAndNotify(C.INDEX_UNSET);
+ } else if (itemIndex < currentItemIndex) {
+ maybeSetCurrentItemAndNotify(currentItemIndex - 1);
+ }
+ return true;
+ }
+
+ /**
+ * Moves an item within the queue.
+ *
+ * @param item The item to move.
+ * @param toIndex The target index of the item in the queue.
+ * @return Whether the item move was successful.
+ */
+ public boolean moveItem(MediaItem item, int toIndex) {
+ int fromIndex = mediaQueue.indexOf(item);
+ if (fromIndex == -1) {
+ return false;
+ }
+ // Player update.
+ concatenatingMediaSource.moveMediaSource(fromIndex, toIndex);
+ if (currentPlayer == castPlayer && castPlayer.getPlaybackState() != Player.STATE_IDLE) {
+ Timeline castTimeline = castPlayer.getCurrentTimeline();
+ int periodCount = castTimeline.getPeriodCount();
+ if (periodCount <= fromIndex || periodCount <= toIndex) {
+ return false;
+ }
+ int elementId = (int) castTimeline.getPeriod(fromIndex, new Period()).id;
+ castPlayer.moveItem(elementId, toIndex);
+ }
+
+ mediaQueue.add(toIndex, mediaQueue.remove(fromIndex));
+
+ // Index update.
+ if (fromIndex == currentItemIndex) {
+ maybeSetCurrentItemAndNotify(toIndex);
+ } else if (fromIndex < currentItemIndex && toIndex >= currentItemIndex) {
+ maybeSetCurrentItemAndNotify(currentItemIndex - 1);
+ } else if (fromIndex > currentItemIndex && toIndex <= currentItemIndex) {
+ maybeSetCurrentItemAndNotify(currentItemIndex + 1);
+ }
+
+ return true;
+ }
+
+ /**
+ * Dispatches a given {@link KeyEvent} to the corresponding view of the current player.
+ *
+ * @param event The {@link KeyEvent}.
+ * @return Whether the event was handled by the target view.
+ */
+ public boolean dispatchKeyEvent(KeyEvent event) {
+ if (currentPlayer == exoPlayer) {
+ return localPlayerView.dispatchKeyEvent(event);
+ } else /* currentPlayer == castPlayer */ {
+ return castControlView.dispatchKeyEvent(event);
+ }
+ }
+
+ /** Releases the manager and the players that it holds. */
+ public void release() {
+ currentItemIndex = C.INDEX_UNSET;
+ mediaQueue.clear();
+ concatenatingMediaSource.clear();
+ castPlayer.setSessionAvailabilityListener(null);
+ castPlayer.release();
+ localPlayerView.setPlayer(null);
+ exoPlayer.release();
+ }
+
+ // Player.EventListener implementation.
+
+ @Override
+ public void onPlayerStateChanged(boolean playWhenReady, @Player.State int playbackState) {
+ updateCurrentItemIndex();
+ }
+
+ @Override
+ public void onPositionDiscontinuity(@DiscontinuityReason int reason) {
+ updateCurrentItemIndex();
+ }
+
+ @Override
+ public void onTimelineChanged(Timeline timeline, @TimelineChangeReason int reason) {
+ updateCurrentItemIndex();
+ }
+
+ @Override
+ public void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray trackSelections) {
+ if (currentPlayer == exoPlayer && trackGroups != lastSeenTrackGroupArray) {
+ MappingTrackSelector.MappedTrackInfo mappedTrackInfo =
+ trackSelector.getCurrentMappedTrackInfo();
+ if (mappedTrackInfo != null) {
+ if (mappedTrackInfo.getTypeSupport(C.TRACK_TYPE_VIDEO)
+ == MappingTrackSelector.MappedTrackInfo.RENDERER_SUPPORT_UNSUPPORTED_TRACKS) {
+ listener.onUnsupportedTrack(C.TRACK_TYPE_VIDEO);
+ }
+ if (mappedTrackInfo.getTypeSupport(C.TRACK_TYPE_AUDIO)
+ == MappingTrackSelector.MappedTrackInfo.RENDERER_SUPPORT_UNSUPPORTED_TRACKS) {
+ listener.onUnsupportedTrack(C.TRACK_TYPE_AUDIO);
+ }
+ }
+ lastSeenTrackGroupArray = trackGroups;
+ }
+ }
+
+ // CastPlayer.SessionAvailabilityListener implementation.
+
+ @Override
+ public void onCastSessionAvailable() {
+ setCurrentPlayer(castPlayer);
+ }
+
+ @Override
+ public void onCastSessionUnavailable() {
+ setCurrentPlayer(exoPlayer);
+ }
+
+ // Internal methods.
+
+ private void updateCurrentItemIndex() {
+ int playbackState = currentPlayer.getPlaybackState();
+ maybeSetCurrentItemAndNotify(
+ playbackState != Player.STATE_IDLE && playbackState != Player.STATE_ENDED
+ ? currentPlayer.getCurrentWindowIndex()
+ : C.INDEX_UNSET);
+ }
+
+ private void setCurrentPlayer(Player currentPlayer) {
+ if (this.currentPlayer == currentPlayer) {
+ return;
+ }
+
+ // View management.
+ if (currentPlayer == exoPlayer) {
+ localPlayerView.setVisibility(View.VISIBLE);
+ castControlView.hide();
+ } else /* currentPlayer == castPlayer */ {
+ localPlayerView.setVisibility(View.GONE);
+ castControlView.show();
+ }
+
+ // Player state management.
+ long playbackPositionMs = C.TIME_UNSET;
+ int windowIndex = C.INDEX_UNSET;
+ boolean playWhenReady = false;
+
+ Player previousPlayer = this.currentPlayer;
+ if (previousPlayer != null) {
+ // Save state from the previous player.
+ int playbackState = previousPlayer.getPlaybackState();
+ if (playbackState != Player.STATE_ENDED) {
+ playbackPositionMs = previousPlayer.getCurrentPosition();
+ playWhenReady = previousPlayer.getPlayWhenReady();
+ windowIndex = previousPlayer.getCurrentWindowIndex();
+ if (windowIndex != currentItemIndex) {
+ playbackPositionMs = C.TIME_UNSET;
+ windowIndex = currentItemIndex;
+ }
+ }
+ previousPlayer.stop(true);
+ }
+
+ this.currentPlayer = currentPlayer;
+
+ // Media queue management.
+ if (currentPlayer == exoPlayer) {
+ exoPlayer.prepare(concatenatingMediaSource);
+ }
+
+ // Playback transition.
+ if (windowIndex != C.INDEX_UNSET) {
+ setCurrentItem(windowIndex, playbackPositionMs, playWhenReady);
+ }
+ }
+
+ /**
+ * Starts playback of the item at the given position.
+ *
+ * @param itemIndex The index of the item to play.
+ * @param positionMs The position at which playback should start.
+ * @param playWhenReady Whether the player should proceed when ready to do so.
+ */
+ private void setCurrentItem(int itemIndex, long positionMs, boolean playWhenReady) {
+ maybeSetCurrentItemAndNotify(itemIndex);
+ if (currentPlayer == castPlayer && castPlayer.getCurrentTimeline().isEmpty()) {
+ MediaQueueItem[] items = new MediaQueueItem[mediaQueue.size()];
+ for (int i = 0; i < items.length; i++) {
+ items[i] = mediaItemConverter.toMediaQueueItem(mediaQueue.get(i));
+ }
+ castPlayer.loadItems(items, itemIndex, positionMs, Player.REPEAT_MODE_OFF);
+ } else {
+ currentPlayer.seekTo(itemIndex, positionMs);
+ currentPlayer.setPlayWhenReady(playWhenReady);
+ }
+ }
+
+ private void maybeSetCurrentItemAndNotify(int currentItemIndex) {
+ if (this.currentItemIndex != currentItemIndex) {
+ int oldIndex = this.currentItemIndex;
+ this.currentItemIndex = currentItemIndex;
+ listener.onQueuePositionChanged(oldIndex, currentItemIndex);
+ }
+ }
+
+ private MediaSource buildMediaSource(MediaItem item) {
+ Uri uri = item.uri;
+ String mimeType = item.mimeType;
+ if (mimeType == null) {
+ throw new IllegalArgumentException("mimeType is required");
+ }
+
+ DrmSessionManager drmSessionManager =
+ DrmSessionManager.getDummyDrmSessionManager();
+ MediaItem.DrmConfiguration drmConfiguration = item.drmConfiguration;
+ if (drmConfiguration != null && Util.SDK_INT >= 18) {
+ String licenseServerUrl =
+ drmConfiguration.licenseUri != null ? drmConfiguration.licenseUri.toString() : "";
+ HttpMediaDrmCallback drmCallback =
+ new HttpMediaDrmCallback(licenseServerUrl, DATA_SOURCE_FACTORY);
+ for (Map.Entry requestHeader : drmConfiguration.requestHeaders.entrySet()) {
+ drmCallback.setKeyRequestProperty(requestHeader.getKey(), requestHeader.getValue());
+ }
+ drmSessionManager =
+ new DefaultDrmSessionManager.Builder()
+ .setMultiSession(/* multiSession= */ true)
+ .setUuidAndExoMediaDrmProvider(
+ drmConfiguration.uuid, FrameworkMediaDrm.DEFAULT_PROVIDER)
+ .build(drmCallback);
+ }
+
+ MediaSource createdMediaSource;
+ switch (mimeType) {
+ case DemoUtil.MIME_TYPE_SS:
+ createdMediaSource =
+ new SsMediaSource.Factory(DATA_SOURCE_FACTORY)
+ .setDrmSessionManager(drmSessionManager)
+ .createMediaSource(uri);
+ break;
+ case DemoUtil.MIME_TYPE_DASH:
+ createdMediaSource =
+ new DashMediaSource.Factory(DATA_SOURCE_FACTORY)
+ .setDrmSessionManager(drmSessionManager)
+ .createMediaSource(uri);
+ break;
+ case DemoUtil.MIME_TYPE_HLS:
+ createdMediaSource =
+ new HlsMediaSource.Factory(DATA_SOURCE_FACTORY)
+ .setDrmSessionManager(drmSessionManager)
+ .createMediaSource(uri);
+ break;
+ case DemoUtil.MIME_TYPE_VIDEO_MP4:
+ createdMediaSource =
+ new ProgressiveMediaSource.Factory(DATA_SOURCE_FACTORY)
+ .setDrmSessionManager(drmSessionManager)
+ .createMediaSource(uri);
+ break;
+ default:
+ throw new IllegalArgumentException("mimeType is unsupported: " + mimeType);
+ }
+ return createdMediaSource;
+ }
+}
diff --git a/demos/cast/src/main/res/drawable/ic_plus.xml b/demos/cast/src/main/res/drawable/ic_plus.xml
new file mode 100644
index 0000000000..5a5a5154c9
--- /dev/null
+++ b/demos/cast/src/main/res/drawable/ic_plus.xml
@@ -0,0 +1,24 @@
+
+
+
+
+
diff --git a/demos/cast/src/main/res/layout/cast_context_error.xml b/demos/cast/src/main/res/layout/cast_context_error.xml
new file mode 100644
index 0000000000..0b3fdb63d2
--- /dev/null
+++ b/demos/cast/src/main/res/layout/cast_context_error.xml
@@ -0,0 +1,22 @@
+
+
+
diff --git a/demos/cast/src/main/res/layout/main_activity.xml b/demos/cast/src/main/res/layout/main_activity.xml
new file mode 100644
index 0000000000..71dbcdcd9c
--- /dev/null
+++ b/demos/cast/src/main/res/layout/main_activity.xml
@@ -0,0 +1,60 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/demos/cast/src/main/res/layout/sample_list.xml b/demos/cast/src/main/res/layout/sample_list.xml
new file mode 100644
index 0000000000..183c74eb3a
--- /dev/null
+++ b/demos/cast/src/main/res/layout/sample_list.xml
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
diff --git a/demos/cast/src/main/res/menu/menu.xml b/demos/cast/src/main/res/menu/menu.xml
new file mode 100644
index 0000000000..95419adf3c
--- /dev/null
+++ b/demos/cast/src/main/res/menu/menu.xml
@@ -0,0 +1,25 @@
+
+
+
diff --git a/demos/cast/src/main/res/mipmap-hdpi/ic_launcher.png b/demos/cast/src/main/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 0000000000..52e8dc93d9
Binary files /dev/null and b/demos/cast/src/main/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/demos/cast/src/main/res/mipmap-mdpi/ic_launcher.png b/demos/cast/src/main/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 0000000000..b55576eff3
Binary files /dev/null and b/demos/cast/src/main/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/demos/cast/src/main/res/mipmap-xhdpi/ic_launcher.png b/demos/cast/src/main/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 0000000000..ca84d6a60e
Binary files /dev/null and b/demos/cast/src/main/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/demos/cast/src/main/res/mipmap-xxhdpi/ic_launcher.png b/demos/cast/src/main/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 0000000000..27ab9b1054
Binary files /dev/null and b/demos/cast/src/main/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/demos/cast/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/demos/cast/src/main/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 0000000000..d1eb9b78cf
Binary files /dev/null and b/demos/cast/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/demos/cast/src/main/res/values/strings.xml b/demos/cast/src/main/res/values/strings.xml
new file mode 100644
index 0000000000..69f0691630
--- /dev/null
+++ b/demos/cast/src/main/res/values/strings.xml
@@ -0,0 +1,31 @@
+
+
+
+
+
+ Exo Cast Demo
+
+ Cast
+
+ Add samples
+
+ Failed to get Cast context. Try updating Google Play Services and restart the app.
+
+ Media includes video tracks, but none are playable by this device
+
+ Media includes audio tracks, but none are playable by this device
+
+
diff --git a/demo/README.md b/demos/main/README.md
similarity index 58%
rename from demo/README.md
rename to demos/main/README.md
index ca37392623..bdb04e5ba8 100644
--- a/demo/README.md
+++ b/demos/main/README.md
@@ -1,5 +1,5 @@
-# Demo application #
+# ExoPlayer main demo #
-This folder contains a demo application that uses ExoPlayer to play a number
+This is the main ExoPlayer demo application. It uses ExoPlayer to play a number
of test streams. It can be used as a starting point or reference project when
developing other applications that make use of the ExoPlayer library.
diff --git a/demos/main/build.gradle b/demos/main/build.gradle
new file mode 100644
index 0000000000..ab47b6de81
--- /dev/null
+++ b/demos/main/build.gradle
@@ -0,0 +1,82 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+apply from: '../../constants.gradle'
+apply plugin: 'com.android.application'
+
+android {
+ compileSdkVersion project.ext.compileSdkVersion
+
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_1_8
+ targetCompatibility JavaVersion.VERSION_1_8
+ }
+
+ defaultConfig {
+ versionName project.ext.releaseVersion
+ versionCode project.ext.releaseVersionCode
+ minSdkVersion project.ext.minSdkVersion
+ targetSdkVersion project.ext.appTargetSdkVersion
+ }
+
+ buildTypes {
+ release {
+ shrinkResources true
+ minifyEnabled true
+ proguardFiles = [
+ "proguard-rules.txt",
+ getDefaultProguardFile('proguard-android.txt')
+ ]
+ }
+ debug {
+ jniDebuggable = true
+ }
+ }
+
+ lintOptions {
+ // The demo app isn't indexed, doesn't have translations, and has a
+ // banner for AndroidTV that's only in xhdpi density.
+ disable 'GoogleAppIndexingWarning','MissingTranslation','IconDensities'
+ }
+
+ flavorDimensions "extensions"
+
+ productFlavors {
+ noExtensions {
+ dimension "extensions"
+ }
+ withExtensions {
+ dimension "extensions"
+ }
+ }
+}
+
+dependencies {
+ implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion
+ implementation 'androidx.appcompat:appcompat:' + androidxAppCompatVersion
+ implementation 'com.google.android.material:material:1.0.0'
+ implementation project(modulePrefix + 'library-core')
+ implementation project(modulePrefix + 'library-dash')
+ implementation project(modulePrefix + 'library-hls')
+ implementation project(modulePrefix + 'library-smoothstreaming')
+ implementation project(modulePrefix + 'library-ui')
+ withExtensionsImplementation project(path: modulePrefix + 'extension-av1')
+ withExtensionsImplementation project(path: modulePrefix + 'extension-ffmpeg')
+ withExtensionsImplementation project(path: modulePrefix + 'extension-flac')
+ withExtensionsImplementation project(path: modulePrefix + 'extension-ima')
+ withExtensionsImplementation project(path: modulePrefix + 'extension-opus')
+ withExtensionsImplementation project(path: modulePrefix + 'extension-vp9')
+ withExtensionsImplementation project(path: modulePrefix + 'extension-rtmp')
+}
+
+apply plugin: 'com.google.android.gms.strict-version-matcher-plugin'
diff --git a/demos/main/proguard-rules.txt b/demos/main/proguard-rules.txt
new file mode 100644
index 0000000000..cd201892ab
--- /dev/null
+++ b/demos/main/proguard-rules.txt
@@ -0,0 +1,7 @@
+# Proguard rules specific to the main demo app.
+
+# Constructor accessed via reflection in PlayerActivity
+-dontnote com.google.android.exoplayer2.ext.ima.ImaAdsLoader
+-keepclassmembers class com.google.android.exoplayer2.ext.ima.ImaAdsLoader {
+ (android.content.Context, android.net.Uri);
+}
diff --git a/demo/src/main/AndroidManifest.xml b/demos/main/src/main/AndroidManifest.xml
similarity index 74%
rename from demo/src/main/AndroidManifest.xml
rename to demos/main/src/main/AndroidManifest.xml
index afcddccac9..0240a377ac 100644
--- a/demo/src/main/AndroidManifest.xml
+++ b/demos/main/src/main/AndroidManifest.xml
@@ -15,15 +15,18 @@
-->
+ xmlns:tools="http://schemas.android.com/tools"
+ package="com.google.android.exoplayer2.demo">
+
+
+
+
-
+
+ android:requestLegacyExternalStorage="true"
+ android:name="com.google.android.exoplayer2.demo.DemoApplication"
+ tools:ignore="UnusedAttribute">
+ android:label="@string/application_name"
+ android:theme="@style/Theme.AppCompat">
@@ -75,6 +81,18 @@
+
+
+
+
+
+
+
+
+
diff --git a/demos/main/src/main/assets/media.exolist.json b/demos/main/src/main/assets/media.exolist.json
new file mode 100644
index 0000000000..06f063b1c1
--- /dev/null
+++ b/demos/main/src/main/assets/media.exolist.json
@@ -0,0 +1,620 @@
+[
+ {
+ "name": "YouTube DASH",
+ "samples": [
+ {
+ "name": "Google Glass (MP4,H264)",
+ "uri": "https://www.youtube.com/api/manifest/dash/id/bf5bb2419360daf1/source/youtube?as=fmp4_audio_clear,fmp4_sd_hd_clear&sparams=ip,ipbits,expire,source,id,as&ip=0.0.0.0&ipbits=0&expire=19000000000&signature=51AF5F39AB0CEC3E5497CD9C900EBFEAECCCB5C7.8506521BFC350652163895D4C26DEE124209AA9E&key=ik0",
+ "extension": "mpd"
+ },
+ {
+ "name": "Google Play (MP4,H264)",
+ "uri": "https://www.youtube.com/api/manifest/dash/id/3aa39fa2cc27967f/source/youtube?as=fmp4_audio_clear,fmp4_sd_hd_clear&sparams=ip,ipbits,expire,source,id,as&ip=0.0.0.0&ipbits=0&expire=19000000000&signature=A2716F75795F5D2AF0E88962FFCD10DB79384F29.84308FF04844498CE6FBCE4731507882B8307798&key=ik0",
+ "extension": "mpd"
+ },
+ {
+ "name": "Google Glass (WebM,VP9)",
+ "uri": "https://www.youtube.com/api/manifest/dash/id/bf5bb2419360daf1/source/youtube?as=fmp4_audio_clear,webm2_sd_hd_clear&sparams=ip,ipbits,expire,source,id,as&ip=0.0.0.0&ipbits=0&expire=19000000000&signature=249B04F79E984D7F86B4D8DB48AE6FAF41C17AB3.7B9F0EC0505E1566E59B8E488E9419F253DDF413&key=ik0",
+ "extension": "mpd"
+ },
+ {
+ "name": "Google Play (WebM,VP9)",
+ "uri": "https://www.youtube.com/api/manifest/dash/id/3aa39fa2cc27967f/source/youtube?as=fmp4_audio_clear,webm2_sd_hd_clear&sparams=ip,ipbits,expire,source,id,as&ip=0.0.0.0&ipbits=0&expire=19000000000&signature=B1C2A74783AC1CC4865EB312D7DD2D48230CC9FD.BD153B9882175F1F94BFE5141A5482313EA38E8D&key=ik0",
+ "extension": "mpd"
+ }
+ ]
+ },
+ {
+ "name": "Widevine DASH Policy Tests (GTS)",
+ "samples": [
+ {
+ "name": "WV: HDCP not specified",
+ "uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd",
+ "drm_scheme": "widevine",
+ "drm_license_url": "https://proxy.uat.widevine.com/proxy?video_id=d286538032258a1c&provider=widevine_test"
+ },
+ {
+ "name": "WV: HDCP not required",
+ "uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd",
+ "drm_scheme": "widevine",
+ "drm_license_url": "https://proxy.uat.widevine.com/proxy?video_id=48fcc369939ac96c&provider=widevine_test"
+ },
+ {
+ "name": "WV: HDCP required",
+ "uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd",
+ "drm_scheme": "widevine",
+ "drm_license_url": "https://proxy.uat.widevine.com/proxy?video_id=e06c39f1151da3df&provider=widevine_test"
+ },
+ {
+ "name": "WV: Secure video path required (MP4,H264)",
+ "uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd",
+ "drm_scheme": "widevine",
+ "drm_license_url": "https://proxy.uat.widevine.com/proxy?video_id=0894c7c8719b28a0&provider=widevine_test"
+ },
+ {
+ "name": "WV: Secure video path required (WebM,VP9)",
+ "uri": "https://storage.googleapis.com/wvmedia/cenc/vp9/tears/tears.mpd",
+ "drm_scheme": "widevine",
+ "drm_license_url": "https://proxy.uat.widevine.com/proxy?video_id=0894c7c8719b28a0&provider=widevine_test"
+ },
+ {
+ "name": "WV: Secure video path required (MP4,H265)",
+ "uri": "https://storage.googleapis.com/wvmedia/cenc/hevc/tears/tears.mpd",
+ "drm_scheme": "widevine",
+ "drm_license_url": "https://proxy.uat.widevine.com/proxy?video_id=0894c7c8719b28a0&provider=widevine_test"
+ },
+ {
+ "name": "WV: HDCP + secure video path required",
+ "uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd",
+ "drm_scheme": "widevine",
+ "drm_license_url": "https://proxy.uat.widevine.com/proxy?video_id=efd045b1eb61888a&provider=widevine_test"
+ },
+ {
+ "name": "WV: 30s license duration (fails at ~30s)",
+ "uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd",
+ "drm_scheme": "widevine",
+ "drm_license_url": "https://proxy.uat.widevine.com/proxy?video_id=f9a34cab7b05881a&provider=widevine_test"
+ }
+ ]
+ },
+ {
+ "name": "Widevine HDCP Capabilities Tests",
+ "samples": [
+ {
+ "name": "WV: HDCP: None (not required)",
+ "uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd",
+ "drm_scheme": "widevine",
+ "drm_license_url": "https://proxy.uat.widevine.com/proxy?video_id=HDCP_None&provider=widevine_test"
+ },
+ {
+ "name": "WV: HDCP: 1.0 required",
+ "uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd",
+ "drm_scheme": "widevine",
+ "drm_license_url": "https://proxy.uat.widevine.com/proxy?video_id=HDCP_V1&provider=widevine_test"
+ },
+ {
+ "name": "WV: HDCP: 2.0 required",
+ "uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd",
+ "drm_scheme": "widevine",
+ "drm_license_url": "https://proxy.uat.widevine.com/proxy?video_id=HDCP_V2&provider=widevine_test"
+ },
+ {
+ "name": "WV: HDCP: 2.1 required",
+ "uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd",
+ "drm_scheme": "widevine",
+ "drm_license_url": "https://proxy.uat.widevine.com/proxy?video_id=HDCP_V2_1&provider=widevine_test"
+ },
+ {
+ "name": "WV: HDCP: 2.2 required",
+ "uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd",
+ "drm_scheme": "widevine",
+ "drm_license_url": "https://proxy.uat.widevine.com/proxy?video_id=HDCP_V2_2&provider=widevine_test"
+ },
+ {
+ "name": "WV: HDCP: No digital output",
+ "uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd",
+ "drm_scheme": "widevine",
+ "drm_license_url": "https://proxy.uat.widevine.com/proxy?video_id=HDCP_NO_DIGTAL_OUTPUT&provider=widevine_test"
+ }
+ ]
+ },
+ {
+ "name": "Widevine DASH: MP4,H264",
+ "samples": [
+ {
+ "name": "WV: Clear SD & HD (MP4,H264)",
+ "uri": "https://storage.googleapis.com/wvmedia/clear/h264/tears/tears.mpd"
+ },
+ {
+ "name": "WV: Clear SD (MP4,H264)",
+ "uri": "https://storage.googleapis.com/wvmedia/clear/h264/tears/tears_sd.mpd"
+ },
+ {
+ "name": "WV: Clear HD (MP4,H264)",
+ "uri": "https://storage.googleapis.com/wvmedia/clear/h264/tears/tears_hd.mpd"
+ },
+ {
+ "name": "WV: Clear UHD (MP4,H264)",
+ "uri": "https://storage.googleapis.com/wvmedia/clear/h264/tears/tears_uhd.mpd"
+ },
+ {
+ "name": "WV: Secure SD & HD (cenc,MP4,H264)",
+ "uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd",
+ "drm_scheme": "widevine",
+ "drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
+ },
+ {
+ "name": "WV: Secure SD (cenc,MP4,H264)",
+ "uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears_sd.mpd",
+ "drm_scheme": "widevine",
+ "drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
+ },
+ {
+ "name": "WV: Secure HD (cenc,MP4,H264)",
+ "uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears_hd.mpd",
+ "drm_scheme": "widevine",
+ "drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
+ },
+ {
+ "name": "WV: Secure UHD (cenc,MP4,H264)",
+ "uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears_uhd.mpd",
+ "drm_scheme": "widevine",
+ "drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
+ },
+ {
+ "name": "WV: Secure SD & HD (cbc1,MP4,H264)",
+ "uri": "https://storage.googleapis.com/wvmedia/cbc1/h264/tears/tears_aes_cbc1.mpd",
+ "drm_scheme": "widevine",
+ "drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
+ },
+ {
+ "name": "WV: Secure SD (cbc1,MP4,H264)",
+ "uri": "https://storage.googleapis.com/wvmedia/cbc1/h264/tears/tears_aes_cbc1_sd.mpd",
+ "drm_scheme": "widevine",
+ "drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
+ },
+ {
+ "name": "WV: Secure HD (cbc1,MP4,H264)",
+ "uri": "https://storage.googleapis.com/wvmedia/cbc1/h264/tears/tears_aes_cbc1_hd.mpd",
+ "drm_scheme": "widevine",
+ "drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
+ },
+ {
+ "name": "WV: Secure UHD (cbc1,MP4,H264)",
+ "uri": "https://storage.googleapis.com/wvmedia/cbc1/h264/tears/tears_aes_cbc1_uhd.mpd",
+ "drm_scheme": "widevine",
+ "drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
+ },
+ {
+ "name": "WV: Secure SD & HD (cbcs,MP4,H264)",
+ "uri": "https://storage.googleapis.com/wvmedia/cbcs/h264/tears/tears_aes_cbcs.mpd",
+ "drm_scheme": "widevine",
+ "drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
+ },
+ {
+ "name": "WV: Secure SD (cbcs,MP4,H264)",
+ "uri": "https://storage.googleapis.com/wvmedia/cbcs/h264/tears/tears_aes_cbcs_sd.mpd",
+ "drm_scheme": "widevine",
+ "drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
+ },
+ {
+ "name": "WV: Secure HD (cbcs,MP4,H264)",
+ "uri": "https://storage.googleapis.com/wvmedia/cbcs/h264/tears/tears_aes_cbcs_hd.mpd",
+ "drm_scheme": "widevine",
+ "drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
+ },
+ {
+ "name": "WV: Secure UHD (cbcs,MP4,H264)",
+ "uri": "https://storage.googleapis.com/wvmedia/cbcs/h264/tears/tears_aes_cbcs_uhd.mpd",
+ "drm_scheme": "widevine",
+ "drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
+ },
+ {
+ "name": "WV: Secure and Clear SD & HD (cenc,MP4,H264)",
+ "uri": "https://storage.googleapis.com/exoplayer-test-media-1/widevine/tears_enc_clear_enc.mpd",
+ "drm_scheme": "widevine",
+ "drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test",
+ "drm_session_for_clear_types": ["audio", "video"]
+ }
+ ]
+ },
+ {
+ "name": "Widevine DASH: WebM,VP9",
+ "samples": [
+ {
+ "name": "WV: Clear SD & HD (WebM,VP9)",
+ "uri": "https://storage.googleapis.com/wvmedia/clear/vp9/tears/tears.mpd"
+ },
+ {
+ "name": "WV: Clear SD (WebM,VP9)",
+ "uri": "https://storage.googleapis.com/wvmedia/clear/vp9/tears/tears_sd.mpd"
+ },
+ {
+ "name": "WV: Clear HD (WebM,VP9)",
+ "uri": "https://storage.googleapis.com/wvmedia/clear/vp9/tears/tears_hd.mpd"
+ },
+ {
+ "name": "WV: Clear UHD (WebM,VP9)",
+ "uri": "https://storage.googleapis.com/wvmedia/clear/vp9/tears/tears_uhd.mpd"
+ },
+ {
+ "name": "WV: Secure Fullsample SD & HD (WebM,VP9)",
+ "uri": "https://storage.googleapis.com/wvmedia/cenc/vp9/tears/tears.mpd",
+ "drm_scheme": "widevine",
+ "drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
+ },
+ {
+ "name": "WV: Secure Fullsample SD (WebM,VP9)",
+ "uri": "https://storage.googleapis.com/wvmedia/cenc/vp9/tears/tears_sd.mpd",
+ "drm_scheme": "widevine",
+ "drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
+ },
+ {
+ "name": "WV: Secure Fullsample HD (WebM,VP9)",
+ "uri": "https://storage.googleapis.com/wvmedia/cenc/vp9/tears/tears_hd.mpd",
+ "drm_scheme": "widevine",
+ "drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
+ },
+ {
+ "name": "WV: Secure Fullsample UHD (WebM,VP9)",
+ "uri": "https://storage.googleapis.com/wvmedia/cenc/vp9/tears/tears_uhd.mpd",
+ "drm_scheme": "widevine",
+ "drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
+ },
+ {
+ "name": "WV: Secure Subsample SD & HD (WebM,VP9)",
+ "uri": "https://storage.googleapis.com/wvmedia/cenc/vp9/subsample/24fps/tears/tears.mpd",
+ "drm_scheme": "widevine",
+ "drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
+ },
+ {
+ "name": "WV: Secure Subsample SD (WebM,VP9)",
+ "uri": "https://storage.googleapis.com/wvmedia/cenc/vp9/subsample/24fps/tears/tears_sd.mpd",
+ "drm_scheme": "widevine",
+ "drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
+ },
+ {
+ "name": "WV: Secure Subsample HD (WebM,VP9)",
+ "uri": "https://storage.googleapis.com/wvmedia/cenc/vp9/subsample/24fps/tears/tears_hd.mpd",
+ "drm_scheme": "widevine",
+ "drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
+ },
+ {
+ "name": "WV: Secure Subsample UHD (WebM,VP9)",
+ "uri": "https://storage.googleapis.com/wvmedia/cenc/vp9/subsample/24fps/tears/tears_uhd.mpd",
+ "drm_scheme": "widevine",
+ "drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
+ }
+ ]
+ },
+ {
+ "name": "Widevine DASH: MP4,H265",
+ "samples": [
+ {
+ "name": "WV: Clear SD & HD (MP4,H265)",
+ "uri": "https://storage.googleapis.com/wvmedia/clear/hevc/tears/tears.mpd"
+ },
+ {
+ "name": "WV: Clear SD (MP4,H265)",
+ "uri": "https://storage.googleapis.com/wvmedia/clear/hevc/tears/tears_sd.mpd"
+ },
+ {
+ "name": "WV: Clear HD (MP4,H265)",
+ "uri": "https://storage.googleapis.com/wvmedia/clear/hevc/tears/tears_hd.mpd"
+ },
+ {
+ "name": "WV: Clear UHD (MP4,H265)",
+ "uri": "https://storage.googleapis.com/wvmedia/clear/hevc/tears/tears_uhd.mpd"
+ },
+ {
+ "name": "WV: Secure SD & HD (MP4,H265)",
+ "uri": "https://storage.googleapis.com/wvmedia/cenc/hevc/tears/tears.mpd",
+ "drm_scheme": "widevine",
+ "drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
+ },
+ {
+ "name": "WV: Secure SD (MP4,H265)",
+ "uri": "https://storage.googleapis.com/wvmedia/cenc/hevc/tears/tears_sd.mpd",
+ "drm_scheme": "widevine",
+ "drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
+ },
+ {
+ "name": "WV: Secure HD (MP4,H265)",
+ "uri": "https://storage.googleapis.com/wvmedia/cenc/hevc/tears/tears_hd.mpd",
+ "drm_scheme": "widevine",
+ "drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
+ },
+ {
+ "name": "WV: Secure UHD (MP4,H265)",
+ "uri": "https://storage.googleapis.com/wvmedia/cenc/hevc/tears/tears_uhd.mpd",
+ "drm_scheme": "widevine",
+ "drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
+ }
+ ]
+ },
+ {
+ "name": "SmoothStreaming",
+ "samples": [
+ {
+ "name": "Super speed",
+ "uri": "https://playready.directtaps.net/smoothstreaming/SSWSS720H264/SuperSpeedway_720.ism/Manifest"
+ },
+ {
+ "name": "Super speed (PlayReady)",
+ "uri": "https://playready.directtaps.net/smoothstreaming/SSWSS720H264PR/SuperSpeedway_720.ism/Manifest",
+ "drm_scheme": "playready"
+ }
+ ]
+ },
+ {
+ "name": "HLS",
+ "samples": [
+ {
+ "name": "Apple 4x3 basic stream",
+ "uri": "https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_4x3/bipbop_4x3_variant.m3u8"
+ },
+ {
+ "name": "Apple 16x9 basic stream",
+ "uri": "https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_16x9/bipbop_16x9_variant.m3u8"
+ },
+ {
+ "name": "Apple master playlist advanced (TS)",
+ "uri": "https://devstreaming-cdn.apple.com/videos/streaming/examples/img_bipbop_adv_example_ts/master.m3u8"
+ },
+ {
+ "name": "Apple master playlist advanced (fMP4)",
+ "uri": "https://devstreaming-cdn.apple.com/videos/streaming/examples/img_bipbop_adv_example_fmp4/master.m3u8"
+ },
+ {
+ "name": "Apple TS media playlist",
+ "uri": "https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_4x3/gear1/prog_index.m3u8"
+ },
+ {
+ "name": "Apple AAC media playlist",
+ "uri": "https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_4x3/gear0/prog_index.m3u8"
+ }
+ ]
+ },
+ {
+ "name": "Misc",
+ "samples": [
+ {
+ "name": "Dizzy (MP4)",
+ "uri": "https://html5demos.com/assets/dizzy.mp4"
+ },
+ {
+ "name": "Apple 10s (AAC)",
+ "uri": "https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_4x3/gear0/fileSequence0.aac"
+ },
+ {
+ "name": "Apple 10s (TS)",
+ "uri": "https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_4x3/gear1/fileSequence0.ts"
+ },
+ {
+ "name": "Android screens (MKV)",
+ "uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv"
+ },
+ {
+ "name": "Screens 360p video (WebM,VP9)",
+ "uri": "https://storage.googleapis.com/exoplayer-test-media-1/gen-3/screens/dash-vod-single-segment/video-vp9-360.webm"
+ },
+ {
+ "name": "Screens 480p video (FMP4,H264)",
+ "uri": "https://storage.googleapis.com/exoplayer-test-media-1/gen-3/screens/dash-vod-single-segment/video-avc-baseline-480.mp4"
+ },
+ {
+ "name": "Screens 1080p video (FMP4,H264)",
+ "uri": "https://storage.googleapis.com/exoplayer-test-media-1/gen-3/screens/dash-vod-single-segment/video-137.mp4"
+ },
+ {
+ "name": "Screens audio (FMP4)",
+ "uri": "https://storage.googleapis.com/exoplayer-test-media-1/gen-3/screens/dash-vod-single-segment/audio-141.mp4"
+ },
+ {
+ "name": "Google Play (MP3)",
+ "uri": "https://storage.googleapis.com/exoplayer-test-media-0/play.mp3"
+ },
+ {
+ "name": "Google Play (Ogg/Vorbis)",
+ "uri": "https://storage.googleapis.com/exoplayer-test-media-1/ogg/play.ogg"
+ },
+ {
+ "name": "Google Play (FLAC)",
+ "uri": "https://storage.googleapis.com/exoplayer-test-media-1/flac/play.flac"
+ },
+ {
+ "name": "Big Buck Bunny video (FLV)",
+ "uri": "https://vod.leasewebcdn.com/bbb.flv?ri=1024&rs=150&start=0"
+ },
+ {
+ "name": "Big Buck Bunny 480p video (MP4,AV1)",
+ "uri": "https://storage.googleapis.com/downloads.webmproject.org/av1/exoplayer/bbb-av1-480p.mp4"
+ }
+ ]
+ },
+ {
+ "name": "Playlists",
+ "samples": [
+ {
+ "name": "Cats -> Dogs",
+ "playlist": [
+ {
+ "uri": "https://html5demos.com/assets/dizzy.mp4"
+ },
+ {
+ "uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv"
+ }
+ ]
+ },
+ {
+ "name": "Audio -> Video -> Audio",
+ "playlist": [
+ {
+ "uri": "https://storage.googleapis.com/exoplayer-test-media-1/gen-3/screens/dash-vod-single-segment/audio-141.mp4"
+ },
+ {
+ "uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv"
+ },
+ {
+ "uri": "https://storage.googleapis.com/exoplayer-test-media-1/gen-3/screens/dash-vod-single-segment/audio-141.mp4"
+ }
+ ]
+ },
+ {
+ "name": "Clear -> Enc -> Clear -> Enc -> Enc",
+ "playlist": [
+ {
+ "uri": "https://html5demos.com/assets/dizzy.mp4"
+ },
+ {
+ "uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears_sd.mpd",
+ "drm_scheme": "widevine",
+ "drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
+ },
+ {
+ "uri": "https://html5demos.com/assets/dizzy.mp4"
+ },
+ {
+ "uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears_sd.mpd",
+ "drm_scheme": "widevine",
+ "drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
+ },
+ {
+ "uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears_sd.mpd",
+ "drm_scheme": "widevine",
+ "drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "name": "IMA sample ad tags",
+ "samples": [
+ {
+ "name": "Single inline linear",
+ "uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv",
+ "ad_tag_uri": "https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/single_ad_samples&ciu_szs=300x250&impl=s&gdfp_req=1&env=vp&output=vast&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ct%3Dlinear&correlator="
+ },
+ {
+ "name": "Single skippable inline",
+ "uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv",
+ "ad_tag_uri": "https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/single_ad_samples&ciu_szs=300x250&impl=s&gdfp_req=1&env=vp&output=vast&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ct%3Dskippablelinear&correlator="
+ },
+ {
+ "name": "Single redirect linear",
+ "uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv",
+ "ad_tag_uri": "https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/single_ad_samples&ciu_szs=300x250&impl=s&gdfp_req=1&env=vp&output=vast&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ct%3Dredirectlinear&correlator="
+ },
+ {
+ "name": "Single redirect error",
+ "uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv",
+ "ad_tag_uri": "https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/single_ad_samples&ciu_szs=300x250&impl=s&gdfp_req=1&env=vp&output=vast&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ct%3Dredirecterror&nofb=1&correlator="
+ },
+ {
+ "name": "Single redirect broken (fallback)",
+ "uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv",
+ "ad_tag_uri": "https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/single_ad_samples&ciu_szs=300x250&impl=s&gdfp_req=1&env=vp&output=vast&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ct%3Dredirecterror&correlator="
+ },
+ {
+ "name": "VMAP pre-roll",
+ "uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv",
+ "ad_tag_uri": "https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/ad_rule_samples&ciu_szs=300x250&ad_rule=1&impl=s&gdfp_req=1&env=vp&output=vmap&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ar%3Dpreonly&cmsid=496&vid=short_onecue&correlator="
+ },
+ {
+ "name": "VMAP pre-roll + bumper",
+ "uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv",
+ "ad_tag_uri": "https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/ad_rule_samples&ciu_szs=300x250&ad_rule=1&impl=s&gdfp_req=1&env=vp&output=vmap&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ar%3Dpreonlybumper&cmsid=496&vid=short_onecue&correlator="
+ },
+ {
+ "name": "VMAP post-roll",
+ "uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv",
+ "ad_tag_uri": "https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/ad_rule_samples&ciu_szs=300x250&ad_rule=1&impl=s&gdfp_req=1&env=vp&output=vmap&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ar%3Dpostonly&cmsid=496&vid=short_onecue&correlator="
+ },
+ {
+ "name": "VMAP post-roll + bumper",
+ "uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv",
+ "ad_tag_uri": "https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/ad_rule_samples&ciu_szs=300x250&ad_rule=1&impl=s&gdfp_req=1&env=vp&output=vmap&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ar%3Dpostonlybumper&cmsid=496&vid=short_onecue&correlator="
+ },
+ {
+ "name": "VMAP pre-, mid- and post-rolls, single ads",
+ "uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv",
+ "ad_tag_uri": "https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/ad_rule_samples&ciu_szs=300x250&ad_rule=1&impl=s&gdfp_req=1&env=vp&output=vmap&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ar%3Dpremidpost&cmsid=496&vid=short_onecue&correlator="
+ },
+ {
+ "name": "VMAP pre-roll single ad, mid-roll standard pod with 3 ads, post-roll single ad",
+ "uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv",
+ "ad_tag_uri": "https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/ad_rule_samples&ciu_szs=300x250&ad_rule=1&impl=s&gdfp_req=1&env=vp&output=vmap&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ar%3Dpremidpostpod&cmsid=496&vid=short_onecue&correlator="
+ },
+ {
+ "name": "VMAP pre-roll single ad, mid-roll optimized pod with 3 ads, post-roll single ad",
+ "uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv",
+ "ad_tag_uri": "https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/ad_rule_samples&ciu_szs=300x250&ad_rule=1&impl=s&gdfp_req=1&env=vp&output=vmap&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ar%3Dpremidpostoptimizedpod&cmsid=496&vid=short_onecue&correlator="
+ },
+ {
+ "name": "VMAP pre-roll single ad, mid-roll standard pod with 3 ads, post-roll single ad (bumpers around all ad breaks)",
+ "uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv",
+ "ad_tag_uri": "https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/ad_rule_samples&ciu_szs=300x250&ad_rule=1&impl=s&gdfp_req=1&env=vp&output=vmap&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ar%3Dpremidpostpodbumper&cmsid=496&vid=short_onecue&correlator="
+ },
+ {
+ "name": "VMAP pre-roll single ad, mid-roll optimized pod with 3 ads, post-roll single ad (bumpers around all ad breaks)",
+ "uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv",
+ "ad_tag_uri": "https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/ad_rule_samples&ciu_szs=300x250&ad_rule=1&impl=s&gdfp_req=1&env=vp&output=vmap&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ar%3Dpremidpostoptimizedpodbumper&cmsid=496&vid=short_onecue&correlator="
+ },
+ {
+ "name": "VMAP pre-roll single ad, mid-roll standard pods with 5 ads every 10 seconds for 1:40, post-roll single ad",
+ "uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv",
+ "ad_tag_uri": "https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/ad_rule_samples&ciu_szs=300x250&ad_rule=1&impl=s&gdfp_req=1&env=vp&output=vmap&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ar%3Dpremidpostlongpod&cmsid=496&vid=short_tencue&correlator="
+ },
+ {
+ "name": "VMAP empty midroll",
+ "uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv",
+ "ad_tag_uri": "https://vastsynthesizer.appspot.com/empty-midroll"
+ },
+ {
+ "name": "VMAP full, empty, full midrolls",
+ "uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv",
+ "ad_tag_uri": "https://vastsynthesizer.appspot.com/empty-midroll-2"
+ }
+ ]
+ },
+ {
+ "name": "360",
+ "samples": [
+ {
+ "name": "Congo (360 top-bottom stereo)",
+ "uri": "https://storage.googleapis.com/exoplayer-test-media-1/360/congo.mp4",
+ "spherical_stereo_mode": "top_bottom"
+ },
+ {
+ "name": "Sphericalv2 (180 top-bottom stereo)",
+ "uri": "https://storage.googleapis.com/exoplayer-test-media-1/360/sphericalv2.mp4",
+ "spherical_stereo_mode": "top_bottom"
+ },
+ {
+ "name": "Iceland (360 top-bottom stereo ts)",
+ "uri": "https://storage.googleapis.com/exoplayer-test-media-1/360/iceland0.ts",
+ "spherical_stereo_mode": "top_bottom"
+ }
+ ]
+ },
+ {
+ "name": "Subtitles",
+ "samples": [
+ {
+ "name": "TTML",
+ "uri": "https://html5demos.com/assets/dizzy.mp4",
+ "subtitle_uri": "https://storage.googleapis.com/exoplayer-test-media-1/ttml/netflix_ttml_sample.xml",
+ "subtitle_mime_type": "application/ttml+xml",
+ "subtitle_language": "en"
+ },
+ {
+ "name": "SSA/ASS position & alignment",
+ "uri": "https://storage.googleapis.com/exoplayer-test-media-1/gen-3/screens/dash-vod-single-segment/video-avc-baseline-480.mp4",
+ "subtitle_uri": "https://storage.googleapis.com/exoplayer-test-media-1/ssa/test-subs-position.ass",
+ "subtitle_mime_type": "text/x-ssa",
+ "subtitle_language": "en"
+ }
+ ]
+ }
+]
diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/DemoApplication.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/DemoApplication.java
new file mode 100644
index 0000000000..d83d7076c5
--- /dev/null
+++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/DemoApplication.java
@@ -0,0 +1,173 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.demo;
+
+import android.app.Application;
+import com.google.android.exoplayer2.DefaultRenderersFactory;
+import com.google.android.exoplayer2.RenderersFactory;
+import com.google.android.exoplayer2.database.DatabaseProvider;
+import com.google.android.exoplayer2.database.ExoDatabaseProvider;
+import com.google.android.exoplayer2.offline.ActionFileUpgradeUtil;
+import com.google.android.exoplayer2.offline.DefaultDownloadIndex;
+import com.google.android.exoplayer2.offline.DefaultDownloaderFactory;
+import com.google.android.exoplayer2.offline.DownloadManager;
+import com.google.android.exoplayer2.offline.DownloaderConstructorHelper;
+import com.google.android.exoplayer2.upstream.DataSource;
+import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
+import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory;
+import com.google.android.exoplayer2.upstream.FileDataSource;
+import com.google.android.exoplayer2.upstream.HttpDataSource;
+import com.google.android.exoplayer2.upstream.cache.Cache;
+import com.google.android.exoplayer2.upstream.cache.CacheDataSource;
+import com.google.android.exoplayer2.upstream.cache.CacheDataSourceFactory;
+import com.google.android.exoplayer2.upstream.cache.NoOpCacheEvictor;
+import com.google.android.exoplayer2.upstream.cache.SimpleCache;
+import com.google.android.exoplayer2.util.Log;
+import com.google.android.exoplayer2.util.Util;
+import java.io.File;
+import java.io.IOException;
+
+/**
+ * Placeholder application to facilitate overriding Application methods for debugging and testing.
+ */
+public class DemoApplication extends Application {
+
+ private static final String TAG = "DemoApplication";
+ private static final String DOWNLOAD_ACTION_FILE = "actions";
+ private static final String DOWNLOAD_TRACKER_ACTION_FILE = "tracked_actions";
+ private static final String DOWNLOAD_CONTENT_DIRECTORY = "downloads";
+
+ protected String userAgent;
+
+ private DatabaseProvider databaseProvider;
+ private File downloadDirectory;
+ private Cache downloadCache;
+ private DownloadManager downloadManager;
+ private DownloadTracker downloadTracker;
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+ userAgent = Util.getUserAgent(this, "ExoPlayerDemo");
+ }
+
+ /** Returns a {@link DataSource.Factory}. */
+ public DataSource.Factory buildDataSourceFactory() {
+ DefaultDataSourceFactory upstreamFactory =
+ new DefaultDataSourceFactory(this, buildHttpDataSourceFactory());
+ return buildReadOnlyCacheDataSource(upstreamFactory, getDownloadCache());
+ }
+
+ /** Returns a {@link HttpDataSource.Factory}. */
+ public HttpDataSource.Factory buildHttpDataSourceFactory() {
+ return new DefaultHttpDataSourceFactory(userAgent);
+ }
+
+ /** Returns whether extension renderers should be used. */
+ public boolean useExtensionRenderers() {
+ return "withExtensions".equals(BuildConfig.FLAVOR);
+ }
+
+ public RenderersFactory buildRenderersFactory(boolean preferExtensionRenderer) {
+ @DefaultRenderersFactory.ExtensionRendererMode
+ int extensionRendererMode =
+ useExtensionRenderers()
+ ? (preferExtensionRenderer
+ ? DefaultRenderersFactory.EXTENSION_RENDERER_MODE_PREFER
+ : DefaultRenderersFactory.EXTENSION_RENDERER_MODE_ON)
+ : DefaultRenderersFactory.EXTENSION_RENDERER_MODE_OFF;
+ return new DefaultRenderersFactory(/* context= */ this)
+ .setExtensionRendererMode(extensionRendererMode);
+ }
+
+ public DownloadManager getDownloadManager() {
+ initDownloadManager();
+ return downloadManager;
+ }
+
+ public DownloadTracker getDownloadTracker() {
+ initDownloadManager();
+ return downloadTracker;
+ }
+
+ protected synchronized Cache getDownloadCache() {
+ if (downloadCache == null) {
+ File downloadContentDirectory = new File(getDownloadDirectory(), DOWNLOAD_CONTENT_DIRECTORY);
+ downloadCache =
+ new SimpleCache(downloadContentDirectory, new NoOpCacheEvictor(), getDatabaseProvider());
+ }
+ return downloadCache;
+ }
+
+ private synchronized void initDownloadManager() {
+ if (downloadManager == null) {
+ DefaultDownloadIndex downloadIndex = new DefaultDownloadIndex(getDatabaseProvider());
+ upgradeActionFile(
+ DOWNLOAD_ACTION_FILE, downloadIndex, /* addNewDownloadsAsCompleted= */ false);
+ upgradeActionFile(
+ DOWNLOAD_TRACKER_ACTION_FILE, downloadIndex, /* addNewDownloadsAsCompleted= */ true);
+ DownloaderConstructorHelper downloaderConstructorHelper =
+ new DownloaderConstructorHelper(getDownloadCache(), buildHttpDataSourceFactory());
+ downloadManager =
+ new DownloadManager(
+ this, downloadIndex, new DefaultDownloaderFactory(downloaderConstructorHelper));
+ downloadTracker =
+ new DownloadTracker(/* context= */ this, buildDataSourceFactory(), downloadManager);
+ }
+ }
+
+ private void upgradeActionFile(
+ String fileName, DefaultDownloadIndex downloadIndex, boolean addNewDownloadsAsCompleted) {
+ try {
+ ActionFileUpgradeUtil.upgradeAndDelete(
+ new File(getDownloadDirectory(), fileName),
+ /* downloadIdProvider= */ null,
+ downloadIndex,
+ /* deleteOnFailure= */ true,
+ addNewDownloadsAsCompleted);
+ } catch (IOException e) {
+ Log.e(TAG, "Failed to upgrade action file: " + fileName, e);
+ }
+ }
+
+ private DatabaseProvider getDatabaseProvider() {
+ if (databaseProvider == null) {
+ databaseProvider = new ExoDatabaseProvider(this);
+ }
+ return databaseProvider;
+ }
+
+ private File getDownloadDirectory() {
+ if (downloadDirectory == null) {
+ downloadDirectory = getExternalFilesDir(null);
+ if (downloadDirectory == null) {
+ downloadDirectory = getFilesDir();
+ }
+ }
+ return downloadDirectory;
+ }
+
+ protected static CacheDataSourceFactory buildReadOnlyCacheDataSource(
+ DataSource.Factory upstreamFactory, Cache cache) {
+ return new CacheDataSourceFactory(
+ cache,
+ upstreamFactory,
+ new FileDataSource.Factory(),
+ /* cacheWriteDataSinkFactory= */ null,
+ CacheDataSource.FLAG_IGNORE_CACHE_ON_ERROR,
+ /* eventListener= */ null);
+ }
+}
diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/DemoDownloadService.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/DemoDownloadService.java
new file mode 100644
index 0000000000..c3909dfe46
--- /dev/null
+++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/DemoDownloadService.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.demo;
+
+import android.app.Notification;
+import com.google.android.exoplayer2.offline.Download;
+import com.google.android.exoplayer2.offline.DownloadManager;
+import com.google.android.exoplayer2.offline.DownloadService;
+import com.google.android.exoplayer2.scheduler.PlatformScheduler;
+import com.google.android.exoplayer2.ui.DownloadNotificationHelper;
+import com.google.android.exoplayer2.util.NotificationUtil;
+import com.google.android.exoplayer2.util.Util;
+import java.util.List;
+
+/** A service for downloading media. */
+public class DemoDownloadService extends DownloadService {
+
+ private static final String CHANNEL_ID = "download_channel";
+ private static final int JOB_ID = 1;
+ private static final int FOREGROUND_NOTIFICATION_ID = 1;
+
+ private static int nextNotificationId = FOREGROUND_NOTIFICATION_ID + 1;
+
+ private DownloadNotificationHelper notificationHelper;
+
+ public DemoDownloadService() {
+ super(
+ FOREGROUND_NOTIFICATION_ID,
+ DEFAULT_FOREGROUND_NOTIFICATION_UPDATE_INTERVAL,
+ CHANNEL_ID,
+ R.string.exo_download_notification_channel_name,
+ /* channelDescriptionResourceId= */ 0);
+ nextNotificationId = FOREGROUND_NOTIFICATION_ID + 1;
+ }
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+ notificationHelper = new DownloadNotificationHelper(this, CHANNEL_ID);
+ }
+
+ @Override
+ protected DownloadManager getDownloadManager() {
+ return ((DemoApplication) getApplication()).getDownloadManager();
+ }
+
+ @Override
+ protected PlatformScheduler getScheduler() {
+ return Util.SDK_INT >= 21 ? new PlatformScheduler(this, JOB_ID) : null;
+ }
+
+ @Override
+ protected Notification getForegroundNotification(List downloads) {
+ return notificationHelper.buildProgressNotification(
+ R.drawable.ic_download, /* contentIntent= */ null, /* message= */ null, downloads);
+ }
+
+ @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;
+ }
+ NotificationUtil.setNotification(this, nextNotificationId++, notification);
+ }
+}
diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/DownloadTracker.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/DownloadTracker.java
new file mode 100644
index 0000000000..143eda93df
--- /dev/null
+++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/DownloadTracker.java
@@ -0,0 +1,274 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.demo;
+
+import android.content.Context;
+import android.content.DialogInterface;
+import android.net.Uri;
+import android.widget.Toast;
+import androidx.annotation.Nullable;
+import androidx.fragment.app.FragmentManager;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.RenderersFactory;
+import com.google.android.exoplayer2.offline.Download;
+import com.google.android.exoplayer2.offline.DownloadCursor;
+import com.google.android.exoplayer2.offline.DownloadHelper;
+import com.google.android.exoplayer2.offline.DownloadIndex;
+import com.google.android.exoplayer2.offline.DownloadManager;
+import com.google.android.exoplayer2.offline.DownloadRequest;
+import com.google.android.exoplayer2.offline.DownloadService;
+import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
+import com.google.android.exoplayer2.trackselection.MappingTrackSelector.MappedTrackInfo;
+import com.google.android.exoplayer2.upstream.DataSource;
+import com.google.android.exoplayer2.util.Log;
+import com.google.android.exoplayer2.util.Util;
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.concurrent.CopyOnWriteArraySet;
+
+/** Tracks media that has been downloaded. */
+public class DownloadTracker {
+
+ /** Listens for changes in the tracked downloads. */
+ public interface Listener {
+
+ /** Called when the tracked downloads changed. */
+ void onDownloadsChanged();
+ }
+
+ private static final String TAG = "DownloadTracker";
+
+ private final Context context;
+ private final DataSource.Factory dataSourceFactory;
+ private final CopyOnWriteArraySet listeners;
+ private final HashMap downloads;
+ private final DownloadIndex downloadIndex;
+ private final DefaultTrackSelector.Parameters trackSelectorParameters;
+
+ @Nullable private StartDownloadDialogHelper startDownloadDialogHelper;
+
+ public DownloadTracker(
+ Context context, DataSource.Factory dataSourceFactory, DownloadManager downloadManager) {
+ this.context = context.getApplicationContext();
+ this.dataSourceFactory = dataSourceFactory;
+ listeners = new CopyOnWriteArraySet<>();
+ downloads = new HashMap<>();
+ downloadIndex = downloadManager.getDownloadIndex();
+ trackSelectorParameters = DownloadHelper.getDefaultTrackSelectorParameters(context);
+ downloadManager.addListener(new DownloadManagerListener());
+ loadDownloads();
+ }
+
+ public void addListener(Listener listener) {
+ listeners.add(listener);
+ }
+
+ public void removeListener(Listener listener) {
+ listeners.remove(listener);
+ }
+
+ public boolean isDownloaded(Uri uri) {
+ Download download = downloads.get(uri);
+ return download != null && download.state != Download.STATE_FAILED;
+ }
+
+ public DownloadRequest getDownloadRequest(Uri uri) {
+ Download download = downloads.get(uri);
+ return download != null && download.state != Download.STATE_FAILED ? download.request : null;
+ }
+
+ public void toggleDownload(
+ FragmentManager fragmentManager,
+ String name,
+ Uri uri,
+ String extension,
+ RenderersFactory renderersFactory) {
+ Download download = downloads.get(uri);
+ if (download != null) {
+ DownloadService.sendRemoveDownload(
+ context, DemoDownloadService.class, download.request.id, /* foreground= */ false);
+ } else {
+ if (startDownloadDialogHelper != null) {
+ startDownloadDialogHelper.release();
+ }
+ startDownloadDialogHelper =
+ new StartDownloadDialogHelper(
+ fragmentManager, getDownloadHelper(uri, extension, renderersFactory), name);
+ }
+ }
+
+ private void loadDownloads() {
+ try (DownloadCursor loadedDownloads = downloadIndex.getDownloads()) {
+ while (loadedDownloads.moveToNext()) {
+ Download download = loadedDownloads.getDownload();
+ downloads.put(download.request.uri, download);
+ }
+ } catch (IOException e) {
+ Log.w(TAG, "Failed to query downloads", e);
+ }
+ }
+
+ private DownloadHelper getDownloadHelper(
+ Uri uri, String extension, RenderersFactory renderersFactory) {
+ int type = Util.inferContentType(uri, extension);
+ switch (type) {
+ case C.TYPE_DASH:
+ return DownloadHelper.forDash(context, uri, dataSourceFactory, renderersFactory);
+ case C.TYPE_SS:
+ return DownloadHelper.forSmoothStreaming(context, uri, dataSourceFactory, renderersFactory);
+ case C.TYPE_HLS:
+ return DownloadHelper.forHls(context, uri, dataSourceFactory, renderersFactory);
+ case C.TYPE_OTHER:
+ return DownloadHelper.forProgressive(context, uri);
+ default:
+ throw new IllegalStateException("Unsupported type: " + type);
+ }
+ }
+
+ private class DownloadManagerListener implements DownloadManager.Listener {
+
+ @Override
+ public void onDownloadChanged(DownloadManager downloadManager, Download download) {
+ downloads.put(download.request.uri, download);
+ for (Listener listener : listeners) {
+ listener.onDownloadsChanged();
+ }
+ }
+
+ @Override
+ public void onDownloadRemoved(DownloadManager downloadManager, Download download) {
+ downloads.remove(download.request.uri);
+ for (Listener listener : listeners) {
+ listener.onDownloadsChanged();
+ }
+ }
+ }
+
+ private final class StartDownloadDialogHelper
+ implements DownloadHelper.Callback,
+ DialogInterface.OnClickListener,
+ DialogInterface.OnDismissListener {
+
+ private final FragmentManager fragmentManager;
+ private final DownloadHelper downloadHelper;
+ private final String name;
+
+ private TrackSelectionDialog trackSelectionDialog;
+ private MappedTrackInfo mappedTrackInfo;
+
+ public StartDownloadDialogHelper(
+ FragmentManager fragmentManager, DownloadHelper downloadHelper, String name) {
+ this.fragmentManager = fragmentManager;
+ this.downloadHelper = downloadHelper;
+ this.name = name;
+ downloadHelper.prepare(this);
+ }
+
+ public void release() {
+ downloadHelper.release();
+ if (trackSelectionDialog != null) {
+ trackSelectionDialog.dismiss();
+ }
+ }
+
+ // DownloadHelper.Callback implementation.
+
+ @Override
+ public void onPrepared(DownloadHelper helper) {
+ if (helper.getPeriodCount() == 0) {
+ Log.d(TAG, "No periods found. Downloading entire stream.");
+ startDownload();
+ downloadHelper.release();
+ return;
+ }
+ mappedTrackInfo = downloadHelper.getMappedTrackInfo(/* periodIndex= */ 0);
+ if (!TrackSelectionDialog.willHaveContent(mappedTrackInfo)) {
+ Log.d(TAG, "No dialog content. Downloading entire stream.");
+ startDownload();
+ downloadHelper.release();
+ return;
+ }
+ trackSelectionDialog =
+ TrackSelectionDialog.createForMappedTrackInfoAndParameters(
+ /* titleId= */ R.string.exo_download_description,
+ mappedTrackInfo,
+ trackSelectorParameters,
+ /* allowAdaptiveSelections =*/ false,
+ /* allowMultipleOverrides= */ true,
+ /* onClickListener= */ this,
+ /* onDismissListener= */ this);
+ trackSelectionDialog.show(fragmentManager, /* tag= */ null);
+ }
+
+ @Override
+ public void onPrepareError(DownloadHelper helper, IOException e) {
+ Toast.makeText(context, R.string.download_start_error, Toast.LENGTH_LONG).show();
+ Log.e(
+ TAG,
+ e instanceof DownloadHelper.LiveContentUnsupportedException
+ ? "Downloading live content unsupported"
+ : "Failed to start download",
+ e);
+ }
+
+ // DialogInterface.OnClickListener implementation.
+
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ for (int periodIndex = 0; periodIndex < downloadHelper.getPeriodCount(); periodIndex++) {
+ downloadHelper.clearTrackSelections(periodIndex);
+ for (int i = 0; i < mappedTrackInfo.getRendererCount(); i++) {
+ if (!trackSelectionDialog.getIsDisabled(/* rendererIndex= */ i)) {
+ downloadHelper.addTrackSelectionForSingleRenderer(
+ periodIndex,
+ /* rendererIndex= */ i,
+ trackSelectorParameters,
+ trackSelectionDialog.getOverrides(/* rendererIndex= */ i));
+ }
+ }
+ }
+ DownloadRequest downloadRequest = buildDownloadRequest();
+ if (downloadRequest.streamKeys.isEmpty()) {
+ // All tracks were deselected in the dialog. Don't start the download.
+ return;
+ }
+ startDownload(downloadRequest);
+ }
+
+ // DialogInterface.OnDismissListener implementation.
+
+ @Override
+ public void onDismiss(DialogInterface dialogInterface) {
+ trackSelectionDialog = null;
+ downloadHelper.release();
+ }
+
+ // Internal methods.
+
+ private void startDownload() {
+ startDownload(buildDownloadRequest());
+ }
+
+ private void startDownload(DownloadRequest downloadRequest) {
+ DownloadService.sendAddDownload(
+ context, DemoDownloadService.class, downloadRequest, /* foreground= */ false);
+ }
+
+ private DownloadRequest buildDownloadRequest() {
+ return downloadHelper.getDownloadRequest(Util.getUtf8Bytes(name));
+ }
+ }
+}
diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java
new file mode 100644
index 0000000000..b759c97da5
--- /dev/null
+++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java
@@ -0,0 +1,754 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.demo;
+
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.media.MediaDrm;
+import android.net.Uri;
+import android.os.Bundle;
+import android.util.Pair;
+import android.view.KeyEvent;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.widget.Button;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+import android.widget.Toast;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.appcompat.app.AppCompatActivity;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.C.ContentType;
+import com.google.android.exoplayer2.ExoPlaybackException;
+import com.google.android.exoplayer2.Format;
+import com.google.android.exoplayer2.PlaybackPreparer;
+import com.google.android.exoplayer2.Player;
+import com.google.android.exoplayer2.RenderersFactory;
+import com.google.android.exoplayer2.SimpleExoPlayer;
+import com.google.android.exoplayer2.demo.Sample.UriSample;
+import com.google.android.exoplayer2.drm.DefaultDrmSessionManager;
+import com.google.android.exoplayer2.drm.DrmSessionManager;
+import com.google.android.exoplayer2.drm.ExoMediaCrypto;
+import com.google.android.exoplayer2.drm.FrameworkMediaDrm;
+import com.google.android.exoplayer2.drm.HttpMediaDrmCallback;
+import com.google.android.exoplayer2.drm.MediaDrmCallback;
+import com.google.android.exoplayer2.mediacodec.MediaCodecRenderer.DecoderInitializationException;
+import com.google.android.exoplayer2.mediacodec.MediaCodecUtil.DecoderQueryException;
+import com.google.android.exoplayer2.offline.DownloadHelper;
+import com.google.android.exoplayer2.offline.DownloadRequest;
+import com.google.android.exoplayer2.source.BehindLiveWindowException;
+import com.google.android.exoplayer2.source.MediaSource;
+import com.google.android.exoplayer2.source.MediaSourceFactory;
+import com.google.android.exoplayer2.source.MergingMediaSource;
+import com.google.android.exoplayer2.source.ProgressiveMediaSource;
+import com.google.android.exoplayer2.source.SingleSampleMediaSource;
+import com.google.android.exoplayer2.source.TrackGroupArray;
+import com.google.android.exoplayer2.source.ads.AdsLoader;
+import com.google.android.exoplayer2.source.ads.AdsMediaSource;
+import com.google.android.exoplayer2.source.dash.DashMediaSource;
+import com.google.android.exoplayer2.source.hls.HlsMediaSource;
+import com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource;
+import com.google.android.exoplayer2.trackselection.AdaptiveTrackSelection;
+import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
+import com.google.android.exoplayer2.trackselection.MappingTrackSelector.MappedTrackInfo;
+import com.google.android.exoplayer2.trackselection.RandomTrackSelection;
+import com.google.android.exoplayer2.trackselection.TrackSelection;
+import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
+import com.google.android.exoplayer2.ui.DebugTextViewHelper;
+import com.google.android.exoplayer2.ui.PlayerControlView;
+import com.google.android.exoplayer2.ui.PlayerView;
+import com.google.android.exoplayer2.ui.spherical.SphericalGLSurfaceView;
+import com.google.android.exoplayer2.upstream.DataSource;
+import com.google.android.exoplayer2.upstream.HttpDataSource;
+import com.google.android.exoplayer2.util.ErrorMessageProvider;
+import com.google.android.exoplayer2.util.EventLogger;
+import com.google.android.exoplayer2.util.Util;
+import java.lang.reflect.Constructor;
+import java.net.CookieHandler;
+import java.net.CookieManager;
+import java.net.CookiePolicy;
+import java.util.ArrayList;
+import java.util.List;
+
+/** An activity that plays media using {@link SimpleExoPlayer}. */
+public class PlayerActivity extends AppCompatActivity
+ implements OnClickListener, PlaybackPreparer, PlayerControlView.VisibilityListener {
+
+ // Activity extras.
+
+ public static final String SPHERICAL_STEREO_MODE_EXTRA = "spherical_stereo_mode";
+ public static final String SPHERICAL_STEREO_MODE_MONO = "mono";
+ public static final String SPHERICAL_STEREO_MODE_TOP_BOTTOM = "top_bottom";
+ public static final String SPHERICAL_STEREO_MODE_LEFT_RIGHT = "left_right";
+
+ // Actions.
+
+ public static final String ACTION_VIEW = "com.google.android.exoplayer.demo.action.VIEW";
+ public static final String ACTION_VIEW_LIST =
+ "com.google.android.exoplayer.demo.action.VIEW_LIST";
+
+ // Player configuration extras.
+
+ public static final String ABR_ALGORITHM_EXTRA = "abr_algorithm";
+ public static final String ABR_ALGORITHM_DEFAULT = "default";
+ public static final String ABR_ALGORITHM_RANDOM = "random";
+
+ // Media item configuration extras.
+
+ public static final String URI_EXTRA = "uri";
+ public static final String EXTENSION_EXTRA = "extension";
+ public static final String IS_LIVE_EXTRA = "is_live";
+
+ public static final String DRM_SCHEME_EXTRA = "drm_scheme";
+ public static final String DRM_LICENSE_URL_EXTRA = "drm_license_url";
+ public static final String DRM_KEY_REQUEST_PROPERTIES_EXTRA = "drm_key_request_properties";
+ public static final String DRM_SESSION_FOR_CLEAR_TYPES_EXTRA = "drm_session_for_clear_types";
+ public static final String DRM_MULTI_SESSION_EXTRA = "drm_multi_session";
+ public static final String PREFER_EXTENSION_DECODERS_EXTRA = "prefer_extension_decoders";
+ public static final String TUNNELING_EXTRA = "tunneling";
+ public static final String AD_TAG_URI_EXTRA = "ad_tag_uri";
+ public static final String SUBTITLE_URI_EXTRA = "subtitle_uri";
+ public static final String SUBTITLE_MIME_TYPE_EXTRA = "subtitle_mime_type";
+ public static final String SUBTITLE_LANGUAGE_EXTRA = "subtitle_language";
+ // For backwards compatibility only.
+ public static final String DRM_SCHEME_UUID_EXTRA = "drm_scheme_uuid";
+
+ // Saved instance state keys.
+
+ private static final String KEY_TRACK_SELECTOR_PARAMETERS = "track_selector_parameters";
+ private static final String KEY_WINDOW = "window";
+ private static final String KEY_POSITION = "position";
+ private static final String KEY_AUTO_PLAY = "auto_play";
+
+ private static final CookieManager DEFAULT_COOKIE_MANAGER;
+ static {
+ DEFAULT_COOKIE_MANAGER = new CookieManager();
+ DEFAULT_COOKIE_MANAGER.setCookiePolicy(CookiePolicy.ACCEPT_ORIGINAL_SERVER);
+ }
+
+ private PlayerView playerView;
+ private LinearLayout debugRootView;
+ private Button selectTracksButton;
+ private TextView debugTextView;
+ private boolean isShowingTrackSelectionDialog;
+
+ private DataSource.Factory dataSourceFactory;
+ private SimpleExoPlayer player;
+ private List mediaSources;
+ private DefaultTrackSelector trackSelector;
+ private DefaultTrackSelector.Parameters trackSelectorParameters;
+ private DebugTextViewHelper debugViewHelper;
+ private TrackGroupArray lastSeenTrackGroupArray;
+
+ private boolean startAutoPlay;
+ private int startWindow;
+ private long startPosition;
+
+ // Fields used only for ad playback. The ads loader is loaded via reflection.
+
+ private AdsLoader adsLoader;
+ private Uri loadedAdTagUri;
+
+ // Activity lifecycle
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ Intent intent = getIntent();
+ String sphericalStereoMode = intent.getStringExtra(SPHERICAL_STEREO_MODE_EXTRA);
+ if (sphericalStereoMode != null) {
+ setTheme(R.style.PlayerTheme_Spherical);
+ }
+ super.onCreate(savedInstanceState);
+ dataSourceFactory = buildDataSourceFactory();
+ if (CookieHandler.getDefault() != DEFAULT_COOKIE_MANAGER) {
+ CookieHandler.setDefault(DEFAULT_COOKIE_MANAGER);
+ }
+
+ setContentView(R.layout.player_activity);
+ debugRootView = findViewById(R.id.controls_root);
+ debugTextView = findViewById(R.id.debug_text_view);
+ selectTracksButton = findViewById(R.id.select_tracks_button);
+ selectTracksButton.setOnClickListener(this);
+
+ playerView = findViewById(R.id.player_view);
+ playerView.setControllerVisibilityListener(this);
+ playerView.setErrorMessageProvider(new PlayerErrorMessageProvider());
+ playerView.requestFocus();
+ if (sphericalStereoMode != null) {
+ int stereoMode;
+ if (SPHERICAL_STEREO_MODE_MONO.equals(sphericalStereoMode)) {
+ stereoMode = C.STEREO_MODE_MONO;
+ } else if (SPHERICAL_STEREO_MODE_TOP_BOTTOM.equals(sphericalStereoMode)) {
+ stereoMode = C.STEREO_MODE_TOP_BOTTOM;
+ } else if (SPHERICAL_STEREO_MODE_LEFT_RIGHT.equals(sphericalStereoMode)) {
+ stereoMode = C.STEREO_MODE_LEFT_RIGHT;
+ } else {
+ showToast(R.string.error_unrecognized_stereo_mode);
+ finish();
+ return;
+ }
+ ((SphericalGLSurfaceView) playerView.getVideoSurfaceView()).setDefaultStereoMode(stereoMode);
+ }
+
+ if (savedInstanceState != null) {
+ trackSelectorParameters = savedInstanceState.getParcelable(KEY_TRACK_SELECTOR_PARAMETERS);
+ startAutoPlay = savedInstanceState.getBoolean(KEY_AUTO_PLAY);
+ startWindow = savedInstanceState.getInt(KEY_WINDOW);
+ startPosition = savedInstanceState.getLong(KEY_POSITION);
+ } else {
+ DefaultTrackSelector.ParametersBuilder builder =
+ new DefaultTrackSelector.ParametersBuilder(/* context= */ this);
+ boolean tunneling = intent.getBooleanExtra(TUNNELING_EXTRA, false);
+ if (Util.SDK_INT >= 21 && tunneling) {
+ builder.setTunnelingAudioSessionId(C.generateAudioSessionIdV21(/* context= */ this));
+ }
+ trackSelectorParameters = builder.build();
+ clearStartPosition();
+ }
+ }
+
+ @Override
+ public void onNewIntent(Intent intent) {
+ super.onNewIntent(intent);
+ releasePlayer();
+ releaseAdsLoader();
+ clearStartPosition();
+ setIntent(intent);
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+ if (Util.SDK_INT > 23) {
+ initializePlayer();
+ if (playerView != null) {
+ playerView.onResume();
+ }
+ }
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ if (Util.SDK_INT <= 23 || player == null) {
+ initializePlayer();
+ if (playerView != null) {
+ playerView.onResume();
+ }
+ }
+ }
+
+ @Override
+ public void onPause() {
+ super.onPause();
+ if (Util.SDK_INT <= 23) {
+ if (playerView != null) {
+ playerView.onPause();
+ }
+ releasePlayer();
+ }
+ }
+
+ @Override
+ public void onStop() {
+ super.onStop();
+ if (Util.SDK_INT > 23) {
+ if (playerView != null) {
+ playerView.onPause();
+ }
+ releasePlayer();
+ }
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ releaseAdsLoader();
+ }
+
+ @Override
+ public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions,
+ @NonNull int[] grantResults) {
+ if (grantResults.length == 0) {
+ // Empty results are triggered if a permission is requested while another request was already
+ // pending and can be safely ignored in this case.
+ return;
+ }
+ if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
+ initializePlayer();
+ } else {
+ showToast(R.string.storage_permission_denied);
+ finish();
+ }
+ }
+
+ @Override
+ public void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+ updateTrackSelectorParameters();
+ updateStartPosition();
+ outState.putParcelable(KEY_TRACK_SELECTOR_PARAMETERS, trackSelectorParameters);
+ outState.putBoolean(KEY_AUTO_PLAY, startAutoPlay);
+ outState.putInt(KEY_WINDOW, startWindow);
+ outState.putLong(KEY_POSITION, startPosition);
+ }
+
+ // Activity input
+
+ @Override
+ public boolean dispatchKeyEvent(KeyEvent event) {
+ // See whether the player view wants to handle media or DPAD keys events.
+ return playerView.dispatchKeyEvent(event) || super.dispatchKeyEvent(event);
+ }
+
+ // OnClickListener methods
+
+ @Override
+ public void onClick(View view) {
+ if (view == selectTracksButton
+ && !isShowingTrackSelectionDialog
+ && TrackSelectionDialog.willHaveContent(trackSelector)) {
+ isShowingTrackSelectionDialog = true;
+ TrackSelectionDialog trackSelectionDialog =
+ TrackSelectionDialog.createForTrackSelector(
+ trackSelector,
+ /* onDismissListener= */ dismissedDialog -> isShowingTrackSelectionDialog = false);
+ trackSelectionDialog.show(getSupportFragmentManager(), /* tag= */ null);
+ }
+ }
+
+ // PlaybackControlView.PlaybackPreparer implementation
+
+ @Override
+ public void preparePlayback() {
+ player.retry();
+ }
+
+ // PlaybackControlView.VisibilityListener implementation
+
+ @Override
+ public void onVisibilityChange(int visibility) {
+ debugRootView.setVisibility(visibility);
+ }
+
+ // Internal methods
+
+ private void initializePlayer() {
+ if (player == null) {
+ Intent intent = getIntent();
+ mediaSources = createTopLevelMediaSources(intent);
+ if (mediaSources.isEmpty()) {
+ return;
+ }
+ TrackSelection.Factory trackSelectionFactory;
+ String abrAlgorithm = intent.getStringExtra(ABR_ALGORITHM_EXTRA);
+ if (abrAlgorithm == null || ABR_ALGORITHM_DEFAULT.equals(abrAlgorithm)) {
+ trackSelectionFactory = new AdaptiveTrackSelection.Factory();
+ } else if (ABR_ALGORITHM_RANDOM.equals(abrAlgorithm)) {
+ trackSelectionFactory = new RandomTrackSelection.Factory();
+ } else {
+ showToast(R.string.error_unrecognized_abr_algorithm);
+ finish();
+ return;
+ }
+
+ boolean preferExtensionDecoders =
+ intent.getBooleanExtra(PREFER_EXTENSION_DECODERS_EXTRA, false);
+ RenderersFactory renderersFactory =
+ ((DemoApplication) getApplication()).buildRenderersFactory(preferExtensionDecoders);
+
+ trackSelector = new DefaultTrackSelector(/* context= */ this, trackSelectionFactory);
+ trackSelector.setParameters(trackSelectorParameters);
+ lastSeenTrackGroupArray = null;
+
+ player =
+ new SimpleExoPlayer.Builder(/* context= */ this, renderersFactory)
+ .setTrackSelector(trackSelector)
+ .build();
+ player.addListener(new PlayerEventListener());
+ player.setPlayWhenReady(startAutoPlay);
+ player.addAnalyticsListener(new EventLogger(trackSelector));
+ playerView.setPlayer(player);
+ playerView.setPlaybackPreparer(this);
+ debugViewHelper = new DebugTextViewHelper(player, debugTextView);
+ debugViewHelper.start();
+ if (adsLoader != null) {
+ adsLoader.setPlayer(player);
+ }
+ }
+ boolean haveStartPosition = startWindow != C.INDEX_UNSET;
+ if (haveStartPosition) {
+ player.seekTo(startWindow, startPosition);
+ }
+ player.setMediaSources(mediaSources, /* resetPosition= */ !haveStartPosition);
+ player.prepare();
+ updateButtonVisibility();
+ }
+
+ private List createTopLevelMediaSources(Intent intent) {
+ String action = intent.getAction();
+ boolean actionIsListView = ACTION_VIEW_LIST.equals(action);
+ if (!actionIsListView && !ACTION_VIEW.equals(action)) {
+ showToast(getString(R.string.unexpected_intent_action, action));
+ finish();
+ return null;
+ }
+
+ Sample intentAsSample = Sample.createFromIntent(intent);
+ UriSample[] samples =
+ intentAsSample instanceof Sample.PlaylistSample
+ ? ((Sample.PlaylistSample) intentAsSample).children
+ : new UriSample[] {(UriSample) intentAsSample};
+
+ boolean seenAdsTagUri = false;
+ for (UriSample sample : samples) {
+ seenAdsTagUri |= sample.adTagUri != null;
+ if (!Util.checkCleartextTrafficPermitted(sample.uri)) {
+ showToast(R.string.error_cleartext_not_permitted);
+ return null;
+ }
+ if (Util.maybeRequestReadExternalStoragePermission(/* activity= */ this, sample.uri)) {
+ // The player will be reinitialized if the permission is granted.
+ return null;
+ }
+ }
+
+ List mediaSources = new ArrayList<>();
+ for (UriSample sample : samples) {
+ MediaSource mediaSource = createLeafMediaSource(sample);
+ Sample.SubtitleInfo subtitleInfo = sample.subtitleInfo;
+ if (subtitleInfo != null) {
+ Format subtitleFormat =
+ Format.createTextSampleFormat(
+ /* id= */ null,
+ subtitleInfo.mimeType,
+ C.SELECTION_FLAG_DEFAULT,
+ subtitleInfo.language);
+ MediaSource subtitleMediaSource =
+ new SingleSampleMediaSource.Factory(dataSourceFactory)
+ .createMediaSource(subtitleInfo.uri, subtitleFormat, C.TIME_UNSET);
+ mediaSource = new MergingMediaSource(mediaSource, subtitleMediaSource);
+ }
+ mediaSources.add(mediaSource);
+ }
+ if (seenAdsTagUri && mediaSources.size() == 1) {
+ Uri adTagUri = samples[0].adTagUri;
+ if (!adTagUri.equals(loadedAdTagUri)) {
+ releaseAdsLoader();
+ loadedAdTagUri = adTagUri;
+ }
+ MediaSource adsMediaSource = createAdsMediaSource(mediaSources.get(0), adTagUri);
+ if (adsMediaSource != null) {
+ mediaSources.set(0, adsMediaSource);
+ } else {
+ showToast(R.string.ima_not_loaded);
+ }
+ } else if (seenAdsTagUri && mediaSources.size() > 1) {
+ showToast(R.string.unsupported_ads_in_concatenation);
+ releaseAdsLoader();
+ } else {
+ releaseAdsLoader();
+ }
+
+ return mediaSources;
+ }
+
+ private MediaSource createLeafMediaSource(UriSample parameters) {
+ Sample.DrmInfo drmInfo = parameters.drmInfo;
+ int errorStringId = R.string.error_drm_unknown;
+ DrmSessionManager drmSessionManager = null;
+ if (drmInfo == null) {
+ drmSessionManager = DrmSessionManager.getDummyDrmSessionManager();
+ } else if (Util.SDK_INT < 18) {
+ errorStringId = R.string.error_drm_unsupported_before_api_18;
+ } else if (!MediaDrm.isCryptoSchemeSupported(drmInfo.drmScheme)) {
+ errorStringId = R.string.error_drm_unsupported_scheme;
+ } else {
+ MediaDrmCallback mediaDrmCallback =
+ createMediaDrmCallback(drmInfo.drmLicenseUrl, drmInfo.drmKeyRequestProperties);
+ drmSessionManager =
+ new DefaultDrmSessionManager.Builder()
+ .setUuidAndExoMediaDrmProvider(drmInfo.drmScheme, FrameworkMediaDrm.DEFAULT_PROVIDER)
+ .setMultiSession(drmInfo.drmMultiSession)
+ .setUseDrmSessionsForClearContent(drmInfo.drmSessionForClearTypes)
+ .build(mediaDrmCallback);
+ }
+
+ if (drmSessionManager == null) {
+ showToast(errorStringId);
+ finish();
+ return null;
+ }
+
+ DownloadRequest downloadRequest =
+ ((DemoApplication) getApplication())
+ .getDownloadTracker()
+ .getDownloadRequest(parameters.uri);
+ if (downloadRequest != null) {
+ return DownloadHelper.createMediaSource(downloadRequest, dataSourceFactory);
+ }
+ return createLeafMediaSource(parameters.uri, parameters.extension, drmSessionManager);
+ }
+
+ private MediaSource createLeafMediaSource(
+ Uri uri, String extension, DrmSessionManager> drmSessionManager) {
+ @ContentType int type = Util.inferContentType(uri, extension);
+ switch (type) {
+ case C.TYPE_DASH:
+ return new DashMediaSource.Factory(dataSourceFactory)
+ .setDrmSessionManager(drmSessionManager)
+ .createMediaSource(uri);
+ case C.TYPE_SS:
+ return new SsMediaSource.Factory(dataSourceFactory)
+ .setDrmSessionManager(drmSessionManager)
+ .createMediaSource(uri);
+ case C.TYPE_HLS:
+ return new HlsMediaSource.Factory(dataSourceFactory)
+ .setDrmSessionManager(drmSessionManager)
+ .createMediaSource(uri);
+ case C.TYPE_OTHER:
+ return new ProgressiveMediaSource.Factory(dataSourceFactory)
+ .setDrmSessionManager(drmSessionManager)
+ .createMediaSource(uri);
+ default:
+ throw new IllegalStateException("Unsupported type: " + type);
+ }
+ }
+
+ private HttpMediaDrmCallback createMediaDrmCallback(
+ String licenseUrl, String[] keyRequestPropertiesArray) {
+ HttpDataSource.Factory licenseDataSourceFactory =
+ ((DemoApplication) getApplication()).buildHttpDataSourceFactory();
+ HttpMediaDrmCallback drmCallback =
+ new HttpMediaDrmCallback(licenseUrl, licenseDataSourceFactory);
+ if (keyRequestPropertiesArray != null) {
+ for (int i = 0; i < keyRequestPropertiesArray.length - 1; i += 2) {
+ drmCallback.setKeyRequestProperty(keyRequestPropertiesArray[i],
+ keyRequestPropertiesArray[i + 1]);
+ }
+ }
+ return drmCallback;
+ }
+
+ private void releasePlayer() {
+ if (player != null) {
+ updateTrackSelectorParameters();
+ updateStartPosition();
+ debugViewHelper.stop();
+ debugViewHelper = null;
+ player.release();
+ player = null;
+ mediaSources = null;
+ trackSelector = null;
+ }
+ if (adsLoader != null) {
+ adsLoader.setPlayer(null);
+ }
+ }
+
+ private void releaseAdsLoader() {
+ if (adsLoader != null) {
+ adsLoader.release();
+ adsLoader = null;
+ loadedAdTagUri = null;
+ playerView.getOverlayFrameLayout().removeAllViews();
+ }
+ }
+
+ private void updateTrackSelectorParameters() {
+ if (trackSelector != null) {
+ trackSelectorParameters = trackSelector.getParameters();
+ }
+ }
+
+ private void updateStartPosition() {
+ if (player != null) {
+ startAutoPlay = player.getPlayWhenReady();
+ startWindow = player.getCurrentWindowIndex();
+ startPosition = Math.max(0, player.getContentPosition());
+ }
+ }
+
+ private void clearStartPosition() {
+ startAutoPlay = true;
+ startWindow = C.INDEX_UNSET;
+ startPosition = C.TIME_UNSET;
+ }
+
+ /** Returns a new DataSource factory. */
+ private DataSource.Factory buildDataSourceFactory() {
+ return ((DemoApplication) getApplication()).buildDataSourceFactory();
+ }
+
+ /** Returns an ads media source, reusing the ads loader if one exists. */
+ @Nullable
+ private MediaSource createAdsMediaSource(MediaSource mediaSource, Uri adTagUri) {
+ // Load the extension source using reflection so the demo app doesn't have to depend on it.
+ // The ads loader is reused for multiple playbacks, so that ad playback can resume.
+ try {
+ Class> loaderClass = Class.forName("com.google.android.exoplayer2.ext.ima.ImaAdsLoader");
+ if (adsLoader == null) {
+ // Full class names used so the LINT.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() {
+
+ private DrmSessionManager> drmSessionManager =
+ DrmSessionManager.getDummyDrmSessionManager();
+
+ @Override
+ public MediaSourceFactory setDrmSessionManager(DrmSessionManager> drmSessionManager) {
+ this.drmSessionManager = drmSessionManager;
+ return this;
+ }
+
+ @Override
+ public MediaSource createMediaSource(Uri uri) {
+ return PlayerActivity.this.createLeafMediaSource(
+ uri, /* extension=*/ null, drmSessionManager);
+ }
+
+ @Override
+ public int[] getSupportedTypes() {
+ return new int[] {C.TYPE_DASH, C.TYPE_SS, C.TYPE_HLS, C.TYPE_OTHER};
+ }
+ };
+ return new AdsMediaSource(mediaSource, adMediaSourceFactory, adsLoader, playerView);
+ } catch (ClassNotFoundException e) {
+ // IMA extension not loaded.
+ return null;
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ // User controls
+
+ private void updateButtonVisibility() {
+ selectTracksButton.setEnabled(
+ player != null && TrackSelectionDialog.willHaveContent(trackSelector));
+ }
+
+ private void showControls() {
+ debugRootView.setVisibility(View.VISIBLE);
+ }
+
+ private void showToast(int messageId) {
+ showToast(getString(messageId));
+ }
+
+ private void showToast(String message) {
+ Toast.makeText(getApplicationContext(), message, Toast.LENGTH_LONG).show();
+ }
+
+ private static boolean isBehindLiveWindow(ExoPlaybackException e) {
+ if (e.type != ExoPlaybackException.TYPE_SOURCE) {
+ return false;
+ }
+ Throwable cause = e.getSourceException();
+ while (cause != null) {
+ if (cause instanceof BehindLiveWindowException) {
+ return true;
+ }
+ cause = cause.getCause();
+ }
+ return false;
+ }
+
+ private class PlayerEventListener implements Player.EventListener {
+
+ @Override
+ public void onPlayerStateChanged(boolean playWhenReady, @Player.State int playbackState) {
+ if (playbackState == Player.STATE_ENDED) {
+ showControls();
+ }
+ updateButtonVisibility();
+ }
+
+ @Override
+ public void onPlayerError(ExoPlaybackException e) {
+ if (isBehindLiveWindow(e)) {
+ clearStartPosition();
+ initializePlayer();
+ } else {
+ updateButtonVisibility();
+ showControls();
+ }
+ }
+
+ @Override
+ @SuppressWarnings("ReferenceEquality")
+ public void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray trackSelections) {
+ updateButtonVisibility();
+ if (trackGroups != lastSeenTrackGroupArray) {
+ MappedTrackInfo mappedTrackInfo = trackSelector.getCurrentMappedTrackInfo();
+ if (mappedTrackInfo != null) {
+ if (mappedTrackInfo.getTypeSupport(C.TRACK_TYPE_VIDEO)
+ == MappedTrackInfo.RENDERER_SUPPORT_UNSUPPORTED_TRACKS) {
+ showToast(R.string.error_unsupported_video);
+ }
+ if (mappedTrackInfo.getTypeSupport(C.TRACK_TYPE_AUDIO)
+ == MappedTrackInfo.RENDERER_SUPPORT_UNSUPPORTED_TRACKS) {
+ showToast(R.string.error_unsupported_audio);
+ }
+ }
+ lastSeenTrackGroupArray = trackGroups;
+ }
+ }
+ }
+
+ private class PlayerErrorMessageProvider implements ErrorMessageProvider {
+
+ @Override
+ public Pair getErrorMessage(ExoPlaybackException e) {
+ String errorString = getString(R.string.error_generic);
+ if (e.type == ExoPlaybackException.TYPE_RENDERER) {
+ Exception cause = e.getRendererException();
+ if (cause instanceof DecoderInitializationException) {
+ // Special case for decoder initialization failures.
+ DecoderInitializationException decoderInitializationException =
+ (DecoderInitializationException) cause;
+ if (decoderInitializationException.codecInfo == null) {
+ if (decoderInitializationException.getCause() instanceof DecoderQueryException) {
+ errorString = getString(R.string.error_querying_decoders);
+ } else if (decoderInitializationException.secureDecoderRequired) {
+ errorString =
+ getString(
+ R.string.error_no_secure_decoder, decoderInitializationException.mimeType);
+ } else {
+ errorString =
+ getString(R.string.error_no_decoder, decoderInitializationException.mimeType);
+ }
+ } else {
+ errorString =
+ getString(
+ R.string.error_instantiating_decoder,
+ decoderInitializationException.codecInfo.name);
+ }
+ }
+ }
+ return Pair.create(0, errorString);
+ }
+ }
+}
diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/Sample.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/Sample.java
new file mode 100644
index 0000000000..0bf0d2a80c
--- /dev/null
+++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/Sample.java
@@ -0,0 +1,277 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.demo;
+
+import static com.google.android.exoplayer2.demo.PlayerActivity.ACTION_VIEW_LIST;
+import static com.google.android.exoplayer2.demo.PlayerActivity.AD_TAG_URI_EXTRA;
+import static com.google.android.exoplayer2.demo.PlayerActivity.DRM_KEY_REQUEST_PROPERTIES_EXTRA;
+import static com.google.android.exoplayer2.demo.PlayerActivity.DRM_LICENSE_URL_EXTRA;
+import static com.google.android.exoplayer2.demo.PlayerActivity.DRM_MULTI_SESSION_EXTRA;
+import static com.google.android.exoplayer2.demo.PlayerActivity.DRM_SCHEME_EXTRA;
+import static com.google.android.exoplayer2.demo.PlayerActivity.DRM_SCHEME_UUID_EXTRA;
+import static com.google.android.exoplayer2.demo.PlayerActivity.DRM_SESSION_FOR_CLEAR_TYPES_EXTRA;
+import static com.google.android.exoplayer2.demo.PlayerActivity.EXTENSION_EXTRA;
+import static com.google.android.exoplayer2.demo.PlayerActivity.IS_LIVE_EXTRA;
+import static com.google.android.exoplayer2.demo.PlayerActivity.SUBTITLE_LANGUAGE_EXTRA;
+import static com.google.android.exoplayer2.demo.PlayerActivity.SUBTITLE_MIME_TYPE_EXTRA;
+import static com.google.android.exoplayer2.demo.PlayerActivity.SUBTITLE_URI_EXTRA;
+import static com.google.android.exoplayer2.demo.PlayerActivity.URI_EXTRA;
+
+import android.content.Intent;
+import android.net.Uri;
+import androidx.annotation.Nullable;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.util.Assertions;
+import com.google.android.exoplayer2.util.Util;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.UUID;
+
+/* package */ abstract class Sample {
+
+ public static final class UriSample extends Sample {
+
+ public static UriSample createFromIntent(Uri uri, Intent intent, String extrasKeySuffix) {
+ String extension = intent.getStringExtra(EXTENSION_EXTRA + extrasKeySuffix);
+ String adsTagUriString = intent.getStringExtra(AD_TAG_URI_EXTRA + extrasKeySuffix);
+ boolean isLive =
+ intent.getBooleanExtra(IS_LIVE_EXTRA + extrasKeySuffix, /* defaultValue= */ false);
+ Uri adTagUri = adsTagUriString != null ? Uri.parse(adsTagUriString) : null;
+ return new UriSample(
+ /* name= */ null,
+ uri,
+ extension,
+ isLive,
+ DrmInfo.createFromIntent(intent, extrasKeySuffix),
+ adTagUri,
+ /* sphericalStereoMode= */ null,
+ SubtitleInfo.createFromIntent(intent, extrasKeySuffix));
+ }
+
+ public final Uri uri;
+ public final String extension;
+ public final boolean isLive;
+ public final DrmInfo drmInfo;
+ public final Uri adTagUri;
+ @Nullable public final String sphericalStereoMode;
+ @Nullable SubtitleInfo subtitleInfo;
+
+ public UriSample(
+ String name,
+ Uri uri,
+ String extension,
+ boolean isLive,
+ DrmInfo drmInfo,
+ Uri adTagUri,
+ @Nullable String sphericalStereoMode,
+ @Nullable SubtitleInfo subtitleInfo) {
+ super(name);
+ this.uri = uri;
+ this.extension = extension;
+ this.isLive = isLive;
+ this.drmInfo = drmInfo;
+ this.adTagUri = adTagUri;
+ this.sphericalStereoMode = sphericalStereoMode;
+ this.subtitleInfo = subtitleInfo;
+ }
+
+ @Override
+ public void addToIntent(Intent intent) {
+ intent.setAction(PlayerActivity.ACTION_VIEW).setData(uri);
+ intent.putExtra(PlayerActivity.IS_LIVE_EXTRA, isLive);
+ intent.putExtra(PlayerActivity.SPHERICAL_STEREO_MODE_EXTRA, sphericalStereoMode);
+ addPlayerConfigToIntent(intent, /* extrasKeySuffix= */ "");
+ }
+
+ public void addToPlaylistIntent(Intent intent, String extrasKeySuffix) {
+ intent.putExtra(PlayerActivity.URI_EXTRA + extrasKeySuffix, uri.toString());
+ intent.putExtra(PlayerActivity.IS_LIVE_EXTRA + extrasKeySuffix, isLive);
+ addPlayerConfigToIntent(intent, extrasKeySuffix);
+ }
+
+ private void addPlayerConfigToIntent(Intent intent, String extrasKeySuffix) {
+ intent
+ .putExtra(EXTENSION_EXTRA + extrasKeySuffix, extension)
+ .putExtra(
+ AD_TAG_URI_EXTRA + extrasKeySuffix, adTagUri != null ? adTagUri.toString() : null);
+ if (drmInfo != null) {
+ drmInfo.addToIntent(intent, extrasKeySuffix);
+ }
+ if (subtitleInfo != null) {
+ subtitleInfo.addToIntent(intent, extrasKeySuffix);
+ }
+ }
+ }
+
+ public static final class PlaylistSample extends Sample {
+
+ public final UriSample[] children;
+
+ public PlaylistSample(String name, UriSample... children) {
+ super(name);
+ this.children = children;
+ }
+
+ @Override
+ public void addToIntent(Intent intent) {
+ intent.setAction(PlayerActivity.ACTION_VIEW_LIST);
+ for (int i = 0; i < children.length; i++) {
+ children[i].addToPlaylistIntent(intent, /* extrasKeySuffix= */ "_" + i);
+ }
+ }
+ }
+
+ public static final class DrmInfo {
+
+ public static DrmInfo createFromIntent(Intent intent, String extrasKeySuffix) {
+ String schemeKey = DRM_SCHEME_EXTRA + extrasKeySuffix;
+ String schemeUuidKey = DRM_SCHEME_UUID_EXTRA + extrasKeySuffix;
+ if (!intent.hasExtra(schemeKey) && !intent.hasExtra(schemeUuidKey)) {
+ return null;
+ }
+ String drmSchemeExtra =
+ intent.hasExtra(schemeKey)
+ ? intent.getStringExtra(schemeKey)
+ : intent.getStringExtra(schemeUuidKey);
+ UUID drmScheme = Util.getDrmUuid(drmSchemeExtra);
+ String drmLicenseUrl = intent.getStringExtra(DRM_LICENSE_URL_EXTRA + extrasKeySuffix);
+ String[] keyRequestPropertiesArray =
+ intent.getStringArrayExtra(DRM_KEY_REQUEST_PROPERTIES_EXTRA + extrasKeySuffix);
+ String[] drmSessionForClearTypesExtra =
+ intent.getStringArrayExtra(DRM_SESSION_FOR_CLEAR_TYPES_EXTRA + extrasKeySuffix);
+ int[] drmSessionForClearTypes = toTrackTypeArray(drmSessionForClearTypesExtra);
+ boolean drmMultiSession =
+ intent.getBooleanExtra(DRM_MULTI_SESSION_EXTRA + extrasKeySuffix, false);
+ return new DrmInfo(
+ drmScheme,
+ drmLicenseUrl,
+ keyRequestPropertiesArray,
+ drmSessionForClearTypes,
+ drmMultiSession);
+ }
+
+ public final UUID drmScheme;
+ public final String drmLicenseUrl;
+ public final String[] drmKeyRequestProperties;
+ public final int[] drmSessionForClearTypes;
+ public final boolean drmMultiSession;
+
+ public DrmInfo(
+ UUID drmScheme,
+ String drmLicenseUrl,
+ String[] drmKeyRequestProperties,
+ int[] drmSessionForClearTypes,
+ boolean drmMultiSession) {
+ this.drmScheme = drmScheme;
+ this.drmLicenseUrl = drmLicenseUrl;
+ this.drmKeyRequestProperties = drmKeyRequestProperties;
+ this.drmSessionForClearTypes = drmSessionForClearTypes;
+ this.drmMultiSession = drmMultiSession;
+ }
+
+ public void addToIntent(Intent intent, String extrasKeySuffix) {
+ Assertions.checkNotNull(intent);
+ intent.putExtra(DRM_SCHEME_EXTRA + extrasKeySuffix, drmScheme.toString());
+ intent.putExtra(DRM_LICENSE_URL_EXTRA + extrasKeySuffix, drmLicenseUrl);
+ intent.putExtra(DRM_KEY_REQUEST_PROPERTIES_EXTRA + extrasKeySuffix, drmKeyRequestProperties);
+ ArrayList typeStrings = new ArrayList<>();
+ for (int type : drmSessionForClearTypes) {
+ // Only audio and video are supported.
+ typeStrings.add(type == C.TRACK_TYPE_AUDIO ? "audio" : "video");
+ }
+ intent.putExtra(
+ DRM_SESSION_FOR_CLEAR_TYPES_EXTRA + extrasKeySuffix, typeStrings.toArray(new String[0]));
+ intent.putExtra(DRM_MULTI_SESSION_EXTRA + extrasKeySuffix, drmMultiSession);
+ }
+ }
+
+ public static final class SubtitleInfo {
+
+ @Nullable
+ public static SubtitleInfo createFromIntent(Intent intent, String extrasKeySuffix) {
+ if (!intent.hasExtra(SUBTITLE_URI_EXTRA + extrasKeySuffix)) {
+ return null;
+ }
+ return new SubtitleInfo(
+ Uri.parse(intent.getStringExtra(SUBTITLE_URI_EXTRA + extrasKeySuffix)),
+ intent.getStringExtra(SUBTITLE_MIME_TYPE_EXTRA + extrasKeySuffix),
+ intent.getStringExtra(SUBTITLE_LANGUAGE_EXTRA + extrasKeySuffix));
+ }
+
+ public final Uri uri;
+ public final String mimeType;
+ @Nullable public final String language;
+
+ public SubtitleInfo(Uri uri, String mimeType, @Nullable String language) {
+ this.uri = Assertions.checkNotNull(uri);
+ this.mimeType = Assertions.checkNotNull(mimeType);
+ this.language = language;
+ }
+
+ public void addToIntent(Intent intent, String extrasKeySuffix) {
+ intent.putExtra(SUBTITLE_URI_EXTRA + extrasKeySuffix, uri.toString());
+ intent.putExtra(SUBTITLE_MIME_TYPE_EXTRA + extrasKeySuffix, mimeType);
+ intent.putExtra(SUBTITLE_LANGUAGE_EXTRA + extrasKeySuffix, language);
+ }
+ }
+
+ public static int[] toTrackTypeArray(@Nullable String[] trackTypeStringsArray) {
+ if (trackTypeStringsArray == null) {
+ return new int[0];
+ }
+ HashSet trackTypes = new HashSet<>();
+ for (String trackTypeString : trackTypeStringsArray) {
+ switch (Util.toLowerInvariant(trackTypeString)) {
+ case "audio":
+ trackTypes.add(C.TRACK_TYPE_AUDIO);
+ break;
+ case "video":
+ trackTypes.add(C.TRACK_TYPE_VIDEO);
+ break;
+ default:
+ throw new IllegalArgumentException("Invalid track type: " + trackTypeString);
+ }
+ }
+ return Util.toArray(new ArrayList<>(trackTypes));
+ }
+
+ public static Sample createFromIntent(Intent intent) {
+ if (ACTION_VIEW_LIST.equals(intent.getAction())) {
+ ArrayList 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);
+}
diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/SampleChooserActivity.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/SampleChooserActivity.java
new file mode 100644
index 0000000000..66bf4bad5a
--- /dev/null
+++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/SampleChooserActivity.java
@@ -0,0 +1,553 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.demo;
+
+import android.content.Context;
+import android.content.Intent;
+import android.content.res.AssetManager;
+import android.net.Uri;
+import android.os.AsyncTask;
+import android.os.Bundle;
+import android.util.JsonReader;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.ViewGroup;
+import android.widget.BaseExpandableListAdapter;
+import android.widget.ExpandableListView;
+import android.widget.ExpandableListView.OnChildClickListener;
+import android.widget.ImageButton;
+import android.widget.TextView;
+import android.widget.Toast;
+import androidx.annotation.Nullable;
+import androidx.appcompat.app.AppCompatActivity;
+import com.google.android.exoplayer2.ParserException;
+import com.google.android.exoplayer2.RenderersFactory;
+import com.google.android.exoplayer2.demo.Sample.DrmInfo;
+import com.google.android.exoplayer2.demo.Sample.PlaylistSample;
+import com.google.android.exoplayer2.demo.Sample.UriSample;
+import com.google.android.exoplayer2.offline.DownloadService;
+import com.google.android.exoplayer2.upstream.DataSource;
+import com.google.android.exoplayer2.upstream.DataSourceInputStream;
+import com.google.android.exoplayer2.upstream.DataSpec;
+import com.google.android.exoplayer2.upstream.DefaultDataSource;
+import com.google.android.exoplayer2.util.Assertions;
+import com.google.android.exoplayer2.util.Log;
+import com.google.android.exoplayer2.util.Util;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+/** An activity for selecting from a list of media samples. */
+public class SampleChooserActivity extends AppCompatActivity
+ implements DownloadTracker.Listener, OnChildClickListener {
+
+ private static final String TAG = "SampleChooserActivity";
+
+ private boolean useExtensionRenderers;
+ private DownloadTracker downloadTracker;
+ private SampleAdapter sampleAdapter;
+ private MenuItem preferExtensionDecodersMenuItem;
+ private MenuItem randomAbrMenuItem;
+ private MenuItem tunnelingMenuItem;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.sample_chooser_activity);
+ sampleAdapter = new SampleAdapter();
+ ExpandableListView sampleListView = findViewById(R.id.sample_list);
+ sampleListView.setAdapter(sampleAdapter);
+ sampleListView.setOnChildClickListener(this);
+
+ Intent intent = getIntent();
+ String dataUri = intent.getDataString();
+ String[] uris;
+ if (dataUri != null) {
+ uris = new String[] {dataUri};
+ } else {
+ ArrayList uriList = new ArrayList<>();
+ AssetManager assetManager = getAssets();
+ try {
+ for (String asset : assetManager.list("")) {
+ if (asset.endsWith(".exolist.json")) {
+ uriList.add("asset:///" + asset);
+ }
+ }
+ } catch (IOException e) {
+ Toast.makeText(getApplicationContext(), R.string.sample_list_load_error, Toast.LENGTH_LONG)
+ .show();
+ }
+ uris = new String[uriList.size()];
+ uriList.toArray(uris);
+ Arrays.sort(uris);
+ }
+
+ DemoApplication application = (DemoApplication) getApplication();
+ useExtensionRenderers = application.useExtensionRenderers();
+ downloadTracker = application.getDownloadTracker();
+ SampleListLoader loaderTask = new SampleListLoader();
+ loaderTask.execute(uris);
+
+ // Start the download service if it should be running but it's not currently.
+ // Starting the service in the foreground causes notification flicker if there is no scheduled
+ // action. Starting it in the background throws an exception if the app is in the background too
+ // (e.g. if device screen is locked).
+ try {
+ DownloadService.start(this, DemoDownloadService.class);
+ } catch (IllegalStateException e) {
+ DownloadService.startForeground(this, DemoDownloadService.class);
+ }
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ MenuInflater inflater = getMenuInflater();
+ inflater.inflate(R.menu.sample_chooser_menu, menu);
+ preferExtensionDecodersMenuItem = menu.findItem(R.id.prefer_extension_decoders);
+ preferExtensionDecodersMenuItem.setVisible(useExtensionRenderers);
+ randomAbrMenuItem = menu.findItem(R.id.random_abr);
+ tunnelingMenuItem = menu.findItem(R.id.tunneling);
+ if (Util.SDK_INT < 21) {
+ tunnelingMenuItem.setEnabled(false);
+ }
+ return true;
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ item.setChecked(!item.isChecked());
+ return true;
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+ downloadTracker.addListener(this);
+ sampleAdapter.notifyDataSetChanged();
+ }
+
+ @Override
+ public void onStop() {
+ downloadTracker.removeListener(this);
+ super.onStop();
+ }
+
+ @Override
+ public void onDownloadsChanged() {
+ sampleAdapter.notifyDataSetChanged();
+ }
+
+ private void onSampleGroups(final List groups, boolean sawError) {
+ if (sawError) {
+ Toast.makeText(getApplicationContext(), R.string.sample_list_load_error, Toast.LENGTH_LONG)
+ .show();
+ }
+ sampleAdapter.setSampleGroups(groups);
+ }
+
+ @Override
+ public boolean onChildClick(
+ ExpandableListView parent, View view, int groupPosition, int childPosition, long id) {
+ Sample sample = (Sample) view.getTag();
+ Intent intent = new Intent(this, PlayerActivity.class);
+ intent.putExtra(
+ PlayerActivity.PREFER_EXTENSION_DECODERS_EXTRA,
+ isNonNullAndChecked(preferExtensionDecodersMenuItem));
+ String abrAlgorithm =
+ isNonNullAndChecked(randomAbrMenuItem)
+ ? PlayerActivity.ABR_ALGORITHM_RANDOM
+ : PlayerActivity.ABR_ALGORITHM_DEFAULT;
+ intent.putExtra(PlayerActivity.ABR_ALGORITHM_EXTRA, abrAlgorithm);
+ intent.putExtra(PlayerActivity.TUNNELING_EXTRA, isNonNullAndChecked(tunnelingMenuItem));
+ sample.addToIntent(intent);
+ startActivity(intent);
+ return true;
+ }
+
+ private void onSampleDownloadButtonClicked(Sample sample) {
+ int downloadUnsupportedStringId = getDownloadUnsupportedStringId(sample);
+ if (downloadUnsupportedStringId != 0) {
+ Toast.makeText(getApplicationContext(), downloadUnsupportedStringId, Toast.LENGTH_LONG)
+ .show();
+ } else {
+ UriSample uriSample = (UriSample) sample;
+ RenderersFactory renderersFactory =
+ ((DemoApplication) getApplication())
+ .buildRenderersFactory(isNonNullAndChecked(preferExtensionDecodersMenuItem));
+ downloadTracker.toggleDownload(
+ getSupportFragmentManager(),
+ sample.name,
+ uriSample.uri,
+ uriSample.extension,
+ renderersFactory);
+ }
+ }
+
+ private int getDownloadUnsupportedStringId(Sample sample) {
+ if (sample instanceof PlaylistSample) {
+ return R.string.download_playlist_unsupported;
+ }
+ UriSample uriSample = (UriSample) sample;
+ if (uriSample.drmInfo != null) {
+ return R.string.download_drm_unsupported;
+ }
+ if (uriSample.isLive) {
+ return R.string.download_live_unsupported;
+ }
+ if (uriSample.adTagUri != null) {
+ return R.string.download_ads_unsupported;
+ }
+ String scheme = uriSample.uri.getScheme();
+ if (!("http".equals(scheme) || "https".equals(scheme))) {
+ return R.string.download_scheme_unsupported;
+ }
+ return 0;
+ }
+
+ private static boolean isNonNullAndChecked(@Nullable MenuItem menuItem) {
+ // Temporary workaround for layouts that do not inflate the options menu.
+ return menuItem != null && menuItem.isChecked();
+ }
+
+ private final class SampleListLoader extends AsyncTask> {
+
+ private boolean sawError;
+
+ @Override
+ protected List doInBackground(String... uris) {
+ List result = new ArrayList<>();
+ Context context = getApplicationContext();
+ String userAgent = Util.getUserAgent(context, "ExoPlayerDemo");
+ DataSource dataSource =
+ new DefaultDataSource(context, userAgent, /* allowCrossProtocolRedirects= */ false);
+ for (String uri : uris) {
+ DataSpec dataSpec = new DataSpec(Uri.parse(uri));
+ InputStream inputStream = new DataSourceInputStream(dataSource, dataSpec);
+ try {
+ readSampleGroups(new JsonReader(new InputStreamReader(inputStream, "UTF-8")), result);
+ } catch (Exception e) {
+ Log.e(TAG, "Error loading sample list: " + uri, e);
+ sawError = true;
+ } finally {
+ Util.closeQuietly(dataSource);
+ }
+ }
+ return result;
+ }
+
+ @Override
+ protected void onPostExecute(List result) {
+ onSampleGroups(result, sawError);
+ }
+
+ private void readSampleGroups(JsonReader reader, List groups) throws IOException {
+ reader.beginArray();
+ while (reader.hasNext()) {
+ readSampleGroup(reader, groups);
+ }
+ reader.endArray();
+ }
+
+ private void readSampleGroup(JsonReader reader, List groups) throws IOException {
+ String groupName = "";
+ ArrayList samples = new ArrayList<>();
+
+ reader.beginObject();
+ while (reader.hasNext()) {
+ String name = reader.nextName();
+ switch (name) {
+ case "name":
+ groupName = reader.nextString();
+ break;
+ case "samples":
+ reader.beginArray();
+ while (reader.hasNext()) {
+ samples.add(readEntry(reader, false));
+ }
+ reader.endArray();
+ break;
+ case "_comment":
+ reader.nextString(); // Ignore.
+ break;
+ default:
+ throw new ParserException("Unsupported name: " + name);
+ }
+ }
+ reader.endObject();
+
+ SampleGroup group = getGroup(groupName, groups);
+ group.samples.addAll(samples);
+ }
+
+ private Sample readEntry(JsonReader reader, boolean insidePlaylist) throws IOException {
+ String sampleName = null;
+ Uri uri = null;
+ String extension = null;
+ boolean isLive = false;
+ String drmScheme = null;
+ String drmLicenseUrl = null;
+ String[] drmKeyRequestProperties = null;
+ String[] drmSessionForClearTypes = null;
+ boolean drmMultiSession = false;
+ ArrayList playlistSamples = null;
+ String adTagUri = null;
+ String sphericalStereoMode = null;
+ List subtitleInfos = new ArrayList<>();
+ Uri subtitleUri = null;
+ String subtitleMimeType = null;
+ String subtitleLanguage = null;
+
+ reader.beginObject();
+ while (reader.hasNext()) {
+ String name = reader.nextName();
+ switch (name) {
+ case "name":
+ sampleName = reader.nextString();
+ break;
+ case "uri":
+ uri = Uri.parse(reader.nextString());
+ break;
+ case "extension":
+ extension = reader.nextString();
+ break;
+ case "drm_scheme":
+ drmScheme = reader.nextString();
+ break;
+ case "is_live":
+ isLive = reader.nextBoolean();
+ break;
+ case "drm_license_url":
+ drmLicenseUrl = reader.nextString();
+ break;
+ case "drm_key_request_properties":
+ ArrayList drmKeyRequestPropertiesList = new ArrayList<>();
+ reader.beginObject();
+ while (reader.hasNext()) {
+ drmKeyRequestPropertiesList.add(reader.nextName());
+ drmKeyRequestPropertiesList.add(reader.nextString());
+ }
+ reader.endObject();
+ drmKeyRequestProperties = drmKeyRequestPropertiesList.toArray(new String[0]);
+ break;
+ case "drm_session_for_clear_types":
+ ArrayList drmSessionForClearTypesList = new ArrayList<>();
+ reader.beginArray();
+ while (reader.hasNext()) {
+ drmSessionForClearTypesList.add(reader.nextString());
+ }
+ reader.endArray();
+ drmSessionForClearTypes = drmSessionForClearTypesList.toArray(new String[0]);
+ break;
+ case "drm_multi_session":
+ drmMultiSession = reader.nextBoolean();
+ break;
+ case "playlist":
+ Assertions.checkState(!insidePlaylist, "Invalid nesting of playlists");
+ playlistSamples = new ArrayList<>();
+ reader.beginArray();
+ while (reader.hasNext()) {
+ playlistSamples.add((UriSample) readEntry(reader, /* insidePlaylist= */ true));
+ }
+ reader.endArray();
+ break;
+ case "ad_tag_uri":
+ adTagUri = reader.nextString();
+ break;
+ case "spherical_stereo_mode":
+ Assertions.checkState(
+ !insidePlaylist, "Invalid attribute on nested item: spherical_stereo_mode");
+ sphericalStereoMode = reader.nextString();
+ break;
+ case "subtitle_uri":
+ subtitleUri = Uri.parse(reader.nextString());
+ break;
+ case "subtitle_mime_type":
+ subtitleMimeType = reader.nextString();
+ break;
+ case "subtitle_language":
+ subtitleLanguage = reader.nextString();
+ break;
+ default:
+ throw new ParserException("Unsupported attribute name: " + name);
+ }
+ }
+ reader.endObject();
+ DrmInfo drmInfo =
+ drmScheme == null
+ ? null
+ : new DrmInfo(
+ Util.getDrmUuid(drmScheme),
+ drmLicenseUrl,
+ drmKeyRequestProperties,
+ Sample.toTrackTypeArray(drmSessionForClearTypes),
+ drmMultiSession);
+ Sample.SubtitleInfo subtitleInfo =
+ subtitleUri == null
+ ? null
+ : new Sample.SubtitleInfo(
+ subtitleUri,
+ Assertions.checkNotNull(
+ subtitleMimeType, "subtitle_mime_type is required if subtitle_uri is set."),
+ subtitleLanguage);
+ if (playlistSamples != null) {
+ UriSample[] playlistSamplesArray = playlistSamples.toArray(new UriSample[0]);
+ return new PlaylistSample(sampleName, playlistSamplesArray);
+ } else {
+ return new UriSample(
+ sampleName,
+ uri,
+ extension,
+ isLive,
+ drmInfo,
+ adTagUri != null ? Uri.parse(adTagUri) : null,
+ sphericalStereoMode,
+ subtitleInfo);
+ }
+ }
+
+ private SampleGroup getGroup(String groupName, List groups) {
+ for (int i = 0; i < groups.size(); i++) {
+ if (Util.areEqual(groupName, groups.get(i).title)) {
+ return groups.get(i);
+ }
+ }
+ SampleGroup group = new SampleGroup(groupName);
+ groups.add(group);
+ return group;
+ }
+
+ }
+
+ private final class SampleAdapter extends BaseExpandableListAdapter implements OnClickListener {
+
+ private List sampleGroups;
+
+ public SampleAdapter() {
+ sampleGroups = Collections.emptyList();
+ }
+
+ public void setSampleGroups(List sampleGroups) {
+ this.sampleGroups = sampleGroups;
+ notifyDataSetChanged();
+ }
+
+ @Override
+ public Sample getChild(int groupPosition, int childPosition) {
+ return getGroup(groupPosition).samples.get(childPosition);
+ }
+
+ @Override
+ public long getChildId(int groupPosition, int childPosition) {
+ return childPosition;
+ }
+
+ @Override
+ public View getChildView(int groupPosition, int childPosition, boolean isLastChild,
+ View convertView, ViewGroup parent) {
+ View view = convertView;
+ if (view == null) {
+ view = getLayoutInflater().inflate(R.layout.sample_list_item, parent, false);
+ View downloadButton = view.findViewById(R.id.download_button);
+ downloadButton.setOnClickListener(this);
+ downloadButton.setFocusable(false);
+ }
+ initializeChildView(view, getChild(groupPosition, childPosition));
+ return view;
+ }
+
+ @Override
+ public int getChildrenCount(int groupPosition) {
+ return getGroup(groupPosition).samples.size();
+ }
+
+ @Override
+ public SampleGroup getGroup(int groupPosition) {
+ return sampleGroups.get(groupPosition);
+ }
+
+ @Override
+ public long getGroupId(int groupPosition) {
+ return groupPosition;
+ }
+
+ @Override
+ public View getGroupView(int groupPosition, boolean isExpanded, View convertView,
+ ViewGroup parent) {
+ View view = convertView;
+ if (view == null) {
+ view =
+ getLayoutInflater()
+ .inflate(android.R.layout.simple_expandable_list_item_1, parent, false);
+ }
+ ((TextView) view).setText(getGroup(groupPosition).title);
+ return view;
+ }
+
+ @Override
+ public int getGroupCount() {
+ return sampleGroups.size();
+ }
+
+ @Override
+ public boolean hasStableIds() {
+ return false;
+ }
+
+ @Override
+ public boolean isChildSelectable(int groupPosition, int childPosition) {
+ return true;
+ }
+
+ @Override
+ public void onClick(View view) {
+ onSampleDownloadButtonClicked((Sample) view.getTag());
+ }
+
+ private void initializeChildView(View view, Sample sample) {
+ view.setTag(sample);
+ TextView sampleTitle = view.findViewById(R.id.sample_title);
+ sampleTitle.setText(sample.name);
+
+ boolean canDownload = getDownloadUnsupportedStringId(sample) == 0;
+ boolean isDownloaded = canDownload && downloadTracker.isDownloaded(((UriSample) sample).uri);
+ ImageButton downloadButton = view.findViewById(R.id.download_button);
+ downloadButton.setTag(sample);
+ downloadButton.setColorFilter(
+ canDownload ? (isDownloaded ? 0xFF42A5F5 : 0xFFBDBDBD) : 0xFF666666);
+ downloadButton.setImageResource(
+ isDownloaded ? R.drawable.ic_download_done : R.drawable.ic_download);
+ }
+ }
+
+ private static final class SampleGroup {
+
+ public final String title;
+ public final List samples;
+
+ public SampleGroup(String title) {
+ this.title = title;
+ this.samples = new ArrayList<>();
+ }
+
+ }
+}
diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/TrackSelectionDialog.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/TrackSelectionDialog.java
new file mode 100644
index 0000000000..9e8009388e
--- /dev/null
+++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/TrackSelectionDialog.java
@@ -0,0 +1,368 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.demo;
+
+import android.app.Dialog;
+import android.content.DialogInterface;
+import android.content.res.Resources;
+import android.os.Bundle;
+import android.util.SparseArray;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.Button;
+import androidx.annotation.Nullable;
+import androidx.appcompat.app.AppCompatDialog;
+import androidx.fragment.app.DialogFragment;
+import androidx.fragment.app.Fragment;
+import androidx.fragment.app.FragmentManager;
+import androidx.fragment.app.FragmentPagerAdapter;
+import androidx.viewpager.widget.ViewPager;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.source.TrackGroupArray;
+import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
+import com.google.android.exoplayer2.trackselection.DefaultTrackSelector.SelectionOverride;
+import com.google.android.exoplayer2.trackselection.MappingTrackSelector.MappedTrackInfo;
+import com.google.android.exoplayer2.ui.TrackSelectionView;
+import com.google.android.exoplayer2.util.Assertions;
+import com.google.android.material.tabs.TabLayout;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/** Dialog to select tracks. */
+public final class TrackSelectionDialog extends DialogFragment {
+
+ private final SparseArray tabFragments;
+ private final ArrayList tabTrackTypes;
+
+ private int titleId;
+ private DialogInterface.OnClickListener onClickListener;
+ private DialogInterface.OnDismissListener onDismissListener;
+
+ /**
+ * Returns whether a track selection dialog will have content to display if initialized with the
+ * specified {@link DefaultTrackSelector} in its current state.
+ */
+ public static boolean willHaveContent(DefaultTrackSelector trackSelector) {
+ MappedTrackInfo mappedTrackInfo = trackSelector.getCurrentMappedTrackInfo();
+ return mappedTrackInfo != null && willHaveContent(mappedTrackInfo);
+ }
+
+ /**
+ * Returns whether a track selection dialog will have content to display if initialized with the
+ * specified {@link MappedTrackInfo}.
+ */
+ public static boolean willHaveContent(MappedTrackInfo mappedTrackInfo) {
+ for (int i = 0; i < mappedTrackInfo.getRendererCount(); i++) {
+ if (showTabForRenderer(mappedTrackInfo, i)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Creates a dialog for a given {@link DefaultTrackSelector}, whose parameters will be
+ * automatically updated when tracks are selected.
+ *
+ * @param trackSelector The {@link DefaultTrackSelector}.
+ * @param onDismissListener A {@link DialogInterface.OnDismissListener} to call when the dialog is
+ * dismissed.
+ */
+ public static TrackSelectionDialog createForTrackSelector(
+ DefaultTrackSelector trackSelector, DialogInterface.OnDismissListener onDismissListener) {
+ MappedTrackInfo mappedTrackInfo =
+ Assertions.checkNotNull(trackSelector.getCurrentMappedTrackInfo());
+ TrackSelectionDialog trackSelectionDialog = new TrackSelectionDialog();
+ DefaultTrackSelector.Parameters parameters = trackSelector.getParameters();
+ trackSelectionDialog.init(
+ /* titleId= */ R.string.track_selection_title,
+ mappedTrackInfo,
+ /* initialParameters = */ parameters,
+ /* allowAdaptiveSelections =*/ true,
+ /* allowMultipleOverrides= */ false,
+ /* onClickListener= */ (dialog, which) -> {
+ DefaultTrackSelector.ParametersBuilder builder = parameters.buildUpon();
+ for (int i = 0; i < mappedTrackInfo.getRendererCount(); i++) {
+ builder
+ .clearSelectionOverrides(/* rendererIndex= */ i)
+ .setRendererDisabled(
+ /* rendererIndex= */ i,
+ trackSelectionDialog.getIsDisabled(/* rendererIndex= */ i));
+ List overrides =
+ trackSelectionDialog.getOverrides(/* rendererIndex= */ i);
+ if (!overrides.isEmpty()) {
+ builder.setSelectionOverride(
+ /* rendererIndex= */ i,
+ mappedTrackInfo.getTrackGroups(/* rendererIndex= */ i),
+ overrides.get(0));
+ }
+ }
+ trackSelector.setParameters(builder);
+ },
+ onDismissListener);
+ return trackSelectionDialog;
+ }
+
+ /**
+ * Creates a dialog for given {@link MappedTrackInfo} and {@link DefaultTrackSelector.Parameters}.
+ *
+ * @param titleId The resource id of the dialog title.
+ * @param mappedTrackInfo The {@link MappedTrackInfo} to display.
+ * @param initialParameters The {@link DefaultTrackSelector.Parameters} describing the initial
+ * track selection.
+ * @param allowAdaptiveSelections Whether adaptive selections (consisting of more than one track)
+ * can be made.
+ * @param allowMultipleOverrides Whether tracks from multiple track groups can be selected.
+ * @param onClickListener {@link DialogInterface.OnClickListener} called when tracks are selected.
+ * @param onDismissListener {@link DialogInterface.OnDismissListener} called when the dialog is
+ * dismissed.
+ */
+ public static TrackSelectionDialog createForMappedTrackInfoAndParameters(
+ int titleId,
+ MappedTrackInfo mappedTrackInfo,
+ DefaultTrackSelector.Parameters initialParameters,
+ boolean allowAdaptiveSelections,
+ boolean allowMultipleOverrides,
+ DialogInterface.OnClickListener onClickListener,
+ DialogInterface.OnDismissListener onDismissListener) {
+ TrackSelectionDialog trackSelectionDialog = new TrackSelectionDialog();
+ trackSelectionDialog.init(
+ titleId,
+ mappedTrackInfo,
+ initialParameters,
+ allowAdaptiveSelections,
+ allowMultipleOverrides,
+ onClickListener,
+ onDismissListener);
+ return trackSelectionDialog;
+ }
+
+ public TrackSelectionDialog() {
+ tabFragments = new SparseArray<>();
+ tabTrackTypes = new ArrayList<>();
+ // Retain instance across activity re-creation to prevent losing access to init data.
+ setRetainInstance(true);
+ }
+
+ private void init(
+ int titleId,
+ MappedTrackInfo mappedTrackInfo,
+ DefaultTrackSelector.Parameters initialParameters,
+ boolean allowAdaptiveSelections,
+ boolean allowMultipleOverrides,
+ DialogInterface.OnClickListener onClickListener,
+ DialogInterface.OnDismissListener onDismissListener) {
+ this.titleId = titleId;
+ this.onClickListener = onClickListener;
+ this.onDismissListener = onDismissListener;
+ for (int i = 0; i < mappedTrackInfo.getRendererCount(); i++) {
+ if (showTabForRenderer(mappedTrackInfo, i)) {
+ int trackType = mappedTrackInfo.getRendererType(/* rendererIndex= */ i);
+ TrackGroupArray trackGroupArray = mappedTrackInfo.getTrackGroups(i);
+ TrackSelectionViewFragment tabFragment = new TrackSelectionViewFragment();
+ tabFragment.init(
+ mappedTrackInfo,
+ /* rendererIndex= */ i,
+ initialParameters.getRendererDisabled(/* rendererIndex= */ i),
+ initialParameters.getSelectionOverride(/* rendererIndex= */ i, trackGroupArray),
+ allowAdaptiveSelections,
+ allowMultipleOverrides);
+ tabFragments.put(i, tabFragment);
+ tabTrackTypes.add(trackType);
+ }
+ }
+ }
+
+ /**
+ * Returns whether a renderer is disabled.
+ *
+ * @param rendererIndex Renderer index.
+ * @return Whether the renderer is disabled.
+ */
+ public boolean getIsDisabled(int rendererIndex) {
+ TrackSelectionViewFragment rendererView = tabFragments.get(rendererIndex);
+ return rendererView != null && rendererView.isDisabled;
+ }
+
+ /**
+ * Returns the list of selected track selection overrides for the specified renderer. There will
+ * be at most one override for each track group.
+ *
+ * @param rendererIndex Renderer index.
+ * @return The list of track selection overrides for this renderer.
+ */
+ public List getOverrides(int rendererIndex) {
+ TrackSelectionViewFragment rendererView = tabFragments.get(rendererIndex);
+ return rendererView == null ? Collections.emptyList() : rendererView.overrides;
+ }
+
+ @Override
+ public Dialog onCreateDialog(Bundle savedInstanceState) {
+ // We need to own the view to let tab layout work correctly on all API levels. We can't use
+ // AlertDialog because it owns the view itself, so we use AppCompatDialog instead, themed using
+ // the AlertDialog theme overlay with force-enabled title.
+ AppCompatDialog dialog =
+ new AppCompatDialog(getActivity(), R.style.TrackSelectionDialogThemeOverlay);
+ dialog.setTitle(titleId);
+ return dialog;
+ }
+
+ @Override
+ public void onDismiss(DialogInterface dialog) {
+ super.onDismiss(dialog);
+ onDismissListener.onDismiss(dialog);
+ }
+
+ @Nullable
+ @Override
+ public View onCreateView(
+ LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
+
+ View dialogView = inflater.inflate(R.layout.track_selection_dialog, container, false);
+ TabLayout tabLayout = dialogView.findViewById(R.id.track_selection_dialog_tab_layout);
+ ViewPager viewPager = dialogView.findViewById(R.id.track_selection_dialog_view_pager);
+ Button cancelButton = dialogView.findViewById(R.id.track_selection_dialog_cancel_button);
+ Button okButton = dialogView.findViewById(R.id.track_selection_dialog_ok_button);
+ viewPager.setAdapter(new FragmentAdapter(getChildFragmentManager()));
+ tabLayout.setupWithViewPager(viewPager);
+ tabLayout.setVisibility(tabFragments.size() > 1 ? View.VISIBLE : View.GONE);
+ cancelButton.setOnClickListener(view -> dismiss());
+ okButton.setOnClickListener(
+ view -> {
+ onClickListener.onClick(getDialog(), DialogInterface.BUTTON_POSITIVE);
+ dismiss();
+ });
+ return dialogView;
+ }
+
+ private static boolean showTabForRenderer(MappedTrackInfo mappedTrackInfo, int rendererIndex) {
+ TrackGroupArray trackGroupArray = mappedTrackInfo.getTrackGroups(rendererIndex);
+ if (trackGroupArray.length == 0) {
+ return false;
+ }
+ int trackType = mappedTrackInfo.getRendererType(rendererIndex);
+ return isSupportedTrackType(trackType);
+ }
+
+ private static boolean isSupportedTrackType(int trackType) {
+ switch (trackType) {
+ case C.TRACK_TYPE_VIDEO:
+ case C.TRACK_TYPE_AUDIO:
+ case C.TRACK_TYPE_TEXT:
+ return true;
+ default:
+ return false;
+ }
+ }
+
+ private static String getTrackTypeString(Resources resources, int trackType) {
+ switch (trackType) {
+ case C.TRACK_TYPE_VIDEO:
+ return resources.getString(R.string.exo_track_selection_title_video);
+ case C.TRACK_TYPE_AUDIO:
+ return resources.getString(R.string.exo_track_selection_title_audio);
+ case C.TRACK_TYPE_TEXT:
+ return resources.getString(R.string.exo_track_selection_title_text);
+ default:
+ throw new IllegalArgumentException();
+ }
+ }
+
+ private final class FragmentAdapter extends FragmentPagerAdapter {
+
+ public FragmentAdapter(FragmentManager fragmentManager) {
+ super(fragmentManager);
+ }
+
+ @Override
+ public Fragment getItem(int position) {
+ return tabFragments.valueAt(position);
+ }
+
+ @Override
+ public int getCount() {
+ return tabFragments.size();
+ }
+
+ @Nullable
+ @Override
+ public CharSequence getPageTitle(int position) {
+ return getTrackTypeString(getResources(), tabTrackTypes.get(position));
+ }
+ }
+
+ /** Fragment to show a track selection in tab of the track selection dialog. */
+ public static final class TrackSelectionViewFragment extends Fragment
+ implements TrackSelectionView.TrackSelectionListener {
+
+ private MappedTrackInfo mappedTrackInfo;
+ private int rendererIndex;
+ private boolean allowAdaptiveSelections;
+ private boolean allowMultipleOverrides;
+
+ /* package */ boolean isDisabled;
+ /* package */ List overrides;
+
+ public TrackSelectionViewFragment() {
+ // Retain instance across activity re-creation to prevent losing access to init data.
+ setRetainInstance(true);
+ }
+
+ public void init(
+ MappedTrackInfo mappedTrackInfo,
+ int rendererIndex,
+ boolean initialIsDisabled,
+ @Nullable SelectionOverride initialOverride,
+ boolean allowAdaptiveSelections,
+ boolean allowMultipleOverrides) {
+ this.mappedTrackInfo = mappedTrackInfo;
+ this.rendererIndex = rendererIndex;
+ this.isDisabled = initialIsDisabled;
+ this.overrides =
+ initialOverride == null
+ ? Collections.emptyList()
+ : Collections.singletonList(initialOverride);
+ this.allowAdaptiveSelections = allowAdaptiveSelections;
+ this.allowMultipleOverrides = allowMultipleOverrides;
+ }
+
+ @Nullable
+ @Override
+ public View onCreateView(
+ LayoutInflater inflater,
+ @Nullable ViewGroup container,
+ @Nullable Bundle savedInstanceState) {
+ View rootView =
+ inflater.inflate(
+ R.layout.exo_track_selection_dialog, container, /* attachToRoot= */ false);
+ TrackSelectionView trackSelectionView = rootView.findViewById(R.id.exo_track_selection_view);
+ trackSelectionView.setShowDisableOption(true);
+ trackSelectionView.setAllowMultipleOverrides(allowMultipleOverrides);
+ trackSelectionView.setAllowAdaptiveSelections(allowAdaptiveSelections);
+ trackSelectionView.init(
+ mappedTrackInfo, rendererIndex, isDisabled, overrides, /* listener= */ this);
+ return rootView;
+ }
+
+ @Override
+ public void onTrackSelectionChanged(boolean isDisabled, List overrides) {
+ this.isDisabled = isDisabled;
+ this.overrides = overrides;
+ }
+ }
+}
diff --git a/demos/main/src/main/res/drawable-hdpi/ic_download.png b/demos/main/src/main/res/drawable-hdpi/ic_download.png
new file mode 100644
index 0000000000..fa3ebbb310
Binary files /dev/null and b/demos/main/src/main/res/drawable-hdpi/ic_download.png differ
diff --git a/demos/main/src/main/res/drawable-hdpi/ic_download_done.png b/demos/main/src/main/res/drawable-hdpi/ic_download_done.png
new file mode 100644
index 0000000000..fa0ec9dd68
Binary files /dev/null and b/demos/main/src/main/res/drawable-hdpi/ic_download_done.png differ
diff --git a/demos/main/src/main/res/drawable-mdpi/ic_download.png b/demos/main/src/main/res/drawable-mdpi/ic_download.png
new file mode 100644
index 0000000000..c8a2039c58
Binary files /dev/null and b/demos/main/src/main/res/drawable-mdpi/ic_download.png differ
diff --git a/demos/main/src/main/res/drawable-mdpi/ic_download_done.png b/demos/main/src/main/res/drawable-mdpi/ic_download_done.png
new file mode 100644
index 0000000000..08073a2a6d
Binary files /dev/null and b/demos/main/src/main/res/drawable-mdpi/ic_download_done.png differ
diff --git a/demos/main/src/main/res/drawable-xhdpi/ic_banner.png b/demos/main/src/main/res/drawable-xhdpi/ic_banner.png
new file mode 100644
index 0000000000..09de177387
Binary files /dev/null and b/demos/main/src/main/res/drawable-xhdpi/ic_banner.png differ
diff --git a/demos/main/src/main/res/drawable-xhdpi/ic_download.png b/demos/main/src/main/res/drawable-xhdpi/ic_download.png
new file mode 100644
index 0000000000..671e0b3ece
Binary files /dev/null and b/demos/main/src/main/res/drawable-xhdpi/ic_download.png differ
diff --git a/demos/main/src/main/res/drawable-xhdpi/ic_download_done.png b/demos/main/src/main/res/drawable-xhdpi/ic_download_done.png
new file mode 100644
index 0000000000..2339c0bf16
Binary files /dev/null and b/demos/main/src/main/res/drawable-xhdpi/ic_download_done.png differ
diff --git a/demos/main/src/main/res/drawable-xxhdpi/ic_download.png b/demos/main/src/main/res/drawable-xxhdpi/ic_download.png
new file mode 100644
index 0000000000..4e04a30198
Binary files /dev/null and b/demos/main/src/main/res/drawable-xxhdpi/ic_download.png differ
diff --git a/demos/main/src/main/res/drawable-xxhdpi/ic_download_done.png b/demos/main/src/main/res/drawable-xxhdpi/ic_download_done.png
new file mode 100644
index 0000000000..b631a00088
Binary files /dev/null and b/demos/main/src/main/res/drawable-xxhdpi/ic_download_done.png differ
diff --git a/demos/main/src/main/res/drawable-xxxhdpi/ic_download.png b/demos/main/src/main/res/drawable-xxxhdpi/ic_download.png
new file mode 100644
index 0000000000..f9bfb5edba
Binary files /dev/null and b/demos/main/src/main/res/drawable-xxxhdpi/ic_download.png differ
diff --git a/demos/main/src/main/res/drawable-xxxhdpi/ic_download_done.png b/demos/main/src/main/res/drawable-xxxhdpi/ic_download_done.png
new file mode 100644
index 0000000000..52fe8f6990
Binary files /dev/null and b/demos/main/src/main/res/drawable-xxxhdpi/ic_download_done.png differ
diff --git a/demo/src/main/res/layout/player_activity.xml b/demos/main/src/main/res/layout/player_activity.xml
similarity index 89%
rename from demo/src/main/res/layout/player_activity.xml
rename to demos/main/src/main/res/layout/player_activity.xml
index 3f8cdaa7d6..ea3de257e2 100644
--- a/demo/src/main/res/layout/player_activity.xml
+++ b/demos/main/src/main/res/layout/player_activity.xml
@@ -20,7 +20,7 @@
android:layout_height="match_parent"
android:keepScreenOn="true">
-
@@ -44,11 +44,11 @@
android:orientation="horizontal"
android:visibility="gone">
-
+ android:text="@string/track_selection_title"
+ android:enabled="false"/>
diff --git a/demo/src/main/res/layout/sample_chooser_activity.xml b/demos/main/src/main/res/layout/sample_chooser_activity.xml
similarity index 100%
rename from demo/src/main/res/layout/sample_chooser_activity.xml
rename to demos/main/src/main/res/layout/sample_chooser_activity.xml
diff --git a/demos/main/src/main/res/layout/sample_list_item.xml b/demos/main/src/main/res/layout/sample_list_item.xml
new file mode 100644
index 0000000000..cdb0058688
--- /dev/null
+++ b/demos/main/src/main/res/layout/sample_list_item.xml
@@ -0,0 +1,38 @@
+
+
+
+
+
+
+
+
+
diff --git a/demos/main/src/main/res/layout/track_selection_dialog.xml b/demos/main/src/main/res/layout/track_selection_dialog.xml
new file mode 100644
index 0000000000..7f6c45e131
--- /dev/null
+++ b/demos/main/src/main/res/layout/track_selection_dialog.xml
@@ -0,0 +1,59 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/demos/main/src/main/res/menu/sample_chooser_menu.xml b/demos/main/src/main/res/menu/sample_chooser_menu.xml
new file mode 100644
index 0000000000..f95c0b6460
--- /dev/null
+++ b/demos/main/src/main/res/menu/sample_chooser_menu.xml
@@ -0,0 +1,30 @@
+
+
+
diff --git a/demos/main/src/main/res/mipmap-hdpi/ic_launcher.png b/demos/main/src/main/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 0000000000..adaa93220e
Binary files /dev/null and b/demos/main/src/main/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/demos/main/src/main/res/mipmap-mdpi/ic_launcher.png b/demos/main/src/main/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 0000000000..9b6f7d5e80
Binary files /dev/null and b/demos/main/src/main/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/demos/main/src/main/res/mipmap-xhdpi/ic_launcher.png b/demos/main/src/main/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 0000000000..2101026c9f
Binary files /dev/null and b/demos/main/src/main/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/demos/main/src/main/res/mipmap-xxhdpi/ic_launcher.png b/demos/main/src/main/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 0000000000..223ec8bd11
Binary files /dev/null and b/demos/main/src/main/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/demos/main/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/demos/main/src/main/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 0000000000..698ed68c42
Binary files /dev/null and b/demos/main/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/demo/src/main/res/values/strings.xml b/demos/main/src/main/res/values/strings.xml
similarity index 56%
rename from demo/src/main/res/values/strings.xml
rename to demos/main/src/main/res/values/strings.xml
index 4eb2b89324..671303a522 100644
--- a/demo/src/main/res/values/strings.xml
+++ b/demos/main/src/main/res/values/strings.xml
@@ -13,28 +13,23 @@
See the License for the specific language governing permissions and
limitations under the License.
-->
-
ExoPlayer
- Video
-
- Audio
-
- Text
-
- Retry
-
- Disabled
-
- Default
+ Select tracksUnexpected intent action: %1$s
- Enable random adaptation
+ Cleartext traffic not permitted
- Protected content not supported on API levels below 18
+ Playback failed
+
+ Unrecognized ABR algorithm
+
+ Unrecognized stereo mode
+
+ Protected content not supported on API levels below 18This device does not support the required DRM scheme
@@ -56,4 +51,26 @@
One or more sample lists failed to load
+ Playing sample without ads, as the IMA extension was not loaded
+
+ Playing sample without ads, as ads are not supported in concatenations
+
+ Failed to start download
+
+ This demo app does not support downloading playlists
+
+ This demo app does not support downloading protected content
+
+ This demo app only supports downloading http streams
+
+ This demo app does not support downloading live content
+
+ IMA does not support offline ads
+
+ Prefer extension decoders
+
+ Enable random ABR
+
+ Request multimedia tunneling
+
diff --git a/demo/src/main/res/values/styles.xml b/demos/main/src/main/res/values/styles.xml
similarity index 71%
rename from demo/src/main/res/values/styles.xml
rename to demos/main/src/main/res/values/styles.xml
index 751a224210..a2ebde37bd 100644
--- a/demo/src/main/res/values/styles.xml
+++ b/demos/main/src/main/res/values/styles.xml
@@ -13,12 +13,18 @@
See the License for the specific language governing permissions and
limitations under the License.
-->
-
-
+
+
+
+
diff --git a/demos/surface/README.md b/demos/surface/README.md
new file mode 100644
index 0000000000..312259dbf6
--- /dev/null
+++ b/demos/surface/README.md
@@ -0,0 +1,21 @@
+# ExoPlayer SurfaceControl demo
+
+This app demonstrates how to use the [SurfaceControl][] API to redirect video
+output from ExoPlayer between different views or off-screen. `SurfaceControl`
+is new in Android 10, so the app requires `minSdkVersion` 29.
+
+The app layout has a grid of `SurfaceViews`. Initially video is output to one
+of the views. Tap a `SurfaceView` to move video output to it. You can also tap
+the buttons at the top of the activity to move video output off-screen, to a
+full-screen `SurfaceView` or to a new activity.
+
+When using `SurfaceControl`, the `MediaCodec` always has the same surface
+attached to it, which can be freely 'reparented' to any `SurfaceView` (or
+off-screen) without any interruptions to playback. This works better than
+calling `MediaCodec.setOutputSurface` to change the output surface of the codec
+because `MediaCodec` does not re-render its last frame when that method is
+called, and because you can move output off-screen easily (`setOutputSurface`
+can't take a `null` surface, so the player has to use a `DummySurface`, which
+doesn't handle protected output on all devices).
+
+[SurfaceControl]: https://developer.android.com/reference/android/view/SurfaceControl
diff --git a/demo/build.gradle b/demos/surface/build.gradle
similarity index 55%
rename from demo/build.gradle
rename to demos/surface/build.gradle
index be5e52a25c..bff05901b5 100644
--- a/demo/build.gradle
+++ b/demos/surface/build.gradle
@@ -1,4 +1,4 @@
-// Copyright (C) 2016 The Android Open Source Project
+// Copyright (C) 2019 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@@ -11,15 +11,22 @@
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// 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
- buildToolsVersion project.ext.buildToolsVersion
+
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_1_8
+ targetCompatibility JavaVersion.VERSION_1_8
+ }
defaultConfig {
- minSdkVersion 16
- targetSdkVersion project.ext.targetSdkVersion
+ versionName project.ext.releaseVersion
+ versionCode project.ext.releaseVersionCode
+ minSdkVersion 29
+ targetSdkVersion project.ext.appTargetSdkVersion
}
buildTypes {
@@ -28,30 +35,17 @@ android {
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android.txt')
}
- debug {
- jniDebuggable = true
- }
}
lintOptions {
- // The demo app does not have translations.
+ // This demo app does not have translations.
disable 'MissingTranslation'
}
-
- productFlavors {
- noExtensions
- withExtensions
- }
}
dependencies {
- compile project(':library-core')
- compile project(':library-dash')
- compile project(':library-hls')
- compile project(':library-smoothstreaming')
- compile project(':library-ui')
- withExtensionsCompile project(path: ':extension-ffmpeg')
- withExtensionsCompile project(path: ':extension-flac')
- withExtensionsCompile project(path: ':extension-opus')
- withExtensionsCompile project(path: ':extension-vp9')
+ implementation project(modulePrefix + 'library-core')
+ implementation project(modulePrefix + 'library-ui')
+ implementation project(modulePrefix + 'library-dash')
+ implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion
}
diff --git a/demos/surface/src/main/AndroidManifest.xml b/demos/surface/src/main/AndroidManifest.xml
new file mode 100644
index 0000000000..c33a9e646b
--- /dev/null
+++ b/demos/surface/src/main/AndroidManifest.xml
@@ -0,0 +1,41 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/demos/surface/src/main/java/com/google/android/exoplayer2/surfacedemo/MainActivity.java b/demos/surface/src/main/java/com/google/android/exoplayer2/surfacedemo/MainActivity.java
new file mode 100644
index 0000000000..402a71ebb3
--- /dev/null
+++ b/demos/surface/src/main/java/com/google/android/exoplayer2/surfacedemo/MainActivity.java
@@ -0,0 +1,282 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.surfacedemo;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Bundle;
+import android.view.Surface;
+import android.view.SurfaceControl;
+import android.view.SurfaceHolder;
+import android.view.SurfaceView;
+import android.view.View;
+import android.widget.Button;
+import android.widget.GridLayout;
+import androidx.annotation.Nullable;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.Player;
+import com.google.android.exoplayer2.SimpleExoPlayer;
+import com.google.android.exoplayer2.drm.DefaultDrmSessionManager;
+import com.google.android.exoplayer2.drm.DrmSessionManager;
+import com.google.android.exoplayer2.drm.ExoMediaCrypto;
+import com.google.android.exoplayer2.drm.FrameworkMediaDrm;
+import com.google.android.exoplayer2.drm.HttpMediaDrmCallback;
+import com.google.android.exoplayer2.source.MediaSource;
+import com.google.android.exoplayer2.source.ProgressiveMediaSource;
+import com.google.android.exoplayer2.source.dash.DashMediaSource;
+import com.google.android.exoplayer2.ui.PlayerControlView;
+import com.google.android.exoplayer2.upstream.DataSource;
+import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
+import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory;
+import com.google.android.exoplayer2.upstream.HttpDataSource;
+import com.google.android.exoplayer2.util.Assertions;
+import com.google.android.exoplayer2.util.Util;
+import java.util.UUID;
+
+/** Activity that demonstrates use of {@link SurfaceControl} with ExoPlayer. */
+public final class MainActivity extends Activity {
+
+ private static final String DEFAULT_MEDIA_URI =
+ "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv";
+ private static final String SURFACE_CONTROL_NAME = "surfacedemo";
+
+ private static final String ACTION_VIEW = "com.google.android.exoplayer.surfacedemo.action.VIEW";
+ private static final String EXTENSION_EXTRA = "extension";
+ private static final String DRM_SCHEME_EXTRA = "drm_scheme";
+ private static final String DRM_LICENSE_URL_EXTRA = "drm_license_url";
+ private static final String OWNER_EXTRA = "owner";
+
+ private boolean isOwner;
+ @Nullable private PlayerControlView playerControlView;
+ @Nullable private SurfaceView fullScreenView;
+ @Nullable private SurfaceView nonFullScreenView;
+ @Nullable private SurfaceView currentOutputView;
+
+ @Nullable private static SimpleExoPlayer player;
+ @Nullable private static SurfaceControl surfaceControl;
+ @Nullable private static Surface videoSurface;
+
+ @Override
+ protected void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.main_activity);
+ playerControlView = findViewById(R.id.player_control_view);
+ fullScreenView = findViewById(R.id.full_screen_view);
+ fullScreenView.setOnClickListener(
+ v -> {
+ setCurrentOutputView(nonFullScreenView);
+ Assertions.checkNotNull(fullScreenView).setVisibility(View.GONE);
+ });
+ attachSurfaceListener(fullScreenView);
+ isOwner = getIntent().getBooleanExtra(OWNER_EXTRA, /* defaultValue= */ true);
+ GridLayout gridLayout = findViewById(R.id.grid_layout);
+ for (int i = 0; i < 9; i++) {
+ View view;
+ if (i == 0) {
+ Button button = new Button(/* context= */ this);
+ view = button;
+ button.setText(getString(R.string.no_output_label));
+ button.setOnClickListener(v -> reparent(/* surfaceView= */ null));
+ } else if (i == 1) {
+ Button button = new Button(/* context= */ this);
+ view = button;
+ button.setText(getString(R.string.full_screen_label));
+ button.setOnClickListener(
+ v -> {
+ setCurrentOutputView(fullScreenView);
+ Assertions.checkNotNull(fullScreenView).setVisibility(View.VISIBLE);
+ });
+ } else if (i == 2) {
+ Button button = new Button(/* context= */ this);
+ view = button;
+ button.setText(getString(R.string.new_activity_label));
+ button.setOnClickListener(
+ v ->
+ startActivity(
+ new Intent(MainActivity.this, MainActivity.class)
+ .putExtra(OWNER_EXTRA, /* value= */ false)));
+ } else {
+ SurfaceView surfaceView = new SurfaceView(this);
+ view = surfaceView;
+ attachSurfaceListener(surfaceView);
+ surfaceView.setOnClickListener(
+ v -> {
+ setCurrentOutputView(surfaceView);
+ nonFullScreenView = surfaceView;
+ });
+ if (nonFullScreenView == null) {
+ nonFullScreenView = surfaceView;
+ }
+ }
+ gridLayout.addView(view);
+ GridLayout.LayoutParams layoutParams = new GridLayout.LayoutParams();
+ layoutParams.width = 0;
+ layoutParams.height = 0;
+ layoutParams.columnSpec = GridLayout.spec(i % 3, 1f);
+ layoutParams.rowSpec = GridLayout.spec(i / 3, 1f);
+ layoutParams.bottomMargin = 10;
+ layoutParams.leftMargin = 10;
+ layoutParams.topMargin = 10;
+ layoutParams.rightMargin = 10;
+ view.setLayoutParams(layoutParams);
+ }
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+
+ if (isOwner && player == null) {
+ initializePlayer();
+ }
+
+ setCurrentOutputView(nonFullScreenView);
+
+ PlayerControlView playerControlView = Assertions.checkNotNull(this.playerControlView);
+ playerControlView.setPlayer(player);
+ playerControlView.show();
+ }
+
+ @Override
+ public void onPause() {
+ super.onPause();
+
+ Assertions.checkNotNull(playerControlView).setPlayer(null);
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ if (isOwner && isFinishing()) {
+ if (surfaceControl != null) {
+ surfaceControl.release();
+ surfaceControl = null;
+ }
+ if (videoSurface != null) {
+ videoSurface.release();
+ videoSurface = null;
+ }
+ if (player != null) {
+ player.release();
+ player = null;
+ }
+ }
+ }
+
+ private void initializePlayer() {
+ Intent intent = getIntent();
+ String action = intent.getAction();
+ Uri uri =
+ ACTION_VIEW.equals(action)
+ ? Assertions.checkNotNull(intent.getData())
+ : Uri.parse(DEFAULT_MEDIA_URI);
+ String userAgent = Util.getUserAgent(this, getString(R.string.application_name));
+ DrmSessionManager drmSessionManager;
+ if (intent.hasExtra(DRM_SCHEME_EXTRA)) {
+ String drmScheme = Assertions.checkNotNull(intent.getStringExtra(DRM_SCHEME_EXTRA));
+ String drmLicenseUrl = Assertions.checkNotNull(intent.getStringExtra(DRM_LICENSE_URL_EXTRA));
+ UUID drmSchemeUuid = Assertions.checkNotNull(Util.getDrmUuid(drmScheme));
+ HttpDataSource.Factory licenseDataSourceFactory = new DefaultHttpDataSourceFactory(userAgent);
+ HttpMediaDrmCallback drmCallback =
+ new HttpMediaDrmCallback(drmLicenseUrl, licenseDataSourceFactory);
+ drmSessionManager =
+ new DefaultDrmSessionManager.Builder()
+ .setUuidAndExoMediaDrmProvider(drmSchemeUuid, FrameworkMediaDrm.DEFAULT_PROVIDER)
+ .build(drmCallback);
+ } else {
+ drmSessionManager = DrmSessionManager.getDummyDrmSessionManager();
+ }
+
+ DataSource.Factory dataSourceFactory =
+ new DefaultDataSourceFactory(
+ this, Util.getUserAgent(this, getString(R.string.application_name)));
+ MediaSource mediaSource;
+ @C.ContentType int type = Util.inferContentType(uri, intent.getStringExtra(EXTENSION_EXTRA));
+ if (type == C.TYPE_DASH) {
+ mediaSource =
+ new DashMediaSource.Factory(dataSourceFactory)
+ .setDrmSessionManager(drmSessionManager)
+ .createMediaSource(uri);
+ } else if (type == C.TYPE_OTHER) {
+ mediaSource =
+ new ProgressiveMediaSource.Factory(dataSourceFactory)
+ .setDrmSessionManager(drmSessionManager)
+ .createMediaSource(uri);
+ } else {
+ throw new IllegalStateException();
+ }
+ SimpleExoPlayer player = new SimpleExoPlayer.Builder(getApplicationContext()).build();
+ player.prepare(mediaSource);
+ player.play();
+ player.setRepeatMode(Player.REPEAT_MODE_ALL);
+
+ surfaceControl =
+ new SurfaceControl.Builder()
+ .setName(SURFACE_CONTROL_NAME)
+ .setBufferSize(/* width= */ 0, /* height= */ 0)
+ .build();
+ videoSurface = new Surface(surfaceControl);
+ player.setVideoSurface(videoSurface);
+ MainActivity.player = player;
+ }
+
+ private void setCurrentOutputView(@Nullable SurfaceView surfaceView) {
+ currentOutputView = surfaceView;
+ if (surfaceView != null && surfaceView.getHolder().getSurface() != null) {
+ reparent(surfaceView);
+ }
+ }
+
+ private void attachSurfaceListener(SurfaceView surfaceView) {
+ surfaceView
+ .getHolder()
+ .addCallback(
+ new SurfaceHolder.Callback() {
+ @Override
+ public void surfaceCreated(SurfaceHolder surfaceHolder) {
+ if (surfaceView == currentOutputView) {
+ reparent(surfaceView);
+ }
+ }
+
+ @Override
+ public void surfaceChanged(
+ SurfaceHolder surfaceHolder, int format, int width, int height) {}
+
+ @Override
+ public void surfaceDestroyed(SurfaceHolder surfaceHolder) {}
+ });
+ }
+
+ private static void reparent(@Nullable SurfaceView surfaceView) {
+ SurfaceControl surfaceControl = Assertions.checkNotNull(MainActivity.surfaceControl);
+ if (surfaceView == null) {
+ new SurfaceControl.Transaction()
+ .reparent(surfaceControl, /* newParent= */ null)
+ .setBufferSize(surfaceControl, /* w= */ 0, /* h= */ 0)
+ .setVisibility(surfaceControl, /* visible= */ false)
+ .apply();
+ } else {
+ SurfaceControl newParentSurfaceControl = surfaceView.getSurfaceControl();
+ new SurfaceControl.Transaction()
+ .reparent(surfaceControl, newParentSurfaceControl)
+ .setBufferSize(surfaceControl, surfaceView.getWidth(), surfaceView.getHeight())
+ .setVisibility(surfaceControl, /* visible= */ true)
+ .apply();
+ }
+ }
+}
diff --git a/demos/surface/src/main/res/layout/main_activity.xml b/demos/surface/src/main/res/layout/main_activity.xml
new file mode 100644
index 0000000000..829602275d
--- /dev/null
+++ b/demos/surface/src/main/res/layout/main_activity.xml
@@ -0,0 +1,51 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/demos/surface/src/main/res/mipmap-hdpi/ic_launcher.png b/demos/surface/src/main/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 0000000000..adaa93220e
Binary files /dev/null and b/demos/surface/src/main/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/demos/surface/src/main/res/mipmap-mdpi/ic_launcher.png b/demos/surface/src/main/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 0000000000..9b6f7d5e80
Binary files /dev/null and b/demos/surface/src/main/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/demos/surface/src/main/res/mipmap-xhdpi/ic_launcher.png b/demos/surface/src/main/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 0000000000..2101026c9f
Binary files /dev/null and b/demos/surface/src/main/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/demos/surface/src/main/res/mipmap-xxhdpi/ic_launcher.png b/demos/surface/src/main/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 0000000000..223ec8bd11
Binary files /dev/null and b/demos/surface/src/main/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/demos/surface/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/demos/surface/src/main/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 0000000000..698ed68c42
Binary files /dev/null and b/demos/surface/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/library/ui/src/main/res/values-et-rEE/strings.xml b/demos/surface/src/main/res/values/strings.xml
similarity index 52%
rename from library/ui/src/main/res/values-et-rEE/strings.xml
rename to demos/surface/src/main/res/values/strings.xml
index 7a01bd9d5a..9ba24bd368 100644
--- a/library/ui/src/main/res/values-et-rEE/strings.xml
+++ b/demos/surface/src/main/res/values/strings.xml
@@ -1,6 +1,5 @@
-
-
- "Eelmine lugu"
- "Järgmine lugu"
- "Peata"
- "Esita"
- "Peata"
- "Keri tagasi"
- "Keri edasi"
+
+ ExoPlayer SurfaceControl demo
+ No output
+ Full screen
+ New activity
+
diff --git a/extensions/README.md b/extensions/README.md
new file mode 100644
index 0000000000..bf0effb358
--- /dev/null
+++ b/extensions/README.md
@@ -0,0 +1,5 @@
+# ExoPlayer extensions #
+
+ExoPlayer extensions are modules that depend on external libraries to provide
+additional functionality. Browse the individual extensions and their READMEs to
+learn more.
diff --git a/extensions/av1/README.md b/extensions/av1/README.md
new file mode 100644
index 0000000000..54e27a3b87
--- /dev/null
+++ b/extensions/av1/README.md
@@ -0,0 +1,134 @@
+# ExoPlayer AV1 extension #
+
+The AV1 extension provides `Libgav1VideoRenderer`, which uses libgav1 native
+library to decode AV1 videos.
+
+## License note ##
+
+Please note that whilst the code in this repository is licensed under
+[Apache 2.0][], using this extension also requires building and including one or
+more external libraries as described below. These are licensed separately.
+
+[Apache 2.0]: https://github.com/google/ExoPlayer/blob/release-v2/LICENSE
+
+## Build instructions (Linux, macOS) ##
+
+To use this extension you need to clone the ExoPlayer repository and depend on
+its modules locally. Instructions for doing this can be found in ExoPlayer's
+[top level README][].
+
+In addition, it's necessary to fetch cpu_features library and libgav1 with its
+dependencies as follows:
+
+* Set the following environment variables:
+
+```
+cd ""
+EXOPLAYER_ROOT="$(pwd)"
+AV1_EXT_PATH="${EXOPLAYER_ROOT}/extensions/av1/src/main"
+```
+
+* Fetch cpu_features library:
+
+```
+cd "${AV1_EXT_PATH}/jni" && \
+git clone https://github.com/google/cpu_features
+```
+
+* Fetch libgav1:
+
+```
+cd "${AV1_EXT_PATH}/jni" && \
+git clone https://chromium.googlesource.com/codecs/libgav1 libgav1
+```
+
+* Fetch Abseil:
+
+```
+cd "${AV1_EXT_PATH}/jni/libgav1" && \
+git clone https://github.com/abseil/abseil-cpp.git third_party/abseil-cpp
+```
+
+* [Install CMake][].
+
+Having followed these steps, gradle will build the extension automatically when
+run on the command line or via Android Studio, using [CMake][] and [Ninja][]
+to configure and build libgav1 and the extension's [JNI wrapper library][].
+
+[top level README]: https://github.com/google/ExoPlayer/blob/release-v2/README.md
+[Install CMake]: https://developer.android.com/studio/projects/install-ndk
+[CMake]: https://cmake.org/
+[Ninja]: https://ninja-build.org
+[JNI wrapper library]: https://github.com/google/ExoPlayer/blob/release-v2/extensions/av1/src/main/jni/gav1_jni.cc
+
+## Build instructions (Windows) ##
+
+We do not provide support for building this extension on Windows, however it
+should be possible to follow the Linux instructions in [Windows PowerShell][].
+
+[Windows PowerShell]: https://docs.microsoft.com/en-us/powershell/scripting/getting-started/getting-started-with-windows-powershell
+
+## Using the extension ##
+
+Once you've followed the instructions above to check out, build and depend on
+the extension, the next step is to tell ExoPlayer to use `Libgav1VideoRenderer`.
+How you do this depends on which player API you're using:
+
+* If you're passing a `DefaultRenderersFactory` to `SimpleExoPlayer.Builder`,
+ you can enable using the extension by setting the `extensionRendererMode`
+ parameter of the `DefaultRenderersFactory` constructor to
+ `EXTENSION_RENDERER_MODE_ON`. This will use `Libgav1VideoRenderer` for
+ playback if `MediaCodecVideoRenderer` doesn't support decoding the input AV1
+ stream. Pass `EXTENSION_RENDERER_MODE_PREFER` to give `Libgav1VideoRenderer`
+ priority over `MediaCodecVideoRenderer`.
+* If you've subclassed `DefaultRenderersFactory`, add a `Libvgav1VideoRenderer`
+ to the output list in `buildVideoRenderers`. ExoPlayer will use the first
+ `Renderer` in the list that supports the input media format.
+* If you've implemented your own `RenderersFactory`, return a
+ `Libgav1VideoRenderer` instance from `createRenderers`. ExoPlayer will use the
+ first `Renderer` in the returned array that supports the input media format.
+* If you're using `ExoPlayer.Builder`, pass a `Libgav1VideoRenderer` in the
+ array of `Renderer`s. ExoPlayer will use the first `Renderer` in the list that
+ supports the input media format.
+
+Note: These instructions assume you're using `DefaultTrackSelector`. If you have
+a custom track selector the choice of `Renderer` is up to your implementation.
+You need to make sure you are passing a `Libgav1VideoRenderer` to the player and
+then you need to implement your own logic to use the renderer for a given track.
+
+## Using the extension in the demo application ##
+
+To try out playback using the extension in the [demo application][], see
+[enabling extension decoders][].
+
+[demo application]: https://exoplayer.dev/demo-application.html
+[enabling extension decoders]: https://exoplayer.dev/demo-application.html#enabling-extension-decoders
+
+## Rendering options ##
+
+There are two possibilities for rendering the output `Libgav1VideoRenderer`
+gets from the libgav1 decoder:
+
+* GL rendering using GL shader for color space conversion
+ * If you are using `SimpleExoPlayer` with `PlayerView`, enable this option by
+ setting `surface_type` of `PlayerView` to be
+ `video_decoder_gl_surface_view`.
+ * Otherwise, enable this option by sending `Libgav1VideoRenderer` a message
+ of type `C.MSG_SET_VIDEO_DECODER_OUTPUT_BUFFER_RENDERER` with an instance of
+ `VideoDecoderOutputBufferRenderer` as its object.
+
+* Native rendering using `ANativeWindow`
+ * If you are using `SimpleExoPlayer` with `PlayerView`, this option is enabled
+ by default.
+ * Otherwise, enable this option by sending `Libgav1VideoRenderer` a message of
+ type `C.MSG_SET_SURFACE` with an instance of `SurfaceView` as its object.
+
+Note: Although the default option uses `ANativeWindow`, based on our testing the
+GL rendering mode has better performance, so should be preferred
+
+## Links ##
+
+* [Javadoc][]: Classes matching `com.google.android.exoplayer2.ext.av1.*`
+ belong to this module.
+
+[Javadoc]: https://exoplayer.dev/doc/reference/index.html
diff --git a/extensions/av1/build.gradle b/extensions/av1/build.gradle
new file mode 100644
index 0000000000..0b539d551b
--- /dev/null
+++ b/extensions/av1/build.gradle
@@ -0,0 +1,73 @@
+// Copyright (C) 2019 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+apply from: '../../constants.gradle'
+apply plugin: 'com.android.library'
+
+android {
+ compileSdkVersion project.ext.compileSdkVersion
+
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_1_8
+ targetCompatibility JavaVersion.VERSION_1_8
+ }
+
+ defaultConfig {
+ minSdkVersion project.ext.minSdkVersion
+ targetSdkVersion project.ext.targetSdkVersion
+ consumerProguardFiles 'proguard-rules.txt'
+
+ externalNativeBuild {
+ cmake {
+ // Debug CMake build type causes video frames to drop,
+ // so native library should always use Release build type.
+ arguments "-DCMAKE_BUILD_TYPE=Release"
+ targets "gav1JNI"
+ }
+ }
+ }
+
+ // This option resolves the problem of finding libgav1JNI.so
+ // on multiple paths. The first one found is picked.
+ packagingOptions {
+ pickFirst 'lib/arm64-v8a/libgav1JNI.so'
+ pickFirst 'lib/armeabi-v7a/libgav1JNI.so'
+ pickFirst 'lib/x86/libgav1JNI.so'
+ pickFirst 'lib/x86_64/libgav1JNI.so'
+ }
+
+ sourceSets.main {
+ // As native JNI library build is invoked from gradle, this is
+ // not needed. However, it exposes the built library and keeps
+ // consistency with the other extensions.
+ jniLibs.srcDir 'src/main/libs'
+ }
+}
+
+// Configure the native build only if libgav1 is present, to avoid gradle sync
+// failures if libgav1 hasn't been checked out according to the README and CMake
+// isn't installed.
+if (project.file('src/main/jni/libgav1').exists()) {
+ android.externalNativeBuild.cmake.path = 'src/main/jni/CMakeLists.txt'
+ android.externalNativeBuild.cmake.version = '3.7.1+'
+}
+
+dependencies {
+ implementation project(modulePrefix + 'library-core')
+ implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion
+}
+
+ext {
+ javadocTitle = 'AV1 extension'
+}
+apply from: '../../javadoc_library.gradle'
diff --git a/extensions/av1/proguard-rules.txt b/extensions/av1/proguard-rules.txt
new file mode 100644
index 0000000000..9d73f7e2b5
--- /dev/null
+++ b/extensions/av1/proguard-rules.txt
@@ -0,0 +1,7 @@
+# Proguard rules specific to the AV1 extension.
+
+# This prevents the names of native methods from being obfuscated.
+-keepclasseswithmembernames class * {
+ native ;
+}
+
diff --git a/extensions/av1/src/main/AndroidManifest.xml b/extensions/av1/src/main/AndroidManifest.xml
new file mode 100644
index 0000000000..af85bacdf6
--- /dev/null
+++ b/extensions/av1/src/main/AndroidManifest.xml
@@ -0,0 +1,17 @@
+
+
+
+
diff --git a/extensions/av1/src/main/java/com/google/android/exoplayer2/ext/av1/Gav1Decoder.java b/extensions/av1/src/main/java/com/google/android/exoplayer2/ext/av1/Gav1Decoder.java
new file mode 100644
index 0000000000..687ac47f2a
--- /dev/null
+++ b/extensions/av1/src/main/java/com/google/android/exoplayer2/ext/av1/Gav1Decoder.java
@@ -0,0 +1,234 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.ext.av1;
+
+import android.view.Surface;
+import androidx.annotation.Nullable;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.decoder.SimpleDecoder;
+import com.google.android.exoplayer2.util.Util;
+import com.google.android.exoplayer2.video.VideoDecoderInputBuffer;
+import com.google.android.exoplayer2.video.VideoDecoderOutputBuffer;
+import java.nio.ByteBuffer;
+
+/** Gav1 decoder. */
+/* package */ final class Gav1Decoder
+ extends SimpleDecoder {
+
+ // LINT.IfChange
+ private static final int GAV1_ERROR = 0;
+ private static final int GAV1_OK = 1;
+ private static final int GAV1_DECODE_ONLY = 2;
+ // LINT.ThenChange(../../../../../../../jni/gav1_jni.cc)
+
+ private final long gav1DecoderContext;
+
+ @C.VideoOutputMode private volatile int outputMode;
+
+ /**
+ * Creates a Gav1Decoder.
+ *
+ * @param numInputBuffers Number of input buffers.
+ * @param numOutputBuffers Number of output buffers.
+ * @param initialInputBufferSize The initial size of each input buffer, in bytes.
+ * @param threads Number of threads libgav1 will use to decode.
+ * @throws Gav1DecoderException Thrown if an exception occurs when initializing the decoder.
+ */
+ public Gav1Decoder(
+ int numInputBuffers, int numOutputBuffers, int initialInputBufferSize, int threads)
+ throws Gav1DecoderException {
+ super(
+ new VideoDecoderInputBuffer[numInputBuffers],
+ new VideoDecoderOutputBuffer[numOutputBuffers]);
+ if (!Gav1Library.isAvailable()) {
+ throw new Gav1DecoderException("Failed to load decoder native library.");
+ }
+ gav1DecoderContext = gav1Init(threads);
+ if (gav1DecoderContext == GAV1_ERROR || gav1CheckError(gav1DecoderContext) == GAV1_ERROR) {
+ throw new Gav1DecoderException(
+ "Failed to initialize decoder. Error: " + gav1GetErrorMessage(gav1DecoderContext));
+ }
+ setInitialInputBufferSize(initialInputBufferSize);
+ }
+
+ @Override
+ public String getName() {
+ return "libgav1";
+ }
+
+ /**
+ * Sets the output mode for frames rendered by the decoder.
+ *
+ * @param outputMode The output mode.
+ */
+ public void setOutputMode(@C.VideoOutputMode int outputMode) {
+ this.outputMode = outputMode;
+ }
+
+ @Override
+ protected VideoDecoderInputBuffer createInputBuffer() {
+ return new VideoDecoderInputBuffer();
+ }
+
+ @Override
+ protected VideoDecoderOutputBuffer createOutputBuffer() {
+ return new VideoDecoderOutputBuffer(this::releaseOutputBuffer);
+ }
+
+ @Nullable
+ @Override
+ protected Gav1DecoderException decode(
+ VideoDecoderInputBuffer inputBuffer, VideoDecoderOutputBuffer outputBuffer, boolean reset) {
+ ByteBuffer inputData = Util.castNonNull(inputBuffer.data);
+ int inputSize = inputData.limit();
+ if (gav1Decode(gav1DecoderContext, inputData, inputSize) == GAV1_ERROR) {
+ return new Gav1DecoderException(
+ "gav1Decode error: " + gav1GetErrorMessage(gav1DecoderContext));
+ }
+
+ boolean decodeOnly = inputBuffer.isDecodeOnly();
+ if (!decodeOnly) {
+ outputBuffer.init(inputBuffer.timeUs, outputMode, /* supplementalData= */ null);
+ }
+ // We need to dequeue the decoded frame from the decoder even when the input data is
+ // decode-only.
+ int getFrameResult = gav1GetFrame(gav1DecoderContext, outputBuffer, decodeOnly);
+ if (getFrameResult == GAV1_ERROR) {
+ return new Gav1DecoderException(
+ "gav1GetFrame error: " + gav1GetErrorMessage(gav1DecoderContext));
+ }
+ if (getFrameResult == GAV1_DECODE_ONLY) {
+ outputBuffer.addFlag(C.BUFFER_FLAG_DECODE_ONLY);
+ }
+ if (!decodeOnly) {
+ outputBuffer.colorInfo = inputBuffer.colorInfo;
+ }
+
+ return null;
+ }
+
+ @Override
+ protected Gav1DecoderException createUnexpectedDecodeException(Throwable error) {
+ return new Gav1DecoderException("Unexpected decode error", error);
+ }
+
+ @Override
+ public void release() {
+ super.release();
+ gav1Close(gav1DecoderContext);
+ }
+
+ @Override
+ protected void releaseOutputBuffer(VideoDecoderOutputBuffer buffer) {
+ // Decode only frames do not acquire a reference on the internal decoder buffer and thus do not
+ // require a call to gav1ReleaseFrame.
+ if (buffer.mode == C.VIDEO_OUTPUT_MODE_SURFACE_YUV && !buffer.isDecodeOnly()) {
+ gav1ReleaseFrame(gav1DecoderContext, buffer);
+ }
+ super.releaseOutputBuffer(buffer);
+ }
+
+ /**
+ * Renders output buffer to the given surface. Must only be called when in {@link
+ * C#VIDEO_OUTPUT_MODE_SURFACE_YUV} mode.
+ *
+ * @param outputBuffer Output buffer.
+ * @param surface Output surface.
+ * @throws Gav1DecoderException Thrown if called with invalid output mode or frame rendering
+ * fails.
+ */
+ public void renderToSurface(VideoDecoderOutputBuffer outputBuffer, Surface surface)
+ throws Gav1DecoderException {
+ if (outputBuffer.mode != C.VIDEO_OUTPUT_MODE_SURFACE_YUV) {
+ throw new Gav1DecoderException("Invalid output mode.");
+ }
+ if (gav1RenderFrame(gav1DecoderContext, surface, outputBuffer) == GAV1_ERROR) {
+ throw new Gav1DecoderException(
+ "Buffer render error: " + gav1GetErrorMessage(gav1DecoderContext));
+ }
+ }
+
+ /**
+ * Initializes a libgav1 decoder.
+ *
+ * @param threads Number of threads to be used by a libgav1 decoder.
+ * @return The address of the decoder context or {@link #GAV1_ERROR} if there was an error.
+ */
+ private native long gav1Init(int threads);
+
+ /**
+ * Deallocates the decoder context.
+ *
+ * @param context Decoder context.
+ */
+ private native void gav1Close(long context);
+
+ /**
+ * Decodes the encoded data passed.
+ *
+ * @param context Decoder context.
+ * @param encodedData Encoded data.
+ * @param length Length of the data buffer.
+ * @return {@link #GAV1_OK} if successful, {@link #GAV1_ERROR} if an error occurred.
+ */
+ private native int gav1Decode(long context, ByteBuffer encodedData, int length);
+
+ /**
+ * Gets the decoded frame.
+ *
+ * @param context Decoder context.
+ * @param outputBuffer Output buffer for the decoded frame.
+ * @return {@link #GAV1_OK} if successful, {@link #GAV1_DECODE_ONLY} if successful but the frame
+ * is decode-only, {@link #GAV1_ERROR} if an error occurred.
+ */
+ private native int gav1GetFrame(
+ long context, VideoDecoderOutputBuffer outputBuffer, boolean decodeOnly);
+
+ /**
+ * Renders the frame to the surface. Used with {@link C#VIDEO_OUTPUT_MODE_SURFACE_YUV} only.
+ *
+ * @param context Decoder context.
+ * @param surface Output surface.
+ * @param outputBuffer Output buffer with the decoded frame.
+ * @return {@link #GAV1_OK} if successful, {@link #GAV1_ERROR} if an error occured.
+ */
+ private native int gav1RenderFrame(
+ long context, Surface surface, VideoDecoderOutputBuffer outputBuffer);
+
+ /**
+ * Releases the frame. Used with {@link C#VIDEO_OUTPUT_MODE_SURFACE_YUV} only.
+ *
+ * @param context Decoder context.
+ * @param outputBuffer Output buffer.
+ */
+ private native void gav1ReleaseFrame(long context, VideoDecoderOutputBuffer outputBuffer);
+
+ /**
+ * Returns a human-readable string describing the last error encountered in the given context.
+ *
+ * @param context Decoder context.
+ * @return A string describing the last encountered error.
+ */
+ private native String gav1GetErrorMessage(long context);
+
+ /**
+ * Returns whether an error occured.
+ *
+ * @param context Decoder context.
+ * @return {@link #GAV1_OK} if there was no error, {@link #GAV1_ERROR} if an error occured.
+ */
+ private native int gav1CheckError(long context);
+}
diff --git a/extensions/av1/src/main/java/com/google/android/exoplayer2/ext/av1/Gav1DecoderException.java b/extensions/av1/src/main/java/com/google/android/exoplayer2/ext/av1/Gav1DecoderException.java
new file mode 100644
index 0000000000..9d8692c581
--- /dev/null
+++ b/extensions/av1/src/main/java/com/google/android/exoplayer2/ext/av1/Gav1DecoderException.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.ext.av1;
+
+import com.google.android.exoplayer2.video.VideoDecoderException;
+
+/** Thrown when a libgav1 decoder error occurs. */
+public final class Gav1DecoderException extends VideoDecoderException {
+
+ /* package */ Gav1DecoderException(String message) {
+ super(message);
+ }
+
+ /* package */ Gav1DecoderException(String message, Throwable cause) {
+ super(message, cause);
+ }
+}
diff --git a/extensions/av1/src/main/java/com/google/android/exoplayer2/ext/av1/Gav1Library.java b/extensions/av1/src/main/java/com/google/android/exoplayer2/ext/av1/Gav1Library.java
new file mode 100644
index 0000000000..7907fa4623
--- /dev/null
+++ b/extensions/av1/src/main/java/com/google/android/exoplayer2/ext/av1/Gav1Library.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.ext.av1;
+
+import com.google.android.exoplayer2.ExoPlayerLibraryInfo;
+import com.google.android.exoplayer2.util.LibraryLoader;
+
+/** Configures and queries the underlying native library. */
+public final class Gav1Library {
+
+ static {
+ ExoPlayerLibraryInfo.registerModule("goog.exo.gav1");
+ }
+
+ private static final LibraryLoader LOADER = new LibraryLoader("gav1JNI");
+
+ private Gav1Library() {}
+
+ /** Returns whether the underlying library is available, loading it if necessary. */
+ public static boolean isAvailable() {
+ return LOADER.isAvailable();
+ }
+}
diff --git a/extensions/av1/src/main/java/com/google/android/exoplayer2/ext/av1/Libgav1VideoRenderer.java b/extensions/av1/src/main/java/com/google/android/exoplayer2/ext/av1/Libgav1VideoRenderer.java
new file mode 100644
index 0000000000..3d10c2579b
--- /dev/null
+++ b/extensions/av1/src/main/java/com/google/android/exoplayer2/ext/av1/Libgav1VideoRenderer.java
@@ -0,0 +1,197 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.ext.av1;
+
+import static java.lang.Runtime.getRuntime;
+
+import android.os.Handler;
+import android.view.Surface;
+import androidx.annotation.Nullable;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.ExoPlaybackException;
+import com.google.android.exoplayer2.ExoPlayer;
+import com.google.android.exoplayer2.Format;
+import com.google.android.exoplayer2.PlayerMessage.Target;
+import com.google.android.exoplayer2.RendererCapabilities;
+import com.google.android.exoplayer2.decoder.SimpleDecoder;
+import com.google.android.exoplayer2.drm.DrmSessionManager;
+import com.google.android.exoplayer2.drm.ExoMediaCrypto;
+import com.google.android.exoplayer2.util.MimeTypes;
+import com.google.android.exoplayer2.util.TraceUtil;
+import com.google.android.exoplayer2.util.Util;
+import com.google.android.exoplayer2.video.SimpleDecoderVideoRenderer;
+import com.google.android.exoplayer2.video.VideoDecoderException;
+import com.google.android.exoplayer2.video.VideoDecoderInputBuffer;
+import com.google.android.exoplayer2.video.VideoDecoderOutputBuffer;
+import com.google.android.exoplayer2.video.VideoDecoderOutputBufferRenderer;
+import com.google.android.exoplayer2.video.VideoRendererEventListener;
+
+/**
+ * Decodes and renders video using libgav1 decoder.
+ *
+ *