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..cb4cfaada1 100644 --- a/.gitignore +++ b/.gitignore @@ -37,16 +37,30 @@ 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/cpu_features +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..e2c54bcf61 --- /dev/null +++ b/.hgignore @@ -0,0 +1,83 @@ +# 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 +jacoco.exec +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 +extensions/av1/src/main/jni/cpu_features + +# 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..ac4c41b0fe 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# ExoPlayer # +# ExoPlayer # ExoPlayer is an application level media player for Android. It provides an alternative to Android’s MediaPlayer API for playing audio and video both @@ -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..ffdfc1c1e7 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -1,398 +1,2554 @@ -# Release notes # +# Release notes -### r2.4.4 ### +### dev-v2 (not yet released) -* HLS/MPEG-TS: Some initial optimizations of MPEG-TS extractor performance - ([#3040](https://github.com/google/ExoPlayer/issues/3040)). -* HLS: Fix propagation of format identifier for CEA-608 - ([#3033](https://github.com/google/ExoPlayer/issues/3033)). -* HLS: Detect playlist stuck and reset conditions - ([#2872](https://github.com/google/ExoPlayer/issues/2872)). -* Video: Fix video dimension reporting on some devices - ([#3007](https://github.com/google/ExoPlayer/issues/3007)). +* Track selection: + * Add option to specify multiple preferred audio or text languages. +* Data sources: + * Add support for `android.resource` URI scheme in `RawResourceDataSource` + ([#7866](https://github.com/google/ExoPlayer/issues/7866)). +* Text: + * Add support for `\h` SSA/ASS style override code (non-breaking space). -### r2.4.3 ### +### 2.12.0 (2020-09-11) ### -* Audio: Workaround custom audio decoders misreporting their maximum supported - channel counts ([#2940](https://github.com/google/ExoPlayer/issues/2940)). -* Audio: Workaround for broken MediaTek raw decoder on some devices - ([#2873](https://github.com/google/ExoPlayer/issues/2873)). -* Captions: Fix TTML captions appearing at the top of the screen - ([#2953](https://github.com/google/ExoPlayer/issues/2953)). -* Captions: Fix handling of some DVB subtitles - ([#2957](https://github.com/google/ExoPlayer/issues/2957)). -* Track selection: Fix setSelectionOverride(index, tracks, null) - ([#2988](https://github.com/google/ExoPlayer/issues/2988)). -* GVR extension: Add support for mono input - ([#2710](https://github.com/google/ExoPlayer/issues/2710)). -* FLAC extension: Fix failing build - ([#2977](https://github.com/google/ExoPlayer/pull/2977)). -* Misc bugfixes. +To learn more about what's new in 2.12, read the corresponding +[blog post](https://medium.com/google-exoplayer/exoplayer-2-12-whats-new-e43ef8ff72e7). -### r2.4.2 ### +* Core library: + * `Player`: + * Add a top level playlist API based on a new `MediaItem` class + ([#6161](https://github.com/google/ExoPlayer/issues/6161)). The new + methods for playlist manipulation are `setMediaItem(s)`, + `addMediaItem(s)`, `moveMediaItem(s)`, `removeMediaItem(s)` and + `clearMediaItems`. The playlist can be queried using + `getMediaItemCount` and `getMediaItemAt`. This API should be used + instead of `ConcatenatingMediaSource` in most cases. Learn more by + reading + [this blog post](https://medium.com/google-exoplayer/a-top-level-playlist-api-for-exoplayer-abe0a24edb55). + * Add `getCurrentMediaItem` for getting the currently playing item in + the playlist. + * Add `EventListener.onMediaItemTransition` to report when playback + transitions from one item to another in the playlist. + * Add `play` and `pause` convenience methods. They are equivalent to + `setPlayWhenReady(true)` and `setPlayWhenReady(false)` respectively. + * Add `getCurrentLiveOffset` for getting the offset of the current + playback position from the live edge of a live stream. + * Add `getTrackSelector` for getting the `TrackSelector` used by the + player. + * Add `AudioComponent.setAudioSessionId` to set the audio session ID. + This method is also available on `SimpleExoPlayer`. + * Remove `PlaybackParameters.skipSilence`, and replace it with + `AudioComponent.setSkipSilenceEnabled`. This method is also + available on `SimpleExoPlayer`. An + `AudioListener.onSkipSilenceEnabledChanged` callback is also + added. + * Add `TextComponent.getCurrentCues` to get the current cues. This + method is also available on `SimpleExoPlayer`. The current cues are + no longer automatically forwarded to a `TextOutput` when it's added + to a `SimpleExoPlayer`. + * Add `Player.DeviceComponent` to query and control the device volume. + `SimpleExoPlayer` implements this interface. + * Deprecate and rename `getPlaybackError` to `getPlayerError` for + consistency. + * Deprecate and rename `onLoadingChanged` to `onIsLoadingChanged` for + consistency. + * Deprecate `EventListener.onPlayerStateChanged`, replacing it with + `EventListener.onPlayWhenReadyChanged` and + `EventListener.onPlaybackStateChanged`. + * Deprecate `EventListener.onSeekProcessed` because seek changes now + happen instantly and listening to `onPositionDiscontinuity` is + sufficient. + * `ExoPlayer`: + * Add `setMediaSource(s)` and `addMediaSource(s)` to `ExoPlayer`, for + adding `MediaSource` instances directly to the playlist. + * Add `ExoPlayer.setPauseAtEndOfMediaItems` to let the player pause at + the end of each media item + ([#5660](https://github.com/google/ExoPlayer/issues/5660)). + * Allow passing `C.TIME_END_OF_SOURCE` to `PlayerMessage.setPosition` + to send a `PlayerMessage` at the end of a stream. + * `SimpleExoPlayer`: + * `SimpleExoPlayer` implements the new `MediaItem` based playlist API, + using a `MediaSourceFactory` to convert `MediaItem` instances to + playable `MediaSource` instances. A `DefaultMediaSourceFactory` is + used by default. `Builder.setMediaSourceFactory` allows setting a + custom factory. + * Add additional options to `Builder` that were previously only + accessible via setters. + * Add opt-in to verify correct thread usage with + `setThrowsWhenUsingWrongThread(true)` + ([#4463](https://github.com/google/ExoPlayer/issues/4463)). + * `Format`: + * Add a `Builder` and deprecate all `create` methods and most + `Format.copyWith` methods. + * Split `bitrate` into `averageBitrate` and `peakBitrate` + ([#2863](https://github.com/google/ExoPlayer/issues/2863)). + * `LoadControl`: + * Add a `playbackPositionUs` parameter to `shouldContinueLoading`. + * Set the default minimum buffer duration in `DefaultLoadControl` to + 50 seconds (equal to the default maximum buffer), and treat audio + and video the same. + * Add a `MetadataRetriever` API for retrieving track information and + static metadata for a media item + ([#3609](https://github.com/google/ExoPlayer/issues/3609)). + * Attach an identifier and extra information to load error events passed + to `LoadErrorHandlingPolicy` + ([#7309](https://github.com/google/ExoPlayer/issues/7309)). + `LoadErrorHandlingPolicy` implementations should migrate to implementing + the non-deprecated methods of the interface. + * Add an option to `MergingMediaSource` to adjust the time offsets between + the merged sources + ([#6103](https://github.com/google/ExoPlayer/issues/6103)). + * Move `MediaSourceEventListener.LoadEventInfo` and + `MediaSourceEventListener.MediaLoadData` to be top-level classes in + `com.google.android.exoplayer2.source`. + * Move `SimpleDecoderVideoRenderer` and `SimpleDecoderAudioRenderer` to + `DecoderVideoRenderer` and `DecoderAudioRenderer` respectively, and + generalize them to work with `Decoder` rather than `SimpleDecoder`. + * Deprecate `C.MSG_*` constants, replacing them with constants in + `Renderer`. + * Split the `library-core` module into `library-core`, `library-common` + and `library-extractor`. The `library-core` module has an API dependency + on both of the new modules, so this change should be transparent to + developers including ExoPlayer using Gradle dependencies. + * Add a dependency on Guava. +* Video: + * Pass frame rate hint to `Surface.setFrameRate` on Android 11. + * Fix incorrect aspect ratio when transitioning from one video to another + with the same resolution, but a different pixel aspect ratio + ([#6646](https://github.com/google/ExoPlayer/issues/6646)). +* Audio: + * Add experimental support for power efficient playback using audio + offload. + * Add support for using framework audio speed adjustment instead of + ExoPlayer's implementation + ([#7502](https://github.com/google/ExoPlayer/issues/7502)). This option + can be set using + `DefaultRenderersFactory.setEnableAudioTrackPlaybackParams`. + * Add an event for the audio position starting to advance, to make it + easier for apps to determine when audio playout started + ([#7577](https://github.com/google/ExoPlayer/issues/7577)). + * Generalize support for floating point audio. + * Add an option to `DefaultAudioSink` for enabling floating point + output. This option can also be set using + `DefaultRenderersFactory.setEnableAudioFloatOutput`. + * Add floating point output capability to `MediaCodecAudioRenderer` + and `LibopusAudioRenderer`, which is enabled automatically if the + audio sink supports floating point output and if it makes sense for + the content being played. + * Enable the floating point output capability of `FfmpegAudioRenderer` + automatically if the audio sink supports floating point output and + if it makes sense for the content being played. The option to + manually enable floating point output has been removed, since this + now done with the generalized option on `DefaultAudioSink`. + * In `MediaCodecAudioRenderer`, stop passing audio samples through + `MediaCodec` when playing PCM audio or encoded audio using passthrough + mode. + * Reuse audio decoders when transitioning through playlists of gapless + audio, rather than reinstantiating them. + * Check `DefaultAudioSink` supports passthrough, in addition to checking + the `AudioCapabilities` + ([#7404](https://github.com/google/ExoPlayer/issues/7404)). +* Text: + * Many of the changes described below improve support for Japanese + subtitles. Read + [this blog post](https://medium.com/google-exoplayer/improved-japanese-subtitle-support-7598fee12cf4) + to learn more. + * Add a WebView-based output option to `SubtitleView`. This can display + some features not supported by the existing Canvas-based output such as + vertical text and rubies. It can be enabled by calling + `SubtitleView#setViewType(VIEW_TYPE_WEB)`. + * Recreate the decoder when handling and swallowing decode errors in + `TextRenderer`. This fixes a case where playback would never end when + playing content with malformed subtitles + ([#7590](https://github.com/google/ExoPlayer/issues/7590)). + * Only apply `CaptionManager` font scaling in + `SubtitleView.setUserDefaultTextSize` if the `CaptionManager` is + enabled. + * Improve positioning of vertical cues when rendered horizontally. + * Redefine `Cue.lineType=LINE_TYPE_NUMBER` in terms of aligning the cue + text lines to grid of viewport lines. Only consider `Cue.lineAnchor` + when `Cue.lineType=LINE_TYPE_FRACTION`. + * WebVTT + * Add support for default + [text](https://www.w3.org/TR/webvtt1/#default-text-color) and + [background](https://www.w3.org/TR/webvtt1/#default-text-background) + colors ([#6581](https://github.com/google/ExoPlayer/issues/6581)). + * Update position alignment parsing to recognise `line-left`, `center` + and `line-right`. + * Implement steps 4-10 of the + [WebVTT line computation algorithm](https://www.w3.org/TR/webvtt1/#cue-computed-line). + * Stop parsing unsupported CSS properties. The spec provides an + [exhaustive list](https://www.w3.org/TR/webvtt1/#the-cue-pseudo-element) + of which properties are supported. + * Parse the `ruby-position` CSS property. + * Parse the `text-combine-upright` CSS property (i.e., tate-chu-yoko). + * Parse the `` and `` tags. + * TTML + * Parse the `tts:combineText` property (i.e., tate-chu-yoko). + * Parse t`tts:ruby` and `tts:rubyPosition` properties. + * CEA-608 + * Implement timing-out of stuck captions, as permitted by + ANSI/CTA-608-E R-2014 Annex C.9. The default timeout is set to 16 + seconds ([#7181](https://github.com/google/ExoPlayer/issues/7181)). + * Trim lines that exceed the maximum length of 32 characters + ([#7341](https://github.com/google/ExoPlayer/issues/7341)). + * Fix positioning of roll-up captions in the top half of the screen + ([#7475](https://github.com/google/ExoPlayer/issues/7475)). + * Stop automatically generating a CEA-608 track when playing + standalone MPEG-TS files. The previous behavior can still be + obtained by manually injecting a customized + `DefaultTsPayloadReaderFactory` into `TsExtractor`. +* Metadata: Add minimal DVB Application Information Table (AIT) support. +* DASH: + * Add support for canceling in-progress segment fetches + ([#2848](https://github.com/google/ExoPlayer/issues/2848)). + * Add support for CEA-708 embedded in FMP4. +* SmoothStreaming: + * Add support for canceling in-progress segment fetches + ([#2848](https://github.com/google/ExoPlayer/issues/2848)). +* HLS: + * Add support for discarding buffered media (e.g., to allow faster + adaptation to a higher quality variant) + ([#6322](https://github.com/google/ExoPlayer/issues/6322)). + * Add support for canceling in-progress segment fetches + ([#2848](https://github.com/google/ExoPlayer/issues/2848)). + * Respect 33-bit PTS wrapping when applying `X-TIMESTAMP-MAP` to WebVTT + timestamps ([#7464](https://github.com/google/ExoPlayer/issues/7464)). +* Extractors: + * Optimize the `Extractor` sniffing order to reduce start-up latency in + `DefaultExtractorsFactory` and `DefaultHlsExtractorsFactory` + ([#6410](https://github.com/google/ExoPlayer/issues/6410)). + * Use filename extensions and response header MIME types to further + optimize `Extractor` sniffing order on a per-media basis. + * MP3: Add `IndexSeeker` for accurate seeks in VBR MP3 streams + ([#6787](https://github.com/google/ExoPlayer/issues/6787)). This seeker + can be enabled by passing `FLAG_ENABLE_INDEX_SEEKING` to the + `Mp3Extractor`. A significant portion of the file may need to be scanned + when a seek is performed, which may be costly for large files. + * MP4: Fix playback of MP4 streams that contain Opus audio. + * FMP4: Add support for partially fragmented MP4s + ([#7308](https://github.com/google/ExoPlayer/issues/7308)). + * Matroska: + * Support Dolby Vision + ([#7267](https://github.com/google/ExoPlayer/issues/7267)). + * Populate `Format.label` with track titles. + * Remove support for the `Invisible` block header flag. + * MPEG-TS: Add support for MPEG-4 Part 2 and H.263 + ([#1603](https://github.com/google/ExoPlayer/issues/1603), + [#5107](https://github.com/google/ExoPlayer/issues/5107)). + * Ogg: Fix handling of non-contiguous pages + ([#7230](https://github.com/google/ExoPlayer/issues/7230)). +* UI: + * Add `StyledPlayerView` and `StyledPlayerControlView`, which provide a + more polished user experience than `PlayerView` and `PlayerControlView` + at the cost of decreased customizability. + * Remove the previously deprecated `SimpleExoPlayerView` and + `PlaybackControlView` classes, along with the corresponding + `exo_simple_player_view.xml` and `exo_playback_control_view.xml` layout + resources. Use the equivalent `PlayerView`, `PlayerControlView`, + `exo_player_view.xml` and `exo_player_control_view.xml` instead. + * Add setter methods to `PlayerView` and `PlayerControlView` to set + whether the rewind, fast forward, previous and next buttons are shown + ([#7410](https://github.com/google/ExoPlayer/issues/7410)). + * Update `TrackSelectionDialogBuilder` to use the AndroidX app compat + `AlertDialog` rather than the platform version, if available + ([#7357](https://github.com/google/ExoPlayer/issues/7357)). + * Make UI components dispatch previous, next, fast forward and rewind + actions via their `ControlDispatcher` + ([#6926](https://github.com/google/ExoPlayer/issues/6926)). +* Downloads and caching: + * Add `DownloadRequest.Builder`. + * Add `DownloadRequest.keySetId` to make it easier to store an offline + license keyset identifier alongside the other information that's + persisted in `DownloadIndex`. + * Support passing an `Executor` to `DefaultDownloaderFactory` on which + data downloads are performed. + * Parallelize and merge downloads in `SegmentDownloader` to improve + download speeds + ([#5978](https://github.com/google/ExoPlayer/issues/5978)). + * Replace `CacheDataSinkFactory` and `CacheDataSourceFactory` with + `CacheDataSink.Factory` and `CacheDataSource.Factory` respectively. + * Remove `DownloadConstructorHelper` and instead use + `CacheDataSource.Factory` directly. + * Add `Requirements.DEVICE_STORAGE_NOT_LOW`, which can be specified as a + requirement to a `DownloadManager` for it to proceed with downloading. + * For failed downloads, propagate the `Exception` that caused the failure + to `DownloadManager.Listener.onDownloadChanged`. + * Support multiple non-overlapping write locks for the same key in + `SimpleCache`. + * Remove `CacheUtil`. Equivalent functionality is provided by a new + `CacheWriter` class, `Cache.getCachedBytes`, `Cache.removeResource` and + `CacheKeyFactory.DEFAULT`. +* DRM: + * Remove previously deprecated APIs to inject `DrmSessionManager` into + `Renderer` instances. `DrmSessionManager` must now be injected into + `MediaSource` instances via the `MediaSource` factories. + * Add the ability to inject a custom `DefaultDrmSessionManager` into + `OfflineLicenseHelper` + ([#7078](https://github.com/google/ExoPlayer/issues/7078)). + * Keep DRM sessions alive for a short time before fully releasing them + ([#7011](https://github.com/google/ExoPlayer/issues/7011), + [#6725](https://github.com/google/ExoPlayer/issues/6725), + [#7066](https://github.com/google/ExoPlayer/issues/7066)). + * Remove support for `cbc1` and `cens` encrytion schemes. Support for + these schemes was removed from the Android platform from API level 30, + and the range of API levels for which they are supported is too small to + be useful. + * Remove generic types from DRM components. +* Track selection: + * Add `TrackSelection.shouldCancelMediaChunkLoad` to check whether an + ongoing load should be canceled + ([#2848](https://github.com/google/ExoPlayer/issues/2848)). + * Add `DefaultTrackSelector` constraints for minimum video resolution, + bitrate and frame rate + ([#4511](https://github.com/google/ExoPlayer/issues/4511)). + * Remove previously deprecated `DefaultTrackSelector` members. +* Data sources: + * Add `HttpDataSource.InvalidResponseCodeException#responseBody` field + ([#6853](https://github.com/google/ExoPlayer/issues/6853)). + * Add `DataSpec.Builder` and deprecate most `DataSpec` constructors. + * Add `DataSpec.customData` to allow applications to pass custom data + through `DataSource` chains. + * Deprecate `CacheDataSinkFactory` and `CacheDataSourceFactory`, which are + replaced by `CacheDataSink.Factory` and `CacheDataSource.Factory` + respectively. +* Analytics: + * Extend `EventTime` with more details about the current player state + ([#7332](https://github.com/google/ExoPlayer/issues/7332)). + * Add `AnalyticsListener.onVideoFrameProcessingOffset` to report how early + or late video frames are processed relative to them needing to be + presented. Video frame processing offset fields are also added to + `DecoderCounters`. + * Fix incorrect `MediaPeriodId` for some renderer errors reported by + `AnalyticsListener.onPlayerError`. + * Remove `onMediaPeriodCreated`, `onMediaPeriodReleased` and + `onReadingStarted` from `AnalyticsListener`. +* Test utils: Add `TestExoPlayer`, a utility class with APIs to create + `SimpleExoPlayer` instances with fake components for testing. +* Media2 extension: This is a new extension that makes it easy to use + ExoPlayer together with AndroidX Media2. Read + [this blog post](https://medium.com/google-exoplayer/the-media2-extension-for-exoplayer-d6b7d89b9063) + to learn more. +* Cast extension: Implement playlist API and deprecate the old queue + manipulation API. +* IMA extension: + * Migrate to new 'friendly obstruction' IMA SDK APIs, and allow apps to + register a purpose and detail reason for overlay views via + `AdsLoader.AdViewProvider`. + * Add support for audio-only ads display containers by returning `null` + from `AdsLoader.AdViewProvider.getAdViewGroup`, and allow skipping + audio-only ads via `ImaAdsLoader.skipAd` + ([#7689](https://github.com/google/ExoPlayer/issues/7689)). + * Add `ImaAdsLoader.Builder.setCompanionAdSlots` so it's possible to set + companion ad slots without accessing the `AdDisplayContainer`. + * Add missing notification of `VideoAdPlayerCallback.onLoaded`. + * Fix handling of incompatible VPAID ads + ([#7832](https://github.com/google/ExoPlayer/issues/7832)). + * Fix handling of empty ads at non-integer cue points + ([#7889](https://github.com/google/ExoPlayer/issues/7889)). +* Demo app: + * Replace the `extensions` variant with `decoderExtensions` and update the + demo app use the Cronet and IMA extensions by default. + * Expand the `exolist.json` schema, as well the structure of intents that + can be used to launch `PlayerActivity`. See the + [Demo application page](https://exoplayer.dev/demo-application.html#playing-your-own-content) + for the latest versions. Changes include: + * Add `drm_session_for_clear_content` to allow attaching DRM sessions + to clear audio and video tracks. + * Add `clip_start_position_ms` and `clip_end_position_ms` to allow + clipped samples. + * Use `StyledPlayerControlView` rather than `PlayerView`. + * Remove support for media tunneling, random ABR and playback of spherical + video. Developers wishing to experiment with these features can enable + them by modifying the demo app source code. + * Add support for downloading DRM-protected content using offline Widevine + licenses. -* Stability: Work around Nexus 10 reboot when playing certain content - ([#2806](https://github.com/google/ExoPlayer/issues/2806)). -* MP3: Correctly treat MP3s with INFO headers as constant bitrate - ([#2895](https://github.com/google/ExoPlayer/issues/2895)). -* HLS: Use average rather than peak bandwidth when available - ([#2863](https://github.com/google/ExoPlayer/issues/2863)). -* SmoothStreaming: Fix timeline for live streams - ([#2760](https://github.com/google/ExoPlayer/issues/2760)). -* UI: Fix DefaultTimeBar invalidation - ([#2871](https://github.com/google/ExoPlayer/issues/2871)). -* Misc bugfixes. +### 2.11.8 (2020-08-25) ### -### r2.4.1 ### +* Fix distorted playback of floating point audio when samples exceed the + `[-1, 1]` nominal range. +* MP4: + * Add support for `piff` and `isml` brands + ([#7584](https://github.com/google/ExoPlayer/issues/7584)). + * Fix playback of very short MP4 files. +* FMP4: + * Fix `saiz` and `senc` sample count checks, resolving a "length + mismatch" `ParserException` when playing certain protected FMP4 streams + ([#7592](https://github.com/google/ExoPlayer/issues/7592)). + * Fix handling of `traf` boxes containing multiple `sbgp` or `sgpd` + boxes. +* FLV: Ignore `SCRIPTDATA` segments with invalid name types, rather than + failing playback ([#7675](https://github.com/google/ExoPlayer/issues/7675)). +* Better infer the content type of `.ism` and `.isml` streaming URLs. +* Workaround an issue on Broadcom based devices where playbacks would not + transition to `STATE_ENDED` when using video tunneling mode + ([#7647](https://github.com/google/ExoPlayer/issues/7647)). +* IMA extension: Upgrade to IMA SDK 3.19.4, bringing in a fix for setting the + media load timeout + ([#7170](https://github.com/google/ExoPlayer/issues/7170)). +* Demo app: Fix playback of ClearKey protected content on API level 26 and + earlier ([#7735](https://github.com/google/ExoPlayer/issues/7735)). -* Stability: Avoid OutOfMemoryError in extractors when parsing malformed media - ([#2780](https://github.com/google/ExoPlayer/issues/2780)). -* Stability: Avoid native crash on Galaxy Nexus. Avoid unnecessarily large codec - input buffer allocations on all devices - ([#2607](https://github.com/google/ExoPlayer/issues/2607)). -* Variable speed playback: Fix interpolation for rate/pitch adjustment - ([#2774](https://github.com/google/ExoPlayer/issues/2774)). -* HLS: Include EXT-X-DATERANGE tags in HlsMediaPlaylist. -* HLS: Don't expose CEA-608 track if CLOSED-CAPTIONS=NONE - ([#2743](https://github.com/google/ExoPlayer/issues/2743)). -* HLS: Correctly propagate errors loading the media playlist - ([#2623](https://github.com/google/ExoPlayer/issues/2623)). -* UI: DefaultTimeBar enhancements and bug fixes - ([#2740](https://github.com/google/ExoPlayer/issues/2740)). -* Ogg: Fix failure to play some Ogg files - ([#2782](https://github.com/google/ExoPlayer/issues/2782)). -* Captions: Don't select text tack with no language by default. -* Captions: TTML positioning fixes - ([#2824](https://github.com/google/ExoPlayer/issues/2824)). -* Misc bugfixes. +### 2.11.7 (2020-06-29) ### -### r2.4.0 ### +* IMA extension: Fix the way postroll "content complete" notifications are + handled to avoid repeatedly refreshing the timeline after playback ends. -* New modular library structure. You can read more about depending on individual - library modules - [here](https://medium.com/google-exoplayer/exoplayers-new-modular-structure-a916c0874907). -* Variable speed playback support on API level 16+. You can read more about - changing the playback speed - [here](https://medium.com/google-exoplayer/variable-speed-playback-with-exoplayer-e6e6a71e0343) - ([#26](https://github.com/google/ExoPlayer/issues/26)). -* New time bar view, including support for displaying ad break markers. -* Support DVB subtitles in MPEG-TS and MKV. -* Support adaptive playback for audio only DASH, HLS and SmoothStreaming - ([#1975](https://github.com/google/ExoPlayer/issues/1975)). -* Support for setting extractor flags on DefaultExtractorsFactory - ([#2657](https://github.com/google/ExoPlayer/issues/2657)). -* Support injecting custom renderers into SimpleExoPlayer using a new - RenderersFactory interface. -* Correctly set ExoPlayer's internal thread priority to `THREAD_PRIORITY_AUDIO`. -* TX3G: Support styling and positioning. -* FLV: - * Support MP3 in FLV. - * Skip unhandled metadata rather than failing - ([#2634](https://github.com/google/ExoPlayer/issues/2634)). - * Fix potential OutOfMemory errors. -* ID3: Better handle malformed ID3 data - ([#2604](https://github.com/google/ExoPlayer/issues/2604), - [#2663](https://github.com/google/ExoPlayer/issues/2663)). -* FFmpeg extension: Fixed build instructions - ([#2561](https://github.com/google/ExoPlayer/issues/2561)). -* VP9 extension: Reduced binary size. -* FLAC extension: Enabled 64 bit targets. -* Misc bugfixes. +### 2.11.6 (2020-06-19) ### -### r2.3.1 ### +* UI: Prevent `PlayerView` from temporarily hiding the video surface when + seeking to an unprepared period within the current window. For example when + seeking over an ad group, or to the next period in a multi-period DASH + stream ([#5507](https://github.com/google/ExoPlayer/issues/5507)). +* IMA extension: + * Add option to skip ads before the start position. + * Catch unexpected errors in `stopAd` to avoid a crash + ([#7492](https://github.com/google/ExoPlayer/issues/7492)). + * Fix a bug that caused playback to be stuck buffering on resuming from + the background after all ads had played to the end + ([#7508](https://github.com/google/ExoPlayer/issues/7508)). + * Fix a bug where the number of ads in an ad group couldn't change + ([#7477](https://github.com/google/ExoPlayer/issues/7477)). + * Work around unexpected `pauseAd`/`stopAd` for ads that have preloaded + on seeking to another position + ([#7492](https://github.com/google/ExoPlayer/issues/7492)). + * Fix incorrect rounding of ad cue points. + * Fix handling of postrolls preloading + ([#7518](https://github.com/google/ExoPlayer/issues/7518)). -* Fix NPE enabling WebVTT subtitles in DASH streams - ([#2596](https://github.com/google/ExoPlayer/issues/2596)). -* Fix skipping to keyframes when MediaCodecVideoRenderer is enabled but without - a Surface ([#2575](https://github.com/google/ExoPlayer/issues/2575)). -* Minor fix for CEA-708 decoder - ([#2595](https://github.com/google/ExoPlayer/issues/2595)). +### 2.11.5 (2020-06-05) ### -### r2.3.0 ### +* Improve the smoothness of video playback immediately after starting, seeking + or resuming a playback + ([#6901](https://github.com/google/ExoPlayer/issues/6901)). +* Add `SilenceMediaSource.Factory` to support tags. +* Enable the configuration of `SilenceSkippingAudioProcessor` + ([#6705](https://github.com/google/ExoPlayer/issues/6705)). +* Fix bug where `PlayerMessages` throw an exception after `MediaSources` + are removed from the playlist + ([#7278](https://github.com/google/ExoPlayer/issues/7278)). +* Fix "Not allowed to start service" `IllegalStateException` in + `DownloadService` + ([#7306](https://github.com/google/ExoPlayer/issues/7306)). +* Fix issue in `AudioTrackPositionTracker` that could cause negative positions + to be reported at the start of playback and immediately after seeking + ([#7456](https://github.com/google/ExoPlayer/issues/7456)). +* Fix further cases where downloads would sometimes not resume after their + network requirements are met + ([#7453](https://github.com/google/ExoPlayer/issues/7453)). +* DASH: + * Merge trick play adaptation sets (i.e., adaptation sets marked with + `http://dashif.org/guidelines/trickmode`) into the same `TrackGroup` as + the main adaptation sets to which they refer. Trick play tracks are + marked with the `C.ROLE_FLAG_TRICK_PLAY` flag. + * Fix assertion failure in `SampleQueue` when playing DASH streams with + EMSG tracks ([#7273](https://github.com/google/ExoPlayer/issues/7273)). +* MP4: Store the Android capture frame rate only in `Format.metadata`. + `Format.frameRate` now stores the calculated frame rate. +* FMP4: Avoid throwing an exception while parsing default sample values whose + most significant bits are set + ([#7207](https://github.com/google/ExoPlayer/issues/7207)). +* MP3: Fix issue parsing the XING headers belonging to files larger than 2GB + ([#7337](https://github.com/google/ExoPlayer/issues/7337)). +* MPEG-TS: Fix issue where SEI NAL units were incorrectly dropped from H.265 + samples ([#7113](https://github.com/google/ExoPlayer/issues/7113)). +* UI: + * Fix `DefaultTimeBar` to respect touch transformations + ([#7303](https://github.com/google/ExoPlayer/issues/7303)). + * Add `showScrubber` and `hideScrubber` methods to `DefaultTimeBar`. +* Text: + * Use anti-aliasing and bitmap filtering when displaying bitmap + subtitles. + * Fix `SubtitlePainter` to render `EDGE_TYPE_OUTLINE` using the correct + color. +* IMA extension: + * Upgrade to IMA SDK version 3.19.0, and migrate to new + preloading APIs + ([#6429](https://github.com/google/ExoPlayer/issues/6429)). This fixes + several issues involving preloading and handling of ad loading error + cases: ([#4140](https://github.com/google/ExoPlayer/issues/4140), + [#5006](https://github.com/google/ExoPlayer/issues/5006), + [#6030](https://github.com/google/ExoPlayer/issues/6030), + [#6097](https://github.com/google/ExoPlayer/issues/6097), + [#6425](https://github.com/google/ExoPlayer/issues/6425), + [#6967](https://github.com/google/ExoPlayer/issues/6967), + [#7041](https://github.com/google/ExoPlayer/issues/7041), + [#7161](https://github.com/google/ExoPlayer/issues/7161), + [#7212](https://github.com/google/ExoPlayer/issues/7212), + [#7340](https://github.com/google/ExoPlayer/issues/7340)). + * Add support for timing out ad preloading, to avoid playback getting + stuck if an ad group unexpectedly fails to load + ([#5444](https://github.com/google/ExoPlayer/issues/5444), + [#5966](https://github.com/google/ExoPlayer/issues/5966), + [#7002](https://github.com/google/ExoPlayer/issues/7002)). + * Fix `AdsMediaSource` child `MediaSource`s not being released. +* Cronet extension: Default to using the Cronet implementation in Google Play + Services rather than Cronet Embedded. This allows Cronet to be used with a + negligible increase in application size, compared to approximately 8MB when + embedding the library. +* OkHttp extension: Upgrade OkHttp dependency to 3.12.11. +* MediaSession extension: + * Only set the playback state to `BUFFERING` if `playWhenReady` is true + ([#7206](https://github.com/google/ExoPlayer/issues/7206)). + * Add missing `@Nullable` annotations to `MediaSessionConnector` + ([#7234](https://github.com/google/ExoPlayer/issues/7234)). +* AV1 extension: Add a heuristic to determine the default number of threads + used for AV1 playback using the extension. -* GVR extension: Wraps the Google VR Audio SDK to provide spatial audio - rendering. You can read more about the GVR extension - [here](https://medium.com/google-exoplayer/spatial-audio-with-exoplayer-and-gvr-cecb00e9da5f#.xdjebjd7g). -* DASH improvements: - * Support embedded CEA-608 closed captions - ([#2362](https://github.com/google/ExoPlayer/issues/2362)). - * Support embedded EMSG events - ([#2176](https://github.com/google/ExoPlayer/issues/2176)). - * Support mspr:pro manifest element - ([#2386](https://github.com/google/ExoPlayer/issues/2386)). - * Correct handling of empty segment indices at the start of live events - ([#1865](https://github.com/google/ExoPlayer/issues/1865)). -* HLS improvements: - * Respect initial track selection - ([#2353](https://github.com/google/ExoPlayer/issues/2353)). - * Reduced frequency of media playlist requests when playback position is close - to the live edge ([#2548](https://github.com/google/ExoPlayer/issues/2548)). - * Exposed the master playlist through ExoPlayer.getCurrentManifest() - ([#2537](https://github.com/google/ExoPlayer/issues/2537)). - * Support CLOSED-CAPTIONS #EXT-X-MEDIA type - ([#341](https://github.com/google/ExoPlayer/issues/341)). - * Fixed handling of negative values in #EXT-X-SUPPORT - ([#2495](https://github.com/google/ExoPlayer/issues/2495)). - * Fixed potential endless buffering state for streams with WebVTT subtitles - ([#2424](https://github.com/google/ExoPlayer/issues/2424)). -* MPEG-TS improvements: - * Support for multiple programs. - * Support for multiple closed captions and caption service descriptors - ([#2161](https://github.com/google/ExoPlayer/issues/2161)). -* MP3: Add `FLAG_ENABLE_CONSTANT_BITRATE_SEEKING` extractor option to enable - constant bitrate seeking in MP3 files that would otherwise be unseekable - ([#2445](https://github.com/google/ExoPlayer/issues/2445)). -* ID3: Better handle malformed ID3 data - ([#2486](https://github.com/google/ExoPlayer/issues/2486)). -* Track selection: Added maxVideoBitrate parameter to DefaultTrackSelector. -* DRM: Add support for CENC ClearKey on API level 21+ - ([#2361](https://github.com/google/ExoPlayer/issues/2361)). -* DRM: Support dynamic setting of key request headers - ([#1924](https://github.com/google/ExoPlayer/issues/1924)). -* SmoothStreaming: Fixed handling of start_time placeholder - ([#2447](https://github.com/google/ExoPlayer/issues/2447)). -* FLAC extension: Fix proguard configuration - ([#2427](https://github.com/google/ExoPlayer/issues/2427)). -* Misc bugfixes. +### 2.11.4 (2020-04-08) -### r2.2.0 ### +* Add `SimpleExoPlayer.setWakeMode` to allow automatic `WifiLock` and + `WakeLock` handling + ([#6914](https://github.com/google/ExoPlayer/issues/6914)). 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. +* Text: + * Catch and log exceptions in `TextRenderer` rather than re-throwing. This + allows playback to continue even if subtitle decoding fails + ([#6885](https://github.com/google/ExoPlayer/issues/6885)). + * Allow missing hours and milliseconds in SubRip (.srt) timecodes + ([#7122](https://github.com/google/ExoPlayer/issues/7122)). +* Audio: + * Enable playback speed adjustment and silence skipping for floating point + PCM audio, via resampling to 16-bit integer PCM. To output the original + floating point audio without adjustment, pass `enableFloatOutput=true` + to the `DefaultAudioSink` constructor + ([#7134](https://github.com/google/ExoPlayer/issues/7134)). + * Workaround issue that could cause slower than realtime playback of AAC + on Android 10 + ([#6671](https://github.com/google/ExoPlayer/issues/6671)). + * Fix case where another app spuriously holding transient audio focus + could prevent ExoPlayer from acquiring audio focus for an indefinite + period of time + ([#7182](https://github.com/google/ExoPlayer/issues/7182)). + * Fix case where the player volume could be permanently ducked if audio + focus was released whilst ducking. + * Fix playback of WAV files with trailing non-media bytes + ([#7129](https://github.com/google/ExoPlayer/issues/7129)). + * Fix playback of ADTS files with mid-stream ID3 metadata. +* DRM: + * Fix stuck ad playbacks with DRM protected content + ([#7188](https://github.com/google/ExoPlayer/issues/7188)). + * Fix playback of Widevine protected content that only provides V1 PSSH + atoms on API levels 21 and 22. + * Fix playback of PlayReady content on Fire TV Stick (Gen 2). +* DASH: + * Update the manifest URI to avoid repeated HTTP redirects + ([#6907](https://github.com/google/ExoPlayer/issues/6907)). + * Parse period `AssetIdentifier` elements. +* HLS: Recognize IMSC subtitles + ([#7185](https://github.com/google/ExoPlayer/issues/7185)). +* UI: Add an option to set whether to use the orientation sensor for rotation + in spherical playbacks + ([#6761](https://github.com/google/ExoPlayer/issues/6761)). +* Analytics: Fix `PlaybackStatsListener` behavior when not keeping history + ([#7160](https://github.com/google/ExoPlayer/issues/7160)). +* FFmpeg extension: Add support for `x86_64` architecture. +* Opus extension: Fix parsing of negative gain values + ([#7046](https://github.com/google/ExoPlayer/issues/7046)). +* Cast extension: Upgrade `play-services-cast-framework` dependency to 18.1.0. + This fixes an issue where `RemoteServiceException` was thrown due to + `Context.startForegroundService()` not calling `Service.startForeground()` + ([#7191](https://github.com/google/ExoPlayer/issues/7191)). -* Demo app: Automatic recovery from BehindLiveWindowException, plus improved - handling of pausing and resuming live streams - ([#2344](https://github.com/google/ExoPlayer/issues/2344)). -* AndroidTV: Added Support for tunneled video playback - ([#1688](https://github.com/google/ExoPlayer/issues/1688)). -* DRM: Renamed StreamingDrmSessionManager to DefaultDrmSessionManager and - added support for using offline licenses - ([#876](https://github.com/google/ExoPlayer/issues/876)). -* DRM: Introduce OfflineLicenseHelper to help with offline license acquisition, - renewal and release. -* UI: Updated player control assets. Added vector drawables for use on API level - 21 and above. -* UI: Made player control seek bar work correctly with key events if focusable - ([#2278](https://github.com/google/ExoPlayer/issues/2278)). -* HLS: Improved support for streams that use EXT-X-DISCONTINUITY without - EXT-X-DISCONTINUITY-SEQUENCE - ([#1789](https://github.com/google/ExoPlayer/issues/1789)). -* HLS: Support for EXT-X-START tag - ([#1544](https://github.com/google/ExoPlayer/issues/1544)). -* HLS: Check #EXTM3U header is present when parsing the playlist. Fail - gracefully if not ([#2301](https://github.com/google/ExoPlayer/issues/2301)). -* HLS: Fix memory leak - ([#2319](https://github.com/google/ExoPlayer/issues/2319)). -* HLS: Fix non-seamless first adaptation where master playlist omits resolution - tags ([#2096](https://github.com/google/ExoPlayer/issues/2096)). -* HLS: Fix handling of WebVTT subtitle renditions with non-standard segment file - extensions ([#2025](https://github.com/google/ExoPlayer/issues/2025) and - [#2355](https://github.com/google/ExoPlayer/issues/2355)). -* HLS: Better handle inconsistent HLS playlist update - ([#2249](https://github.com/google/ExoPlayer/issues/2249)). -* DASH: Don't overflow when dealing with large segment numbers - ([#2311](https://github.com/google/ExoPlayer/issues/2311)). -* DASH: Fix propagation of language from the manifest - ([#2335](https://github.com/google/ExoPlayer/issues/2335)). -* SmoothStreaming: Work around "Offset to sample data was negative" failures - ([#2292](https://github.com/google/ExoPlayer/issues/2292), - [#2101](https://github.com/google/ExoPlayer/issues/2101) and - [#1152](https://github.com/google/ExoPlayer/issues/1152)). -* MP3/ID3: Added support for parsing Chapter and URL link frames - ([#2316](https://github.com/google/ExoPlayer/issues/2316)). -* MP3/ID3: Handle ID3 frames that end with empty text field - ([#2309](https://github.com/google/ExoPlayer/issues/2309)). -* Added ClippingMediaSource for playing clipped portions of media - ([#1988](https://github.com/google/ExoPlayer/issues/1988)). -* Added convenience methods to query whether the current window is dynamic and - seekable ([#2320](https://github.com/google/ExoPlayer/issues/2320)). -* Support setting of default headers on HttpDataSource.Factory implementations - ([#2166](https://github.com/google/ExoPlayer/issues/2166)). -* Fixed cache failures when using an encrypted cache content index. -* Fix visual artifacts when switching output surface - ([#2093](https://github.com/google/ExoPlayer/issues/2093)). -* Fix gradle + proguard configurations. -* Fix player position when replacing the MediaSource - ([#2369](https://github.com/google/ExoPlayer/issues/2369)). -* Misc bug fixes, including - [#2330](https://github.com/google/ExoPlayer/issues/2330), - [#2269](https://github.com/google/ExoPlayer/issues/2269), - [#2252](https://github.com/google/ExoPlayer/issues/2252), - [#2264](https://github.com/google/ExoPlayer/issues/2264) and - [#2290](https://github.com/google/ExoPlayer/issues/2290). +### 2.11.3 (2020-02-19) -### r2.1.1 ### +* SmoothStreaming: Fix regression that broke playback in 2.11.2 + ([#6981](https://github.com/google/ExoPlayer/issues/6981)). +* DRM: Fix issue switching from protected content that uses a 16-byte + initialization vector to one that uses an 8-byte initialization vector + ([#6982](https://github.com/google/ExoPlayer/issues/6982)). -* Fix some subtitle types (e.g. WebVTT) being displayed out of sync - ([#2208](https://github.com/google/ExoPlayer/issues/2208)). -* Fix incorrect position reporting for on-demand HLS media that includes - EXT-X-PROGRAM-DATE-TIME tags - ([#2224](https://github.com/google/ExoPlayer/issues/2224)). -* 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. +### 2.11.2 (2020-02-13) -### r2.1.0 ### +* Add Java FLAC extractor + ([#6406](https://github.com/google/ExoPlayer/issues/6406)). +* Startup latency optimization: + * Reduce startup latency for DASH and SmoothStreaming playbacks by + allowing codec initialization to occur before the network connection for + the first media segment has been established. + * Reduce startup latency for on-demand DASH playbacks by allowing codec + initialization to occur before the sidx box has been loaded. +* Downloads: + * Fix download resumption when the requirements for them to continue are + met ([#6733](https://github.com/google/ExoPlayer/issues/6733), + [#6798](https://github.com/google/ExoPlayer/issues/6798)). + * Fix `DownloadHelper.createMediaSource` to use `customCacheKey` when + creating `ProgressiveMediaSource` instances. +* DRM: Fix `NullPointerException` when playing DRM protected content + ([#6951](https://github.com/google/ExoPlayer/issues/6951)). +* Metadata: + * 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)). + * Select multiple metadata tracks if multiple metadata renderers are + available ([#6676](https://github.com/google/ExoPlayer/issues/6676)). + * Add support for ID3 genres added in Wimamp 5.6 (2010). +* UI: + * Show ad group markers in `DefaultTimeBar` even if they are after the end + of the current window + ([#6552](https://github.com/google/ExoPlayer/issues/6552)). + * Don't use notification chronometer if playback speed is != 1.0 + ([#6816](https://github.com/google/ExoPlayer/issues/6816)). +* HLS: Fix playback of DRM protected content that uses key rotation + ([#6903](https://github.com/google/ExoPlayer/issues/6903)). +* WAV: + * Support IMA ADPCM encoded data. + * Improve support for G.711 A-law and mu-law encoded data. +* MP4: Support "twos" codec (big endian PCM) + ([#5789](https://github.com/google/ExoPlayer/issues/5789)). +* FMP4: Add support for encrypted AC-4 tracks. +* HLS: Fix slow seeking into long MP3 segments + ([#6155](https://github.com/google/ExoPlayer/issues/6155)). +* Fix handling of E-AC-3 streams that contain AC-3 syncframes + ([#6602](https://github.com/google/ExoPlayer/issues/6602)). +* Fix playback of TrueHD streams in Matroska + ([#6845](https://github.com/google/ExoPlayer/issues/6845)). +* Fix MKV subtitles to disappear when intended instead of lasting until the + next cue ([#6833](https://github.com/google/ExoPlayer/issues/6833)). +* OkHttp extension: Upgrade OkHttp dependency to 3.12.8, which fixes a class + of `SocketTimeoutException` issues when using HTTP/2 + ([#4078](https://github.com/google/ExoPlayer/issues/4078)). +* FLAC extension: Fix handling of bit depths other than 16 in `FLACDecoder`. + This issue caused FLAC streams with other bit depths to sound like white + noise on earlier releases, but only when embedded in a non-FLAC container + such as Matroska or MP4. +* Demo apps: Add + [GL demo app](https://github.com/google/ExoPlayer/tree/dev-v2/demos/gl) to + show how to render video to a `GLSurfaceView` while applying a GL shader. + ([#6920](https://github.com/google/ExoPlayer/issues/6920)). -* HLS: Support for seeking in live streams - ([#87](https://github.com/google/ExoPlayer/issues/87)). -* HLS: Improved support: - * Support for EXT-X-PROGRAM-DATE-TIME - ([#747](https://github.com/google/ExoPlayer/issues/747)). - * Improved handling of sample timestamps and their alignment across variants - and renditions. - * Fix issue that could cause playbacks to get stuck in an endless initial - buffering state. - * Correctly propagate BehindLiveWindowException instead of - IndexOutOfBoundsException exception - ([#1695](https://github.com/google/ExoPlayer/issues/1695)). -* MP3/MP4: Support for ID3 metadata, including embedded album art - ([#979](https://github.com/google/ExoPlayer/issues/979)). -* Improved customization of UI components. You can read about customization of - ExoPlayer's UI components - [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. -* MPEG-TS: Improved support: - * Support injection of custom TS payload readers. - * Support injection of custom section payload readers. - * Support SCTE-35 splice information messages. - * Support multiple table sections in a single PSI section. - * Fix NullPointerException when an unsupported stream type is encountered - ([#2149](https://github.com/google/ExoPlayer/issues/2149)). - * Avoid failure when expected ID3 header not found - ([#1966](https://github.com/google/ExoPlayer/issues/1966)). -* Improvements to the upstream cache package. - * Support caching of media segments for DASH, HLS and SmoothStreaming. Note - that caching of manifest and playlist files is still not supported in the - (normal) case where the corresponding responses are compressed. - * Support caching for ExtractorMediaSource based playbacks. -* Improved flexibility of SimpleExoPlayer - ([#2102](https://github.com/google/ExoPlayer/issues/2102)). -* Fix issue where only the audio of a video would play due to capability - detection issues ([#2007](https://github.com/google/ExoPlayer/issues/2007), - [#2034](https://github.com/google/ExoPlayer/issues/2034) and - [#2157](https://github.com/google/ExoPlayer/issues/2157)). -* Fix issues that could cause ExtractorMediaSource based playbacks to get stuck - buffering ([#1962](https://github.com/google/ExoPlayer/issues/1962)). -* Correctly set SimpleExoPlayerView surface aspect ratio when an active player - is attached ([#2077](https://github.com/google/ExoPlayer/issues/2077)). -* OGG: Fix playback of short OGG files - ([#1976](https://github.com/google/ExoPlayer/issues/1976)). -* MP4: Support `.mp3` tracks - ([#2066](https://github.com/google/ExoPlayer/issues/2066)). -* SubRip: Don't fail playbacks if SubRip file contains negative timestamps - ([#2145](https://github.com/google/ExoPlayer/issues/2145)). -* Misc bugfixes. +### 2.11.1 (2019-12-20) -### r2.0.4 ### +* 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)). -* Fix crash on Jellybean devices when using playback controls - ([#1965](https://github.com/google/ExoPlayer/issues/1965)). +### 2.11.0 (2019-12-11) -### r2.0.3 ### +* 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`. +* LoadControl: + * Default `prioritizeTimeOverSizeThresholds` to false to prevent OOM + errors ([#6647](https://github.com/google/ExoPlayer/issues/6647)). +* 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)). -* Fixed NullPointerException in ExtractorMediaSource - ([#1914](https://github.com/google/ExoPlayer/issues/1914)). -* Fixed NullPointerException in HlsMediaPeriod - ([#1907](https://github.com/google/ExoPlayer/issues/1907)). -* Fixed memory leak in PlaybackControlView - ([#1908](https://github.com/google/ExoPlayer/issues/1908)). -* Fixed strict mode violation when using - SimpleExoPlayer.setVideoPlayerTextureView(). -* Fixed L3 Widevine provisioning - ([#1925](https://github.com/google/ExoPlayer/issues/1925)). -* Fixed hiding of controls with use_controller="false" - ([#1919](https://github.com/google/ExoPlayer/issues/1919)). -* Improvements to Cronet network stack extension. -* Misc bug fixes. +### 2.10.8 (2019-11-19) -### r2.0.2 ### +* 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)). -* Fixes for MergingMediaSource and sideloaded subtitles. - ([#1882](https://github.com/google/ExoPlayer/issues/1882), - [#1854](https://github.com/google/ExoPlayer/issues/1854), - [#1900](https://github.com/google/ExoPlayer/issues/1900)). -* Reduced effect of application code leaking player references - ([#1855](https://github.com/google/ExoPlayer/issues/1855)). -* Initial support for fragmented MP4 in HLS. -* Misc bug fixes and minor features. +### 2.10.7 (2019-11-06) -### r2.0.1 ### +* 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. -* Fix playback of short duration content - ([#1837](https://github.com/google/ExoPlayer/issues/1837)). -* Fix MergingMediaSource preparation issue - ([#1853](https://github.com/google/ExoPlayer/issues/1853)). -* Fix live stream buffering (out of memory) issue - ([#1825](https://github.com/google/ExoPlayer/issues/1825)). +### 2.10.6 (2019-10-17) -### r2.0.0 ### +* 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)). +* HLS: Fix propagation of format identifier for CEA-608 + ([#3033](https://github.com/google/ExoPlayer/issues/3033)). +* HLS: Detect playlist stuck and reset conditions + ([#2872](https://github.com/google/ExoPlayer/issues/2872)). +* Video: Fix video dimension reporting on some devices + ([#3007](https://github.com/google/ExoPlayer/issues/3007)). + +### 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)). +* Audio: Workaround for broken MediaTek raw decoder on some devices + ([#2873](https://github.com/google/ExoPlayer/issues/2873)). +* Captions: Fix TTML captions appearing at the top of the screen + ([#2953](https://github.com/google/ExoPlayer/issues/2953)). +* Captions: Fix handling of some DVB subtitles + ([#2957](https://github.com/google/ExoPlayer/issues/2957)). +* Track selection: Fix setSelectionOverride(index, tracks, null) + ([#2988](https://github.com/google/ExoPlayer/issues/2988)). +* GVR extension: Add support for mono input + ([#2710](https://github.com/google/ExoPlayer/issues/2710)). +* FLAC extension: Fix failing build + ([#2977](https://github.com/google/ExoPlayer/pull/2977)). +* Misc bugfixes. + +### r2.4.2 (2017-06-06) + +* Stability: Work around Nexus 10 reboot when playing certain content + ([#2806](https://github.com/google/ExoPlayer/issues/2806)). +* MP3: Correctly treat MP3s with INFO headers as constant bitrate + ([#2895](https://github.com/google/ExoPlayer/issues/2895)). +* HLS: Use average rather than peak bandwidth when available + ([#2863](https://github.com/google/ExoPlayer/issues/2863)). +* SmoothStreaming: Fix timeline for live streams + ([#2760](https://github.com/google/ExoPlayer/issues/2760)). +* UI: Fix DefaultTimeBar invalidation + ([#2871](https://github.com/google/ExoPlayer/issues/2871)). +* Misc bugfixes. + +### r2.4.1 (2017-05-23) + +* Stability: Avoid OutOfMemoryError in extractors when parsing malformed media + ([#2780](https://github.com/google/ExoPlayer/issues/2780)). +* Stability: Avoid native crash on Galaxy Nexus. Avoid unnecessarily large + codec input buffer allocations on all devices + ([#2607](https://github.com/google/ExoPlayer/issues/2607)). +* Variable speed playback: Fix interpolation for rate/pitch adjustment + ([#2774](https://github.com/google/ExoPlayer/issues/2774)). +* HLS: Include EXT-X-DATERANGE tags in HlsMediaPlaylist. +* HLS: Don't expose CEA-608 track if CLOSED-CAPTIONS=NONE + ([#2743](https://github.com/google/ExoPlayer/issues/2743)). +* HLS: Correctly propagate errors loading the media playlist + ([#2623](https://github.com/google/ExoPlayer/issues/2623)). +* UI: DefaultTimeBar enhancements and bug fixes + ([#2740](https://github.com/google/ExoPlayer/issues/2740)). +* Ogg: Fix failure to play some Ogg files + ([#2782](https://github.com/google/ExoPlayer/issues/2782)). +* Captions: Don't select text tack with no language by default. +* Captions: TTML positioning fixes + ([#2824](https://github.com/google/ExoPlayer/issues/2824)). +* Misc bugfixes. + +### r2.4.0 (2017-04-25) + +* New modular library structure. You can read more about depending on + individual library modules + [here](https://medium.com/google-exoplayer/exoplayers-new-modular-structure-a916c0874907). +* Variable speed playback support on API level 16+. You can read more about + changing the playback speed + [here](https://medium.com/google-exoplayer/variable-speed-playback-with-exoplayer-e6e6a71e0343) + ([#26](https://github.com/google/ExoPlayer/issues/26)). +* New time bar view, including support for displaying ad break markers. +* Support DVB subtitles in MPEG-TS and MKV. +* Support adaptive playback for audio only DASH, HLS and SmoothStreaming + ([#1975](https://github.com/google/ExoPlayer/issues/1975)). +* Support for setting extractor flags on DefaultExtractorsFactory + ([#2657](https://github.com/google/ExoPlayer/issues/2657)). +* Support injecting custom renderers into SimpleExoPlayer using a new + RenderersFactory interface. +* Correctly set ExoPlayer's internal thread priority to + `THREAD_PRIORITY_AUDIO`. +* TX3G: Support styling and positioning. +* FLV: + * Support MP3 in FLV. + * Skip unhandled metadata rather than failing + ([#2634](https://github.com/google/ExoPlayer/issues/2634)). + * Fix potential OutOfMemory errors. +* ID3: Better handle malformed ID3 data + ([#2604](https://github.com/google/ExoPlayer/issues/2604), + [#2663](https://github.com/google/ExoPlayer/issues/2663)). +* FFmpeg extension: Fixed build instructions + ([#2561](https://github.com/google/ExoPlayer/issues/2561)). +* VP9 extension: Reduced binary size. +* FLAC extension: Enabled 64 bit targets. +* Misc bugfixes. + +### r2.3.1 (2017-03-23) + +* Fix NPE enabling WebVTT subtitles in DASH streams + ([#2596](https://github.com/google/ExoPlayer/issues/2596)). +* Fix skipping to keyframes when MediaCodecVideoRenderer is enabled but + without a Surface + ([#2575](https://github.com/google/ExoPlayer/issues/2575)). +* Minor fix for CEA-708 decoder + ([#2595](https://github.com/google/ExoPlayer/issues/2595)). + +### 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 + [here](https://medium.com/google-exoplayer/spatial-audio-with-exoplayer-and-gvr-cecb00e9da5f#.xdjebjd7g). +* DASH improvements: + * Support embedded CEA-608 closed captions + ([#2362](https://github.com/google/ExoPlayer/issues/2362)). + * Support embedded EMSG events + ([#2176](https://github.com/google/ExoPlayer/issues/2176)). + * Support mspr:pro manifest element + ([#2386](https://github.com/google/ExoPlayer/issues/2386)). + * Correct handling of empty segment indices at the start of live events + ([#1865](https://github.com/google/ExoPlayer/issues/1865)). +* HLS improvements: + * Respect initial track selection + ([#2353](https://github.com/google/ExoPlayer/issues/2353)). + * Reduced frequency of media playlist requests when playback position is + close to the live edge + ([#2548](https://github.com/google/ExoPlayer/issues/2548)). + * Exposed the master playlist through ExoPlayer.getCurrentManifest() + ([#2537](https://github.com/google/ExoPlayer/issues/2537)). + * Support CLOSED-CAPTIONS #EXT-X-MEDIA type + ([#341](https://github.com/google/ExoPlayer/issues/341)). + * Fixed handling of negative values in #EXT-X-SUPPORT + ([#2495](https://github.com/google/ExoPlayer/issues/2495)). + * Fixed potential endless buffering state for streams with WebVTT + subtitles ([#2424](https://github.com/google/ExoPlayer/issues/2424)). +* MPEG-TS improvements: + * Support for multiple programs. + * Support for multiple closed captions and caption service descriptors + ([#2161](https://github.com/google/ExoPlayer/issues/2161)). +* MP3: Add `FLAG_ENABLE_CONSTANT_BITRATE_SEEKING` extractor option to enable + constant bitrate seeking in MP3 files that would otherwise be unseekable + ([#2445](https://github.com/google/ExoPlayer/issues/2445)). +* ID3: Better handle malformed ID3 data + ([#2486](https://github.com/google/ExoPlayer/issues/2486)). +* Track selection: Added maxVideoBitrate parameter to DefaultTrackSelector. +* DRM: Add support for CENC ClearKey on API level 21+ + ([#2361](https://github.com/google/ExoPlayer/issues/2361)). +* DRM: Support dynamic setting of key request headers + ([#1924](https://github.com/google/ExoPlayer/issues/1924)). +* SmoothStreaming: Fixed handling of start_time placeholder + ([#2447](https://github.com/google/ExoPlayer/issues/2447)). +* FLAC extension: Fix proguard configuration + ([#2427](https://github.com/google/ExoPlayer/issues/2427)). +* Misc bugfixes. + +### r2.2.0 (2017-01-30) + +* Demo app: Automatic recovery from BehindLiveWindowException, plus improved + handling of pausing and resuming live streams + ([#2344](https://github.com/google/ExoPlayer/issues/2344)). +* AndroidTV: Added Support for tunneled video playback + ([#1688](https://github.com/google/ExoPlayer/issues/1688)). +* DRM: Renamed StreamingDrmSessionManager to DefaultDrmSessionManager and + added support for using offline licenses + ([#876](https://github.com/google/ExoPlayer/issues/876)). +* DRM: Introduce OfflineLicenseHelper to help with offline license + acquisition, renewal and release. +* UI: Updated player control assets. Added vector drawables for use on API + level 21 and above. +* UI: Made player control seek bar work correctly with key events if focusable + ([#2278](https://github.com/google/ExoPlayer/issues/2278)). +* HLS: Improved support for streams that use EXT-X-DISCONTINUITY without + EXT-X-DISCONTINUITY-SEQUENCE + ([#1789](https://github.com/google/ExoPlayer/issues/1789)). +* HLS: Support for EXT-X-START tag + ([#1544](https://github.com/google/ExoPlayer/issues/1544)). +* HLS: Check #EXTM3U header is present when parsing the playlist. Fail + gracefully if not + ([#2301](https://github.com/google/ExoPlayer/issues/2301)). +* HLS: Fix memory leak + ([#2319](https://github.com/google/ExoPlayer/issues/2319)). +* HLS: Fix non-seamless first adaptation where master playlist omits + resolution tags ([#2096](https://github.com/google/ExoPlayer/issues/2096)). +* HLS: Fix handling of WebVTT subtitle renditions with non-standard segment + file extensions ([#2025](https://github.com/google/ExoPlayer/issues/2025) + and [#2355](https://github.com/google/ExoPlayer/issues/2355)). +* HLS: Better handle inconsistent HLS playlist update + ([#2249](https://github.com/google/ExoPlayer/issues/2249)). +* DASH: Don't overflow when dealing with large segment numbers + ([#2311](https://github.com/google/ExoPlayer/issues/2311)). +* DASH: Fix propagation of language from the manifest + ([#2335](https://github.com/google/ExoPlayer/issues/2335)). +* SmoothStreaming: Work around "Offset to sample data was negative" failures + ([#2292](https://github.com/google/ExoPlayer/issues/2292), + [#2101](https://github.com/google/ExoPlayer/issues/2101) and + [#1152](https://github.com/google/ExoPlayer/issues/1152)). +* MP3/ID3: Added support for parsing Chapter and URL link frames + ([#2316](https://github.com/google/ExoPlayer/issues/2316)). +* MP3/ID3: Handle ID3 frames that end with empty text field + ([#2309](https://github.com/google/ExoPlayer/issues/2309)). +* Added ClippingMediaSource for playing clipped portions of media + ([#1988](https://github.com/google/ExoPlayer/issues/1988)). +* Added convenience methods to query whether the current window is dynamic and + seekable ([#2320](https://github.com/google/ExoPlayer/issues/2320)). +* Support setting of default headers on HttpDataSource.Factory implementations + ([#2166](https://github.com/google/ExoPlayer/issues/2166)). +* Fixed cache failures when using an encrypted cache content index. +* Fix visual artifacts when switching output surface + ([#2093](https://github.com/google/ExoPlayer/issues/2093)). +* Fix gradle + proguard configurations. +* Fix player position when replacing the MediaSource + ([#2369](https://github.com/google/ExoPlayer/issues/2369)). +* Misc bug fixes, including + [#2330](https://github.com/google/ExoPlayer/issues/2330), + [#2269](https://github.com/google/ExoPlayer/issues/2269), + [#2252](https://github.com/google/ExoPlayer/issues/2252), + [#2264](https://github.com/google/ExoPlayer/issues/2264) and + [#2290](https://github.com/google/ExoPlayer/issues/2290). + +### 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)). +* Fix incorrect position reporting for on-demand HLS media that includes + EXT-X-PROGRAM-DATE-TIME tags + ([#2224](https://github.com/google/ExoPlayer/issues/2224)). +* 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 (2016-12-14) + +* HLS: Support for seeking in live streams + ([#87](https://github.com/google/ExoPlayer/issues/87)). +* HLS: Improved support: + * Support for EXT-X-PROGRAM-DATE-TIME + ([#747](https://github.com/google/ExoPlayer/issues/747)). + * Improved handling of sample timestamps and their alignment across + variants and renditions. + * Fix issue that could cause playbacks to get stuck in an endless initial + buffering state. + * Correctly propagate BehindLiveWindowException instead of + IndexOutOfBoundsException exception + ([#1695](https://github.com/google/ExoPlayer/issues/1695)). +* MP3/MP4: Support for ID3 metadata, including embedded album art + ([#979](https://github.com/google/ExoPlayer/issues/979)). +* Improved customization of UI components. You can read about customization of + ExoPlayer's UI components + [here](https://medium.com/google-exoplayer/customizing-exoplayers-ui-components-728cf55ee07a#.9ewjg7avi). +* Robustness improvements when handling MediaSource timeline changes and + MediaPeriod transitions. +* 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. + * Support SCTE-35 splice information messages. + * Support multiple table sections in a single PSI section. + * Fix NullPointerException when an unsupported stream type is encountered + ([#2149](https://github.com/google/ExoPlayer/issues/2149)). + * Avoid failure when expected ID3 header not found + ([#1966](https://github.com/google/ExoPlayer/issues/1966)). +* Improvements to the upstream cache package. + * Support caching of media segments for DASH, HLS and SmoothStreaming. + Note that caching of manifest and playlist files is still not supported + in the (normal) case where the corresponding responses are compressed. + * Support caching for ExtractorMediaSource based playbacks. +* Improved flexibility of SimpleExoPlayer + ([#2102](https://github.com/google/ExoPlayer/issues/2102)). +* Fix issue where only the audio of a video would play due to capability + detection issues ([#2007](https://github.com/google/ExoPlayer/issues/2007), + [#2034](https://github.com/google/ExoPlayer/issues/2034) and + [#2157](https://github.com/google/ExoPlayer/issues/2157)). +* Fix issues that could cause ExtractorMediaSource based playbacks to get + stuck buffering ([#1962](https://github.com/google/ExoPlayer/issues/1962)). +* Correctly set SimpleExoPlayerView surface aspect ratio when an active player + is attached ([#2077](https://github.com/google/ExoPlayer/issues/2077)). +* OGG: Fix playback of short OGG files + ([#1976](https://github.com/google/ExoPlayer/issues/1976)). +* MP4: Support `.mp3` tracks + ([#2066](https://github.com/google/ExoPlayer/issues/2066)). +* SubRip: Don't fail playbacks if SubRip file contains negative timestamps + ([#2145](https://github.com/google/ExoPlayer/issues/2145)). +* Misc bugfixes. + +### 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 (2016-10-17) + +* Fixed NullPointerException in ExtractorMediaSource + ([#1914](https://github.com/google/ExoPlayer/issues/1914)). +* Fixed NullPointerException in HlsMediaPeriod + ([#1907](https://github.com/google/ExoPlayer/issues/1907)). +* Fixed memory leak in PlaybackControlView + ([#1908](https://github.com/google/ExoPlayer/issues/1908)). +* Fixed strict mode violation when using + SimpleExoPlayer.setVideoPlayerTextureView(). +* Fixed L3 Widevine provisioning + ([#1925](https://github.com/google/ExoPlayer/issues/1925)). +* Fixed hiding of controls with use_controller="false" + ([#1919](https://github.com/google/ExoPlayer/issues/1919)). +* Improvements to Cronet network stack extension. +* Misc bug fixes. + +### r2.0.2 (2016-10-06) + +* Fixes for MergingMediaSource and sideloaded subtitles. + ([#1882](https://github.com/google/ExoPlayer/issues/1882), + [#1854](https://github.com/google/ExoPlayer/issues/1854), + [#1900](https://github.com/google/ExoPlayer/issues/1900)). +* Reduced effect of application code leaking player references + ([#1855](https://github.com/google/ExoPlayer/issues/1855)). +* Initial support for fragmented MP4 in HLS. +* Misc bug fixes and minor features. + +### r2.0.1 (2016-09-30) + +* Fix playback of short duration content + ([#1837](https://github.com/google/ExoPlayer/issues/1837)). +* Fix MergingMediaSource preparation issue + ([#1853](https://github.com/google/ExoPlayer/issues/1853)). +* Fix live stream buffering (out of memory) issue + ([#1825](https://github.com/google/ExoPlayer/issues/1825)). + +### 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 some of the motivations behind ExoPlayer 2.x [here](https://medium.com/google-exoplayer/exoplayer-2-x-why-what-and-when-74fd9cb139#.am7h8nytm). -* Root package name changed to `com.google.android.exoplayer2`. The library - structure and class names have also been sanitized. Read more - [here](https://medium.com/google-exoplayer/exoplayer-2-x-new-package-and-class-names-ef8e1d9ba96f#.lv8sd4nez). -* Key architectural changes: - * Late binding between rendering and media source components. Allows the same - rendering components to be re-used from one playback to another. Enables - features such as gapless playback through playlists and DASH multi-period - support. - * Improved track selection design. More details can be found - [here](https://medium.com/google-exoplayer/exoplayer-2-x-track-selection-2b62ff712cc9#.n00zo76b6). - * LoadControl now used to control buffering and loading across all playback - types. - * Media source components given additional structure. A new MediaSource class - has been introduced. MediaSources expose Timelines that describe the media - they expose, and can consist of multiple MediaPeriods. This enables features - such as seeking in live playbacks and DASH multi-period support. - * Responsibility for loading the initial DASH/SmoothStreaming/HLS manifest is - promoted to the corresponding MediaSource components and is no longer the - application's responsibility. - * Higher level abstractions such as SimpleExoPlayer have been added to the - library. These make the library easier to use for common use cases. The demo - app is halved in size as a result, whilst at the same time gaining more - functionality. Read more - [here](https://medium.com/google-exoplayer/exoplayer-2-x-improved-demo-app-d97171aaaaa1). - * Enhanced library support for implementing audio extensions. Read more - [here](https://medium.com/google-exoplayer/exoplayer-2-x-new-audio-features-cfb26c2883a#.ua75vu4s3). - * Format and MediaFormat are replaced by a single Format class. -* Key new features: - * Playlist support. Includes support for gapless playback between playlist - items and consistent application of LoadControl and TrackSelector policies - when transitioning between items - ([#1270](https://github.com/google/ExoPlayer/issues/1270)). - * Seeking in live playbacks for DASH and SmoothStreaming - ([#291](https://github.com/google/ExoPlayer/issues/291)). - * DASH multi-period support - ([#557](https://github.com/google/ExoPlayer/issues/557)). - * MediaSource composition allows MediaSources to be concatenated into a - playlist, merged and looped. Read more - [here](https://medium.com/google-exoplayer/exoplayer-2-x-mediasource-composition-6c285fcbca1f#.zfha8qupz). - * Looping support (see above) - ([#490](https://github.com/google/ExoPlayer/issues/490)). - * Ability to query information about all tracks in a piece of media (including - those not supported by the device) - ([#1121](https://github.com/google/ExoPlayer/issues/1121)). - * Improved player controls. - * Support for PSSH in fMP4 moof atoms - ([#1143](https://github.com/google/ExoPlayer/issues/1143)). - * Support for Opus in Ogg - ([#1447](https://github.com/google/ExoPlayer/issues/1447)). - * CacheDataSource support for standalone media file playbacks (mp3, mp4 etc). - * FFMPEG extension (for audio only). -* Key bug fixes: - * Removed unnecessary secondary requests when playing standalone media files - ([#1041](https://github.com/google/ExoPlayer/issues/1041)). - * Fixed playback of video only (i.e. no audio) live streams - ([#758](https://github.com/google/ExoPlayer/issues/758)). - * Fixed silent failure when media buffer is too small - ([#583](https://github.com/google/ExoPlayer/issues/583)). - * Suppressed "Sending message to a Handler on a dead thread" warnings - ([#426](https://github.com/google/ExoPlayer/issues/426)). +* Root package name changed to `com.google.android.exoplayer2`. The library + structure and class names have also been sanitized. Read more + [here](https://medium.com/google-exoplayer/exoplayer-2-x-new-package-and-class-names-ef8e1d9ba96f#.lv8sd4nez). +* Key architectural changes: + * Late binding between rendering and media source components. Allows the + same rendering components to be re-used from one playback to another. + Enables features such as gapless playback through playlists and DASH + multi-period support. + * Improved track selection design. More details can be found + [here](https://medium.com/google-exoplayer/exoplayer-2-x-track-selection-2b62ff712cc9#.n00zo76b6). + * LoadControl now used to control buffering and loading across all + playback types. + * Media source components given additional structure. A new MediaSource + class has been introduced. MediaSources expose Timelines that describe + the media they expose, and can consist of multiple MediaPeriods. This + enables features such as seeking in live playbacks and DASH multi-period + support. + * Responsibility for loading the initial DASH/SmoothStreaming/HLS manifest + is promoted to the corresponding MediaSource components and is no longer + the application's responsibility. + * Higher level abstractions such as SimpleExoPlayer have been added to the + library. These make the library easier to use for common use cases. The + demo app is halved in size as a result, whilst at the same time gaining + more functionality. Read more + [here](https://medium.com/google-exoplayer/exoplayer-2-x-improved-demo-app-d97171aaaaa1). + * Enhanced library support for implementing audio extensions. Read more + [here](https://medium.com/google-exoplayer/exoplayer-2-x-new-audio-features-cfb26c2883a#.ua75vu4s3). + * Format and MediaFormat are replaced by a single Format class. +* Key new features: + * Playlist support. Includes support for gapless playback between playlist + items and consistent application of LoadControl and TrackSelector + policies when transitioning between items + ([#1270](https://github.com/google/ExoPlayer/issues/1270)). + * Seeking in live playbacks for DASH and SmoothStreaming + ([#291](https://github.com/google/ExoPlayer/issues/291)). + * DASH multi-period support + ([#557](https://github.com/google/ExoPlayer/issues/557)). + * MediaSource composition allows MediaSources to be concatenated into a + playlist, merged and looped. Read more + [here](https://medium.com/google-exoplayer/exoplayer-2-x-mediasource-composition-6c285fcbca1f#.zfha8qupz). + * Looping support (see above) + ([#490](https://github.com/google/ExoPlayer/issues/490)). + * Ability to query information about all tracks in a piece of media + (including those not supported by the device) + ([#1121](https://github.com/google/ExoPlayer/issues/1121)). + * Improved player controls. + * Support for PSSH in fMP4 moof atoms + ([#1143](https://github.com/google/ExoPlayer/issues/1143)). + * Support for Opus in Ogg + ([#1447](https://github.com/google/ExoPlayer/issues/1447)). + * CacheDataSource support for standalone media file playbacks (mp3, mp4 + etc). + * FFMPEG extension (for audio only). +* Key bug fixes: + * Removed unnecessary secondary requests when playing standalone media + files ([#1041](https://github.com/google/ExoPlayer/issues/1041)). + * Fixed playback of video only (i.e. no audio) live streams + ([#758](https://github.com/google/ExoPlayer/issues/758)). + * Fixed silent failure when media buffer is too small + ([#583](https://github.com/google/ExoPlayer/issues/583)). + * Suppressed "Sending message to a Handler on a dead thread" warnings + ([#426](https://github.com/google/ExoPlayer/issues/426)). -# Legacy release notes # +# Legacy release notes Note: Since ExoPlayer V1 is still being maintained alongside V2, there is some overlap between these notes and the notes above. r2.0.0 followed from r1.5.11, @@ -401,197 +2557,197 @@ in all V2 releases. This cannot be assumed for changes in r1.5.12 and later, however it can be assumed that all such changes are included in the most recent V2 release. -### r1.5.16 ### +### r1.5.16 -* VP9 extension: Reduced binary size. -* FLAC extension: Enabled 64 bit targets and fixed proguard config. -* Misc bugfixes. +* VP9 extension: Reduced binary size. +* FLAC extension: Enabled 64 bit targets and fixed proguard config. +* Misc bugfixes. -### r1.5.15 ### +### r1.5.15 -* SmoothStreaming: Fixed handling of start_time placeholder - ([#2447](https://github.com/google/ExoPlayer/issues/2447)). -* Misc bugfixes. +* SmoothStreaming: Fixed handling of start_time placeholder + ([#2447](https://github.com/google/ExoPlayer/issues/2447)). +* Misc bugfixes. -### r1.5.14 ### +### r1.5.14 -* Fixed cache failures when using an encrypted cache content index. -* SmoothStreaming: Work around "Offset to sample data was negative" failures - ([#2292](https://github.com/google/ExoPlayer/issues/2292), - [#2101](https://github.com/google/ExoPlayer/issues/2101) and - [#1152](https://github.com/google/ExoPlayer/issues/1152)). +* Fixed cache failures when using an encrypted cache content index. +* SmoothStreaming: Work around "Offset to sample data was negative" failures + ([#2292](https://github.com/google/ExoPlayer/issues/2292), + [#2101](https://github.com/google/ExoPlayer/issues/2101) and + [#1152](https://github.com/google/ExoPlayer/issues/1152)). -### r1.5.13 ### +### r1.5.13 -* Improvements to the upstream cache package. -* MP4: Support `.mp3` tracks - ([#2066](https://github.com/google/ExoPlayer/issues/2066)). -* SubRip: Don't fail playbacks if SubRip file contains negative timestamps - ([#2145](https://github.com/google/ExoPlayer/issues/2145)). -* MPEG-TS: Avoid failure when expected ID3 header not found - ([#1966](https://github.com/google/ExoPlayer/issues/1966)). -* Misc bugfixes. +* Improvements to the upstream cache package. +* MP4: Support `.mp3` tracks + ([#2066](https://github.com/google/ExoPlayer/issues/2066)). +* SubRip: Don't fail playbacks if SubRip file contains negative timestamps + ([#2145](https://github.com/google/ExoPlayer/issues/2145)). +* MPEG-TS: Avoid failure when expected ID3 header not found + ([#1966](https://github.com/google/ExoPlayer/issues/1966)). +* Misc bugfixes. -### r1.5.12 ### +### r1.5.12 -* Improvements to Cronet network stack extension. -* Fix bug in demo app introduced in r1.5.11 that caused L3 Widevine - provisioning requests to fail. -* Misc bugfixes. +* Improvements to Cronet network stack extension. +* Fix bug in demo app introduced in r1.5.11 that caused L3 Widevine + provisioning requests to fail. +* Misc bugfixes. -### r1.5.11 ### +### r1.5.11 -* Cronet network stack extension. -* HLS: Fix propagation of language for alternative audio renditions - ([#1784](https://github.com/google/ExoPlayer/issues/1784)). -* WebM: Support for subsample encryption. -* ID3: Fix EOS detection for 2-byte encodings - ([#1774](https://github.com/google/ExoPlayer/issues/1774)). -* MPEG-TS: Support multiple tracks of the same type. -* MPEG-TS: Work toward robust handling of stream corruption. -* Fix ContentDataSource failures triggered by garbage collector - ([#1759](https://github.com/google/ExoPlayer/issues/1759)). +* Cronet network stack extension. +* HLS: Fix propagation of language for alternative audio renditions + ([#1784](https://github.com/google/ExoPlayer/issues/1784)). +* WebM: Support for subsample encryption. +* ID3: Fix EOS detection for 2-byte encodings + ([#1774](https://github.com/google/ExoPlayer/issues/1774)). +* MPEG-TS: Support multiple tracks of the same type. +* MPEG-TS: Work toward robust handling of stream corruption. +* Fix ContentDataSource failures triggered by garbage collector + ([#1759](https://github.com/google/ExoPlayer/issues/1759)). -### r1.5.10 ### +### r1.5.10 -* HLS: Stability fixes. -* MP4: Support for stz2 Atoms. -* Enable 4K format selection on Sony AndroidTV + nVidia SHIELD. -* TX3G caption fixes. +* HLS: Stability fixes. +* MP4: Support for stz2 Atoms. +* Enable 4K format selection on Sony AndroidTV + nVidia SHIELD. +* TX3G caption fixes. -### r1.5.9 ### +### r1.5.9 -* MP4: Fixed incorrect sniffing in some cases (#1523). -* MP4: Improved file compatibility (#1567). -* ID3: Support for TIT2 and APIC frames. -* Fixed querying of platform decoders on some devices. -* Misc bug fixes. +* MP4: Fixed incorrect sniffing in some cases (#1523). +* MP4: Improved file compatibility (#1567). +* ID3: Support for TIT2 and APIC frames. +* Fixed querying of platform decoders on some devices. +* Misc bug fixes. -### r1.5.8 ### +### r1.5.8 -* HLS: Fix handling of HTTP redirects. -* Audio: Minor adjustment to improve A/V sync. -* OGG: Support FLAC in OGG. -* TTML: Support regions. -* WAV/PCM: Support 8, 24 and 32-bit WAV and PCM audio. -* Misc bug fixes and performance optimizations. +* HLS: Fix handling of HTTP redirects. +* Audio: Minor adjustment to improve A/V sync. +* OGG: Support FLAC in OGG. +* TTML: Support regions. +* WAV/PCM: Support 8, 24 and 32-bit WAV and PCM audio. +* Misc bug fixes and performance optimizations. -### r1.5.7 ### +### r1.5.7 -* OGG: Support added for OGG. -* FLAC: Support for FLAC extraction and playback (via an extension). -* HLS: Multiple audio track support (via Renditions). -* FMP4: Support multiple tracks in fragmented MP4 (not applicable to - DASH/SmoothStreaming). -* WAV: Support for 16-bit WAV files. -* MKV: Support non-square pixel formats. -* Misc bug fixes. +* OGG: Support added for OGG. +* FLAC: Support for FLAC extraction and playback (via an extension). +* HLS: Multiple audio track support (via Renditions). +* FMP4: Support multiple tracks in fragmented MP4 (not applicable to + DASH/SmoothStreaming). +* WAV: Support for 16-bit WAV files. +* MKV: Support non-square pixel formats. +* Misc bug fixes. -### r1.5.6 ### +### r1.5.6 -* MP3: Fix mono streams playing at 2x speed on some MediaTek based devices - (#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. -* 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. +* MP3: Fix mono streams playing at 2x speed on some MediaTek based devices + (#801). +* MP3: Fix playback of some streams when stream length is unknown. +* ID3: Support multiple frames of the same type in a single tag. +* 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. -### r1.5.5 ### +### r1.5.5 -* DASH: Enable MP4 embedded WebVTT playback (#1185) -* HLS: Fix handling of extended ID3 tags in MPEG-TS (#1181) -* MP3: Fix incorrect position calculation in VBRI header (#1197) -* Fix issue seeking backward using SingleSampleSource (#1193) +* DASH: Enable MP4 embedded WebVTT playback (#1185) +* HLS: Fix handling of extended ID3 tags in MPEG-TS (#1181) +* MP3: Fix incorrect position calculation in VBRI header (#1197) +* Fix issue seeking backward using SingleSampleSource (#1193) -### r1.5.4 ### +### r1.5.4 -* HLS: Support for variant selection and WebVtt subtitles. -* MP4: Support for embedded WebVtt. -* Improved device compatibility. -* Fix for resource leak (Issue #1066). -* Misc bug fixes + minor features. +* HLS: Support for variant selection and WebVtt subtitles. +* MP4: Support for embedded WebVtt. +* Improved device compatibility. +* Fix for resource leak (Issue #1066). +* Misc bug fixes + minor features. -### r1.5.3 ### +### r1.5.3 -* Support for FLV (without seeking). -* MP4: Fix for playback of media containing basic edit lists. -* QuickTime: Fix parsing of QuickTime style audio sample entry. -* HLS: Add H262 support for devices that have an H262 decoder. -* Allow AudioTrack PlaybackParams (e.g. speed/pitch) on API level 23+. -* Correctly detect 4K displays on API level 23+. -* Misc bug fixes. +* Support for FLV (without seeking). +* MP4: Fix for playback of media containing basic edit lists. +* QuickTime: Fix parsing of QuickTime style audio sample entry. +* HLS: Add H262 support for devices that have an H262 decoder. +* Allow AudioTrack PlaybackParams (e.g. speed/pitch) on API level 23+. +* Correctly detect 4K displays on API level 23+. +* Misc bug fixes. -### r1.5.2 ### +### r1.5.2 -* MPEG-TS/HLS: Fix frame drops playing H265 video. -* SmoothStreaming: Fix parsing of ProtectionHeader. +* MPEG-TS/HLS: Fix frame drops playing H265 video. +* SmoothStreaming: Fix parsing of ProtectionHeader. -### r1.5.1 ### +### r1.5.1 -* Enable smooth frame release by default. -* Added OkHttpDataSource extension. -* AndroidTV: Correctly detect 4K display size on Bravia devices. -* FMP4: Handle non-sample data in mdat boxes. -* TTML: Fix parsing of some colors on Jellybean. -* SmoothStreaming: Ignore tfdt boxes. -* Misc bug fixes. +* Enable smooth frame release by default. +* Added OkHttpDataSource extension. +* AndroidTV: Correctly detect 4K display size on Bravia devices. +* FMP4: Handle non-sample data in mdat boxes. +* TTML: Fix parsing of some colors on Jellybean. +* SmoothStreaming: Ignore tfdt boxes. +* Misc bug fixes. -### r1.5.0 ### +### r1.5.0 -* Multi-track support. -* DASH: Limited support for multi-period manifests. -* HLS: Smoother format adaptation. -* HLS: Support for MP3 media segments. -* TTML: Support for most embedded TTML styling. -* WebVTT: Enhanced positioning support. -* Initial playback tests. -* Misc bug fixes. +* Multi-track support. +* DASH: Limited support for multi-period manifests. +* HLS: Smoother format adaptation. +* HLS: Support for MP3 media segments. +* TTML: Support for most embedded TTML styling. +* WebVTT: Enhanced positioning support. +* Initial playback tests. +* Misc bug fixes. -### r1.4.2 ### +### r1.4.2 -* Implemented automatic format detection for regular container formats. -* Added UdpDataSource for connecting to multicast streams. -* Improved robustness for MP4 playbacks. -* Misc bug fixes. +* Implemented automatic format detection for regular container formats. +* Added UdpDataSource for connecting to multicast streams. +* Improved robustness for MP4 playbacks. +* Misc bug fixes. -### r1.4.1 ### +### r1.4.1 -* HLS: Fix premature playback failures that could occur in some cases. +* HLS: Fix premature playback failures that could occur in some cases. -### r1.4.0 ### +### r1.4.0 -* Support for extracting Matroska streams (implemented by WebmExtractor). -* Support for tx3g captions in MP4 streams. -* Support for H.265 in MPEG-TS streams on supported devices. -* HLS: Added support for MPEG audio (e.g. MP3) in TS media segments. -* HLS: Improved robustness against missing chunks and variants. -* MP4: Added support for embedded MPEG audio (e.g. MP3). -* TTML: Improved handling of whitespace. -* DASH: Support Mpd.Location element. -* Add option to TsExtractor to allow non-IDR keyframes. -* Added MulticastDataSource for connecting to multicast streams. -* (WorkInProgress) - First steps to supporting seeking in DASH DVR window. -* (WorkInProgress) - First steps to supporting styled + positioned subtitles. -* Misc bug fixes. +* Support for extracting Matroska streams (implemented by WebmExtractor). +* Support for tx3g captions in MP4 streams. +* Support for H.265 in MPEG-TS streams on supported devices. +* HLS: Added support for MPEG audio (e.g. MP3) in TS media segments. +* HLS: Improved robustness against missing chunks and variants. +* MP4: Added support for embedded MPEG audio (e.g. MP3). +* TTML: Improved handling of whitespace. +* DASH: Support Mpd.Location element. +* Add option to TsExtractor to allow non-IDR keyframes. +* Added MulticastDataSource for connecting to multicast streams. +* (WorkInProgress) - First steps to supporting seeking in DASH DVR window. +* (WorkInProgress) - First steps to supporting styled + positioned subtitles. +* Misc bug fixes. -### r1.3.3 ### +### r1.3.3 -* HLS: Fix failure when playing HLS AAC streams. -* Misc bug fixes. +* HLS: Fix failure when playing HLS AAC streams. +* Misc bug fixes. -### r1.3.2 ### +### r1.3.2 -* DataSource improvements: `DefaultUriDataSource` now handles http://, https://, - file://, asset:// and content:// URIs automatically. It also handles - file:///android_asset/* URIs, and file paths like /path/to/media.mp4 where the - scheme is omitted. -* HLS: Fix for some ID3 events being dropped. -* HLS: Correctly handle 0x0 and floating point RESOLUTION tags. -* Mp3Extractor: robustness improvements. +* DataSource improvements: `DefaultUriDataSource` now handles http://, + https://, file://, asset:// and content:// URIs automatically. It also + handles file:///android_asset/* URIs, and file paths like /path/to/media.mp4 + where the scheme is omitted. +* HLS: Fix for some ID3 events being dropped. +* HLS: Correctly handle 0x0 and floating point RESOLUTION tags. +* Mp3Extractor: robustness improvements. -### r1.3.1 ### +### r1.3.1 -* No notes provided. +* No notes provided. diff --git a/build.gradle b/build.gradle index a4ae1f175e..00277b8cee 100644 --- a/build.gradle +++ b/build.gradle @@ -13,43 +13,23 @@ // 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:4.0.0' + classpath 'com.novoda:bintray-release:0.9.1' + classpath 'com.google.android.gms:strict-version-matcher-plugin:1.2.1' } } allprojects { repositories { + google() jcenter() + maven { url "https://oss.sonatype.org/content/repositories/snapshots" } } 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 +37,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/common_library_config.gradle b/common_library_config.gradle new file mode 100644 index 0000000000..431a7ab14d --- /dev/null +++ b/common_library_config.gradle @@ -0,0 +1,34 @@ +// Copyright (C) 2020 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +apply from: "$gradle.ext.exoplayerSettingsDir/constants.gradle" +apply plugin: 'com.android.library' + +android { + compileSdkVersion project.ext.compileSdkVersion + + defaultConfig { + minSdkVersion project.ext.minSdkVersion + targetSdkVersion project.ext.targetSdkVersion + consumerProguardFiles 'proguard-rules.txt' + testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner' + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + testOptions.unitTests.includeAndroidResources = true +} diff --git a/constants.gradle b/constants.gradle new file mode 100644 index 0000000000..c2b0000368 --- /dev/null +++ b/constants.gradle @@ -0,0 +1,47 @@ +// Copyright 2017 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// 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.12.0' + releaseVersionCode = 2012000 + minSdkVersion = 16 + appTargetSdkVersion = 29 + targetSdkVersion = 28 // TODO: Bump once b/143232359 is resolved. Also fix TODOs in UtilTest. + compileSdkVersion = 29 + dexmakerVersion = '2.21.0' + junitVersion = '4.13-rc-2' + guavaVersion = '27.1-android' + mockitoVersion = '2.28.2' + mockWebServerVersion = '3.12.0' + robolectricVersion = '4.4' + checkerframeworkVersion = '3.3.0' + checkerframeworkCompatVersion = '2.5.0' + jsr305Version = '3.0.2' + kotlinAnnotationsVersion = '1.3.70' + androidxAnnotationVersion = '1.1.0' + androidxAppCompatVersion = '1.1.0' + androidxCollectionVersion = '1.1.0' + androidxMediaVersion = '1.0.1' + androidxMultidexVersion = '2.0.0' + androidxRecyclerViewVersion = '1.1.0' + 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..b508243371 --- /dev/null +++ b/core_settings.gradle @@ -0,0 +1,76 @@ +// 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 +if (!gradle.ext.has('exoplayerSettingsDir')) { + gradle.ext.exoplayerSettingsDir = + new File(rootDir.toString()).getCanonicalPath() +} +def modulePrefix = ':' +if (gradle.ext.has('exoplayerModulePrefix')) { + modulePrefix += gradle.ext.exoplayerModulePrefix +} + +include modulePrefix + 'library' +include modulePrefix + 'library-common' +include modulePrefix + 'library-core' +include modulePrefix + 'library-dash' +include modulePrefix + 'library-extractor' +include modulePrefix + 'library-hls' +include modulePrefix + 'library-smoothstreaming' +include modulePrefix + 'library-ui' +include modulePrefix + 'testutils' +include modulePrefix + 'testdata' +include modulePrefix + 'extension-av1' +include modulePrefix + 'extension-ffmpeg' +include modulePrefix + 'extension-flac' +include modulePrefix + 'extension-gvr' +include modulePrefix + 'extension-ima' +include modulePrefix + 'extension-cast' +include modulePrefix + 'extension-cronet' +include modulePrefix + 'extension-mediasession' +include modulePrefix + 'extension-media2' +include modulePrefix + 'extension-okhttp' +include modulePrefix + 'extension-opus' +include modulePrefix + 'extension-vp9' +include modulePrefix + 'extension-rtmp' +include modulePrefix + 'extension-leanback' +include modulePrefix + 'extension-jobdispatcher' +include modulePrefix + 'extension-workmanager' + +project(modulePrefix + 'library').projectDir = new File(rootDir, 'library/all') +project(modulePrefix + 'library-common').projectDir = new File(rootDir, 'library/common') +project(modulePrefix + 'library-core').projectDir = new File(rootDir, 'library/core') +project(modulePrefix + 'library-dash').projectDir = new File(rootDir, 'library/dash') +project(modulePrefix + 'library-extractor').projectDir = new File(rootDir, 'library/extractor') +project(modulePrefix + 'library-hls').projectDir = new File(rootDir, 'library/hls') +project(modulePrefix + 'library-smoothstreaming').projectDir = new File(rootDir, 'library/smoothstreaming') +project(modulePrefix + 'library-ui').projectDir = new File(rootDir, 'library/ui') +project(modulePrefix + 'testutils').projectDir = new File(rootDir, 'testutils') +project(modulePrefix + 'testdata').projectDir = new File(rootDir, 'testdata') +project(modulePrefix + 'extension-av1').projectDir = new File(rootDir, 'extensions/av1') +project(modulePrefix + 'extension-ffmpeg').projectDir = new File(rootDir, 'extensions/ffmpeg') +project(modulePrefix + 'extension-flac').projectDir = new File(rootDir, 'extensions/flac') +project(modulePrefix + 'extension-gvr').projectDir = new File(rootDir, 'extensions/gvr') +project(modulePrefix + 'extension-ima').projectDir = new File(rootDir, 'extensions/ima') +project(modulePrefix + 'extension-cast').projectDir = new File(rootDir, 'extensions/cast') +project(modulePrefix + 'extension-cronet').projectDir = new File(rootDir, 'extensions/cronet') +project(modulePrefix + 'extension-mediasession').projectDir = new File(rootDir, 'extensions/mediasession') +project(modulePrefix + 'extension-media2').projectDir = new File(rootDir, 'extensions/media2') +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/README.md b/demo/README.md deleted file mode 100644 index ca37392623..0000000000 --- a/demo/README.md +++ /dev/null @@ -1,5 +0,0 @@ -# Demo application # - -This folder contains a demo application that 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/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..2360e01137 --- /dev/null +++ b/demos/README.md @@ -0,0 +1,25 @@ +# ExoPlayer demos # + +This directory contains applications that demonstrate how to use ExoPlayer. +Browse the individual demos and their READMEs to learn more. + +## Running a demo ## + +### From Android Studio ### + +* File -> New -> Import Project -> Specify the root ExoPlayer folder. +* Choose the demo from the run configuration dropdown list. +* Click Run. + +### Using gradle from the command line: ### + +* Open a Terminal window at the root ExoPlayer folder. +* Run `./gradlew projects` to show all projects. Demo projects start with `demo`. +* Run `./gradlew ::tasks` to view the list of available tasks for +the demo project. Choose an install option from the `Install tasks` section. +* Run `./gradlew ::`. + +**Example**: + +`./gradlew :demo:installNoExtensionsDebug` installs the main ExoPlayer demo app + in debug mode with no extensions. diff --git a/demos/cast/README.md b/demos/cast/README.md new file mode 100644 index 0000000000..fd682433f9 --- /dev/null +++ b/demos/cast/README.md @@ -0,0 +1,7 @@ +# Cast demo application # + +This folder contains a demo application that showcases ExoPlayer integration +with Google Cast. + +Please see the [demos README](../README.md) for instructions on how to build and +run this demo. diff --git a/demos/cast/build.gradle b/demos/cast/build.gradle new file mode 100644 index 0000000000..868e3c7b43 --- /dev/null +++ b/demos/cast/build.gradle @@ -0,0 +1,66 @@ +// 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 + multiDexEnabled true + } + + 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.multidex:multidex:' + androidxMultidexVersion + implementation 'androidx.recyclerview:recyclerview:1.1.0' + implementation 'com.google.android.material:material:1.2.1' +} + +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..d92d9e2303 --- /dev/null +++ b/demos/cast/src/main/AndroidManifest.xml @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/DemoApplication.java b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/DemoApplication.java new file mode 100644 index 0000000000..f2d2288b6a --- /dev/null +++ b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/DemoApplication.java @@ -0,0 +1,23 @@ +/* + * Copyright 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2.castdemo; + +import androidx.multidex.MultiDexApplication; + +// Note: Multidex is enabled in code not AndroidManifest.xml because the internal build system +// doesn't dejetify MultiDexApplication in AndroidManifest.xml. +/** Application for multidex support. */ +public final class DemoApplication extends MultiDexApplication {} 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..50343f9205 --- /dev/null +++ b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/DemoUtil.java @@ -0,0 +1,94 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2.castdemo; + +import android.net.Uri; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.MediaItem; +import com.google.android.exoplayer2.MediaMetadata; +import com.google.android.exoplayer2.util.MimeTypes; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** Utility methods and constants for the Cast demo application. */ +/* package */ final class DemoUtil { + + public static final String MIME_TYPE_DASH = MimeTypes.APPLICATION_MPD; + public static final String MIME_TYPE_HLS = MimeTypes.APPLICATION_M3U8; + public static final String MIME_TYPE_SS = MimeTypes.APPLICATION_SS; + public static final String MIME_TYPE_VIDEO_MP4 = MimeTypes.VIDEO_MP4; + + /** The list of samples available in the cast demo app. */ + public static final List SAMPLES; + + static { + ArrayList samples = new ArrayList<>(); + + // Clear content. + samples.add( + new MediaItem.Builder() + .setUri("https://storage.googleapis.com/wvmedia/clear/h264/tears/tears.mpd") + .setMediaMetadata(new MediaMetadata.Builder().setTitle("Clear DASH: Tears").build()) + .setMimeType(MIME_TYPE_DASH) + .build()); + samples.add( + new MediaItem.Builder() + .setUri("https://storage.googleapis.com/shaka-demo-assets/angel-one-hls/hls.m3u8") + .setMediaMetadata(new MediaMetadata.Builder().setTitle("Clear HLS: Angel one").build()) + .setMimeType(MIME_TYPE_HLS) + .build()); + samples.add( + new MediaItem.Builder() + .setUri("https://html5demos.com/assets/dizzy.mp4") + .setMediaMetadata(new MediaMetadata.Builder().setTitle("Clear MP4: Dizzy").build()) + .setMimeType(MIME_TYPE_VIDEO_MP4) + .build()); + + // DRM content. + samples.add( + new MediaItem.Builder() + .setUri(Uri.parse("https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd")) + .setMediaMetadata( + new MediaMetadata.Builder().setTitle("Widevine DASH cenc: Tears").build()) + .setMimeType(MIME_TYPE_DASH) + .setDrmUuid(C.WIDEVINE_UUID) + .setDrmLicenseUri("https://proxy.uat.widevine.com/proxy?provider=widevine_test") + .build()); + samples.add( + new MediaItem.Builder() + .setUri("https://storage.googleapis.com/wvmedia/cbc1/h264/tears/tears_aes_cbc1.mpd") + .setMediaMetadata( + new MediaMetadata.Builder().setTitle("Widevine DASH cbc1: Tears").build()) + .setMimeType(MIME_TYPE_DASH) + .setDrmUuid(C.WIDEVINE_UUID) + .setDrmLicenseUri("https://proxy.uat.widevine.com/proxy?provider=widevine_test") + .build()); + samples.add( + new MediaItem.Builder() + .setUri("https://storage.googleapis.com/wvmedia/cbcs/h264/tears/tears_aes_cbcs.mpd") + .setMediaMetadata( + new MediaMetadata.Builder().setTitle("Widevine DASH cbcs: Tears").build()) + .setMimeType(MIME_TYPE_DASH) + .setDrmUuid(C.WIDEVINE_UUID) + .setDrmLicenseUri("https://proxy.uat.widevine.com/proxy?provider=widevine_test") + .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..cf8b02a515 --- /dev/null +++ b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/MainActivity.java @@ -0,0 +1,315 @@ +/* + * 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.MediaItem; +import com.google.android.exoplayer2.SimpleExoPlayer; +import com.google.android.exoplayer2.ui.PlayerControlView; +import com.google.android.exoplayer2.ui.PlayerView; +import com.google.android.exoplayer2.util.Assertions; +import com.google.android.exoplayer2.util.Util; +import com.google.android.gms.cast.framework.CastButtonFactory; +import com.google.android.gms.cast.framework.CastContext; +import com.google.android.gms.dynamite.DynamiteModule; + +/** + * An activity that plays video using {@link SimpleExoPlayer} and supports casting using ExoPlayer's + * Cast extension. + */ +public class MainActivity extends AppCompatActivity + implements OnClickListener, PlayerManager.Listener { + + private PlayerView localPlayerView; + private PlayerControlView castControlView; + private PlayerManager playerManager; + private RecyclerView mediaQueueList; + private MediaQueueListAdapter mediaQueueListAdapter; + private CastContext castContext; + + // Activity lifecycle methods. + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + // Getting the cast context later than onStart can cause device discovery not to take place. + try { + castContext = CastContext.getSharedInstance(this); + } catch (RuntimeException e) { + Throwable cause = e.getCause(); + while (cause != null) { + if (cause instanceof DynamiteModule.LoadingException) { + setContentView(R.layout.cast_context_error); + return; + } + cause = cause.getCause(); + } + // Unknown error. We propagate it. + throw e; + } + + setContentView(R.layout.main_activity); + + localPlayerView = findViewById(R.id.local_player_view); + localPlayerView.requestFocus(); + + castControlView = findViewById(R.id.cast_control_view); + + mediaQueueList = findViewById(R.id.sample_list); + ItemTouchHelper helper = new ItemTouchHelper(new RecyclerViewCallback()); + helper.attachToRecyclerView(mediaQueueList); + mediaQueueList.setLayoutManager(new LinearLayoutManager(this)); + mediaQueueList.setHasFixedSize(true); + mediaQueueListAdapter = new MediaQueueListAdapter(); + + findViewById(R.id.add_sample_button).setOnClickListener(this); + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + super.onCreateOptionsMenu(menu); + getMenuInflater().inflate(R.menu.menu, menu); + CastButtonFactory.setUpMediaRouteButton(this, menu, R.id.media_route_menu_item); + return true; + } + + @Override + public void onResume() { + super.onResume(); + if (castContext == null) { + // There is no Cast context to work with. Do nothing. + return; + } + playerManager = + new PlayerManager( + /* listener= */ this, + localPlayerView, + castControlView, + /* context= */ this, + castContext); + mediaQueueList.setAdapter(mediaQueueListAdapter); + } + + @Override + public void onPause() { + super.onPause(); + if (castContext == null) { + // Nothing to release. + return; + } + mediaQueueListAdapter.notifyItemRangeRemoved(0, mediaQueueListAdapter.getItemCount()); + mediaQueueList.setAdapter(null); + playerManager.release(); + playerManager = null; + } + + // Activity input. + + @Override + public boolean dispatchKeyEvent(KeyEvent event) { + // If the event was not handled then see if the player view can handle it. + return super.dispatchKeyEvent(event) || playerManager.dispatchKeyEvent(event); + } + + @Override + public void onClick(View view) { + new AlertDialog.Builder(this) + .setTitle(R.string.add_samples) + .setView(buildSampleListView()) + .setPositiveButton(android.R.string.ok, null) + .create() + .show(); + } + + // PlayerManager.Listener implementation. + + @Override + public void onQueuePositionChanged(int previousIndex, int newIndex) { + if (previousIndex != C.INDEX_UNSET) { + mediaQueueListAdapter.notifyItemChanged(previousIndex); + } + if (newIndex != C.INDEX_UNSET) { + mediaQueueListAdapter.notifyItemChanged(newIndex); + } + } + + @Override + public void onUnsupportedTrack(int trackType) { + if (trackType == C.TRACK_TYPE_AUDIO) { + showToast(R.string.error_unsupported_audio); + } else if (trackType == C.TRACK_TYPE_VIDEO) { + showToast(R.string.error_unsupported_video); + } + } + + // Internal methods. + + private void showToast(int messageId) { + Toast.makeText(getApplicationContext(), messageId, Toast.LENGTH_LONG).show(); + } + + private View buildSampleListView() { + View dialogList = getLayoutInflater().inflate(R.layout.sample_list, null); + ListView sampleList = dialogList.findViewById(R.id.sample_list); + sampleList.setAdapter(new SampleListAdapter(this)); + sampleList.setOnItemClickListener( + (parent, view, position, id) -> { + playerManager.addItem(DemoUtil.SAMPLES.get(position)); + mediaQueueListAdapter.notifyItemInserted(playerManager.getMediaQueueSize() - 1); + }); + return dialogList; + } + + // Internal classes. + + private class MediaQueueListAdapter extends RecyclerView.Adapter { + + @Override + @NonNull + public QueueItemViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { + TextView v = (TextView) LayoutInflater.from(parent.getContext()) + .inflate(android.R.layout.simple_list_item_1, parent, false); + return new QueueItemViewHolder(v); + } + + @Override + public void onBindViewHolder(QueueItemViewHolder holder, int position) { + holder.item = Assertions.checkNotNull(playerManager.getItem(position)); + + TextView view = holder.textView; + view.setText(holder.item.mediaMetadata.title); + // TODO: Solve coloring using the theme's ColorStateList. + view.setTextColor( + ColorUtils.setAlphaComponent( + view.getCurrentTextColor(), + position == playerManager.getCurrentItemIndex() ? 255 : 100)); + } + + @Override + public int getItemCount() { + return playerManager.getMediaQueueSize(); + } + + } + + private class RecyclerViewCallback extends ItemTouchHelper.SimpleCallback { + + private int draggingFromPosition; + private int draggingToPosition; + + public RecyclerViewCallback() { + super(ItemTouchHelper.UP | ItemTouchHelper.DOWN, ItemTouchHelper.START | ItemTouchHelper.END); + draggingFromPosition = C.INDEX_UNSET; + draggingToPosition = C.INDEX_UNSET; + } + + @Override + public boolean onMove( + @NonNull RecyclerView list, + RecyclerView.ViewHolder origin, + RecyclerView.ViewHolder target) { + int fromPosition = origin.getAdapterPosition(); + int toPosition = target.getAdapterPosition(); + if (draggingFromPosition == C.INDEX_UNSET) { + // A drag has started, but changes to the media queue will be reflected in clearView(). + draggingFromPosition = fromPosition; + } + draggingToPosition = toPosition; + mediaQueueListAdapter.notifyItemMoved(fromPosition, toPosition); + return true; + } + + @Override + public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) { + int position = viewHolder.getAdapterPosition(); + QueueItemViewHolder queueItemHolder = (QueueItemViewHolder) viewHolder; + if (playerManager.removeItem(queueItemHolder.item)) { + mediaQueueListAdapter.notifyItemRemoved(position); + // Update whichever item took its place, in case it became the new selected item. + mediaQueueListAdapter.notifyItemChanged(position); + } + } + + @Override + public void clearView(@NonNull RecyclerView recyclerView, @NonNull ViewHolder viewHolder) { + super.clearView(recyclerView, viewHolder); + if (draggingFromPosition != C.INDEX_UNSET) { + QueueItemViewHolder queueItemHolder = (QueueItemViewHolder) viewHolder; + // A drag has ended. We reflect the media queue change in the player. + if (!playerManager.moveItem(queueItemHolder.item, draggingToPosition)) { + // The move failed. The entire sequence of onMove calls since the drag started needs to be + // invalidated. + mediaQueueListAdapter.notifyDataSetChanged(); + } + } + draggingFromPosition = C.INDEX_UNSET; + draggingToPosition = C.INDEX_UNSET; + } + } + + private class QueueItemViewHolder extends RecyclerView.ViewHolder implements OnClickListener { + + public final TextView textView; + public MediaItem item; + + public QueueItemViewHolder(TextView textView) { + super(textView); + this.textView = textView; + textView.setOnClickListener(this); + } + + @Override + public void onClick(View v) { + playerManager.selectQueueItem(getAdapterPosition()); + } + } + + private static final class SampleListAdapter extends ArrayAdapter { + + public SampleListAdapter(Context context) { + super(context, android.R.layout.simple_list_item_1, DemoUtil.SAMPLES); + } + + @Override + @NonNull + public View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) { + View view = super.getView(position, convertView, parent); + ((TextView) view).setText(Util.castNonNull(getItem(position)).mediaMetadata.title); + return view; + } + } +} 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..9dc82e0b23 --- /dev/null +++ b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/PlayerManager.java @@ -0,0 +1,350 @@ +/* + * 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.view.KeyEvent; +import android.view.View; +import androidx.annotation.NonNull; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.MediaItem; +import com.google.android.exoplayer2.Player; +import com.google.android.exoplayer2.Player.DiscontinuityReason; +import com.google.android.exoplayer2.Player.EventListener; +import com.google.android.exoplayer2.Player.TimelineChangeReason; +import com.google.android.exoplayer2.SimpleExoPlayer; +import com.google.android.exoplayer2.Timeline; +import com.google.android.exoplayer2.ext.cast.CastPlayer; +import com.google.android.exoplayer2.ext.cast.SessionAvailabilityListener; +import com.google.android.exoplayer2.source.TrackGroupArray; +import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; +import com.google.android.exoplayer2.trackselection.MappingTrackSelector; +import com.google.android.exoplayer2.trackselection.TrackSelectionArray; +import com.google.android.exoplayer2.ui.PlayerControlView; +import com.google.android.exoplayer2.ui.PlayerView; +import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory; +import com.google.android.gms.cast.framework.CastContext; +import java.util.ArrayList; + +/** 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 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; + + 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); + } + + /** 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); + currentPlayer.addMediaItem(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; + } + currentPlayer.removeMediaItem(itemIndex); + 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 newIndex The target index of the item in the queue. + * @return Whether the item move was successful. + */ + public boolean moveItem(MediaItem item, int newIndex) { + int fromIndex = mediaQueue.indexOf(item); + if (fromIndex == -1) { + return false; + } + + // Player update. + currentPlayer.moveMediaItem(fromIndex, newIndex); + mediaQueue.add(newIndex, mediaQueue.remove(fromIndex)); + + // Index update. + if (fromIndex == currentItemIndex) { + maybeSetCurrentItemAndNotify(newIndex); + } else if (fromIndex < currentItemIndex && newIndex >= currentItemIndex) { + maybeSetCurrentItemAndNotify(currentItemIndex - 1); + } else if (fromIndex > currentItemIndex && newIndex <= 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(); + castPlayer.setSessionAvailabilityListener(null); + castPlayer.release(); + localPlayerView.setPlayer(null); + exoPlayer.release(); + } + + // Player.EventListener implementation. + + @Override + public void onPlaybackStateChanged(@Player.State int playbackState) { + updateCurrentItemIndex(); + } + + @Override + public void onPositionDiscontinuity(@DiscontinuityReason int reason) { + updateCurrentItemIndex(); + } + + @Override + public void onTimelineChanged(@NonNull Timeline timeline, @TimelineChangeReason int reason) { + updateCurrentItemIndex(); + } + + @Override + public void onTracksChanged( + @NonNull TrackGroupArray trackGroups, @NonNull 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. + currentPlayer.setMediaItems(mediaQueue, windowIndex, playbackPositionMs); + currentPlayer.setPlayWhenReady(playWhenReady); + currentPlayer.prepare(); + } + + /** + * Starts playback of the item at the given index. + * + * @param itemIndex The index of the item to play. + */ + private void setCurrentItem(int itemIndex) { + maybeSetCurrentItemAndNotify(itemIndex); + if (currentPlayer.getCurrentTimeline().getWindowCount() != mediaQueue.size()) { + // This only happens with the cast player. The receiver app in the cast device clears the + // timeline when the last item of the timeline has been played to end. + currentPlayer.setMediaItems(mediaQueue, itemIndex, C.TIME_UNSET); + } else { + currentPlayer.seekTo(itemIndex, C.TIME_UNSET); + } + currentPlayer.setPlayWhenReady(true); + } + + private void maybeSetCurrentItemAndNotify(int currentItemIndex) { + if (this.currentItemIndex != currentItemIndex) { + int oldIndex = this.currentItemIndex; + this.currentItemIndex = currentItemIndex; + listener.onQueuePositionChanged(oldIndex, currentItemIndex); + } + } +} diff --git a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/package-info.java b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/package-info.java new file mode 100644 index 0000000000..70e2af79df --- /dev/null +++ b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +@NonNullApi +package com.google.android.exoplayer2.castdemo; + +import com.google.android.exoplayer2.util.NonNullApi; 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/demos/gl/README.md b/demos/gl/README.md new file mode 100644 index 0000000000..9bffc3edea --- /dev/null +++ b/demos/gl/README.md @@ -0,0 +1,14 @@ +# ExoPlayer GL demo + +This app demonstrates how to render video to a [GLSurfaceView][] while applying +a GL shader. + +The shader shows an overlap bitmap on top of the video. The overlay bitmap is +drawn using an Android canvas, and includes the current frame's presentation +timestamp, to show how to get the timestamp of the frame currently in the +off-screen surface texture. + +Please see the [demos README](../README.md) for instructions on how to build and +run this demo. + +[GLSurfaceView]: https://developer.android.com/reference/android/opengl/GLSurfaceView diff --git a/demos/gl/build.gradle b/demos/gl/build.gradle new file mode 100644 index 0000000000..e065f9b8f2 --- /dev/null +++ b/demos/gl/build.gradle @@ -0,0 +1,53 @@ +// Copyright (C) 2020 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +apply from: '../../constants.gradle' +apply plugin: 'com.android.application' + +android { + compileSdkVersion project.ext.compileSdkVersion + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + defaultConfig { + versionName project.ext.releaseVersion + versionCode project.ext.releaseVersionCode + minSdkVersion project.ext.minSdkVersion + targetSdkVersion project.ext.appTargetSdkVersion + } + + buildTypes { + release { + shrinkResources true + minifyEnabled true + proguardFiles getDefaultProguardFile('proguard-android.txt') + } + } + + lintOptions { + // This demo app does not have translations. + disable 'MissingTranslation' + } +} + +dependencies { + implementation project(modulePrefix + 'library-core') + implementation project(modulePrefix + 'library-ui') + implementation project(modulePrefix + 'library-dash') + implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion + compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion + compileOnly 'org.checkerframework:checker-compat-qual:' + checkerframeworkCompatVersion +} diff --git a/demos/gl/src/main/AndroidManifest.xml b/demos/gl/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..4c95d1ec2f --- /dev/null +++ b/demos/gl/src/main/AndroidManifest.xml @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/demos/gl/src/main/assets/bitmap_overlay_video_processor_fragment.glsl b/demos/gl/src/main/assets/bitmap_overlay_video_processor_fragment.glsl new file mode 100644 index 0000000000..17fec0601d --- /dev/null +++ b/demos/gl/src/main/assets/bitmap_overlay_video_processor_fragment.glsl @@ -0,0 +1,34 @@ +// Copyright 2020 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#extension GL_OES_EGL_image_external : require +precision mediump float; +// External texture containing video decoder output. +uniform samplerExternalOES tex_sampler_0; +// Texture containing the overlap bitmap. +uniform sampler2D tex_sampler_1; +// Horizontal scaling factor for the overlap bitmap. +uniform float scaleX; +// Vertical scaling factory for the overlap bitmap. +uniform float scaleY; +varying vec2 v_texcoord; +void main() { + vec4 videoColor = texture2D(tex_sampler_0, v_texcoord); + vec4 overlayColor = texture2D(tex_sampler_1, + vec2(v_texcoord.x * scaleX, + v_texcoord.y * scaleY)); + // Blend the video decoder output and the overlay bitmap. + gl_FragColor = videoColor * (1.0 - overlayColor.a) + + overlayColor * overlayColor.a; +} diff --git a/demos/gl/src/main/assets/bitmap_overlay_video_processor_vertex.glsl b/demos/gl/src/main/assets/bitmap_overlay_video_processor_vertex.glsl new file mode 100644 index 0000000000..0c07c12a70 --- /dev/null +++ b/demos/gl/src/main/assets/bitmap_overlay_video_processor_vertex.glsl @@ -0,0 +1,20 @@ +// Copyright 2020 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +attribute vec2 a_position; +attribute vec2 a_texcoord; +varying vec2 v_texcoord; +void main() { + gl_Position = vec4(a_position.x, a_position.y, 0, 1); + v_texcoord = a_texcoord; +} diff --git a/demos/gl/src/main/java/com/google/android/exoplayer2/gldemo/BitmapOverlayVideoProcessor.java b/demos/gl/src/main/java/com/google/android/exoplayer2/gldemo/BitmapOverlayVideoProcessor.java new file mode 100644 index 0000000000..89bea32581 --- /dev/null +++ b/demos/gl/src/main/java/com/google/android/exoplayer2/gldemo/BitmapOverlayVideoProcessor.java @@ -0,0 +1,167 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2.gldemo; + +import android.content.Context; +import android.content.pm.PackageManager; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.drawable.BitmapDrawable; +import android.opengl.GLES20; +import android.opengl.GLUtils; +import androidx.annotation.Nullable; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.util.Assertions; +import com.google.android.exoplayer2.util.GlUtil; +import com.google.android.exoplayer2.util.Util; +import java.io.IOException; +import java.io.InputStream; +import java.util.Locale; +import javax.microedition.khronos.opengles.GL10; + +/** + * Video processor that demonstrates how to overlay a bitmap on video output using a GL shader. The + * bitmap is drawn using an Android {@link Canvas}. + */ +/* package */ final class BitmapOverlayVideoProcessor + implements VideoProcessingGLSurfaceView.VideoProcessor { + + private static final int OVERLAY_WIDTH = 512; + private static final int OVERLAY_HEIGHT = 256; + + private final Context context; + private final Paint paint; + private final int[] textures; + private final Bitmap overlayBitmap; + private final Bitmap logoBitmap; + private final Canvas overlayCanvas; + + private int program; + @Nullable private GlUtil.Attribute[] attributes; + @Nullable private GlUtil.Uniform[] uniforms; + + private float bitmapScaleX; + private float bitmapScaleY; + + public BitmapOverlayVideoProcessor(Context context) { + this.context = context.getApplicationContext(); + paint = new Paint(); + paint.setTextSize(64); + paint.setAntiAlias(true); + paint.setARGB(0xFF, 0xFF, 0xFF, 0xFF); + textures = new int[1]; + overlayBitmap = Bitmap.createBitmap(OVERLAY_WIDTH, OVERLAY_HEIGHT, Bitmap.Config.ARGB_8888); + overlayCanvas = new Canvas(overlayBitmap); + try { + logoBitmap = + ((BitmapDrawable) + context.getPackageManager().getApplicationIcon(context.getPackageName())) + .getBitmap(); + } catch (PackageManager.NameNotFoundException e) { + throw new IllegalStateException(e); + } + } + + @Override + public void initialize() { + String vertexShaderCode = + loadAssetAsString(context, "bitmap_overlay_video_processor_vertex.glsl"); + String fragmentShaderCode = + loadAssetAsString(context, "bitmap_overlay_video_processor_fragment.glsl"); + program = GlUtil.compileProgram(vertexShaderCode, fragmentShaderCode); + GlUtil.Attribute[] attributes = GlUtil.getAttributes(program); + GlUtil.Uniform[] uniforms = GlUtil.getUniforms(program); + for (GlUtil.Attribute attribute : attributes) { + if (attribute.name.equals("a_position")) { + attribute.setBuffer(new float[] {-1, -1, 1, -1, -1, 1, 1, 1}, 2); + } else if (attribute.name.equals("a_texcoord")) { + attribute.setBuffer(new float[] {0, 1, 1, 1, 0, 0, 1, 0}, 2); + } + } + this.attributes = attributes; + this.uniforms = uniforms; + GLES20.glGenTextures(1, textures, 0); + GLES20.glBindTexture(GL10.GL_TEXTURE_2D, textures[0]); + GLES20.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_MIN_FILTER, GL10.GL_NEAREST); + GLES20.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_MAG_FILTER, GL10.GL_LINEAR); + GLES20.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_WRAP_S, GL10.GL_REPEAT); + GLES20.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_WRAP_T, GL10.GL_REPEAT); + GLUtils.texImage2D(GL10.GL_TEXTURE_2D, /* level= */ 0, overlayBitmap, /* border= */ 0); + } + + @Override + public void setSurfaceSize(int width, int height) { + bitmapScaleX = (float) width / OVERLAY_WIDTH; + bitmapScaleY = (float) height / OVERLAY_HEIGHT; + } + + @Override + public void draw(int frameTexture, long frameTimestampUs) { + // Draw to the canvas and store it in a texture. + String text = String.format(Locale.US, "%.02f", frameTimestampUs / (float) C.MICROS_PER_SECOND); + overlayBitmap.eraseColor(Color.TRANSPARENT); + overlayCanvas.drawBitmap(logoBitmap, /* left= */ 32, /* top= */ 32, paint); + overlayCanvas.drawText(text, /* x= */ 200, /* y= */ 130, paint); + GLES20.glBindTexture(GL10.GL_TEXTURE_2D, textures[0]); + GLUtils.texSubImage2D( + GL10.GL_TEXTURE_2D, /* level= */ 0, /* xoffset= */ 0, /* yoffset= */ 0, overlayBitmap); + GlUtil.checkGlError(); + + // Run the shader program. + GlUtil.Uniform[] uniforms = Assertions.checkNotNull(this.uniforms); + GlUtil.Attribute[] attributes = Assertions.checkNotNull(this.attributes); + GLES20.glUseProgram(program); + for (GlUtil.Uniform uniform : uniforms) { + switch (uniform.name) { + case "tex_sampler_0": + uniform.setSamplerTexId(frameTexture, /* unit= */ 0); + break; + case "tex_sampler_1": + uniform.setSamplerTexId(textures[0], /* unit= */ 1); + break; + case "scaleX": + uniform.setFloat(bitmapScaleX); + break; + case "scaleY": + uniform.setFloat(bitmapScaleY); + break; + } + } + for (GlUtil.Attribute copyExternalAttribute : attributes) { + copyExternalAttribute.bind(); + } + for (GlUtil.Uniform copyExternalUniform : uniforms) { + copyExternalUniform.bind(); + } + GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT); + GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, /* first= */ 0, /* count= */ 4); + GlUtil.checkGlError(); + } + + private static String loadAssetAsString(Context context, String assetFileName) { + @Nullable InputStream inputStream = null; + try { + inputStream = context.getAssets().open(assetFileName); + return Util.fromUtf8Bytes(Util.toByteArray(inputStream)); + } catch (IOException e) { + throw new IllegalStateException(e); + } finally { + Util.closeQuietly(inputStream); + } + } +} diff --git a/demos/gl/src/main/java/com/google/android/exoplayer2/gldemo/MainActivity.java b/demos/gl/src/main/java/com/google/android/exoplayer2/gldemo/MainActivity.java new file mode 100644 index 0000000000..dc0a8b990a --- /dev/null +++ b/demos/gl/src/main/java/com/google/android/exoplayer2/gldemo/MainActivity.java @@ -0,0 +1,197 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2.gldemo; + +import android.app.Activity; +import android.content.Context; +import android.content.Intent; +import android.net.Uri; +import android.os.Bundle; +import android.widget.FrameLayout; +import android.widget.Toast; +import androidx.annotation.Nullable; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.MediaItem; +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.FrameworkMediaDrm; +import com.google.android.exoplayer2.drm.HttpMediaDrmCallback; +import com.google.android.exoplayer2.source.MediaSource; +import com.google.android.exoplayer2.source.ProgressiveMediaSource; +import com.google.android.exoplayer2.source.dash.DashMediaSource; +import com.google.android.exoplayer2.ui.PlayerView; +import com.google.android.exoplayer2.upstream.DataSource; +import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory; +import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory; +import com.google.android.exoplayer2.upstream.HttpDataSource; +import com.google.android.exoplayer2.util.Assertions; +import com.google.android.exoplayer2.util.EventLogger; +import com.google.android.exoplayer2.util.GlUtil; +import com.google.android.exoplayer2.util.Util; +import java.util.UUID; + +/** + * Activity that demonstrates playback of video to an {@link android.opengl.GLSurfaceView} with + * postprocessing of the video content using GL. + */ +public final class MainActivity extends Activity { + + private static final String TAG = "MainActivity"; + + private static final String DEFAULT_MEDIA_URI = + "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv"; + + private static final String ACTION_VIEW = "com.google.android.exoplayer.gldemo.action.VIEW"; + private static final String EXTENSION_EXTRA = "extension"; + private static final String DRM_SCHEME_EXTRA = "drm_scheme"; + private static final String DRM_LICENSE_URL_EXTRA = "drm_license_url"; + + @Nullable private PlayerView playerView; + @Nullable private VideoProcessingGLSurfaceView videoProcessingGLSurfaceView; + + @Nullable private SimpleExoPlayer player; + + @Override + protected void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.main_activity); + playerView = findViewById(R.id.player_view); + + Context context = getApplicationContext(); + boolean requestSecureSurface = getIntent().hasExtra(DRM_SCHEME_EXTRA); + if (requestSecureSurface && !GlUtil.isProtectedContentExtensionSupported(context)) { + Toast.makeText( + context, R.string.error_protected_content_extension_not_supported, Toast.LENGTH_LONG) + .show(); + } + + VideoProcessingGLSurfaceView videoProcessingGLSurfaceView = + new VideoProcessingGLSurfaceView( + context, requestSecureSurface, new BitmapOverlayVideoProcessor(context)); + FrameLayout contentFrame = findViewById(R.id.exo_content_frame); + contentFrame.addView(videoProcessingGLSurfaceView); + this.videoProcessingGLSurfaceView = videoProcessingGLSurfaceView; + } + + @Override + public void onStart() { + super.onStart(); + if (Util.SDK_INT > 23) { + initializePlayer(); + if (playerView != null) { + playerView.onResume(); + } + } + } + + @Override + public void onResume() { + super.onResume(); + if (Util.SDK_INT <= 23 || player == null) { + initializePlayer(); + if (playerView != null) { + playerView.onResume(); + } + } + } + + @Override + public void onPause() { + super.onPause(); + if (Util.SDK_INT <= 23) { + if (playerView != null) { + playerView.onPause(); + } + releasePlayer(); + } + } + + @Override + public void onStop() { + super.onStop(); + if (Util.SDK_INT > 23) { + if (playerView != null) { + playerView.onPause(); + } + releasePlayer(); + } + } + + private void initializePlayer() { + Intent intent = getIntent(); + String action = intent.getAction(); + Uri uri = + ACTION_VIEW.equals(action) + ? Assertions.checkNotNull(intent.getData()) + : Uri.parse(DEFAULT_MEDIA_URI); + DrmSessionManager drmSessionManager; + if (Util.SDK_INT >= 18 && intent.hasExtra(DRM_SCHEME_EXTRA)) { + String drmScheme = Assertions.checkNotNull(intent.getStringExtra(DRM_SCHEME_EXTRA)); + String drmLicenseUrl = Assertions.checkNotNull(intent.getStringExtra(DRM_LICENSE_URL_EXTRA)); + UUID drmSchemeUuid = Assertions.checkNotNull(Util.getDrmUuid(drmScheme)); + HttpDataSource.Factory licenseDataSourceFactory = new DefaultHttpDataSourceFactory(); + 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); + MediaSource mediaSource; + @C.ContentType int type = Util.inferContentType(uri, intent.getStringExtra(EXTENSION_EXTRA)); + if (type == C.TYPE_DASH) { + mediaSource = + new DashMediaSource.Factory(dataSourceFactory) + .setDrmSessionManager(drmSessionManager) + .createMediaSource(MediaItem.fromUri(uri)); + } else if (type == C.TYPE_OTHER) { + mediaSource = + new ProgressiveMediaSource.Factory(dataSourceFactory) + .setDrmSessionManager(drmSessionManager) + .createMediaSource(MediaItem.fromUri(uri)); + } else { + throw new IllegalStateException(); + } + + SimpleExoPlayer player = new SimpleExoPlayer.Builder(getApplicationContext()).build(); + player.setRepeatMode(Player.REPEAT_MODE_ALL); + player.setMediaSource(mediaSource); + player.prepare(); + player.play(); + VideoProcessingGLSurfaceView videoProcessingGLSurfaceView = + Assertions.checkNotNull(this.videoProcessingGLSurfaceView); + videoProcessingGLSurfaceView.setVideoComponent( + Assertions.checkNotNull(player.getVideoComponent())); + Assertions.checkNotNull(playerView).setPlayer(player); + player.addAnalyticsListener(new EventLogger(/* trackSelector= */ null)); + this.player = player; + } + + private void releasePlayer() { + Assertions.checkNotNull(playerView).setPlayer(null); + if (player != null) { + player.release(); + Assertions.checkNotNull(videoProcessingGLSurfaceView).setVideoComponent(null); + player = null; + } + } +} diff --git a/demos/gl/src/main/java/com/google/android/exoplayer2/gldemo/VideoProcessingGLSurfaceView.java b/demos/gl/src/main/java/com/google/android/exoplayer2/gldemo/VideoProcessingGLSurfaceView.java new file mode 100644 index 0000000000..7aee74801f --- /dev/null +++ b/demos/gl/src/main/java/com/google/android/exoplayer2/gldemo/VideoProcessingGLSurfaceView.java @@ -0,0 +1,292 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2.gldemo; + +import android.content.Context; +import android.graphics.SurfaceTexture; +import android.media.MediaFormat; +import android.opengl.EGL14; +import android.opengl.GLES20; +import android.opengl.GLSurfaceView; +import android.os.Handler; +import android.view.Surface; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.Player; +import com.google.android.exoplayer2.util.Assertions; +import com.google.android.exoplayer2.util.GlUtil; +import com.google.android.exoplayer2.util.TimedValueQueue; +import com.google.android.exoplayer2.video.VideoFrameMetadataListener; +import java.util.concurrent.atomic.AtomicBoolean; +import javax.microedition.khronos.egl.EGL10; +import javax.microedition.khronos.egl.EGLConfig; +import javax.microedition.khronos.egl.EGLContext; +import javax.microedition.khronos.egl.EGLDisplay; +import javax.microedition.khronos.egl.EGLSurface; +import javax.microedition.khronos.opengles.GL10; + +/** + * {@link GLSurfaceView} that creates a GL context (optionally for protected content) and passes + * video frames to a {@link VideoProcessor} for drawing to the view. + * + *

This view must be created programmatically, as it is necessary to specify whether a context + * supporting protected content should be created at construction time. + */ +public final class VideoProcessingGLSurfaceView extends GLSurfaceView { + + /** Processes video frames, provided via a GL texture. */ + public interface VideoProcessor { + /** Performs any required GL initialization. */ + void initialize(); + + /** Sets the size of the output surface in pixels. */ + void setSurfaceSize(int width, int height); + + /** + * Draws using GL operations. + * + * @param frameTexture The ID of a GL texture containing a video frame. + * @param frameTimestampUs The presentation timestamp of the frame, in microseconds. + */ + void draw(int frameTexture, long frameTimestampUs); + } + + private static final int EGL_PROTECTED_CONTENT_EXT = 0x32C0; + + private final VideoRenderer renderer; + private final Handler mainHandler; + + @Nullable private SurfaceTexture surfaceTexture; + @Nullable private Surface surface; + @Nullable private Player.VideoComponent videoComponent; + + /** + * Creates a new instance. Pass {@code true} for {@code requireSecureContext} if the {@link + * GLSurfaceView GLSurfaceView's} associated GL context should handle secure content (if the + * device supports it). + * + * @param context The {@link Context}. + * @param requireSecureContext Whether a GL context supporting protected content should be + * created, if supported by the device. + * @param videoProcessor Processor that draws to the view. + */ + @SuppressWarnings("InlinedApi") + public VideoProcessingGLSurfaceView( + Context context, boolean requireSecureContext, VideoProcessor videoProcessor) { + super(context); + renderer = new VideoRenderer(videoProcessor); + mainHandler = new Handler(); + setEGLContextClientVersion(2); + setEGLConfigChooser( + /* redSize= */ 8, + /* greenSize= */ 8, + /* blueSize= */ 8, + /* alphaSize= */ 8, + /* depthSize= */ 0, + /* stencilSize= */ 0); + setEGLContextFactory( + new EGLContextFactory() { + @Override + public EGLContext createContext(EGL10 egl, EGLDisplay display, EGLConfig eglConfig) { + int[] glAttributes; + if (requireSecureContext) { + glAttributes = + new int[] { + EGL14.EGL_CONTEXT_CLIENT_VERSION, + 2, + EGL_PROTECTED_CONTENT_EXT, + EGL14.EGL_TRUE, + EGL14.EGL_NONE + }; + } else { + glAttributes = new int[] {EGL14.EGL_CONTEXT_CLIENT_VERSION, 2, EGL14.EGL_NONE}; + } + return egl.eglCreateContext( + display, eglConfig, /* share_context= */ EGL10.EGL_NO_CONTEXT, glAttributes); + } + + @Override + public void destroyContext(EGL10 egl, EGLDisplay display, EGLContext context) { + egl.eglDestroyContext(display, context); + } + }); + setEGLWindowSurfaceFactory( + new EGLWindowSurfaceFactory() { + @Override + public EGLSurface createWindowSurface( + EGL10 egl, EGLDisplay display, EGLConfig config, Object nativeWindow) { + int[] attribsList = + requireSecureContext + ? new int[] {EGL_PROTECTED_CONTENT_EXT, EGL14.EGL_TRUE, EGL10.EGL_NONE} + : new int[] {EGL10.EGL_NONE}; + return egl.eglCreateWindowSurface(display, config, nativeWindow, attribsList); + } + + @Override + public void destroySurface(EGL10 egl, EGLDisplay display, EGLSurface surface) { + egl.eglDestroySurface(display, surface); + } + }); + setRenderer(renderer); + setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY); + } + + /** + * Attaches or detaches (if {@code newVideoComponent} is {@code null}) this view from the video + * component of the player. + * + * @param newVideoComponent The new video component, or {@code null} to detach this view. + */ + public void setVideoComponent(@Nullable Player.VideoComponent newVideoComponent) { + if (newVideoComponent == videoComponent) { + return; + } + if (videoComponent != null) { + if (surface != null) { + videoComponent.clearVideoSurface(surface); + } + videoComponent.clearVideoFrameMetadataListener(renderer); + } + videoComponent = newVideoComponent; + if (videoComponent != null) { + videoComponent.setVideoFrameMetadataListener(renderer); + videoComponent.setVideoSurface(surface); + } + } + + @Override + protected void onDetachedFromWindow() { + super.onDetachedFromWindow(); + // Post to make sure we occur in order with any onSurfaceTextureAvailable calls. + mainHandler.post( + () -> { + if (surface != null) { + if (videoComponent != null) { + videoComponent.setVideoSurface(null); + } + releaseSurface(surfaceTexture, surface); + surfaceTexture = null; + surface = null; + } + }); + } + + private void onSurfaceTextureAvailable(SurfaceTexture surfaceTexture) { + mainHandler.post( + () -> { + SurfaceTexture oldSurfaceTexture = this.surfaceTexture; + Surface oldSurface = VideoProcessingGLSurfaceView.this.surface; + this.surfaceTexture = surfaceTexture; + this.surface = new Surface(surfaceTexture); + releaseSurface(oldSurfaceTexture, oldSurface); + if (videoComponent != null) { + videoComponent.setVideoSurface(surface); + } + }); + } + + private static void releaseSurface( + @Nullable SurfaceTexture oldSurfaceTexture, @Nullable Surface oldSurface) { + if (oldSurfaceTexture != null) { + oldSurfaceTexture.release(); + } + if (oldSurface != null) { + oldSurface.release(); + } + } + + private final class VideoRenderer implements GLSurfaceView.Renderer, VideoFrameMetadataListener { + + private final VideoProcessor videoProcessor; + private final AtomicBoolean frameAvailable; + private final TimedValueQueue sampleTimestampQueue; + + private int texture; + @Nullable private SurfaceTexture surfaceTexture; + + private boolean initialized; + private int width; + private int height; + private long frameTimestampUs; + + public VideoRenderer(VideoProcessor videoProcessor) { + this.videoProcessor = videoProcessor; + frameAvailable = new AtomicBoolean(); + sampleTimestampQueue = new TimedValueQueue<>(); + width = -1; + height = -1; + } + + @Override + public synchronized void onSurfaceCreated(GL10 gl, EGLConfig config) { + texture = GlUtil.createExternalTexture(); + surfaceTexture = new SurfaceTexture(texture); + surfaceTexture.setOnFrameAvailableListener( + surfaceTexture -> { + frameAvailable.set(true); + requestRender(); + }); + onSurfaceTextureAvailable(surfaceTexture); + } + + @Override + public void onSurfaceChanged(GL10 gl, int width, int height) { + GLES20.glViewport(0, 0, width, height); + this.width = width; + this.height = height; + } + + @Override + public void onDrawFrame(GL10 gl) { + if (videoProcessor == null) { + return; + } + + if (!initialized) { + videoProcessor.initialize(); + initialized = true; + } + + if (width != -1 && height != -1) { + videoProcessor.setSurfaceSize(width, height); + width = -1; + height = -1; + } + + if (frameAvailable.compareAndSet(true, false)) { + SurfaceTexture surfaceTexture = Assertions.checkNotNull(this.surfaceTexture); + surfaceTexture.updateTexImage(); + long lastFrameTimestampNs = surfaceTexture.getTimestamp(); + Long frameTimestampUs = sampleTimestampQueue.poll(lastFrameTimestampNs); + if (frameTimestampUs != null) { + this.frameTimestampUs = frameTimestampUs; + } + } + + videoProcessor.draw(texture, frameTimestampUs); + } + + @Override + public void onVideoFrameAboutToBeRendered( + long presentationTimeUs, + long releaseTimeNs, + @NonNull Format format, + @Nullable MediaFormat mediaFormat) { + sampleTimestampQueue.add(releaseTimeNs, presentationTimeUs); + } + } +} diff --git a/demos/gl/src/main/java/com/google/android/exoplayer2/gldemo/package-info.java b/demos/gl/src/main/java/com/google/android/exoplayer2/gldemo/package-info.java new file mode 100644 index 0000000000..59ad052449 --- /dev/null +++ b/demos/gl/src/main/java/com/google/android/exoplayer2/gldemo/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +@NonNullApi +package com.google.android.exoplayer2.gldemo; + +import com.google.android.exoplayer2.util.NonNullApi; diff --git a/demos/gl/src/main/res/layout/main_activity.xml b/demos/gl/src/main/res/layout/main_activity.xml new file mode 100644 index 0000000000..4728dc2d49 --- /dev/null +++ b/demos/gl/src/main/res/layout/main_activity.xml @@ -0,0 +1,29 @@ + + + + + + + diff --git a/demos/gl/src/main/res/mipmap-hdpi/ic_launcher.png b/demos/gl/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000000..adaa93220e Binary files /dev/null and b/demos/gl/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/demos/gl/src/main/res/mipmap-mdpi/ic_launcher.png b/demos/gl/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000000..9b6f7d5e80 Binary files /dev/null and b/demos/gl/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/demos/gl/src/main/res/mipmap-xhdpi/ic_launcher.png b/demos/gl/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000000..2101026c9f Binary files /dev/null and b/demos/gl/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/demos/gl/src/main/res/mipmap-xxhdpi/ic_launcher.png b/demos/gl/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000000..223ec8bd11 Binary files /dev/null and b/demos/gl/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/demos/gl/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/demos/gl/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000000..698ed68c42 Binary files /dev/null and b/demos/gl/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/demos/gl/src/main/res/values/strings.xml b/demos/gl/src/main/res/values/strings.xml new file mode 100644 index 0000000000..7e9e5d9961 --- /dev/null +++ b/demos/gl/src/main/res/values/strings.xml @@ -0,0 +1,22 @@ + + + + + ExoPlayer GL demo + + The GL protected content extension is not supported. + + diff --git a/demos/main/README.md b/demos/main/README.md new file mode 100644 index 0000000000..00072c070b --- /dev/null +++ b/demos/main/README.md @@ -0,0 +1,8 @@ +# ExoPlayer main demo # + +This is the main ExoPlayer demo application. It uses ExoPlayer to play a number +of test streams. It can be used as a starting point or reference project when +developing other applications that make use of the ExoPlayer library. + +Please see the [demos README](../README.md) for instructions on how to build and +run this demo. diff --git a/demos/main/build.gradle b/demos/main/build.gradle new file mode 100644 index 0000000000..716b3c1f99 --- /dev/null +++ b/demos/main/build.gradle @@ -0,0 +1,95 @@ +// 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 + multiDexEnabled true + } + + 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 "decoderExtensions" + + productFlavors { + noDecoderExtensions { + dimension "decoderExtensions" + buildConfigField "boolean", "USE_DECODER_EXTENSIONS", "false" + } + withDecoderExtensions { + dimension "decoderExtensions" + buildConfigField "boolean", "USE_DECODER_EXTENSIONS", "true" + } + } +} + +dependencies { + compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion + implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion + implementation 'androidx.appcompat:appcompat:' + androidxAppCompatVersion + implementation 'androidx.multidex:multidex:' + androidxMultidexVersion + implementation 'com.google.android.material:material:1.2.1' + implementation ('com.google.guava:guava:' + guavaVersion) { + exclude group: 'com.google.code.findbugs', module: 'jsr305' + exclude group: 'org.checkerframework', module: 'checker-compat-qual' + exclude group: 'com.google.errorprone', module: 'error_prone_annotations' + exclude group: 'com.google.j2objc', module: 'j2objc-annotations' + exclude group: 'org.codehaus.mojo', module: 'animal-sniffer-annotations' + } + 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-cronet') + implementation project(modulePrefix + 'extension-ima') + withDecoderExtensionsImplementation project(modulePrefix + 'extension-av1') + withDecoderExtensionsImplementation project(modulePrefix + 'extension-ffmpeg') + withDecoderExtensionsImplementation project(modulePrefix + 'extension-flac') + withDecoderExtensionsImplementation project(modulePrefix + 'extension-opus') + withDecoderExtensionsImplementation project(modulePrefix + 'extension-vp9') + withDecoderExtensionsImplementation project(modulePrefix + 'extension-rtmp') +} + +apply plugin: 'com.google.android.gms.strict-version-matcher-plugin' diff --git a/demos/main/proguard-rules.txt b/demos/main/proguard-rules.txt new file mode 100644 index 0000000000..5358f3cec7 --- /dev/null +++ b/demos/main/proguard-rules.txt @@ -0,0 +1,2 @@ +# Proguard rules specific to the main demo app. + diff --git a/demo/src/main/AndroidManifest.xml b/demos/main/src/main/AndroidManifest.xml similarity index 73% rename from demo/src/main/AndroidManifest.xml rename to demos/main/src/main/AndroidManifest.xml index afcddccac9..053665502b 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="androidx.multidex.MultiDexApplication" + tools:targetApi="29"> + 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..ce1854db85 --- /dev/null +++ b/demos/main/src/main/assets/media.exolist.json @@ -0,0 +1,558 @@ +[ + { + "name": "YouTube DASH", + "samples": [ + { + "name": "Google Glass H264 (MP4)", + "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 H264 (MP4)", + "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 VP9 (WebM)", + "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 VP9 (WebM)", + "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 GTS policy tests", + "samples": [ + { + "name": "SW secure crypto (L3)", + "uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd", + "drm_scheme": "widevine", + "drm_license_uri": "https://proxy.uat.widevine.com/proxy?video_id=GTS_SW_SECURE_CRYPTO&provider=widevine_test" + }, + { + "name": "SW secure decode", + "uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd", + "drm_scheme": "widevine", + "drm_license_uri": "https://proxy.uat.widevine.com/proxy?video_id=GTS_SW_SECURE_DECODE&provider=widevine_test" + }, + { + "name": "HW secure crypto", + "uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd", + "drm_scheme": "widevine", + "drm_license_uri": "https://proxy.uat.widevine.com/proxy?video_id=GTS_HW_SECURE_CRYPTO&provider=widevine_test" + }, + { + "name": "HW secure decode", + "uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd", + "drm_scheme": "widevine", + "drm_license_uri": "https://proxy.uat.widevine.com/proxy?video_id=GTS_HW_SECURE_DECODE&provider=widevine_test" + }, + { + "name": "HW secure all (L1)", + "uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd", + "drm_scheme": "widevine", + "drm_license_uri": "https://proxy.uat.widevine.com/proxy?video_id=GTS_HW_SECURE_ALL&provider=widevine_test" + }, + { + "name": "30s license (fails at ~30s)", + "uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd", + "drm_scheme": "widevine", + "drm_license_uri": "https://proxy.uat.widevine.com/proxy?video_id=GTS_CAN_RENEW_FALSE_LICENSE_30S_PLAYBACK_30S&provider=widevine_test" + }, + { + "name": "HDCP not required", + "uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd", + "drm_scheme": "widevine", + "drm_license_uri": "https://proxy.uat.widevine.com/proxy?video_id=GTS_SW_SECURE_CRYPTO_HDCP_NONE&provider=widevine_test" + }, + { + "name": "HDCP 1.0 required", + "uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd", + "drm_scheme": "widevine", + "drm_license_uri": "https://proxy.uat.widevine.com/proxy?video_id=GTS_SW_SECURE_CRYPTO_HDCP_V1&provider=widevine_test" + }, + { + "name": "HDCP 2.0 required", + "uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd", + "drm_scheme": "widevine", + "drm_license_uri": "https://proxy.uat.widevine.com/proxy?video_id=GTS_SW_SECURE_CRYPTO_HDCP_V2&provider=widevine_test" + }, + { + "name": "HDCP 2.1 required", + "uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd", + "drm_scheme": "widevine", + "drm_license_uri": "https://proxy.uat.widevine.com/proxy?video_id=GTS_SW_SECURE_CRYPTO_HDCP_V2_1&provider=widevine_test" + }, + { + "name": "HDCP 2.2 required", + "uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd", + "drm_scheme": "widevine", + "drm_license_uri": "https://proxy.uat.widevine.com/proxy?video_id=GTS_SW_SECURE_CRYPTO_HDCP_V2_2&provider=widevine_test" + }, + { + "name": "HDCP no digital output", + "uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd", + "drm_scheme": "widevine", + "drm_license_uri": "https://proxy.uat.widevine.com/proxy?video_id=GTS_SW_SECURE_CRYPTO_HDCP_NO_DIGITAL_OUTPUT&provider=widevine_test" + } + ] + }, + { + "name": "Widevine DASH H264 (MP4)", + "samples": [ + { + "name": "Clear", + "uri": "https://storage.googleapis.com/wvmedia/clear/h264/tears/tears.mpd" + }, + { + "name": "Clear UHD", + "uri": "https://storage.googleapis.com/wvmedia/clear/h264/tears/tears_uhd.mpd" + }, + { + "name": "Secure (cenc)", + "uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd", + "drm_scheme": "widevine", + "drm_license_uri": "https://proxy.uat.widevine.com/proxy?provider=widevine_test" + }, + { + "name": "Secure UHD (cenc)", + "uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears_uhd.mpd", + "drm_scheme": "widevine", + "drm_license_uri": "https://proxy.uat.widevine.com/proxy?provider=widevine_test" + }, + { + "name": "Secure (cbcs)", + "uri": "https://storage.googleapis.com/wvmedia/cbcs/h264/tears/tears_aes_cbcs.mpd", + "drm_scheme": "widevine", + "drm_license_uri": "https://proxy.uat.widevine.com/proxy?provider=widevine_test" + }, + { + "name": "Secure UHD (cbcs)", + "uri": "https://storage.googleapis.com/wvmedia/cbcs/h264/tears/tears_aes_cbcs_uhd.mpd", + "drm_scheme": "widevine", + "drm_license_uri": "https://proxy.uat.widevine.com/proxy?provider=widevine_test" + }, + { + "name": "Secure -> Clear -> Secure (cenc)", + "uri": "https://storage.googleapis.com/exoplayer-test-media-1/widevine/tears_enc_clear_enc.mpd", + "drm_scheme": "widevine", + "drm_license_uri": "https://proxy.uat.widevine.com/proxy?provider=widevine_test", + "drm_session_for_clear_content": true + } + ] + }, + { + "name": "Widevine DASH VP9 (WebM)", + "samples": [ + { + "name": "Clear", + "uri": "https://storage.googleapis.com/wvmedia/clear/vp9/tears/tears.mpd" + }, + { + "name": "Clear UHD", + "uri": "https://storage.googleapis.com/wvmedia/clear/vp9/tears/tears_uhd.mpd" + }, + { + "name": "Secure (full-sample)", + "uri": "https://storage.googleapis.com/wvmedia/cenc/vp9/tears/tears.mpd", + "drm_scheme": "widevine", + "drm_license_uri": "https://proxy.uat.widevine.com/proxy?provider=widevine_test" + }, + { + "name": "Secure UHD (full-sample)", + "uri": "https://storage.googleapis.com/wvmedia/cenc/vp9/tears/tears_uhd.mpd", + "drm_scheme": "widevine", + "drm_license_uri": "https://proxy.uat.widevine.com/proxy?provider=widevine_test" + }, + { + "name": "Secure (sub-sample)", + "uri": "https://storage.googleapis.com/wvmedia/cenc/vp9/subsample/24fps/tears/tears.mpd", + "drm_scheme": "widevine", + "drm_license_uri": "https://proxy.uat.widevine.com/proxy?provider=widevine_test" + }, + { + "name": "Secure UHD (sub-sample)", + "uri": "https://storage.googleapis.com/wvmedia/cenc/vp9/subsample/24fps/tears/tears_uhd.mpd", + "drm_scheme": "widevine", + "drm_license_uri": "https://proxy.uat.widevine.com/proxy?provider=widevine_test" + } + ] + }, + { + "name": "Widevine DASH H265 (MP4)", + "samples": [ + { + "name": "Clear", + "uri": "https://storage.googleapis.com/wvmedia/clear/hevc/tears/tears.mpd" + }, + { + "name": "Clear UHD", + "uri": "https://storage.googleapis.com/wvmedia/clear/hevc/tears/tears_uhd.mpd" + }, + { + "name": "Secure", + "uri": "https://storage.googleapis.com/wvmedia/cenc/hevc/tears/tears.mpd", + "drm_scheme": "widevine", + "drm_license_uri": "https://proxy.uat.widevine.com/proxy?provider=widevine_test" + }, + { + "name": "Secure UHD", + "uri": "https://storage.googleapis.com/wvmedia/cenc/hevc/tears/tears_uhd.mpd", + "drm_scheme": "widevine", + "drm_license_uri": "https://proxy.uat.widevine.com/proxy?provider=widevine_test" + } + ] + }, + { + "name": "Widevine AV1 (WebM)", + "samples": [ + { + "name": "Clear", + "uri": "https://storage.googleapis.com/wvmedia/2019/clear/av1/24/webm/llama_av1_480p_400.webm" + }, + { + "name": "Secure L3", + "uri": "https://storage.googleapis.com/wvmedia/2019/cenc/av1/24/webm/llama_av1_480p_400.webm", + "drm_scheme": "widevine", + "drm_license_uri": "https://proxy.uat.widevine.com/proxy?video_id=GTS_SW_SECURE_CRYPTO&provider=widevine_test" + }, + { + "name": "Secure L1", + "uri": "https://storage.googleapis.com/wvmedia/2019/cenc/av1/24/webm/llama_av1_480p_400.webm", + "drm_scheme": "widevine", + "drm_license_uri": "https://proxy.uat.widevine.com/proxy?video_id=GTS_HW_SECURE_ALL&provider=widevine_test" + } + ] + }, + { + "name": "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": "One hour frame counter (MP4)", + "uri": "https://storage.googleapis.com/exoplayer-test-media-1/mp4/frame-counter-one-hour.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_uri": "https://proxy.uat.widevine.com/proxy?provider=widevine_test" + }, + { + "uri": "https://html5demos.com/assets/dizzy.mp4" + }, + { + "uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears_sd.mpd", + "drm_scheme": "widevine", + "drm_license_uri": "https://proxy.uat.widevine.com/proxy?provider=widevine_test" + }, + { + "uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears_sd.mpd", + "drm_scheme": "widevine", + "drm_license_uri": "https://proxy.uat.widevine.com/proxy?provider=widevine_test" + } + ] + }, + { + "name": "Manual ad insertion", + "playlist": [ + { + "uri": "https://html5demos.com/assets/dizzy.mp4", + "clip_end_position_ms": 10000 + }, + { + "uri": "https://storage.googleapis.com/exoplayer-test-media-1/mp4/frame-counter-one-hour.mp4", + "clip_end_position_ms": 5000 + }, + { + "uri": "https://html5demos.com/assets/dizzy.mp4", + "clip_start_position_ms": 10000 + } + ] + } + ] + }, + { + "name": "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": "VMAP midroll at 1765 s", + "uri": "https://storage.googleapis.com/exoplayer-test-media-1/mp4/frame-counter-one-hour.mp4", + "ad_tag_uri": "https://vastsynthesizer.appspot.com/midroll-large" + } + ] + }, + { + "name": "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": "WebVTT line positioning", + "uri": "https://html5demos.com/assets/dizzy.mp4", + "subtitle_uri": "https://storage.googleapis.com/exoplayer-test-media-1/webvtt/numeric-lines.vtt", + "subtitle_mime_type": "text/vtt", + "subtitle_language": "en" + }, + { + "name": "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" + }, + { + "name": "MPEG-4 Timed Text (tx3g, mov_text)", + "uri": "https://storage.googleapis.com/exoplayer-test-media-1/mp4/dizzy-with-tx3g.mp4" + } + ] + }, + { + "name": "60fps", + "samples": [ + { + "name": "Big Buck Bunny (DASH,H264,1080p,Clear)", + "uri": "https://storage.googleapis.com/exoplayer-test-media-1/60fps/bbb-clear-1080/manifest.mpd" + }, + { + "name": "Big Buck Bunny (DASH,H264,4K,Clear)", + "uri": "https://storage.googleapis.com/exoplayer-test-media-1/60fps/bbb-clear-2160/manifest.mpd" + }, + { + "name": "Big Buck Bunny (DASH,H264,1080p,Widevine)", + "uri": "https://storage.googleapis.com/exoplayer-test-media-1/60fps/bbb-wv-1080/manifest.mpd", + "drm_scheme": "widevine", + "drm_license_uri": "https://proxy.uat.widevine.com/proxy?provider=widevine_test" + }, + { + "name": "Big Buck Bunny (DASH,H264,4K,Widevine)", + "uri": "https://storage.googleapis.com/exoplayer-test-media-1/60fps/bbb-wv-2160/manifest.mpd", + "drm_scheme": "widevine", + "drm_license_uri": "https://proxy.uat.widevine.com/proxy?provider=widevine_test" + } + ] + } +] 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..c462c14c75 --- /dev/null +++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/DemoDownloadService.java @@ -0,0 +1,123 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2.demo; + +import static com.google.android.exoplayer2.demo.DemoUtil.DOWNLOAD_NOTIFICATION_CHANNEL_ID; + +import android.app.Notification; +import android.content.Context; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import com.google.android.exoplayer2.offline.Download; +import com.google.android.exoplayer2.offline.DownloadManager; +import com.google.android.exoplayer2.offline.DownloadService; +import com.google.android.exoplayer2.scheduler.PlatformScheduler; +import com.google.android.exoplayer2.ui.DownloadNotificationHelper; +import com.google.android.exoplayer2.util.NotificationUtil; +import com.google.android.exoplayer2.util.Util; +import java.util.List; + +/** A service for downloading media. */ +public class DemoDownloadService extends DownloadService { + + private static final int JOB_ID = 1; + private static final int FOREGROUND_NOTIFICATION_ID = 1; + + public DemoDownloadService() { + super( + FOREGROUND_NOTIFICATION_ID, + DEFAULT_FOREGROUND_NOTIFICATION_UPDATE_INTERVAL, + DOWNLOAD_NOTIFICATION_CHANNEL_ID, + R.string.exo_download_notification_channel_name, + /* channelDescriptionResourceId= */ 0); + } + + @Override + @NonNull + protected DownloadManager getDownloadManager() { + // This will only happen once, because getDownloadManager is guaranteed to be called only once + // in the life cycle of the process. + DownloadManager downloadManager = DemoUtil.getDownloadManager(/* context= */ this); + DownloadNotificationHelper downloadNotificationHelper = + DemoUtil.getDownloadNotificationHelper(/* context= */ this); + downloadManager.addListener( + new TerminalStateNotificationHelper( + this, downloadNotificationHelper, FOREGROUND_NOTIFICATION_ID + 1)); + return downloadManager; + } + + @Override + protected PlatformScheduler getScheduler() { + return Util.SDK_INT >= 21 ? new PlatformScheduler(this, JOB_ID) : null; + } + + @Override + @NonNull + protected Notification getForegroundNotification(@NonNull List downloads) { + return DemoUtil.getDownloadNotificationHelper(/* context= */ this) + .buildProgressNotification( + /* context= */ this, + R.drawable.ic_download, + /* contentIntent= */ null, + /* message= */ null, + downloads); + } + + /** + * Creates and displays notifications for downloads when they complete or fail. + * + *

This helper will outlive the lifespan of a single instance of {@link DemoDownloadService}. + * It is static to avoid leaking the first {@link DemoDownloadService} instance. + */ + private static final class TerminalStateNotificationHelper implements DownloadManager.Listener { + + private final Context context; + private final DownloadNotificationHelper notificationHelper; + + private int nextNotificationId; + + public TerminalStateNotificationHelper( + Context context, DownloadNotificationHelper notificationHelper, int firstNotificationId) { + this.context = context.getApplicationContext(); + this.notificationHelper = notificationHelper; + nextNotificationId = firstNotificationId; + } + + @Override + public void onDownloadChanged( + DownloadManager downloadManager, Download download, @Nullable Exception finalException) { + Notification notification; + if (download.state == Download.STATE_COMPLETED) { + notification = + notificationHelper.buildDownloadCompletedNotification( + context, + R.drawable.ic_download_done, + /* contentIntent= */ null, + Util.fromUtf8Bytes(download.request.data)); + } else if (download.state == Download.STATE_FAILED) { + notification = + notificationHelper.buildDownloadFailedNotification( + context, + R.drawable.ic_download_done, + /* contentIntent= */ null, + Util.fromUtf8Bytes(download.request.data)); + } else { + return; + } + NotificationUtil.setNotification(context, nextNotificationId++, notification); + } + } +} diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/DemoUtil.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/DemoUtil.java new file mode 100644 index 0000000000..2d15dfcbb4 --- /dev/null +++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/DemoUtil.java @@ -0,0 +1,196 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2.demo; + +import android.content.Context; +import com.google.android.exoplayer2.DefaultRenderersFactory; +import com.google.android.exoplayer2.RenderersFactory; +import com.google.android.exoplayer2.database.DatabaseProvider; +import com.google.android.exoplayer2.database.ExoDatabaseProvider; +import com.google.android.exoplayer2.ext.cronet.CronetDataSourceFactory; +import com.google.android.exoplayer2.ext.cronet.CronetEngineWrapper; +import com.google.android.exoplayer2.offline.ActionFileUpgradeUtil; +import com.google.android.exoplayer2.offline.DefaultDownloadIndex; +import com.google.android.exoplayer2.offline.DownloadManager; +import com.google.android.exoplayer2.ui.DownloadNotificationHelper; +import com.google.android.exoplayer2.upstream.DataSource; +import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory; +import com.google.android.exoplayer2.upstream.HttpDataSource; +import com.google.android.exoplayer2.upstream.cache.Cache; +import com.google.android.exoplayer2.upstream.cache.CacheDataSource; +import com.google.android.exoplayer2.upstream.cache.NoOpCacheEvictor; +import com.google.android.exoplayer2.upstream.cache.SimpleCache; +import com.google.android.exoplayer2.util.Log; +import java.io.File; +import java.io.IOException; +import java.util.concurrent.Executors; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; + +/** Utility methods for the demo app. */ +public final class DemoUtil { + + public static final String DOWNLOAD_NOTIFICATION_CHANNEL_ID = "download_channel"; + + private static final String TAG = "DemoUtil"; + private static final String DOWNLOAD_ACTION_FILE = "actions"; + private static final String DOWNLOAD_TRACKER_ACTION_FILE = "tracked_actions"; + private static final String DOWNLOAD_CONTENT_DIRECTORY = "downloads"; + + private static DataSource.@MonotonicNonNull Factory dataSourceFactory; + private static HttpDataSource.@MonotonicNonNull Factory httpDataSourceFactory; + private static @MonotonicNonNull DatabaseProvider databaseProvider; + private static @MonotonicNonNull File downloadDirectory; + private static @MonotonicNonNull Cache downloadCache; + private static @MonotonicNonNull DownloadManager downloadManager; + private static @MonotonicNonNull DownloadTracker downloadTracker; + private static @MonotonicNonNull DownloadNotificationHelper downloadNotificationHelper; + + /** Returns whether extension renderers should be used. */ + public static boolean useExtensionRenderers() { + return BuildConfig.USE_DECODER_EXTENSIONS; + } + + public static RenderersFactory buildRenderersFactory( + Context context, boolean preferExtensionRenderer) { + @DefaultRenderersFactory.ExtensionRendererMode + int extensionRendererMode = + useExtensionRenderers() + ? (preferExtensionRenderer + ? DefaultRenderersFactory.EXTENSION_RENDERER_MODE_PREFER + : DefaultRenderersFactory.EXTENSION_RENDERER_MODE_ON) + : DefaultRenderersFactory.EXTENSION_RENDERER_MODE_OFF; + return new DefaultRenderersFactory(context.getApplicationContext()) + .setExtensionRendererMode(extensionRendererMode); + } + + public static synchronized HttpDataSource.Factory getHttpDataSourceFactory(Context context) { + if (httpDataSourceFactory == null) { + context = context.getApplicationContext(); + CronetEngineWrapper cronetEngineWrapper = new CronetEngineWrapper(context); + httpDataSourceFactory = + new CronetDataSourceFactory(cronetEngineWrapper, Executors.newSingleThreadExecutor()); + } + return httpDataSourceFactory; + } + + /** Returns a {@link DataSource.Factory}. */ + public static synchronized DataSource.Factory getDataSourceFactory(Context context) { + if (dataSourceFactory == null) { + context = context.getApplicationContext(); + DefaultDataSourceFactory upstreamFactory = + new DefaultDataSourceFactory(context, getHttpDataSourceFactory(context)); + dataSourceFactory = buildReadOnlyCacheDataSource(upstreamFactory, getDownloadCache(context)); + } + return dataSourceFactory; + } + + public static synchronized DownloadNotificationHelper getDownloadNotificationHelper( + Context context) { + if (downloadNotificationHelper == null) { + downloadNotificationHelper = + new DownloadNotificationHelper(context, DOWNLOAD_NOTIFICATION_CHANNEL_ID); + } + return downloadNotificationHelper; + } + + public static synchronized DownloadManager getDownloadManager(Context context) { + ensureDownloadManagerInitialized(context); + return downloadManager; + } + + public static synchronized DownloadTracker getDownloadTracker(Context context) { + ensureDownloadManagerInitialized(context); + return downloadTracker; + } + + private static synchronized Cache getDownloadCache(Context context) { + if (downloadCache == null) { + File downloadContentDirectory = + new File(getDownloadDirectory(context), DOWNLOAD_CONTENT_DIRECTORY); + downloadCache = + new SimpleCache( + downloadContentDirectory, new NoOpCacheEvictor(), getDatabaseProvider(context)); + } + return downloadCache; + } + + private static synchronized void ensureDownloadManagerInitialized(Context context) { + if (downloadManager == null) { + DefaultDownloadIndex downloadIndex = new DefaultDownloadIndex(getDatabaseProvider(context)); + upgradeActionFile( + context, DOWNLOAD_ACTION_FILE, downloadIndex, /* addNewDownloadsAsCompleted= */ false); + upgradeActionFile( + context, + DOWNLOAD_TRACKER_ACTION_FILE, + downloadIndex, + /* addNewDownloadsAsCompleted= */ true); + downloadManager = + new DownloadManager( + context, + getDatabaseProvider(context), + getDownloadCache(context), + getHttpDataSourceFactory(context), + Executors.newFixedThreadPool(/* nThreads= */ 6)); + downloadTracker = + new DownloadTracker(context, getHttpDataSourceFactory(context), downloadManager); + } + } + + private static synchronized void upgradeActionFile( + Context context, + String fileName, + DefaultDownloadIndex downloadIndex, + boolean addNewDownloadsAsCompleted) { + try { + ActionFileUpgradeUtil.upgradeAndDelete( + new File(getDownloadDirectory(context), fileName), + /* downloadIdProvider= */ null, + downloadIndex, + /* deleteOnFailure= */ true, + addNewDownloadsAsCompleted); + } catch (IOException e) { + Log.e(TAG, "Failed to upgrade action file: " + fileName, e); + } + } + + private static synchronized DatabaseProvider getDatabaseProvider(Context context) { + if (databaseProvider == null) { + databaseProvider = new ExoDatabaseProvider(context); + } + return databaseProvider; + } + + private static synchronized File getDownloadDirectory(Context context) { + if (downloadDirectory == null) { + downloadDirectory = context.getExternalFilesDir(/* type= */ null); + if (downloadDirectory == null) { + downloadDirectory = context.getFilesDir(); + } + } + return downloadDirectory; + } + + private static CacheDataSource.Factory buildReadOnlyCacheDataSource( + DataSource.Factory upstreamFactory, Cache cache) { + return new CacheDataSource.Factory() + .setCache(cache) + .setUpstreamDataSourceFactory(upstreamFactory) + .setCacheWriteDataSinkFactory(null) + .setFlags(CacheDataSource.FLAG_IGNORE_CACHE_ON_ERROR); + } + + private DemoUtil() {} +} 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..07f4dd2f6e --- /dev/null +++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/DownloadTracker.java @@ -0,0 +1,423 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2.demo; + +import static com.google.android.exoplayer2.util.Assertions.checkNotNull; +import static com.google.android.exoplayer2.util.Assertions.checkStateNotNull; + +import android.content.Context; +import android.content.DialogInterface; +import android.net.Uri; +import android.os.AsyncTask; +import android.widget.Toast; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; +import androidx.fragment.app.FragmentManager; +import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.MediaItem; +import com.google.android.exoplayer2.RenderersFactory; +import com.google.android.exoplayer2.drm.DrmInitData; +import com.google.android.exoplayer2.drm.DrmSession; +import com.google.android.exoplayer2.drm.DrmSessionEventListener; +import com.google.android.exoplayer2.drm.OfflineLicenseHelper; +import com.google.android.exoplayer2.offline.Download; +import com.google.android.exoplayer2.offline.DownloadCursor; +import com.google.android.exoplayer2.offline.DownloadHelper; +import com.google.android.exoplayer2.offline.DownloadHelper.LiveContentUnsupportedException; +import com.google.android.exoplayer2.offline.DownloadIndex; +import com.google.android.exoplayer2.offline.DownloadManager; +import com.google.android.exoplayer2.offline.DownloadRequest; +import com.google.android.exoplayer2.offline.DownloadService; +import com.google.android.exoplayer2.source.TrackGroup; +import com.google.android.exoplayer2.source.TrackGroupArray; +import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; +import com.google.android.exoplayer2.trackselection.MappingTrackSelector.MappedTrackInfo; +import com.google.android.exoplayer2.upstream.HttpDataSource; +import com.google.android.exoplayer2.util.Log; +import com.google.android.exoplayer2.util.Util; +import 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 HttpDataSource.Factory httpDataSourceFactory; + 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, + HttpDataSource.Factory httpDataSourceFactory, + DownloadManager downloadManager) { + this.context = context.getApplicationContext(); + this.httpDataSourceFactory = httpDataSourceFactory; + listeners = new CopyOnWriteArraySet<>(); + downloads = new HashMap<>(); + downloadIndex = downloadManager.getDownloadIndex(); + trackSelectorParameters = DownloadHelper.getDefaultTrackSelectorParameters(context); + downloadManager.addListener(new DownloadManagerListener()); + loadDownloads(); + } + + public void addListener(Listener listener) { + checkNotNull(listener); + listeners.add(listener); + } + + public void removeListener(Listener listener) { + listeners.remove(listener); + } + + public boolean isDownloaded(MediaItem mediaItem) { + Download download = downloads.get(checkNotNull(mediaItem.playbackProperties).uri); + return download != null && download.state != Download.STATE_FAILED; + } + + @Nullable + 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, MediaItem mediaItem, RenderersFactory renderersFactory) { + Download download = downloads.get(checkNotNull(mediaItem.playbackProperties).uri); + if (download != null) { + DownloadService.sendRemoveDownload( + context, DemoDownloadService.class, download.request.id, /* foreground= */ false); + } else { + if (startDownloadDialogHelper != null) { + startDownloadDialogHelper.release(); + } + startDownloadDialogHelper = + new StartDownloadDialogHelper( + fragmentManager, + DownloadHelper.forMediaItem( + context, mediaItem, renderersFactory, httpDataSourceFactory), + mediaItem); + } + } + + 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 class DownloadManagerListener implements DownloadManager.Listener { + + @Override + public void onDownloadChanged( + @NonNull DownloadManager downloadManager, + @NonNull Download download, + @Nullable Exception finalException) { + downloads.put(download.request.uri, download); + for (Listener listener : listeners) { + listener.onDownloadsChanged(); + } + } + + @Override + public void onDownloadRemoved( + @NonNull DownloadManager downloadManager, @NonNull 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 MediaItem mediaItem; + + private TrackSelectionDialog trackSelectionDialog; + private MappedTrackInfo mappedTrackInfo; + private WidevineOfflineLicenseFetchTask widevineOfflineLicenseFetchTask; + @Nullable private byte[] keySetId; + + public StartDownloadDialogHelper( + FragmentManager fragmentManager, DownloadHelper downloadHelper, MediaItem mediaItem) { + this.fragmentManager = fragmentManager; + this.downloadHelper = downloadHelper; + this.mediaItem = mediaItem; + downloadHelper.prepare(this); + } + + public void release() { + downloadHelper.release(); + if (trackSelectionDialog != null) { + trackSelectionDialog.dismiss(); + } + if (widevineOfflineLicenseFetchTask != null) { + widevineOfflineLicenseFetchTask.cancel(false); + } + } + + // DownloadHelper.Callback implementation. + + @Override + public void onPrepared(@NonNull DownloadHelper helper) { + @Nullable Format format = getFirstFormatWithDrmInitData(helper); + if (format == null) { + onDownloadPrepared(helper); + return; + } + + // The content is DRM protected. We need to acquire an offline license. + if (Util.SDK_INT < 18) { + Toast.makeText(context, R.string.error_drm_unsupported_before_api_18, Toast.LENGTH_LONG) + .show(); + Log.e(TAG, "Downloading DRM protected content is not supported on API versions below 18"); + return; + } + // TODO(internal b/163107948): Support cases where DrmInitData are not in the manifest. + if (!hasSchemaData(format.drmInitData)) { + Toast.makeText(context, R.string.download_start_error_offline_license, Toast.LENGTH_LONG) + .show(); + Log.e( + TAG, + "Downloading content where DRM scheme data is not located in the manifest is not" + + " supported"); + return; + } + widevineOfflineLicenseFetchTask = + new WidevineOfflineLicenseFetchTask( + format, + mediaItem.playbackProperties.drmConfiguration.licenseUri, + httpDataSourceFactory, + /* dialogHelper= */ this, + helper); + widevineOfflineLicenseFetchTask.execute(); + } + + @Override + public void onPrepareError(@NonNull DownloadHelper helper, @NonNull IOException e) { + boolean isLiveContent = e instanceof LiveContentUnsupportedException; + int toastStringId = + isLiveContent ? R.string.download_live_unsupported : R.string.download_start_error; + String logMessage = + isLiveContent ? "Downloading live content unsupported" : "Failed to start download"; + Toast.makeText(context, toastStringId, Toast.LENGTH_LONG).show(); + Log.e(TAG, logMessage, e); + } + + // DialogInterface.OnClickListener implementation. + + @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. + + /** + * Returns the first {@link Format} with a non-null {@link Format#drmInitData} found in the + * content's tracks, or null if none is found. + */ + @Nullable + private Format getFirstFormatWithDrmInitData(DownloadHelper helper) { + for (int periodIndex = 0; periodIndex < helper.getPeriodCount(); periodIndex++) { + MappedTrackInfo mappedTrackInfo = helper.getMappedTrackInfo(periodIndex); + for (int rendererIndex = 0; + rendererIndex < mappedTrackInfo.getRendererCount(); + rendererIndex++) { + TrackGroupArray trackGroups = mappedTrackInfo.getTrackGroups(rendererIndex); + for (int trackGroupIndex = 0; trackGroupIndex < trackGroups.length; trackGroupIndex++) { + TrackGroup trackGroup = trackGroups.get(trackGroupIndex); + for (int formatIndex = 0; formatIndex < trackGroup.length; formatIndex++) { + Format format = trackGroup.getFormat(formatIndex); + if (format.drmInitData != null) { + return format; + } + } + } + } + } + return null; + } + + private void onOfflineLicenseFetched(DownloadHelper helper, byte[] keySetId) { + this.keySetId = keySetId; + onDownloadPrepared(helper); + } + + private void onOfflineLicenseFetchedError(DrmSession.DrmSessionException e) { + Toast.makeText(context, R.string.download_start_error_offline_license, Toast.LENGTH_LONG) + .show(); + Log.e(TAG, "Failed to fetch offline DRM license", e); + } + + private void onDownloadPrepared(DownloadHelper helper) { + if (helper.getPeriodCount() == 0) { + Log.d(TAG, "No periods found. Downloading entire stream."); + startDownload(); + downloadHelper.release(); + return; + } + + mappedTrackInfo = downloadHelper.getMappedTrackInfo(/* periodIndex= */ 0); + if (!TrackSelectionDialog.willHaveContent(mappedTrackInfo)) { + Log.d(TAG, "No dialog content. Downloading entire stream."); + startDownload(); + downloadHelper.release(); + return; + } + trackSelectionDialog = + TrackSelectionDialog.createForMappedTrackInfoAndParameters( + /* titleId= */ R.string.exo_download_description, + mappedTrackInfo, + trackSelectorParameters, + /* allowAdaptiveSelections =*/ false, + /* allowMultipleOverrides= */ true, + /* onClickListener= */ this, + /* onDismissListener= */ this); + trackSelectionDialog.show(fragmentManager, /* tag= */ null); + } + + /** + * Returns whether any the {@link DrmInitData.SchemeData} contained in {@code drmInitData} has + * non-null {@link DrmInitData.SchemeData#data}. + */ + private boolean hasSchemaData(DrmInitData drmInitData) { + for (int i = 0; i < drmInitData.schemeDataCount; i++) { + if (drmInitData.get(i).hasData()) { + return true; + } + } + return false; + } + + private void startDownload() { + startDownload(buildDownloadRequest()); + } + + private void startDownload(DownloadRequest downloadRequest) { + DownloadService.sendAddDownload( + context, DemoDownloadService.class, downloadRequest, /* foreground= */ false); + } + + private DownloadRequest buildDownloadRequest() { + return downloadHelper + .getDownloadRequest(Util.getUtf8Bytes(checkNotNull(mediaItem.mediaMetadata.title))) + .copyWithKeySetId(keySetId); + } + } + + /** Downloads a Widevine offline license in a background thread. */ + @RequiresApi(18) + private static final class WidevineOfflineLicenseFetchTask extends AsyncTask { + + private final Format format; + private final Uri licenseUri; + private final HttpDataSource.Factory httpDataSourceFactory; + private final StartDownloadDialogHelper dialogHelper; + private final DownloadHelper downloadHelper; + + @Nullable private byte[] keySetId; + @Nullable private DrmSession.DrmSessionException drmSessionException; + + public WidevineOfflineLicenseFetchTask( + Format format, + Uri licenseUri, + HttpDataSource.Factory httpDataSourceFactory, + StartDownloadDialogHelper dialogHelper, + DownloadHelper downloadHelper) { + this.format = format; + this.licenseUri = licenseUri; + this.httpDataSourceFactory = httpDataSourceFactory; + this.dialogHelper = dialogHelper; + this.downloadHelper = downloadHelper; + } + + @Override + protected Void doInBackground(Void... voids) { + OfflineLicenseHelper offlineLicenseHelper = + OfflineLicenseHelper.newWidevineInstance( + licenseUri.toString(), + httpDataSourceFactory, + new DrmSessionEventListener.EventDispatcher()); + try { + keySetId = offlineLicenseHelper.downloadLicense(format); + } catch (DrmSession.DrmSessionException e) { + drmSessionException = e; + } finally { + offlineLicenseHelper.release(); + } + return null; + } + + @Override + protected void onPostExecute(Void aVoid) { + if (drmSessionException != null) { + dialogHelper.onOfflineLicenseFetchedError(drmSessionException); + } else { + dialogHelper.onOfflineLicenseFetched(downloadHelper, checkStateNotNull(keySetId)); + } + } + } +} diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/IntentUtil.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/IntentUtil.java new file mode 100644 index 0000000000..d2d962c568 --- /dev/null +++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/IntentUtil.java @@ -0,0 +1,230 @@ +/* + * Copyright 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2.demo; + +import static com.google.android.exoplayer2.util.Assertions.checkNotNull; +import static com.google.android.exoplayer2.util.Assertions.checkState; + +import android.content.Intent; +import android.net.Uri; +import androidx.annotation.Nullable; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.MediaItem; +import com.google.android.exoplayer2.util.Assertions; +import com.google.android.exoplayer2.util.Util; +import com.google.common.collect.ImmutableList; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** Util to read from and populate an intent. */ +public class IntentUtil { + + // Actions. + + public static final String ACTION_VIEW = "com.google.android.exoplayer.demo.action.VIEW"; + public static final String ACTION_VIEW_LIST = + "com.google.android.exoplayer.demo.action.VIEW_LIST"; + + // Activity extras. + + public static final String PREFER_EXTENSION_DECODERS_EXTRA = "prefer_extension_decoders"; + + // Media item configuration extras. + + public static final String URI_EXTRA = "uri"; + public static final String MIME_TYPE_EXTRA = "mime_type"; + public static final String CLIP_START_POSITION_MS_EXTRA = "clip_start_position_ms"; + public static final String CLIP_END_POSITION_MS_EXTRA = "clip_end_position_ms"; + + public static final String AD_TAG_URI_EXTRA = "ad_tag_uri"; + + public static final String DRM_SCHEME_EXTRA = "drm_scheme"; + public static final String DRM_LICENSE_URI_EXTRA = "drm_license_uri"; + public static final String DRM_KEY_REQUEST_PROPERTIES_EXTRA = "drm_key_request_properties"; + public static final String DRM_SESSION_FOR_CLEAR_CONTENT = "drm_session_for_clear_content"; + public static final String DRM_MULTI_SESSION_EXTRA = "drm_multi_session"; + public static final String DRM_FORCE_DEFAULT_LICENSE_URI_EXTRA = "drm_force_default_license_uri"; + + public static final String SUBTITLE_URI_EXTRA = "subtitle_uri"; + public static final String SUBTITLE_MIME_TYPE_EXTRA = "subtitle_mime_type"; + public static final String SUBTITLE_LANGUAGE_EXTRA = "subtitle_language"; + + /** Creates a list of {@link MediaItem media items} from an {@link Intent}. */ + public static List createMediaItemsFromIntent(Intent intent) { + List mediaItems = new ArrayList<>(); + if (ACTION_VIEW_LIST.equals(intent.getAction())) { + int index = 0; + while (intent.hasExtra(URI_EXTRA + "_" + index)) { + Uri uri = Uri.parse(intent.getStringExtra(URI_EXTRA + "_" + index)); + mediaItems.add(createMediaItemFromIntent(uri, intent, /* extrasKeySuffix= */ "_" + index)); + index++; + } + } else { + Uri uri = intent.getData(); + mediaItems.add(createMediaItemFromIntent(uri, intent, /* extrasKeySuffix= */ "")); + } + return mediaItems; + } + + /** Populates the intent with the given list of {@link MediaItem media items}. */ + public static void addToIntent(List mediaItems, Intent intent) { + Assertions.checkArgument(!mediaItems.isEmpty()); + if (mediaItems.size() == 1) { + MediaItem mediaItem = mediaItems.get(0); + MediaItem.PlaybackProperties playbackProperties = checkNotNull(mediaItem.playbackProperties); + intent.setAction(ACTION_VIEW).setData(mediaItem.playbackProperties.uri); + addPlaybackPropertiesToIntent(playbackProperties, intent, /* extrasKeySuffix= */ ""); + addClippingPropertiesToIntent( + mediaItem.clippingProperties, intent, /* extrasKeySuffix= */ ""); + } else { + intent.setAction(ACTION_VIEW_LIST); + for (int i = 0; i < mediaItems.size(); i++) { + MediaItem mediaItem = mediaItems.get(i); + MediaItem.PlaybackProperties playbackProperties = + checkNotNull(mediaItem.playbackProperties); + intent.putExtra(URI_EXTRA + ("_" + i), playbackProperties.uri.toString()); + addPlaybackPropertiesToIntent(playbackProperties, intent, /* extrasKeySuffix= */ "_" + i); + addClippingPropertiesToIntent( + mediaItem.clippingProperties, intent, /* extrasKeySuffix= */ "_" + i); + } + } + } + + private static MediaItem createMediaItemFromIntent( + Uri uri, Intent intent, String extrasKeySuffix) { + @Nullable String mimeType = intent.getStringExtra(MIME_TYPE_EXTRA + extrasKeySuffix); + MediaItem.Builder builder = + new MediaItem.Builder() + .setUri(uri) + .setMimeType(mimeType) + .setAdTagUri(intent.getStringExtra(AD_TAG_URI_EXTRA + extrasKeySuffix)) + .setSubtitles(createSubtitlesFromIntent(intent, extrasKeySuffix)) + .setClipStartPositionMs( + intent.getLongExtra(CLIP_START_POSITION_MS_EXTRA + extrasKeySuffix, 0)) + .setClipEndPositionMs( + intent.getLongExtra( + CLIP_END_POSITION_MS_EXTRA + extrasKeySuffix, C.TIME_END_OF_SOURCE)); + + return populateDrmPropertiesFromIntent(builder, intent, extrasKeySuffix).build(); + } + + private static List createSubtitlesFromIntent( + Intent intent, String extrasKeySuffix) { + if (!intent.hasExtra(SUBTITLE_URI_EXTRA + extrasKeySuffix)) { + return Collections.emptyList(); + } + return Collections.singletonList( + new MediaItem.Subtitle( + Uri.parse(intent.getStringExtra(SUBTITLE_URI_EXTRA + extrasKeySuffix)), + checkNotNull(intent.getStringExtra(SUBTITLE_MIME_TYPE_EXTRA + extrasKeySuffix)), + intent.getStringExtra(SUBTITLE_LANGUAGE_EXTRA + extrasKeySuffix), + C.SELECTION_FLAG_DEFAULT)); + } + + private static MediaItem.Builder populateDrmPropertiesFromIntent( + MediaItem.Builder builder, Intent intent, String extrasKeySuffix) { + String schemeKey = DRM_SCHEME_EXTRA + extrasKeySuffix; + @Nullable String drmSchemeExtra = intent.getStringExtra(schemeKey); + if (drmSchemeExtra == null) { + return builder; + } + Map headers = new HashMap<>(); + @Nullable + String[] keyRequestPropertiesArray = + intent.getStringArrayExtra(DRM_KEY_REQUEST_PROPERTIES_EXTRA + extrasKeySuffix); + if (keyRequestPropertiesArray != null) { + for (int i = 0; i < keyRequestPropertiesArray.length; i += 2) { + headers.put(keyRequestPropertiesArray[i], keyRequestPropertiesArray[i + 1]); + } + } + builder + .setDrmUuid(Util.getDrmUuid(Util.castNonNull(drmSchemeExtra))) + .setDrmLicenseUri(intent.getStringExtra(DRM_LICENSE_URI_EXTRA + extrasKeySuffix)) + .setDrmMultiSession( + intent.getBooleanExtra(DRM_MULTI_SESSION_EXTRA + extrasKeySuffix, false)) + .setDrmForceDefaultLicenseUri( + intent.getBooleanExtra(DRM_FORCE_DEFAULT_LICENSE_URI_EXTRA + extrasKeySuffix, false)) + .setDrmLicenseRequestHeaders(headers); + if (intent.getBooleanExtra(DRM_SESSION_FOR_CLEAR_CONTENT + extrasKeySuffix, false)) { + builder.setDrmSessionForClearTypes(ImmutableList.of(C.TRACK_TYPE_VIDEO, C.TRACK_TYPE_AUDIO)); + } + return builder; + } + + private static void addPlaybackPropertiesToIntent( + MediaItem.PlaybackProperties playbackProperties, Intent intent, String extrasKeySuffix) { + intent + .putExtra(MIME_TYPE_EXTRA + extrasKeySuffix, playbackProperties.mimeType) + .putExtra( + AD_TAG_URI_EXTRA + extrasKeySuffix, + playbackProperties.adTagUri != null ? playbackProperties.adTagUri.toString() : null); + if (playbackProperties.drmConfiguration != null) { + addDrmConfigurationToIntent(playbackProperties.drmConfiguration, intent, extrasKeySuffix); + } + if (!playbackProperties.subtitles.isEmpty()) { + checkState(playbackProperties.subtitles.size() == 1); + MediaItem.Subtitle subtitle = playbackProperties.subtitles.get(0); + intent.putExtra(SUBTITLE_URI_EXTRA + extrasKeySuffix, subtitle.uri.toString()); + intent.putExtra(SUBTITLE_MIME_TYPE_EXTRA + extrasKeySuffix, subtitle.mimeType); + intent.putExtra(SUBTITLE_LANGUAGE_EXTRA + extrasKeySuffix, subtitle.language); + } + } + + private static void addDrmConfigurationToIntent( + MediaItem.DrmConfiguration drmConfiguration, Intent intent, String extrasKeySuffix) { + intent.putExtra(DRM_SCHEME_EXTRA + extrasKeySuffix, drmConfiguration.uuid.toString()); + intent.putExtra( + DRM_LICENSE_URI_EXTRA + extrasKeySuffix, + drmConfiguration.licenseUri != null ? drmConfiguration.licenseUri.toString() : null); + intent.putExtra(DRM_MULTI_SESSION_EXTRA + extrasKeySuffix, drmConfiguration.multiSession); + intent.putExtra( + DRM_FORCE_DEFAULT_LICENSE_URI_EXTRA + extrasKeySuffix, + drmConfiguration.forceDefaultLicenseUri); + + String[] drmKeyRequestProperties = new String[drmConfiguration.requestHeaders.size() * 2]; + int index = 0; + for (Map.Entry entry : drmConfiguration.requestHeaders.entrySet()) { + drmKeyRequestProperties[index++] = entry.getKey(); + drmKeyRequestProperties[index++] = entry.getValue(); + } + intent.putExtra(DRM_KEY_REQUEST_PROPERTIES_EXTRA + extrasKeySuffix, drmKeyRequestProperties); + + List drmSessionForClearTypes = drmConfiguration.sessionForClearTypes; + if (!drmSessionForClearTypes.isEmpty()) { + // Only video and audio together are supported. + Assertions.checkState( + drmSessionForClearTypes.size() == 2 + && drmSessionForClearTypes.contains(C.TRACK_TYPE_VIDEO) + && drmSessionForClearTypes.contains(C.TRACK_TYPE_AUDIO)); + intent.putExtra(DRM_SESSION_FOR_CLEAR_CONTENT + extrasKeySuffix, true); + } + } + + private static void addClippingPropertiesToIntent( + MediaItem.ClippingProperties clippingProperties, Intent intent, String extrasKeySuffix) { + if (clippingProperties.startPositionMs != 0) { + intent.putExtra( + CLIP_START_POSITION_MS_EXTRA + extrasKeySuffix, clippingProperties.startPositionMs); + } + if (clippingProperties.endPositionMs != C.TIME_END_OF_SOURCE) { + intent.putExtra( + CLIP_END_POSITION_MS_EXTRA + extrasKeySuffix, clippingProperties.endPositionMs); + } + } +} 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..eae302887e --- /dev/null +++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java @@ -0,0 +1,562 @@ +/* + * 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 static com.google.android.exoplayer2.util.Assertions.checkNotNull; + +import android.content.Intent; +import android.content.pm.PackageManager; +import android.net.Uri; +import android.os.Bundle; +import 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.ExoPlaybackException; +import com.google.android.exoplayer2.MediaItem; +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.audio.AudioAttributes; +import com.google.android.exoplayer2.drm.FrameworkMediaDrm; +import com.google.android.exoplayer2.ext.ima.ImaAdsLoader; +import com.google.android.exoplayer2.mediacodec.MediaCodecRenderer.DecoderInitializationException; +import com.google.android.exoplayer2.mediacodec.MediaCodecUtil.DecoderQueryException; +import com.google.android.exoplayer2.offline.DownloadRequest; +import com.google.android.exoplayer2.source.BehindLiveWindowException; +import com.google.android.exoplayer2.source.DefaultMediaSourceFactory; +import com.google.android.exoplayer2.source.MediaSourceFactory; +import com.google.android.exoplayer2.source.TrackGroupArray; +import com.google.android.exoplayer2.source.ads.AdsLoader; +import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; +import com.google.android.exoplayer2.trackselection.MappingTrackSelector.MappedTrackInfo; +import com.google.android.exoplayer2.trackselection.TrackSelectionArray; +import com.google.android.exoplayer2.ui.DebugTextViewHelper; +import com.google.android.exoplayer2.ui.StyledPlayerControlView; +import com.google.android.exoplayer2.ui.StyledPlayerView; +import com.google.android.exoplayer2.upstream.DataSource; +import com.google.android.exoplayer2.util.ErrorMessageProvider; +import com.google.android.exoplayer2.util.EventLogger; +import com.google.android.exoplayer2.util.Util; +import java.net.CookieHandler; +import java.net.CookieManager; +import java.net.CookiePolicy; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** An activity that plays media using {@link SimpleExoPlayer}. */ +public class PlayerActivity extends AppCompatActivity + implements OnClickListener, PlaybackPreparer, StyledPlayerControlView.VisibilityListener { + + // 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); + } + + protected StyledPlayerView playerView; + protected LinearLayout debugRootView; + protected TextView debugTextView; + protected SimpleExoPlayer player; + + private boolean isShowingTrackSelectionDialog; + private Button selectTracksButton; + private DataSource.Factory dataSourceFactory; + private List mediaItems; + 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) { + super.onCreate(savedInstanceState); + dataSourceFactory = DemoUtil.getDataSourceFactory(/* context= */ this); + if (CookieHandler.getDefault() != DEFAULT_COOKIE_MANAGER) { + CookieHandler.setDefault(DEFAULT_COOKIE_MANAGER); + } + + setContentView(); + 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 (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); + 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) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults); + if (grantResults.length == 0) { + // Empty results are triggered if a permission is requested while another request was already + // pending and can be safely ignored in this case. + return; + } + if (grantResults[0] == PackageManager.PERMISSION_GRANTED) { + initializePlayer(); + } else { + showToast(R.string.storage_permission_denied); + finish(); + } + } + + @Override + public void onSaveInstanceState(@NonNull 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); + } + } + + // PlaybackPreparer implementation + + @Override + public void preparePlayback() { + player.prepare(); + } + + // PlayerControlView.VisibilityListener implementation + + @Override + public void onVisibilityChange(int visibility) { + debugRootView.setVisibility(visibility); + } + + // Internal methods + + protected void setContentView() { + setContentView(R.layout.player_activity); + } + + /** @return Whether initialization was successful. */ + protected boolean initializePlayer() { + if (player == null) { + Intent intent = getIntent(); + + mediaItems = createMediaItems(intent); + if (mediaItems.isEmpty()) { + return false; + } + + boolean preferExtensionDecoders = + intent.getBooleanExtra(IntentUtil.PREFER_EXTENSION_DECODERS_EXTRA, false); + RenderersFactory renderersFactory = + DemoUtil.buildRenderersFactory(/* context= */ this, preferExtensionDecoders); + MediaSourceFactory mediaSourceFactory = + new DefaultMediaSourceFactory(dataSourceFactory) + .setAdsLoaderProvider(this::getAdsLoader) + .setAdViewProvider(playerView); + + trackSelector = new DefaultTrackSelector(/* context= */ this); + trackSelector.setParameters(trackSelectorParameters); + lastSeenTrackGroupArray = null; + player = + new SimpleExoPlayer.Builder(/* context= */ this, renderersFactory) + .setMediaSourceFactory(mediaSourceFactory) + .setTrackSelector(trackSelector) + .build(); + player.addListener(new PlayerEventListener()); + player.addAnalyticsListener(new EventLogger(trackSelector)); + player.setAudioAttributes(AudioAttributes.DEFAULT, /* handleAudioFocus= */ true); + player.setPlayWhenReady(startAutoPlay); + playerView.setPlayer(player); + playerView.setPlaybackPreparer(this); + debugViewHelper = new DebugTextViewHelper(player, debugTextView); + debugViewHelper.start(); + } + boolean haveStartPosition = startWindow != C.INDEX_UNSET; + if (haveStartPosition) { + player.seekTo(startWindow, startPosition); + } + player.setMediaItems(mediaItems, /* resetPosition= */ !haveStartPosition); + player.prepare(); + updateButtonVisibility(); + return true; + } + + private List createMediaItems(Intent intent) { + String action = intent.getAction(); + boolean actionIsListView = IntentUtil.ACTION_VIEW_LIST.equals(action); + if (!actionIsListView && !IntentUtil.ACTION_VIEW.equals(action)) { + showToast(getString(R.string.unexpected_intent_action, action)); + finish(); + return Collections.emptyList(); + } + + List mediaItems = + createMediaItems(intent, DemoUtil.getDownloadTracker(/* context= */ this)); + boolean hasAds = false; + for (int i = 0; i < mediaItems.size(); i++) { + MediaItem mediaItem = mediaItems.get(i); + + if (!Util.checkCleartextTrafficPermitted(mediaItem)) { + showToast(R.string.error_cleartext_not_permitted); + return Collections.emptyList(); + } + if (Util.maybeRequestReadExternalStoragePermission(/* activity= */ this, mediaItem)) { + // The player will be reinitialized if the permission is granted. + return Collections.emptyList(); + } + + MediaItem.DrmConfiguration drmConfiguration = + checkNotNull(mediaItem.playbackProperties).drmConfiguration; + if (drmConfiguration != null) { + if (Util.SDK_INT < 18) { + showToast(R.string.error_drm_unsupported_before_api_18); + finish(); + return Collections.emptyList(); + } else if (!FrameworkMediaDrm.isCryptoSchemeSupported(drmConfiguration.uuid)) { + showToast(R.string.error_drm_unsupported_scheme); + finish(); + return Collections.emptyList(); + } + } + hasAds |= mediaItem.playbackProperties.adTagUri != null; + } + if (!hasAds) { + releaseAdsLoader(); + } + return mediaItems; + } + + private AdsLoader getAdsLoader(Uri adTagUri) { + if (mediaItems.size() > 1) { + showToast(R.string.unsupported_ads_in_playlist); + releaseAdsLoader(); + return null; + } + if (!adTagUri.equals(loadedAdTagUri)) { + releaseAdsLoader(); + loadedAdTagUri = adTagUri; + } + // The ads loader is reused for multiple playbacks, so that ad playback can resume. + if (adsLoader == null) { + adsLoader = new ImaAdsLoader(/* context= */ PlayerActivity.this, adTagUri); + } + adsLoader.setPlayer(player); + return adsLoader; + } + + protected void releasePlayer() { + if (player != null) { + updateTrackSelectorParameters(); + updateStartPosition(); + debugViewHelper.stop(); + debugViewHelper = null; + player.release(); + player = null; + mediaItems = Collections.emptyList(); + 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()); + } + } + + protected void clearStartPosition() { + startAutoPlay = true; + startWindow = C.INDEX_UNSET; + startPosition = C.TIME_UNSET; + } + + // 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 onPlaybackStateChanged(@Player.State int playbackState) { + if (playbackState == Player.STATE_ENDED) { + showControls(); + } + updateButtonVisibility(); + } + + @Override + public void onPlayerError(@NonNull ExoPlaybackException e) { + if (isBehindLiveWindow(e)) { + clearStartPosition(); + initializePlayer(); + } else { + updateButtonVisibility(); + showControls(); + } + } + + @Override + @SuppressWarnings("ReferenceEquality") + public void onTracksChanged( + @NonNull TrackGroupArray trackGroups, @NonNull 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 + @NonNull + public Pair getErrorMessage(@NonNull 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); + } + } + + private static List createMediaItems(Intent intent, DownloadTracker downloadTracker) { + List mediaItems = new ArrayList<>(); + for (MediaItem item : IntentUtil.createMediaItemsFromIntent(intent)) { + @Nullable + DownloadRequest downloadRequest = + downloadTracker.getDownloadRequest(checkNotNull(item.playbackProperties).uri); + if (downloadRequest != null) { + MediaItem.Builder builder = item.buildUpon(); + builder + .setMediaId(downloadRequest.id) + .setUri(downloadRequest.uri) + .setCustomCacheKey(downloadRequest.customCacheKey) + .setMimeType(downloadRequest.mimeType) + .setStreamKeys(downloadRequest.streamKeys) + .setDrmKeySetId(downloadRequest.keySetId); + mediaItems.add(builder.build()); + } else { + mediaItems.add(item); + } + } + return mediaItems; + } +} 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..ea5b38ce8e --- /dev/null +++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/SampleChooserActivity.java @@ -0,0 +1,591 @@ +/* + * 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 static com.google.android.exoplayer2.util.Assertions.checkArgument; +import static com.google.android.exoplayer2.util.Assertions.checkNotNull; +import static com.google.android.exoplayer2.util.Assertions.checkState; + +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +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.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.app.AppCompatActivity; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.MediaItem; +import com.google.android.exoplayer2.MediaMetadata; +import com.google.android.exoplayer2.ParserException; +import com.google.android.exoplayer2.RenderersFactory; +import com.google.android.exoplayer2.offline.DownloadService; +import com.google.android.exoplayer2.upstream.DataSource; +import com.google.android.exoplayer2.upstream.DataSourceInputStream; +import com.google.android.exoplayer2.upstream.DataSpec; +import com.google.android.exoplayer2.util.Log; +import com.google.android.exoplayer2.util.Util; +import com.google.common.collect.ImmutableList; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** An activity for selecting from a list of media samples. */ +public class SampleChooserActivity extends AppCompatActivity + implements DownloadTracker.Listener, OnChildClickListener { + + private static final String TAG = "SampleChooserActivity"; + private static final String GROUP_POSITION_PREFERENCE_KEY = "sample_chooser_group_position"; + private static final String CHILD_POSITION_PREFERENCE_KEY = "sample_chooser_child_position"; + + private String[] uris; + private boolean useExtensionRenderers; + private DownloadTracker downloadTracker; + private SampleAdapter sampleAdapter; + private MenuItem preferExtensionDecodersMenuItem; + private ExpandableListView sampleListView; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.sample_chooser_activity); + sampleAdapter = new SampleAdapter(); + sampleListView = findViewById(R.id.sample_list); + + sampleListView.setAdapter(sampleAdapter); + sampleListView.setOnChildClickListener(this); + + Intent intent = getIntent(); + String dataUri = intent.getDataString(); + 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); + } + + useExtensionRenderers = DemoUtil.useExtensionRenderers(); + downloadTracker = DemoUtil.getDownloadTracker(/* context= */ this); + loadSample(); + + // Start the download service if it should be running but it's not currently. + // Starting the service in the foreground causes notification flicker if there is no scheduled + // 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); + 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(); + } + + @Override + public void onRequestPermissionsResult( + int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults); + if (grantResults.length == 0) { + // Empty results are triggered if a permission is requested while another request was already + // pending and can be safely ignored in this case. + return; + } + if (grantResults[0] == PackageManager.PERMISSION_GRANTED) { + loadSample(); + } else { + Toast.makeText(getApplicationContext(), R.string.sample_list_load_error, Toast.LENGTH_LONG) + .show(); + finish(); + } + } + + private void loadSample() { + checkNotNull(uris); + + for (int i = 0; i < uris.length; i++) { + Uri uri = Uri.parse(uris[i]); + if (Util.maybeRequestReadExternalStoragePermission(this, uri)) { + return; + } + } + + SampleListLoader loaderTask = new SampleListLoader(); + loaderTask.execute(uris); + } + + private void onPlaylistGroups(final List groups, boolean sawError) { + if (sawError) { + Toast.makeText(getApplicationContext(), R.string.sample_list_load_error, Toast.LENGTH_LONG) + .show(); + } + sampleAdapter.setPlaylistGroups(groups); + + SharedPreferences preferences = getPreferences(MODE_PRIVATE); + int groupPosition = preferences.getInt(GROUP_POSITION_PREFERENCE_KEY, /* defValue= */ -1); + int childPosition = preferences.getInt(CHILD_POSITION_PREFERENCE_KEY, /* defValue= */ -1); + // Clear the group and child position if either are unset or if either are out of bounds. + if (groupPosition != -1 + && childPosition != -1 + && groupPosition < groups.size() + && childPosition < groups.get(groupPosition).playlists.size()) { + sampleListView.expandGroup(groupPosition); // shouldExpandGroup does not work without this. + sampleListView.setSelectedChild(groupPosition, childPosition, /* shouldExpandGroup= */ true); + } + } + + @Override + public boolean onChildClick( + ExpandableListView parent, View view, int groupPosition, int childPosition, long id) { + // Save the selected item first to be able to restore it if the tested code crashes. + SharedPreferences.Editor prefEditor = getPreferences(MODE_PRIVATE).edit(); + prefEditor.putInt(GROUP_POSITION_PREFERENCE_KEY, groupPosition); + prefEditor.putInt(CHILD_POSITION_PREFERENCE_KEY, childPosition); + prefEditor.apply(); + + PlaylistHolder playlistHolder = (PlaylistHolder) view.getTag(); + Intent intent = new Intent(this, PlayerActivity.class); + intent.putExtra( + IntentUtil.PREFER_EXTENSION_DECODERS_EXTRA, + isNonNullAndChecked(preferExtensionDecodersMenuItem)); + IntentUtil.addToIntent(playlistHolder.mediaItems, intent); + startActivity(intent); + return true; + } + + private void onSampleDownloadButtonClicked(PlaylistHolder playlistHolder) { + int downloadUnsupportedStringId = getDownloadUnsupportedStringId(playlistHolder); + if (downloadUnsupportedStringId != 0) { + Toast.makeText(getApplicationContext(), downloadUnsupportedStringId, Toast.LENGTH_LONG) + .show(); + } else { + RenderersFactory renderersFactory = + DemoUtil.buildRenderersFactory( + /* context= */ this, isNonNullAndChecked(preferExtensionDecodersMenuItem)); + downloadTracker.toggleDownload( + getSupportFragmentManager(), playlistHolder.mediaItems.get(0), renderersFactory); + } + } + + private int getDownloadUnsupportedStringId(PlaylistHolder playlistHolder) { + if (playlistHolder.mediaItems.size() > 1) { + return R.string.download_playlist_unsupported; + } + MediaItem.PlaybackProperties playbackProperties = + checkNotNull(playlistHolder.mediaItems.get(0).playbackProperties); + if (playbackProperties.adTagUri != null) { + return R.string.download_ads_unsupported; + } + String scheme = playbackProperties.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(); + DataSource dataSource = DemoUtil.getDataSourceFactory(context).createDataSource(); + for (String uri : uris) { + DataSpec dataSpec = new DataSpec(Uri.parse(uri)); + InputStream inputStream = new DataSourceInputStream(dataSource, dataSpec); + try { + readPlaylistGroups(new JsonReader(new InputStreamReader(inputStream, "UTF-8")), result); + } catch (Exception e) { + Log.e(TAG, "Error loading sample list: " + uri, e); + sawError = true; + } finally { + Util.closeQuietly(dataSource); + } + } + return result; + } + + @Override + protected void onPostExecute(List result) { + onPlaylistGroups(result, sawError); + } + + private void readPlaylistGroups(JsonReader reader, List groups) + throws IOException { + reader.beginArray(); + while (reader.hasNext()) { + readPlaylistGroup(reader, groups); + } + reader.endArray(); + } + + private void readPlaylistGroup(JsonReader reader, List groups) + throws IOException { + String groupName = ""; + ArrayList playlistHolders = 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()) { + playlistHolders.add(readEntry(reader, false)); + } + reader.endArray(); + break; + case "_comment": + reader.nextString(); // Ignore. + break; + default: + throw new ParserException("Unsupported name: " + name); + } + } + reader.endObject(); + + PlaylistGroup group = getGroup(groupName, groups); + group.playlists.addAll(playlistHolders); + } + + private PlaylistHolder readEntry(JsonReader reader, boolean insidePlaylist) throws IOException { + Uri uri = null; + String extension = null; + String title = null; + ArrayList children = null; + Uri subtitleUri = null; + String subtitleMimeType = null; + String subtitleLanguage = null; + + MediaItem.Builder mediaItem = new MediaItem.Builder(); + reader.beginObject(); + while (reader.hasNext()) { + String name = reader.nextName(); + switch (name) { + case "name": + title = reader.nextString(); + break; + case "uri": + uri = Uri.parse(reader.nextString()); + break; + case "extension": + extension = reader.nextString(); + break; + case "clip_start_position_ms": + mediaItem.setClipStartPositionMs(reader.nextLong()); + break; + case "clip_end_position_ms": + mediaItem.setClipEndPositionMs(reader.nextLong()); + break; + case "ad_tag_uri": + mediaItem.setAdTagUri(reader.nextString()); + break; + case "drm_scheme": + mediaItem.setDrmUuid(Util.getDrmUuid(reader.nextString())); + break; + case "drm_license_uri": + case "drm_license_url": // For backward compatibility only. + mediaItem.setDrmLicenseUri(reader.nextString()); + break; + case "drm_key_request_properties": + Map requestHeaders = new HashMap<>(); + reader.beginObject(); + while (reader.hasNext()) { + requestHeaders.put(reader.nextName(), reader.nextString()); + } + reader.endObject(); + mediaItem.setDrmLicenseRequestHeaders(requestHeaders); + break; + case "drm_session_for_clear_content": + if (reader.nextBoolean()) { + mediaItem.setDrmSessionForClearTypes( + ImmutableList.of(C.TRACK_TYPE_VIDEO, C.TRACK_TYPE_AUDIO)); + } + break; + case "drm_multi_session": + mediaItem.setDrmMultiSession(reader.nextBoolean()); + break; + case "drm_force_default_license_uri": + mediaItem.setDrmForceDefaultLicenseUri(reader.nextBoolean()); + break; + case "subtitle_uri": + subtitleUri = Uri.parse(reader.nextString()); + break; + case "subtitle_mime_type": + subtitleMimeType = reader.nextString(); + break; + case "subtitle_language": + subtitleLanguage = reader.nextString(); + break; + case "playlist": + checkState(!insidePlaylist, "Invalid nesting of playlists"); + children = new ArrayList<>(); + reader.beginArray(); + while (reader.hasNext()) { + children.add(readEntry(reader, /* insidePlaylist= */ true)); + } + reader.endArray(); + break; + default: + throw new ParserException("Unsupported attribute name: " + name); + } + } + reader.endObject(); + + if (children != null) { + List mediaItems = new ArrayList<>(); + for (int i = 0; i < children.size(); i++) { + mediaItems.addAll(children.get(i).mediaItems); + } + return new PlaylistHolder(title, mediaItems); + } else { + @Nullable + String adaptiveMimeType = + Util.getAdaptiveMimeTypeForContentType(Util.inferContentType(uri, extension)); + mediaItem + .setUri(uri) + .setMediaMetadata(new MediaMetadata.Builder().setTitle(title).build()) + .setMimeType(adaptiveMimeType); + if (subtitleUri != null) { + MediaItem.Subtitle subtitle = + new MediaItem.Subtitle( + subtitleUri, + checkNotNull( + subtitleMimeType, "subtitle_mime_type is required if subtitle_uri is set."), + subtitleLanguage); + mediaItem.setSubtitles(Collections.singletonList(subtitle)); + } + return new PlaylistHolder(title, Collections.singletonList(mediaItem.build())); + } + } + + private PlaylistGroup 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); + } + } + PlaylistGroup group = new PlaylistGroup(groupName); + groups.add(group); + return group; + } + } + + private final class SampleAdapter extends BaseExpandableListAdapter implements OnClickListener { + + private List playlistGroups; + + public SampleAdapter() { + playlistGroups = Collections.emptyList(); + } + + public void setPlaylistGroups(List playlistGroups) { + this.playlistGroups = playlistGroups; + notifyDataSetChanged(); + } + + @Override + public PlaylistHolder getChild(int groupPosition, int childPosition) { + return getGroup(groupPosition).playlists.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).playlists.size(); + } + + @Override + public PlaylistGroup getGroup(int groupPosition) { + return playlistGroups.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 playlistGroups.size(); + } + + @Override + public boolean hasStableIds() { + return false; + } + + @Override + public boolean isChildSelectable(int groupPosition, int childPosition) { + return true; + } + + @Override + public void onClick(View view) { + onSampleDownloadButtonClicked((PlaylistHolder) view.getTag()); + } + + private void initializeChildView(View view, PlaylistHolder playlistHolder) { + view.setTag(playlistHolder); + TextView sampleTitle = view.findViewById(R.id.sample_title); + sampleTitle.setText(playlistHolder.title); + + boolean canDownload = getDownloadUnsupportedStringId(playlistHolder) == 0; + boolean isDownloaded = + canDownload && downloadTracker.isDownloaded(playlistHolder.mediaItems.get(0)); + ImageButton downloadButton = view.findViewById(R.id.download_button); + downloadButton.setTag(playlistHolder); + downloadButton.setColorFilter( + canDownload ? (isDownloaded ? 0xFF42A5F5 : 0xFFBDBDBD) : 0xFF666666); + downloadButton.setImageResource( + isDownloaded ? R.drawable.ic_download_done : R.drawable.ic_download); + } + } + + private static final class PlaylistHolder { + + public final String title; + public final List mediaItems; + + private PlaylistHolder(String title, List mediaItems) { + checkArgument(!mediaItems.isEmpty()); + this.title = title; + this.mediaItems = Collections.unmodifiableList(new ArrayList<>(mediaItems)); + } + } + + private static final class PlaylistGroup { + + public final String title; + public final List playlists; + + public PlaylistGroup(String title) { + this.title = title; + this.playlists = 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..5cf2353f21 --- /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.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.app.AppCompatDialog; +import androidx.fragment.app.DialogFragment; +import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentManager; +import androidx.fragment.app.FragmentPagerAdapter; +import androidx.viewpager.widget.ViewPager; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.source.TrackGroupArray; +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 + @NonNull + public Dialog onCreateDialog(Bundle savedInstanceState) { + // We need to own the view to let tab layout work correctly on all API levels. We can't use + // AlertDialog because it owns the view itself, so we use AppCompatDialog instead, themed using + // 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(@NonNull DialogInterface dialog) { + super.onDismiss(dialog); + onDismissListener.onDismiss(dialog); + } + + @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, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT); + } + + @Override + @NonNull + public Fragment getItem(int position) { + return tabFragments.valueAt(position); + } + + @Override + public int getCount() { + return tabFragments.size(); + } + + @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; + } + + @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, @NonNull List overrides) { + this.isDisabled = isDisabled; + this.overrides = overrides; + } + } +} diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/package-info.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/package-info.java new file mode 100644 index 0000000000..cc22be27e0 --- /dev/null +++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +@NonNullApi +package com.google.android.exoplayer2.demo; + +import com.google.android.exoplayer2.util.NonNullApi; 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 81% rename from demo/src/main/res/layout/player_activity.xml rename to demos/main/src/main/res/layout/player_activity.xml index 3f8cdaa7d6..5b897fa7ea 100644 --- a/demo/src/main/res/layout/player_activity.xml +++ b/demos/main/src/main/res/layout/player_activity.xml @@ -15,14 +15,17 @@ --> - + android:layout_height="match_parent" + app:show_shuffle_button="true" + app:show_subtitle_button="true"/> -