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..7819a90ac5 --- /dev/null +++ b/.hgignore @@ -0,0 +1,81 @@ +# Mercurial's .hgignore files can only be used in the root directory. +# You can still apply these rules by adding +# include:path/to/this/directory/.hgignore to the top-level .hgignore file. + +# Ensure same syntax as in .gitignore can be used +syntax:glob + +# Android generated +bin +gen +libs +obj +lint.xml + +# IntelliJ IDEA & Android Studio +.idea +*.iml +*.ipr +*.iws +classes +gen-external-apklibs +*.li + +# Eclipse +.project +.classpath +.settings +.checkstyle +.cproject + +# Gradle +.gradle +build +buildout +out + +# Maven +target +release.properties +pom.xml.* + +# Ant +ant.properties +local.properties +proguard.cfg +proguard-project.txt + +# Bazel +bazel-bin +bazel-genfiles +bazel-out +bazel-testlogs + +# Other +.DS_Store +cmake-build-debug +dist +tmp + +# VP9 extension +extensions/vp9/src/main/jni/libvpx +extensions/vp9/src/main/jni/libvpx_android_configs +extensions/vp9/src/main/jni/libyuv + +# AV1 extension +extensions/av1/src/main/jni/libgav1 + +# Opus extension +extensions/opus/src/main/jni/libopus + +# FLAC extension +extensions/flac/src/main/jni/flac + +# FFmpeg extension +extensions/ffmpeg/src/main/jni/ffmpeg + +# Cronet extension +extensions/cronet/jniLibs/* +!extensions/cronet/jniLibs/README.md +extensions/cronet/libs/* +!extensions/cronet/libs/README.md diff --git a/.idea/codeStyleSettings.xml b/.idea/codeStyleSettings.xml new file mode 100644 index 0000000000..056b47a1e8 --- /dev/null +++ b/.idea/codeStyleSettings.xml @@ -0,0 +1,495 @@ + + + + + + diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 43c4809480..94b349b217 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -16,9 +16,8 @@ all of the information requested in the issue template. ## Pull requests ## We will also consider high quality pull requests. These should normally merge -into the `dev-vX` branch with the highest major version number. Bug fixes may -be suitable for merging into older `dev-vX` branches. Before a pull request can -be accepted you must submit a Contributor License Agreement, as described below. +into the `dev-v2` branch. Before a pull request can be accepted you must submit +a Contributor License Agreement, as described below. [dev]: https://github.com/google/ExoPlayer/tree/dev diff --git a/ISSUE_TEMPLATE b/ISSUE_TEMPLATE deleted file mode 100644 index 1b912312d1..0000000000 --- a/ISSUE_TEMPLATE +++ /dev/null @@ -1,44 +0,0 @@ -*** ISSUES THAT IGNORE THIS TEMPLATE WILL BE CLOSED WITHOUT INVESTIGATION *** - -Before filing an issue: ------------------------ -- Search existing issues, including issues that are closed. -- Consult our FAQs, supported devices and supported formats pages. These can be - found at https://google.github.io/ExoPlayer/. -- Rule out issues in your own code. A good way to do this is to try and - reproduce the issue in the ExoPlayer demo app. -- This issue tracker is intended for bugs, feature requests and ExoPlayer - specific questions. If you're asking a general Android development question, - please do so on Stack Overflow. - -When reporting a bug: ------------------------ -Fill out the sections below, leaving the headers but replacing the content. If -you're unable to provide certain information, please explain why in the relevant -section. We may close issues if they do not include sufficient information. - -### Issue description -Describe the issue in detail, including observed and expected behavior. - -### Reproduction steps -Describe how the issue can be reproduced, ideally using the ExoPlayer demo app. - -### Link to test content -Provide a link to media that reproduces the issue. If you don't wish to post it -publicly, please submit the issue, then email the link to -dev.exoplayer@gmail.com including the issue number in the subject line. - -### Version of ExoPlayer being used -Specify the absolute version number. Avoid using terms such as "latest". - -### Device(s) and version(s) of Android being used -Specify the devices and versions of Android on which the issue can be -reproduced, and how easily it reproduces. If possible, please test on multiple -devices and Android versions. - -### A full bug report captured from the device -Capture a full bug report using "adb bugreport". Output from "adb logcat" or a -log snippet is NOT sufficient. Please attach the captured bug report as a file. -If you don't wish to post it publicly, please submit the issue, then email the -bug report to dev.exoplayer@gmail.com including the issue number in the subject -line. diff --git a/README.md b/README.md index 3de86d21a3..d488f4113e 100644 --- a/README.md +++ b/README.md @@ -9,47 +9,61 @@ and extend, and can be updated through Play Store application updates. ## Documentation ## -* The [developer guide][] provides a wealth of information to help you get - started. -* The [class reference][] documents the ExoPlayer library classes. +* The [developer guide][] provides a wealth of information. +* The [class reference][] documents ExoPlayer classes. * The [release notes][] document the major changes in each release. +* Follow our [developer blog][] to keep up to date with the latest ExoPlayer + developments! -[developer guide]: https://google.github.io/ExoPlayer/guide.html -[class reference]: https://google.github.io/ExoPlayer/doc/reference -[release notes]: https://github.com/google/ExoPlayer/blob/dev-v2/RELEASENOTES.md +[developer guide]: https://exoplayer.dev/guide.html +[class reference]: https://exoplayer.dev/doc/reference +[release notes]: https://github.com/google/ExoPlayer/blob/release-v2/RELEASENOTES.md +[developer blog]: https://medium.com/google-exoplayer ## Using ExoPlayer ## +ExoPlayer modules can be obtained from JCenter. It's also possible to clone the +repository and depend on the modules locally. + +### From JCenter ### + +#### 1. Add repositories #### + The easiest way to get started using ExoPlayer is to add it as a gradle -dependency. You need to make sure you have the jcenter repository included in -the `build.gradle` file in the root of your project: +dependency. You need to make sure you have the Google and JCenter repositories +included in the `build.gradle` file in the root of your project: ```gradle repositories { + google() jcenter() } ``` -Next add a gradle compile dependency to the `build.gradle` file of your app -module. The following will add a dependency to the full ExoPlayer library: +#### 2. Add ExoPlayer module dependencies #### + +Next add a dependency in the `build.gradle` file of your app module. The +following will add a dependency to the full library: ```gradle -compile 'com.google.android.exoplayer:exoplayer:r2.X.X' +implementation 'com.google.android.exoplayer:exoplayer:2.X.X' ``` -where `r2.X.X` is your preferred version. Alternatively, you can depend on only -the library modules that you actually need. For example the following will add -dependencies on the Core, DASH and UI library modules, as might be required for -an app that plays DASH content: +where `2.X.X` is your preferred version. + +As an alternative to the full library, you can depend on only the library +modules that you actually need. For example the following will add dependencies +on the Core, DASH and UI library modules, as might be required for an app that +plays DASH content: ```gradle -compile 'com.google.android.exoplayer:exoplayer-core:r2.X.X' -compile 'com.google.android.exoplayer:exoplayer-dash:r2.X.X' -compile 'com.google.android.exoplayer:exoplayer-ui:r2.X.X' +implementation 'com.google.android.exoplayer:exoplayer-core:2.X.X' +implementation 'com.google.android.exoplayer:exoplayer-dash:2.X.X' +implementation 'com.google.android.exoplayer:exoplayer-ui:2.X.X' ``` -The available modules are listed below. Adding a dependency to the full -ExoPlayer library is equivalent to adding dependencies on all of the modules +The available library modules are listed below. Adding a dependency to the full +library is equivalent to adding dependencies on all of the library modules individually. * `exoplayer-core`: Core functionality (required). @@ -58,25 +72,70 @@ individually. * `exoplayer-smoothstreaming`: Support for SmoothStreaming content. * `exoplayer-ui`: UI components and resources for use with ExoPlayer. -For more details, see the project on [Bintray][]. For information about the -latest versions, see the [Release notes][]. +In addition to library modules, ExoPlayer has multiple extension modules that +depend on external libraries to provide additional functionality. Some +extensions are available from JCenter, whereas others must be built manually. +Browse the [extensions directory][] and their individual READMEs for details. +More information on the library and extension modules that are available from +JCenter can be found on [Bintray][]. + +[extensions directory]: https://github.com/google/ExoPlayer/tree/release-v2/extensions/ [Bintray]: https://bintray.com/google/exoplayer -[Release notes]: https://github.com/google/ExoPlayer/blob/release-v2/RELEASENOTES.md + +#### 3. Turn on Java 8 support #### + +If not enabled already, you also need to turn on Java 8 support in all +`build.gradle` files depending on ExoPlayer, by adding the following to the +`android` section: + +```gradle +compileOptions { + targetCompatibility JavaVersion.VERSION_1_8 +} +``` + +### Locally ### + +Cloning the repository and depending on the modules locally is required when +using some ExoPlayer extension modules. It's also a suitable approach if you +want to make local changes to ExoPlayer, or if you want to use a development +branch. + +First, clone the repository into a local directory and checkout the desired +branch: + +```sh +git clone https://github.com/google/ExoPlayer.git +cd ExoPlayer +git checkout release-v2 +``` + +Next, add the following to your project's `settings.gradle` file, replacing +`path/to/exoplayer` with the path to your local copy: + +```gradle +gradle.ext.exoplayerRoot = 'path/to/exoplayer' +gradle.ext.exoplayerModulePrefix = 'exoplayer-' +apply from: new File(gradle.ext.exoplayerRoot, 'core_settings.gradle') +``` + +You should now see the ExoPlayer modules appear as part of your project. You can +depend on them as you would on any other local module, for example: + +```gradle +implementation project(':exoplayer-library-core') +implementation project(':exoplayer-library-dash') +implementation project(':exoplayer-library-ui') +``` ## Developing ExoPlayer ## #### Project branches #### - * The project has `dev-vX` and `release-vX` branches, where `X` is the major - version number. - * Most development work happens on the `dev-vX` branch with the highest major - version number. Pull requests should normally be made to this branch. - * Bug fixes may be submitted to older `dev-vX` branches. When doing this, the - same (or an equivalent) fix should also be submitted to all subsequent - `dev-vX` branches. - * A `release-vX` branch holds the most recent stable release for major version - `X`. +* Development work happens on the `dev-v2` branch. Pull requests should + normally be made to this branch. +* The `release-v2` branch holds the most recent release. #### Using Android Studio #### diff --git a/RELEASENOTES.md b/RELEASENOTES.md index ff1bd42fde..ac573cc81d 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -1,6 +1,1575 @@ # Release notes # -### r2.4.4 ### +### dev-v2 (not yet released) ### + +* Core library: + * Add playlist API ([#6161](https://github.com/google/ExoPlayer/issues/6161)). + * Add `play` and `pause` methods to `Player`. + * Add `Player.getCurrentLiveOffset` to conveniently return the live offset. + * Add `Player.onPlayWhenReadyChanged` with reasons. + * Make `MediaSourceEventListener.LoadEventInfo` and + `MediaSourceEventListener.MediaLoadData` top-level classes. + * Rename `MediaCodecRenderer.onOutputFormatChanged` to + `MediaCodecRenderer.onOutputMediaFormatChanged`, further + clarifying the distinction between `Format` and `MediaFormat`. + * Move player message-related constants from `C` to `Renderer`, to avoid + having the constants class depend on player/renderer classes. + * Split out `common` and `extractor` submodules. +* Text: + * Parse `` and `` tags in WebVTT subtitles (rendering is coming + later). + * Parse `text-combine-upright` CSS property (i.e. tate-chu-yoko) in WebVTT + subtitles (rendering is coming later). + * Parse `tts:combineText` property (i.e. tate-chu-yoko) in TTML subtitles + (rendering is coming later). + * Fix `SubtitlePainter` to render `EDGE_TYPE_OUTLINE` using the correct color + ([#6724](https://github.com/google/ExoPlayer/pull/6724)). +* DRM: Add support for attaching DRM sessions to clear content in the demo app. +* Downloads: Merge downloads in `SegmentDownloader` to improve overall download + speed ([#5978](https://github.com/google/ExoPlayer/issues/5978)). +* MP3: Add `IndexSeeker` for accurate seeks in VBR streams + ([#6787](https://github.com/google/ExoPlayer/issues/6787)). + This seeker is enabled by passing `FLAG_ENABLE_INDEX_SEEKING` to the + `Mp3Extractor`. It may require to scan a significant portion of the file for + seeking, which may be costly on large files. +* MP4: Store the Android capture frame rate only in `Format.metadata`. + `Format.frameRate` now stores the calculated frame rate. +* Testing + * Upgrade Truth dependency from 0.44 to 1.0. + * Upgrade to JUnit 4.13-rc-2. + +### 2.11.2 (TBD) ### + +* 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. +* 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)). +* 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. +* Add support for ID3 genres added in Wimamp 5.6 (2010). + +### 2.11.1 (2019-12-20) ### + +* UI: Exclude `DefaultTimeBar` region from system gesture detection + ([#6685](https://github.com/google/ExoPlayer/issues/6685)). +* ProGuard fixes: + * Ensure `Libgav1VideoRenderer` constructor is kept for use by + `DefaultRenderersFactory` + ([#6773](https://github.com/google/ExoPlayer/issues/6773)). + * Ensure `VideoDecoderOutputBuffer` and its members are kept for use by video + decoder extensions. + * Ensure raw resources used with `RawResourceDataSource` are kept. + * Suppress spurious warnings about the `javax.annotation` package, and + restructure use of `IntDef` annotations to remove spurious warnings about + `SsaStyle$SsaAlignment` + ([#6771](https://github.com/google/ExoPlayer/issues/6771)). +* Fix `CacheDataSource` to correctly propagate `DataSpec.httpRequestHeaders`. +* Fix issue with `DefaultDownloadIndex` that could result in an + `IllegalStateException` being thrown from + `DefaultDownloadIndex.getDownloadForCurrentRow` + ([#6785](https://github.com/google/ExoPlayer/issues/6785)). +* Fix `IndexOutOfBoundsException` in `SinglePeriodTimeline.getWindow` + ([#6776](https://github.com/google/ExoPlayer/issues/6776)). +* Add missing `@Nullable` to `MediaCodecAudioRenderer.getMediaClock` and + `SimpleDecoderAudioRenderer.getMediaClock` + ([#6792](https://github.com/google/ExoPlayer/issues/6792)). + +### 2.11.0 (2019-12-11) ### + +* Core library: + * Replace `ExoPlayerFactory` by `SimpleExoPlayer.Builder` and + `ExoPlayer.Builder`. + * Add automatic `WakeLock` handling to `SimpleExoPlayer`, which can be enabled + by calling `SimpleExoPlayer.setHandleWakeLock` + ([#5846](https://github.com/google/ExoPlayer/issues/5846)). To use this + feature, you must add the + [WAKE_LOCK](https://developer.android.com/reference/android/Manifest.permission.html#WAKE_LOCK) + permission to your application's manifest file. + * Add automatic "audio becoming noisy" handling to `SimpleExoPlayer`, which + can be enabled by calling `SimpleExoPlayer.setHandleAudioBecomingNoisy`. + * Wrap decoder exceptions in a new `DecoderException` class and report them as + renderer errors. + * Add `Timeline.Window.isLive` to indicate that a window is a live stream + ([#2668](https://github.com/google/ExoPlayer/issues/2668) and + [#5973](https://github.com/google/ExoPlayer/issues/5973)). + * Add `Timeline.Window.uid` to uniquely identify window instances. + * Deprecate `setTag` parameter of `Timeline.getWindow`. Tags will always be + set. + * Deprecate passing the manifest directly to + `Player.EventListener.onTimelineChanged`. It can be accessed through + `Timeline.Window.manifest` or `Player.getCurrentManifest()` + * Add `MediaSource.enable` and `MediaSource.disable` to improve resource + management in playlists. + * Add `MediaPeriod.isLoading` to improve `Player.isLoading` state. + * Fix issue where player errors are thrown too early at playlist transitions + ([#5407](https://github.com/google/ExoPlayer/issues/5407)). + * Add `Format` and renderer support flags to renderer `ExoPlaybackException`s. + * Where there are multiple platform decoders for a given MIME type, prefer to + use one that advertises support for the profile and level of the media being + played over one that does not, even if it does not come first in the + `MediaCodecList`. +* DRM: + * Inject `DrmSessionManager` into the `MediaSources` instead of `Renderers`. + This allows each `MediaSource` in a `ConcatenatingMediaSource` to use a + different `DrmSessionManager` + ([#5619](https://github.com/google/ExoPlayer/issues/5619)). + * Add `DefaultDrmSessionManager.Builder`, and remove + `DefaultDrmSessionManager` static factory methods that leaked + `ExoMediaDrm` instances + ([#4721](https://github.com/google/ExoPlayer/issues/4721)). + * Add support for the use of secure decoders when playing clear content + ([#4867](https://github.com/google/ExoPlayer/issues/4867)). This can + be enabled using `DefaultDrmSessionManager.Builder`'s + `setUseDrmSessionsForClearContent` method. + * Add support for custom `LoadErrorHandlingPolicies` in key and provisioning + requests ([#6334](https://github.com/google/ExoPlayer/issues/6334)). Custom + policies can be passed via `DefaultDrmSessionManager.Builder`'s + `setLoadErrorHandlingPolicy` method. + * Use `ExoMediaDrm.Provider` in `OfflineLicenseHelper` to avoid leaking + `ExoMediaDrm` instances + ([#4721](https://github.com/google/ExoPlayer/issues/4721)). +* Track selection: + * Update `DefaultTrackSelector` to set a viewport constraint for the default + display by default. + * Update `DefaultTrackSelector` to set text language and role flag + constraints for the device's accessibility settings by default + ([#5749](https://github.com/google/ExoPlayer/issues/5749)). + * Add option to set preferred text role flags using + `DefaultTrackSelector.ParametersBuilder.setPreferredTextRoleFlags`. +* Android 10: + * Set `compileSdkVersion` to 29 to enable use of Android 10 APIs. + * Expose new `isHardwareAccelerated`, `isSoftwareOnly` and `isVendor` flags + in `MediaCodecInfo` + ([#5839](https://github.com/google/ExoPlayer/issues/5839)). + * Add `allowedCapturePolicy` field to `AudioAttributes` to allow to + configuration of the audio capture policy. +* Video: + * Pass the codec output `MediaFormat` to `VideoFrameMetadataListener`. + * Fix byte order of HDR10+ static metadata to match CTA-861.3. + * Support out-of-band HDR10+ dynamic metadata for VP9 in WebM/Matroska. + * Assume that protected content requires a secure decoder when evaluating + whether `MediaCodecVideoRenderer` supports a given video format + ([#5568](https://github.com/google/ExoPlayer/issues/5568)). + * Fix Dolby Vision fallback to AVC and HEVC. + * Fix early end-of-stream detection when using video tunneling, on API level + 23 and above. + * Fix an issue where a keyframe was rendered rather than skipped when + performing an exact seek to a non-zero position close to the start of the + stream. +* Audio: + * Fix the start of audio getting truncated when transitioning to a new + item in a playlist of Opus streams. + * Workaround broken raw audio decoding on Oppo R9 + ([#5782](https://github.com/google/ExoPlayer/issues/5782)). + * Reconfigure audio sink when PCM encoding changes + ([#6601](https://github.com/google/ExoPlayer/issues/6601)). + * Allow `AdtsExtractor` to encounter EOF when calculating average frame size + ([#6700](https://github.com/google/ExoPlayer/issues/6700)). +* Text: + * Add support for position and overlapping start/end times in SSA/ASS + subtitles ([#6320](https://github.com/google/ExoPlayer/issues/6320)). + * Require an end time or duration for SubRip (SRT) and SubStation Alpha + (SSA/ASS) subtitles. This applies to both sidecar files & subtitles + [embedded in Matroska streams](https://matroska.org/technical/specs/subtitles/index.html). +* UI: + * Make showing and hiding player controls accessible to TalkBack in + `PlayerView`. + * Rename `spherical_view` surface type to `spherical_gl_surface_view`. + * Make it easier to override the shuffle, repeat, fullscreen, VR and small + notification icon assets + ([#6709](https://github.com/google/ExoPlayer/issues/6709)). +* Analytics: + * Remove `AnalyticsCollector.Factory`. Instances should be created directly, + and the `Player` should be set by calling `AnalyticsCollector.setPlayer`. + * Add `PlaybackStatsListener` to collect `PlaybackStats` for analysis and + analytics reporting. +* DataSource + * Add `DataSpec.httpRequestHeaders` to support setting per-request headers for + HTTP and HTTPS. + * Remove the `DataSpec.FLAG_ALLOW_ICY_METADATA` flag. Use is replaced by + setting the `IcyHeaders.REQUEST_HEADER_ENABLE_METADATA_NAME` header in + `DataSpec.httpRequestHeaders`. + * Fail more explicitly when local file URIs contain invalid parts (e.g. a + fragment) ([#6470](https://github.com/google/ExoPlayer/issues/6470)). +* DASH: Support negative @r values in segment timelines + ([#1787](https://github.com/google/ExoPlayer/issues/1787)). +* HLS: + * Use peak bitrate rather than average bitrate for adaptive track selection. + * Fix issue where streams could get stuck in an infinite buffering state + after a postroll ad + ([#6314](https://github.com/google/ExoPlayer/issues/6314)). +* Matroska: Support lacing in Blocks + ([#3026](https://github.com/google/ExoPlayer/issues/3026)). +* AV1 extension: + * New in this release. The AV1 extension allows use of the + [libgav1 software decoder](https://chromium.googlesource.com/codecs/libgav1/) + in ExoPlayer. You can read more about playing AV1 videos with ExoPlayer + [here](https://medium.com/google-exoplayer/playing-av1-videos-with-exoplayer-a7cb19bedef9). +* VP9 extension: + * Update to use NDK r20. + * Rename `VpxVideoSurfaceView` to `VideoDecoderSurfaceView` and move it to the + core library. + * Move `LibvpxVideoRenderer.MSG_SET_OUTPUT_BUFFER_RENDERER` to + `C.MSG_SET_OUTPUT_BUFFER_RENDERER`. + * Use `VideoDecoderRenderer` as an implementation of + `VideoDecoderOutputBufferRenderer`, instead of `VideoDecoderSurfaceView`. +* FLAC extension: Update to use NDK r20. +* Opus extension: Update to use NDK r20. +* FFmpeg extension: + * Update to use NDK r20. + * Update to use FFmpeg version 4.2. It is necessary to rebuild the native part + of the extension after this change, following the instructions in the + extension's readme. +* MediaSession extension: Add `MediaSessionConnector.setCaptionCallback` to + support `ACTION_SET_CAPTIONING_ENABLED` events. +* GVR extension: This extension is now deprecated. +* Demo apps: + * Add [SurfaceControl demo app](https://github.com/google/ExoPlayer/tree/r2.11.0/demos/surface) + to show how to use the Android 10 `SurfaceControl` API with ExoPlayer + ([#677](https://github.com/google/ExoPlayer/issues/677)). + * Add support for subtitle files to the + [Main demo app](https://github.com/google/ExoPlayer/tree/r2.11.0/demos/main) + ([#5523](https://github.com/google/ExoPlayer/issues/5523)). + * Remove the IMA demo app. IMA functionality is demonstrated by the + [main demo app](https://github.com/google/ExoPlayer/tree/r2.11.0/demos/main). + * Add basic DRM support to the + [Cast demo app](https://github.com/google/ExoPlayer/tree/r2.11.0/demos/cast). +* TestUtils: Publish the `testutils` module to simplify unit testing with + ExoPlayer ([#6267](https://github.com/google/ExoPlayer/issues/6267)). +* IMA extension: Remove `AdsManager` listeners on release to avoid leaking an + `AdEventListener` provided by the app + ([#6687](https://github.com/google/ExoPlayer/issues/6687)). + +### 2.10.8 (2019-11-19) ### + +* E-AC3 JOC + * Handle new signaling in DASH manifests + ([#6636](https://github.com/google/ExoPlayer/issues/6636)). + * Fix E-AC3 JOC passthrough playback failing to initialize due to incorrect + channel count check. +* FLAC + * Fix sniffing for some FLAC streams. + * Fix FLAC `Format.bitrate` values. +* Parse ALAC channel count and sample rate information from a more robust source + when contained in MP4 + ([#6648](https://github.com/google/ExoPlayer/issues/6648)). +* Fix seeking into multi-period content in the edge case that the period + containing the seek position has just been removed + ([#6641](https://github.com/google/ExoPlayer/issues/6641)). + +### 2.10.7 (2019-11-06) ### + +* HLS: Fix detection of Dolby Atmos to match the HLS authoring specification. +* MediaSession extension: Update shuffle and repeat modes when playback state + is invalidated ([#6582](https://github.com/google/ExoPlayer/issues/6582)). +* Fix the start of audio getting truncated when transitioning to a new + item in a playlist of Opus streams. + +### 2.10.6 (2019-10-17) ### + +* Add `Player.onPlaybackSuppressionReasonChanged` to allow listeners to + detect playbacks suppressions (e.g. transient audio focus loss) directly + ([#6203](https://github.com/google/ExoPlayer/issues/6203)). +* DASH: + * Support `Label` elements + ([#6297](https://github.com/google/ExoPlayer/issues/6297)). + * Support legacy audio channel configuration + ([#6523](https://github.com/google/ExoPlayer/issues/6523)). +* HLS: Add support for ID3 in EMSG when using FMP4 streams + ([spec](https://aomediacodec.github.io/av1-id3/)). +* MP3: Add workaround to avoid prematurely ending playback of some SHOUTcast + live streams ([#6537](https://github.com/google/ExoPlayer/issues/6537), + [#6315](https://github.com/google/ExoPlayer/issues/6315) and + [#5658](https://github.com/google/ExoPlayer/issues/5658)). +* Metadata: Expose the raw ICY metadata through `IcyInfo` + ([#6476](https://github.com/google/ExoPlayer/issues/6476)). +* UI: + * Setting `app:played_color` on `PlayerView` and `PlayerControlView` no longer + adjusts the colors of the scrubber handle , buffered and unplayed parts of + the time bar. These can be set separately using `app:scrubber_color`, + `app:buffered_color` and `app_unplayed_color` respectively. + * Setting `app:ad_marker_color` on `PlayerView` and `PlayerControlView` no + longer adjusts the color of played ad markers. The color of played ad + markers can be set separately using `app:played_ad_marker_color`. + +### 2.10.5 (2019-09-20) ### + +* Add `Player.isPlaying` and `EventListener.onIsPlayingChanged` to check whether + the playback position is advancing. This helps to determine if playback is + suppressed due to audio focus loss. Also add + `Player.getPlaybackSuppressedReason` to determine the reason of the + suppression ([#6203](https://github.com/google/ExoPlayer/issues/6203)). +* Track selection + * Add `allowAudioMixedChannelCountAdaptiveness` parameter to + `DefaultTrackSelector` to allow adaptive selections of audio tracks with + different channel counts. + * Improve text selection logic to always prefer the better language matches + over other selection parameters. + * Fix audio selection issue where languages are compared by bitrate + ([#6335](https://github.com/google/ExoPlayer/issues/6335)). +* Performance + * Increase maximum video buffer size from 13MB to 32MB. The previous default + was too small for high quality streams. + * Reset `DefaultBandwidthMeter` to initial values on network change. + * Bypass sniffing in `ProgressiveMediaPeriod` in case a single extractor is + provided ([#6325](https://github.com/google/ExoPlayer/issues/6325)). +* Metadata + * Support EMSG V1 boxes in FMP4. + * Support unwrapping of nested metadata (e.g. ID3 and SCTE-35 in EMSG). +* Add `HttpDataSource.getResponseCode` to provide the status code associated + with the most recent HTTP response. +* Fix transitions between packed audio and non-packed audio segments in HLS + ([#6444](https://github.com/google/ExoPlayer/issues/6444)). +* Fix issue where a request would be retried after encountering an error, even + though the `LoadErrorHandlingPolicy` classified the error as fatal. +* Fix initialization data handling for FLAC in MP4 + ([#6396](https://github.com/google/ExoPlayer/issues/6396), + [#6397](https://github.com/google/ExoPlayer/issues/6397)). +* Fix decoder selection for E-AC3 JOC streams + ([#6398](https://github.com/google/ExoPlayer/issues/6398)). +* Fix `PlayerNotificationManager` to show play icon rather than pause icon when + playback is ended ([#6324](https://github.com/google/ExoPlayer/issues/6324)). +* RTMP extension: Upgrade LibRtmp-Client-for-Android to fix RTMP playback issues + ([#4200](https://github.com/google/ExoPlayer/issues/4200), + [#4249](https://github.com/google/ExoPlayer/issues/4249), + [#4319](https://github.com/google/ExoPlayer/issues/4319), + [#4337](https://github.com/google/ExoPlayer/issues/4337)). +* IMA extension: Fix crash in `ImaAdsLoader.onTimelineChanged` + ([#5831](https://github.com/google/ExoPlayer/issues/5831)). + +### 2.10.4 (2019-07-26) ### + +* Offline: Add `Scheduler` implementation that uses `WorkManager`. +* Add ability to specify a description when creating notification channels via + ExoPlayer library classes. +* Switch normalized BCP-47 language codes to use 2-letter ISO 639-1 language + tags instead of 3-letter ISO 639-2 language tags. +* Ensure the `SilenceMediaSource` position is in range + ([#6229](https://github.com/google/ExoPlayer/issues/6229)). +* WAV: Calculate correct duration for clipped streams + ([#6241](https://github.com/google/ExoPlayer/issues/6241)). +* MP3: Use CBR header bitrate, not calculated bitrate. This reverts a change + from 2.9.3 ([#6238](https://github.com/google/ExoPlayer/issues/6238)). +* FLAC extension: Parse `VORBIS_COMMENT` and `PICTURE` metadata + ([#5527](https://github.com/google/ExoPlayer/issues/5527)). +* Fix issue where initial seek positions get ignored when playing a preroll ad + ([#6201](https://github.com/google/ExoPlayer/issues/6201)). +* Fix issue where invalid language tags were normalized to "und" instead of + keeping the original + ([#6153](https://github.com/google/ExoPlayer/issues/6153)). +* Fix `DataSchemeDataSource` re-opening and range requests + ([#6192](https://github.com/google/ExoPlayer/issues/6192)). +* Fix FLAC and ALAC playback on some LG devices + ([#5938](https://github.com/google/ExoPlayer/issues/5938)). +* Fix issue when calling `performClick` on `PlayerView` without + `PlayerControlView` + ([#6260](https://github.com/google/ExoPlayer/issues/6260)). +* Fix issue where playback speeds are not used in adaptive track selections + after manual selection changes for other renderers + ([#6256](https://github.com/google/ExoPlayer/issues/6256)). + +### 2.10.3 (2019-07-09) ### + +* Display last frame when seeking to end of stream + ([#2568](https://github.com/google/ExoPlayer/issues/2568)). +* Audio: + * Fix an issue where not all audio was played out when the configuration + for the underlying track was changing (e.g., at some period transitions). + * Fix an issue where playback speed was applied inaccurately in playlists + ([#6117](https://github.com/google/ExoPlayer/issues/6117)). +* UI: Fix `PlayerView` incorrectly consuming touch events if no controller is + attached ([#6109](https://github.com/google/ExoPlayer/issues/6109)). +* CEA608: Fix repetition of special North American characters + ([#6133](https://github.com/google/ExoPlayer/issues/6133)). +* FLV: Fix bug that caused playback of some live streams to not start + ([#6111](https://github.com/google/ExoPlayer/issues/6111)). +* SmoothStreaming: Parse text stream `Subtype` into `Format.roleFlags`. +* MediaSession extension: Fix `MediaSessionConnector.play()` not resuming + playback ([#6093](https://github.com/google/ExoPlayer/issues/6093)). + +### 2.10.2 (2019-06-03) ### + +* Add `ResolvingDataSource` for just-in-time resolution of `DataSpec`s + ([#5779](https://github.com/google/ExoPlayer/issues/5779)). +* Add `SilenceMediaSource` that can be used to play silence of a given + duration ([#5735](https://github.com/google/ExoPlayer/issues/5735)). +* Offline: + * Prevent unexpected `DownloadHelper.Callback.onPrepared` callbacks after + preparation of a `DownloadHelper` fails + ([#5915](https://github.com/google/ExoPlayer/issues/5915)). + * Fix `CacheUtil.cache()` downloading too much data + ([#5927](https://github.com/google/ExoPlayer/issues/5927)). + * Fix misreporting cached bytes when caching is paused + ([#5573](https://github.com/google/ExoPlayer/issues/5573)). +* UI: + * Allow setting `DefaultTimeBar` attributes on `PlayerView` and + `PlayerControlView`. + * Change playback controls toggle from touch down to touch up events + ([#5784](https://github.com/google/ExoPlayer/issues/5784)). + * Fix issue where playback controls were not kept visible on key presses + ([#5963](https://github.com/google/ExoPlayer/issues/5963)). +* Subtitles: + * CEA-608: Handle XDS and TEXT modes + ([#5807](https://github.com/google/ExoPlayer/pull/5807)). + * TTML: Fix bitmap rendering + ([#5633](https://github.com/google/ExoPlayer/pull/5633)). +* IMA: Fix ad pod index offset calculation without preroll + ([#5928](https://github.com/google/ExoPlayer/issues/5928)). +* Add a `playWhenReady` flag to MediaSessionConnector.PlaybackPreparer methods + to indicate whether a controller sent a play or only a prepare command. This + allows to take advantage of decoder reuse with the MediaSessionConnector + ([#5891](https://github.com/google/ExoPlayer/issues/5891)). +* Add `ProgressUpdateListener` to `PlayerControlView` + ([#5834](https://github.com/google/ExoPlayer/issues/5834)). +* Add support for auto-detecting UDP streams in `DefaultDataSource` + ([#6036](https://github.com/google/ExoPlayer/pull/6036)). +* Allow enabling decoder fallback with `DefaultRenderersFactory` + ([#5942](https://github.com/google/ExoPlayer/issues/5942)). +* Gracefully handle revoked `ACCESS_NETWORK_STATE` permission + ([#6019](https://github.com/google/ExoPlayer/issues/6019)). +* Fix decoding problems when seeking back after seeking beyond a mid-roll ad + ([#6009](https://github.com/google/ExoPlayer/issues/6009)). +* Fix application of `maxAudioBitrate` for adaptive audio track groups + ([#6006](https://github.com/google/ExoPlayer/issues/6006)). +* Fix bug caused by parallel adaptive track selection using `Format`s without + bitrate information + ([#5971](https://github.com/google/ExoPlayer/issues/5971)). +* Fix bug in `CastPlayer.getCurrentWindowIndex()` + ([#5955](https://github.com/google/ExoPlayer/issues/5955)). + +### 2.10.1 (2019-05-16) ### + +* Offline: Add option to remove all downloads. +* HLS: Fix `NullPointerException` when using HLS chunkless preparation + ([#5868](https://github.com/google/ExoPlayer/issues/5868)). +* Fix handling of empty values and line terminators in SHOUTcast ICY metadata + ([#5876](https://github.com/google/ExoPlayer/issues/5876)). +* Fix DVB subtitles for SDK 28 + ([#5862](https://github.com/google/ExoPlayer/issues/5862)). +* Add a workaround for a decoder failure on ZTE Axon7 mini devices when playing + 48kHz audio ([#5821](https://github.com/google/ExoPlayer/issues/5821)). + +### 2.10.0 (2019-04-15) ### + +* Core library: + * Improve decoder re-use between playbacks + ([#2826](https://github.com/google/ExoPlayer/issues/2826)). Read + [this blog post](https://medium.com/google-exoplayer/improved-decoder-reuse-in-exoplayer-ef4c6d99591d) + for more details. + * Rename `ExtractorMediaSource` to `ProgressiveMediaSource`. + * Fix issue where using `ProgressiveMediaSource.Factory` would mean that + `DefaultExtractorsFactory` would be kept by proguard. Custom + `ExtractorsFactory` instances must now be passed via the + `ProgressiveMediaSource.Factory` constructor, and `setExtractorsFactory` is + deprecated. + * Make the default minimum buffer size equal the maximum buffer size for video + playbacks ([#2083](https://github.com/google/ExoPlayer/issues/2083)). + * Move `PriorityTaskManager` from `DefaultLoadControl` to `SimpleExoPlayer`. + * Add new `ExoPlaybackException` types for remote exceptions and out-of-memory + errors. + * Use full BCP 47 language tags in `Format`. + * Do not retry failed loads whose error is `FileNotFoundException`. + * Fix issue where not resetting the position for a new `MediaSource` in calls + to `ExoPlayer.prepare` causes an `IndexOutOfBoundsException` + ([#5520](https://github.com/google/ExoPlayer/issues/5520)). +* Offline: + * Improve offline support. `DownloadManager` now tracks all offline content, + not just tasks in progress. Read + [this page](https://exoplayer.dev/downloading-media.html) for more details. +* Caching: + * Improve performance of `SimpleCache` + ([#4253](https://github.com/google/ExoPlayer/issues/4253)). + * Cache data with unknown length by default. The previous flag to opt in to + this behavior (`DataSpec.FLAG_ALLOW_CACHING_UNKNOWN_LENGTH`) has been + replaced with an opt out flag + (`DataSpec.FLAG_DONT_CACHE_IF_LENGTH_UNKNOWN`). +* Extractors: + * MP4/FMP4: Add support for Dolby Vision. + * MP4: Fix issue handling meta atoms in some streams + ([#5698](https://github.com/google/ExoPlayer/issues/5698), + [#5694](https://github.com/google/ExoPlayer/issues/5694)). + * MP3: Add support for SHOUTcast ICY metadata + ([#3735](https://github.com/google/ExoPlayer/issues/3735)). + * MP3: Fix ID3 frame unsychronization + ([#5673](https://github.com/google/ExoPlayer/issues/5673)). + * MP3: Fix playback of badly clipped files + ([#5772](https://github.com/google/ExoPlayer/issues/5772)). + * MPEG-TS: Enable HDMV DTS stream detection only if a flag is set. By default + (i.e. if the flag is not set), the 0x82 elementary stream type is now + treated as an SCTE subtitle track + ([#5330](https://github.com/google/ExoPlayer/issues/5330)). +* Track selection: + * Add options for controlling audio track selections to `DefaultTrackSelector` + ([#3314](https://github.com/google/ExoPlayer/issues/3314)). + * Update `TrackSelection.Factory` interface to support creating all track + selections together. + * Allow to specify a selection reason for a `SelectionOverride`. + * Select audio track based on system language if no preference is provided. + * When no text language preference matches, only select forced text tracks + whose language matches the selected audio language. +* UI: + * Update `DefaultTimeBar` based on duration of media and add parameter to set + the minimum update interval to control the smoothness of the updates + ([#5040](https://github.com/google/ExoPlayer/issues/5040)). + * Move creation of dialogs for `TrackSelectionView`s to + `TrackSelectionDialogBuilder` and add option to select multiple overrides. + * Change signature of `PlayerNotificationManager.NotificationListener` to + better fit service requirements. + * Add option to include navigation actions in the compact mode of + notifications created using `PlayerNotificationManager`. + * Fix issues with flickering notifications on KitKat when using + `PlayerNotificationManager` and `DownloadNotificationUtil`. For the latter, + applications should switch to using `DownloadNotificationHelper`. + * Fix accuracy of D-pad seeking in `DefaultTimeBar` + ([#5767](https://github.com/google/ExoPlayer/issues/5767)). +* Audio: + * Allow `AudioProcessor`s to be drained of pending output after they are + reconfigured. + * Fix an issue that caused audio to be truncated at the end of a period + when switching to a new period where gapless playback information was newly + present or newly absent. + * Add support for reading AC-4 streams + ([#5303](https://github.com/google/ExoPlayer/pull/5303)). +* Video: + * Remove `MediaCodecSelector.DEFAULT_WITH_FALLBACK`. Apps should instead + signal that fallback should be used by passing `true` as the + `enableDecoderFallback` parameter when instantiating the video renderer. + * Support video tunneling when the decoder is not listed first for the MIME + type ([#3100](https://github.com/google/ExoPlayer/issues/3100)). + * Query `MediaCodecList.ALL_CODECS` when selecting a tunneling decoder + ([#5547](https://github.com/google/ExoPlayer/issues/5547)). +* DRM: + * Fix black flicker when keys rotate in DRM protected content + ([#3561](https://github.com/google/ExoPlayer/issues/3561)). + * Work around lack of LA_URL attribute in PlayReady key request init data. +* CEA-608: Improved conformance to the specification + ([#3860](https://github.com/google/ExoPlayer/issues/3860)). +* DASH: + * Parse role and accessibility descriptors into `Format.roleFlags`. + * Support multiple CEA-608 channels muxed into FMP4 representations + ([#5656](https://github.com/google/ExoPlayer/issues/5656)). +* HLS: + * Prevent unnecessary reloads of initialization segments. + * Form an adaptive track group out of audio renditions with matching name. + * Support encrypted initialization segments + ([#5441](https://github.com/google/ExoPlayer/issues/5441)). + * Parse `EXT-X-MEDIA` `CHARACTERISTICS` attribute into `Format.roleFlags`. + * Add metadata entry for HLS tracks to expose master playlist information. + * Prevent `IndexOutOfBoundsException` in some live HLS scenarios + ([#5816](https://github.com/google/ExoPlayer/issues/5816)). +* Support for playing spherical videos on Daydream. +* Cast extension: Work around Cast framework returning a limited-size queue + items list ([#4964](https://github.com/google/ExoPlayer/issues/4964)). +* VP9 extension: Remove RGB output mode and libyuv dependency, and switch to + surface YUV output as the default. Remove constructor parameters `scaleToFit` + and `useSurfaceYuvOutput`. +* MediaSession extension: + * Let apps intercept media button events + ([#5179](https://github.com/google/ExoPlayer/issues/5179)). + * Fix issue with `TimelineQueueNavigator` not publishing the queue in shuffled + order when in shuffle mode. + * Allow handling of custom commands via `registerCustomCommandReceiver`. + * Add ability to include an extras `Bundle` when reporting a custom error. +* Log warnings when extension native libraries can't be used, to help with + diagnosing playback failures + ([#5788](https://github.com/google/ExoPlayer/issues/5788)). + +### 2.9.6 (2019-02-19) ### + +* Remove `player` and `isTopLevelSource` parameters from `MediaSource.prepare`. +* IMA extension: + * Require setting the `Player` on `AdsLoader` instances before + playback. + * Remove deprecated `ImaAdsMediaSource`. Create `AdsMediaSource` with an + `ImaAdsLoader` instead. + * Remove deprecated `AdsMediaSource` constructors. Listen for media source + events using `AdsMediaSource.addEventListener`, and ad interaction events by + adding a listener when building `ImaAdsLoader`. + * Allow apps to register playback-related obstructing views that are on top of + their ad display containers via `AdsLoader.AdViewProvider`. `PlayerView` + implements this interface and will register its control view. This makes it + possible for ad loading SDKs to calculate ad viewability accurately. +* DASH: Fix issue handling large `EventStream` presentation timestamps + ([#5490](https://github.com/google/ExoPlayer/issues/5490)). +* HLS: Fix transition to STATE_ENDED when playing fragmented mp4 in chunkless + preparation ([#5524](https://github.com/google/ExoPlayer/issues/5524)). +* Revert workaround for video quality problems with Amlogic decoders, as this + may cause problems for some devices and/or non-interlaced content + ([#5003](https://github.com/google/ExoPlayer/issues/5003)). + +### 2.9.5 (2019-01-31) ### + +* HLS: Parse `CHANNELS` attribute from `EXT-X-MEDIA` tag. +* ConcatenatingMediaSource: + * Add `Handler` parameter to methods that take a callback `Runnable`. + * Fix issue with dropped messages when releasing the source + ([#5464](https://github.com/google/ExoPlayer/issues/5464)). +* ExtractorMediaSource: Fix issue that could cause the player to get stuck + buffering at the end of the media. +* PlayerView: Fix issue preventing `OnClickListener` from receiving events + ([#5433](https://github.com/google/ExoPlayer/issues/5433)). +* IMA extension: Upgrade IMA dependency to 3.10.6. +* Cronet extension: Upgrade Cronet dependency to 71.3578.98. +* OkHttp extension: Upgrade OkHttp dependency to 3.12.1. +* MP3: Wider fix for issue where streams would play twice on some Samsung + devices ([#4519](https://github.com/google/ExoPlayer/issues/4519)). + +### 2.9.4 (2019-01-15) ### + +* IMA extension: Clear ads loader listeners on release + ([#4114](https://github.com/google/ExoPlayer/issues/4114)). +* SmoothStreaming: Fix support for subtitles in DRM protected streams + ([#5378](https://github.com/google/ExoPlayer/issues/5378)). +* FFmpeg extension: Treat invalid data errors as non-fatal to match the behavior + of MediaCodec ([#5293](https://github.com/google/ExoPlayer/issues/5293)). +* GVR extension: upgrade GVR SDK dependency to 1.190.0. +* Associate fatal player errors of type SOURCE with the loading source in + `AnalyticsListener.EventTime` + ([#5407](https://github.com/google/ExoPlayer/issues/5407)). +* Add `startPositionUs` to `MediaSource.createPeriod`. This fixes an issue where + using lazy preparation in `ConcatenatingMediaSource` with an + `ExtractorMediaSource` overrides initial seek positions + ([#5350](https://github.com/google/ExoPlayer/issues/5350)). +* Add subtext to the `MediaDescriptionAdapter` of the + `PlayerNotificationManager`. +* Add workaround for video quality problems with Amlogic decoders + ([#5003](https://github.com/google/ExoPlayer/issues/5003)). +* Fix issue where sending callbacks for playlist changes may cause problems + because of parallel player access + ([#5240](https://github.com/google/ExoPlayer/issues/5240)). +* Fix issue with reusing a `ClippingMediaSource` with an inner + `ExtractorMediaSource` and a non-zero start position + ([#5351](https://github.com/google/ExoPlayer/issues/5351)). +* Fix issue where uneven track durations in MP4 streams can cause OOM problems + ([#3670](https://github.com/google/ExoPlayer/issues/3670)). + +### 2.9.3 (2018-12-20) ### + +* Captions: Support PNG subtitles in SMPTE-TT + ([#1583](https://github.com/google/ExoPlayer/issues/1583)). +* MPEG-TS: Use random access indicators to minimize the need for + `FLAG_ALLOW_NON_IDR_KEYFRAMES`. +* Downloading: Reduce time taken to remove downloads + ([#5136](https://github.com/google/ExoPlayer/issues/5136)). +* MP3: + * Use the true bitrate for constant-bitrate MP3 seeking. + * Fix issue where streams would play twice on some Samsung devices + ([#4519](https://github.com/google/ExoPlayer/issues/4519)). +* Fix regression where some audio formats were incorrectly marked as being + unplayable due to under-reporting of platform decoder capabilities + ([#5145](https://github.com/google/ExoPlayer/issues/5145)). +* Fix decode-only frame skipping on Nvidia Shield TV devices. +* Workaround for MiTV (dangal) issue when swapping output surface + ([#5169](https://github.com/google/ExoPlayer/issues/5169)). + +### 2.9.2 (2018-11-28) ### + +* HLS: + * Fix issue causing unnecessary media playlist requests when playing live + streams ([#5059](https://github.com/google/ExoPlayer/issues/5059)). + * Fix decoder re-instantiation issue for packed audio streams + ([#5063](https://github.com/google/ExoPlayer/issues/5063)). +* MP4: Support Opus and FLAC in the MP4 container, and in DASH + ([#4883](https://github.com/google/ExoPlayer/issues/4883)). +* DASH: Fix detecting the end of live events + ([#4780](https://github.com/google/ExoPlayer/issues/4780)). +* Spherical video: Fall back to `TYPE_ROTATION_VECTOR` if + `TYPE_GAME_ROTATION_VECTOR` is unavailable + ([#5119](https://github.com/google/ExoPlayer/issues/5119)). +* Support seeking for a wider range of MPEG-TS streams + ([#5097](https://github.com/google/ExoPlayer/issues/5097)). +* Include channel count in audio capabilities check + ([#4690](https://github.com/google/ExoPlayer/issues/4690)). +* Fix issue with applying the `show_buffering` attribute in `PlayerView` + ([#5139](https://github.com/google/ExoPlayer/issues/5139)). +* Fix issue where null `Metadata` was output when it failed to decode + ([#5149](https://github.com/google/ExoPlayer/issues/5149)). +* Fix playback of some invalid but playable MP4 streams by replacing assertions + with logged warnings in sample table parsing code + ([#5162](https://github.com/google/ExoPlayer/issues/5162)). +* Fix UUID passed to `MediaCrypto` when using `C.CLEARKEY_UUID` before API 27. + +### 2.9.1 (2018-11-01) ### + +* Add convenience methods `Player.next`, `Player.previous`, `Player.hasNext` + and `Player.hasPrevious` + ([#4863](https://github.com/google/ExoPlayer/issues/4863)). +* Improve initial bandwidth meter estimates using the current country and + network type. +* IMA extension: + * For preroll to live stream transitions, project forward the loading position + to avoid being behind the live window. + * Let apps specify whether to focus the skip button on ATV + ([#5019](https://github.com/google/ExoPlayer/issues/5019)). +* MP3: + * Support seeking based on MLLT metadata + ([#3241](https://github.com/google/ExoPlayer/issues/3241)). + * Fix handling of streams with appended data + ([#4954](https://github.com/google/ExoPlayer/issues/4954)). +* DASH: Parse ProgramInformation element if present in the manifest. +* HLS: + * Add constructor to `DefaultHlsExtractorFactory` for adding TS payload + reader factory flags + ([#4861](https://github.com/google/ExoPlayer/issues/4861)). + * Fix bug in segment sniffing + ([#5039](https://github.com/google/ExoPlayer/issues/5039)). +* SubRip: Add support for alignment tags, and remove tags from the displayed + captions ([#4306](https://github.com/google/ExoPlayer/issues/4306)). +* Fix issue with blind seeking to windows with non-zero offset in a + `ConcatenatingMediaSource` + ([#4873](https://github.com/google/ExoPlayer/issues/4873)). +* Fix logic for enabling next and previous actions in `TimelineQueueNavigator` + ([#5065](https://github.com/google/ExoPlayer/issues/5065)). +* Fix issue where audio focus handling could not be disabled after enabling it + ([#5055](https://github.com/google/ExoPlayer/issues/5055)). +* Fix issue where subtitles were positioned incorrectly if `SubtitleView` had a + non-zero position offset to its parent + ([#4788](https://github.com/google/ExoPlayer/issues/4788)). +* Fix issue where the buffered position was not updated correctly when + transitioning between periods + ([#4899](https://github.com/google/ExoPlayer/issues/4899)). +* Fix issue where a `NullPointerException` is thrown when removing an unprepared + media source from a `ConcatenatingMediaSource` with the `useLazyPreparation` + option enabled ([#4986](https://github.com/google/ExoPlayer/issues/4986)). +* Work around an issue where a non-empty end-of-stream audio buffer would be + output with timestamp zero, causing the player position to jump backwards + ([#5045](https://github.com/google/ExoPlayer/issues/5045)). +* Suppress a spurious assertion failure on some Samsung devices + ([#4532](https://github.com/google/ExoPlayer/issues/4532)). +* Suppress spurious "references unknown class member" shrinking warning + ([#4890](https://github.com/google/ExoPlayer/issues/4890)). +* Swap recommended order for google() and jcenter() in gradle config + ([#4997](https://github.com/google/ExoPlayer/issues/4997)). + +### 2.9.0 (2018-09-06) ### + +* Turn on Java 8 compiler support for the ExoPlayer library. Apps may need to + add `compileOptions { targetCompatibility JavaVersion.VERSION_1_8 }` to their + gradle settings to ensure bytecode compatibility. +* Set `compileSdkVersion` and `targetSdkVersion` to 28. +* Support for automatic audio focus handling via + `SimpleExoPlayer.setAudioAttributes`. +* Add `ExoPlayer.retry` convenience method. +* Add `AudioListener` for listening to changes in audio configuration during + playback ([#3994](https://github.com/google/ExoPlayer/issues/3994)). +* Add `LoadErrorHandlingPolicy` to allow configuration of load error handling + across `MediaSource` implementations + ([#3370](https://github.com/google/ExoPlayer/issues/3370)). +* Allow passing a `Looper`, which specifies the thread that must be used to + access the player, when instantiating player instances using + `ExoPlayerFactory` ([#4278](https://github.com/google/ExoPlayer/issues/4278)). +* Allow setting log level for ExoPlayer logcat output + ([#4665](https://github.com/google/ExoPlayer/issues/4665)). +* Simplify `BandwidthMeter` injection: The `BandwidthMeter` should now be + passed directly to `ExoPlayerFactory`, instead of to `TrackSelection.Factory` + and `DataSource.Factory`. The `BandwidthMeter` is passed to the components + that need it internally. The `BandwidthMeter` may also be omitted, in which + case a default instance will be used. +* Spherical video: + * Support for spherical video by setting `surface_type="spherical_view"` on + `PlayerView`. + * Support for + [VR180](https://github.com/google/spatial-media/blob/master/docs/vr180.md). +* HLS: + * Support PlayReady. + * Add container format sniffing + ([#2025](https://github.com/google/ExoPlayer/issues/2025)). + * Support alternative `EXT-X-KEY` tags. + * Support `EXT-X-INDEPENDENT-SEGMENTS` in the master playlist. + * Support variable substitution + ([#4422](https://github.com/google/ExoPlayer/issues/4422)). + * Fix the bitrate being unset on primary track sample formats + ([#3297](https://github.com/google/ExoPlayer/issues/3297)). + * Make `HlsMediaSource.Factory` take a factory of trackers instead of a + tracker instance ([#4814](https://github.com/google/ExoPlayer/issues/4814)). +* DASH: + * Support `messageData` attribute for in-manifest event streams. + * Clip periods to their specified durations + ([#4185](https://github.com/google/ExoPlayer/issues/4185)). +* Improve seeking support for progressive streams: + * Support seeking in MPEG-TS + ([#966](https://github.com/google/ExoPlayer/issues/966)). + * Support seeking in MPEG-PS + ([#4476](https://github.com/google/ExoPlayer/issues/4476)). + * Support approximate seeking in ADTS using a constant bitrate assumption + ([#4548](https://github.com/google/ExoPlayer/issues/4548)). The + `FLAG_ENABLE_CONSTANT_BITRATE_SEEKING` flag must be set on the extractor to + enable this functionality. + * Support approximate seeking in AMR using a constant bitrate assumption. + The `FLAG_ENABLE_CONSTANT_BITRATE_SEEKING` flag must be set on the extractor + to enable this functionality. + * Add `DefaultExtractorsFactory.setConstantBitrateSeekingEnabled` to enable + approximate seeking using a constant bitrate assumption on all extractors + that support it. +* Video: + * Add callback to `VideoListener` to notify of surface size changes. + * Improve performance when playing high frame-rate content, and when playing + at greater than 1x speed + ([#2777](https://github.com/google/ExoPlayer/issues/2777)). + * Scale up the initial video decoder maximum input size so playlist + transitions with small increases in maximum sample size do not require + reinitialization ([#4510](https://github.com/google/ExoPlayer/issues/4510)). + * Fix a bug where the player would not transition to the ended state when + playing video in tunneled mode. +* Audio: + * Support attaching auxiliary audio effects to the `AudioTrack` via + `Player.setAuxEffectInfo` and `Player.clearAuxEffectInfo`. + * Support seamless adaptation while playing xHE-AAC streams. + ([#4360](https://github.com/google/ExoPlayer/issues/4360)). + * Increase `AudioTrack` buffer sizes to the theoretical maximum required for + each encoding for passthrough playbacks + ([#3803](https://github.com/google/ExoPlayer/issues/3803)). + * WAV: Fix issue where white noise would be output at the end of playback + ([#4724](https://github.com/google/ExoPlayer/issues/4724)). + * MP3: Fix issue where streams would play twice on the SM-T530 + ([#4519](https://github.com/google/ExoPlayer/issues/4519)). +* Analytics: + * Add callbacks to `DefaultDrmSessionEventListener` and `AnalyticsListener` to + be notified of acquired and released DRM sessions. + * Add uri field to `LoadEventInfo` in `MediaSourceEventListener` and + `AnalyticsListener` callbacks. This uri is the redirected uri if redirection + occurred ([#2054](https://github.com/google/ExoPlayer/issues/2054)). + * Add response headers field to `LoadEventInfo` in `MediaSourceEventListener` + and `AnalyticsListener` callbacks + ([#4361](https://github.com/google/ExoPlayer/issues/4361) and + [#4615](https://github.com/google/ExoPlayer/issues/4615)). +* UI: + * Add option to `PlayerView` to show buffering view when playWhenReady is + false ([#4304](https://github.com/google/ExoPlayer/issues/4304)). + * Allow any `Drawable` to be used as `PlayerView` default artwork. +* ConcatenatingMediaSource: + * Support lazy preparation of playlist media sources + ([#3972](https://github.com/google/ExoPlayer/issues/3972)). + * Support range removal with `removeMediaSourceRange` methods + ([#4542](https://github.com/google/ExoPlayer/issues/4542)). + * Support setting a new shuffle order with `setShuffleOrder` + ([#4791](https://github.com/google/ExoPlayer/issues/4791)). +* MPEG-TS: Support CEA-608/708 in H262 + ([#2565](https://github.com/google/ExoPlayer/issues/2565)). +* Allow configuration of the back buffer in `DefaultLoadControl.Builder` + ([#4857](https://github.com/google/ExoPlayer/issues/4857)). +* Allow apps to pass a `CacheKeyFactory` for setting custom cache keys when + creating a `CacheDataSource`. +* Provide additional information for adaptive track selection. + `TrackSelection.updateSelectedTrack` has two new parameters for the current + queue of media chunks and iterators for information about upcoming chunks. +* Allow `MediaCodecSelector`s to return multiple compatible decoders for + `MediaCodecRenderer`, and provide an (optional) `MediaCodecSelector` that + falls back to less preferred decoders like `MediaCodec.createDecoderByType` + ([#273](https://github.com/google/ExoPlayer/issues/273)). +* Enable gzip for requests made by `SingleSampleMediaSource` + ([#4771](https://github.com/google/ExoPlayer/issues/4771)). +* Fix bug reporting buffered position for multi-period windows, and add + convenience methods `Player.getTotalBufferedDuration` and + `Player.getContentBufferedDuration` + ([#4023](https://github.com/google/ExoPlayer/issues/4023)). +* Fix bug where transitions to clipped media sources would happen too early + ([#4583](https://github.com/google/ExoPlayer/issues/4583)). +* Fix bugs reporting events for multi-period media sources + ([#4492](https://github.com/google/ExoPlayer/issues/4492) and + [#4634](https://github.com/google/ExoPlayer/issues/4634)). +* Fix issue where removing looping media from a playlist throws an exception + ([#4871](https://github.com/google/ExoPlayer/issues/4871). +* Fix issue where the preferred audio or text track would not be selected if + mapped onto a secondary renderer of the corresponding type + ([#4711](http://github.com/google/ExoPlayer/issues/4711)). +* Fix issue where errors of upcoming playlist items are thrown too early + ([#4661](https://github.com/google/ExoPlayer/issues/4661)). +* Allow edit lists which do not start with a sync sample. + ([#4774](https://github.com/google/ExoPlayer/issues/4774)). +* Fix issue with audio discontinuities at period transitions, e.g. when + looping ([#3829](https://github.com/google/ExoPlayer/issues/3829)). +* Fix issue where `player.getCurrentTag()` throws an `IndexOutOfBoundsException` + ([#4822](https://github.com/google/ExoPlayer/issues/4822)). +* Fix bug preventing use of multiple key session support (`multiSession=true`) + for non-Widevine `DefaultDrmSessionManager` instances + ([#4834](https://github.com/google/ExoPlayer/issues/4834)). +* Fix issue where audio and video would desynchronize when playing + concatenations of gapless content + ([#4559](https://github.com/google/ExoPlayer/issues/4559)). +* IMA extension: + * Refine the previous fix for empty ad groups to avoid discarding ad breaks + unnecessarily ([#4030](https://github.com/google/ExoPlayer/issues/4030) and + [#4280](https://github.com/google/ExoPlayer/issues/4280)). + * Fix handling of empty postrolls + ([#4681](https://github.com/google/ExoPlayer/issues/4681)). + * Fix handling of postrolls with multiple ads + ([#4710](https://github.com/google/ExoPlayer/issues/4710)). +* MediaSession extension: + * Add `MediaSessionConnector.setCustomErrorMessage` to support setting custom + error messages. + * Add `MediaMetadataProvider` to support setting custom metadata + ([#3497](https://github.com/google/ExoPlayer/issues/3497)). +* Cronet extension: Now distributed via jCenter. +* FFmpeg extension: Support mu-law and A-law PCM. + +### 2.8.4 (2018-08-17) ### + +* IMA extension: Improve handling of consecutive empty ad groups + ([#4030](https://github.com/google/ExoPlayer/issues/4030)), + ([#4280](https://github.com/google/ExoPlayer/issues/4280)). + +### 2.8.3 (2018-07-23) ### + +* IMA extension: + * Fix behavior when creating/releasing the player then releasing + `ImaAdsLoader` ([#3879](https://github.com/google/ExoPlayer/issues/3879)). + * Add support for setting slots for companion ads. +* Captions: + * TTML: Fix an issue with TTML using font size as % of cell resolution that + makes `SubtitleView.setApplyEmbeddedFontSizes()` not work correctly. + ([#4491](https://github.com/google/ExoPlayer/issues/4491)). + * CEA-608: Improve handling of embedded styles + ([#4321](https://github.com/google/ExoPlayer/issues/4321)). +* DASH: + * Exclude text streams from duration calculations + ([#4029](https://github.com/google/ExoPlayer/issues/4029)). + * Fix freezing when playing multi-period manifests with `EventStream`s + ([#4492](https://github.com/google/ExoPlayer/issues/4492)). +* DRM: Allow DrmInitData to carry a license server URL + ([#3393](https://github.com/google/ExoPlayer/issues/3393)). +* MPEG-TS: Fix bug preventing SCTE-35 cues from being output + ([#4573](https://github.com/google/ExoPlayer/issues/4573)). +* Expose all internal ID3 data stored in MP4 udta boxes, and switch from using + CommentFrame to InternalFrame for frames with gapless metadata in MP4. +* Add `PlayerView.isControllerVisible` + ([#4385](https://github.com/google/ExoPlayer/issues/4385)). +* Fix issue playing DRM protected streams on Asus Zenfone 2 + ([#4403](https://github.com/google/ExoPlayer/issues/4413)). +* Add support for multiple audio and video tracks in MPEG-PS streams + ([#4406](https://github.com/google/ExoPlayer/issues/4406)). +* Add workaround for track index mismatches between trex and tkhd boxes in + fragmented MP4 files + ([#4477](https://github.com/google/ExoPlayer/issues/4477)). +* Add workaround for track index mismatches between tfhd and tkhd boxes in + fragmented MP4 files + ([#4083](https://github.com/google/ExoPlayer/issues/4083)). +* Ignore all MP4 edit lists if one edit list couldn't be handled + ([#4348](https://github.com/google/ExoPlayer/issues/4348)). +* Fix issue when switching track selection from an embedded track to a primary + track in DASH ([#4477](https://github.com/google/ExoPlayer/issues/4477)). +* Fix accessibility class name for `DefaultTimeBar` + ([#4611](https://github.com/google/ExoPlayer/issues/4611)). +* Improved compatibility with FireOS devices. + +### 2.8.2 (2018-06-06) ### + +* IMA extension: Don't advertise support for video/mpeg ad media, as we don't + have an extractor for this + ([#4297](https://github.com/google/ExoPlayer/issues/4297)). +* DASH: Fix playback getting stuck when playing representations that have both + sidx atoms and non-zero presentationTimeOffset values. +* HLS: + * Allow injection of custom playlist trackers. + * Fix adaptation in live playlists with EXT-X-PROGRAM-DATE-TIME tags. +* Mitigate memory leaks when `MediaSource` loads are slow to cancel + ([#4249](https://github.com/google/ExoPlayer/issues/4249)). +* Fix inconsistent `Player.EventListener` invocations for recursive player state + changes ([#4276](https://github.com/google/ExoPlayer/issues/4276)). +* Fix `MediaCodec.native_setSurface` crash on Moto C + ([#4315](https://github.com/google/ExoPlayer/issues/4315)). +* Fix missing whitespace in CEA-608 + ([#3906](https://github.com/google/ExoPlayer/issues/3906)). +* Fix crash downloading HLS media playlists + ([#4396](https://github.com/google/ExoPlayer/issues/4396)). +* Fix a bug where download cancellation was ignored + ([#4403](https://github.com/google/ExoPlayer/issues/4403)). +* Set `METADATA_KEY_TITLE` on media descriptions + ([#4292](https://github.com/google/ExoPlayer/issues/4292)). +* Allow apps to register custom MIME types + ([#4264](https://github.com/google/ExoPlayer/issues/4264)). + +### 2.8.1 (2018-05-22) ### + +* HLS: + * Fix playback of livestreams with EXT-X-PROGRAM-DATE-TIME tags + ([#4239](https://github.com/google/ExoPlayer/issues/4239)). + * Fix playback of clipped streams starting from non-keyframe positions + ([#4241](https://github.com/google/ExoPlayer/issues/4241)). +* OkHttp extension: Fix to correctly include response headers in thrown + `InvalidResponseCodeException`s. +* Add possibility to cancel `PlayerMessage`s. +* UI: + * Add `PlayerView.setKeepContentOnPlayerReset` to keep the currently displayed + video frame or media artwork visible when the player is reset + ([#2843](https://github.com/google/ExoPlayer/issues/2843)). +* Fix crash when switching surface on Moto E(4) + ([#4134](https://github.com/google/ExoPlayer/issues/4134)). +* Fix a bug that could cause event listeners to be called with inconsistent + information if an event listener interacted with the player + ([#4262](https://github.com/google/ExoPlayer/issues/4262)). +* Audio: + * Fix extraction of PCM in MP4/MOV + ([#4228](https://github.com/google/ExoPlayer/issues/4228)). + * FLAC: Supports seeking for FLAC files without SEEKTABLE + ([#1808](https://github.com/google/ExoPlayer/issues/1808)). +* Captions: + * TTML: + * Fix a styling issue when there are multiple regions displayed at the same + time that can make text size of each region much smaller than defined. + * Fix an issue when the caption line has no text (empty line or only line + break), and the line's background is still displayed. + * Support TTML font size using % correctly (as percentage of document cell + resolution). + +### 2.8.0 (2018-05-03) ### + +* Downloading: + * Add `DownloadService`, `DownloadManager` and related classes + ([#2643](https://github.com/google/ExoPlayer/issues/2643)). Information on + using these components to download progressive formats can be found + [here](https://medium.com/google-exoplayer/downloading-streams-6d259eec7f95). + To see how to download DASH, HLS and SmoothStreaming media, take a look at + the app. + * Updated main demo app to support downloading DASH, HLS, SmoothStreaming and + progressive media. +* MediaSources: + * Allow reusing media sources after they have been released and + also in parallel to allow adding them multiple times to a concatenation. + ([#3498](https://github.com/google/ExoPlayer/issues/3498)). + * Merged `DynamicConcatenatingMediaSource` into `ConcatenatingMediaSource` and + deprecated `DynamicConcatenatingMediaSource`. + * Allow clipping of child media sources where the period and window have a + non-zero offset with `ClippingMediaSource`. + * Allow adding and removing `MediaSourceEventListener`s to MediaSources after + they have been created. Listening to events is now supported for all + media sources including composite sources. + * Added callbacks to `MediaSourceEventListener` to get notified when media + periods are created, released and being read from. + * Support live stream clipping with `ClippingMediaSource`. + * Allow setting tags for all media sources in their factories. The tag of the + current window can be retrieved with `Player.getCurrentTag`. +* UI: + * Add support for displaying error messages and a buffering spinner in + `PlayerView`. + * Add support for listening to `AspectRatioFrameLayout`'s aspect ratio update + ([#3736](https://github.com/google/ExoPlayer/issues/3736)). + * Add `PlayerNotificationManager` for displaying notifications reflecting the + player state. + * Add `TrackSelectionView` for selecting tracks with `DefaultTrackSelector`. + * Add `TrackNameProvider` for converting track `Format`s to textual + descriptions, and `DefaultTrackNameProvider` as a default implementation. +* Track selection: + * Reworked `MappingTrackSelector` and `DefaultTrackSelector`. + * `DefaultTrackSelector.Parameters` now implements `Parcelable`. + * Added UI components for track selection (see above). +* Audio: + * Support extracting data from AMR container formats, including both narrow + and wide band ([#2527](https://github.com/google/ExoPlayer/issues/2527)). + * FLAC: + * Sniff FLAC files correctly if they have ID3 headers + ([#4055](https://github.com/google/ExoPlayer/issues/4055)). + * Supports FLAC files with high sample rate (176400 and 192000) + ([#3769](https://github.com/google/ExoPlayer/issues/3769)). + * Factor out `AudioTrack` position tracking from `DefaultAudioSink`. + * Fix an issue where the playback position would pause just after playback + begins, and poll the audio timestamp less frequently once it starts + advancing ([#3841](https://github.com/google/ExoPlayer/issues/3841)). + * Add an option to skip silent audio in `PlaybackParameters` + ([#2635](https://github.com/google/ExoPlayer/issues/2635)). + * Fix an issue where playback of TrueHD streams would get stuck after seeking + due to not finding a syncframe + ([#3845](https://github.com/google/ExoPlayer/issues/3845)). + * Fix an issue with eac3-joc playback where a codec would fail to configure + ([#4165](https://github.com/google/ExoPlayer/issues/4165)). + * Handle non-empty end-of-stream buffers, to fix gapless playback of streams + with encoder padding when the decoder returns a non-empty final buffer. + * Allow trimming more than one sample when applying an elst audio edit via + gapless playback info. + * Allow overriding skipping/scaling with custom `AudioProcessor`s + ([#3142](https://github.com/google/ExoPlayer/issues/3142)). +* Caching: + * Add release method to the `Cache` interface, and prevent multiple instances + of `SimpleCache` using the same folder at the same time. + * Cache redirect URLs + ([#2360](https://github.com/google/ExoPlayer/issues/2360)). +* DRM: + * Allow multiple listeners for `DefaultDrmSessionManager`. + * Pass `DrmSessionManager` to `ExoPlayerFactory` instead of `RendererFactory`. + * Change minimum API requirement for CBC and pattern encryption from 24 to 25 + ([#4022](https://github.com/google/ExoPlayer/issues/4022)). + * Fix handling of 307/308 redirects when making license requests + ([#4108](https://github.com/google/ExoPlayer/issues/4108)). +* HLS: + * Fix playlist loading error propagation when the current selection does + not include all of the playlist's variants. + * Fix SAMPLE-AES-CENC and SAMPLE-AES-CTR EXT-X-KEY methods + ([#4145](https://github.com/google/ExoPlayer/issues/4145)). + * Preeptively declare an ID3 track in chunkless preparation + ([#4016](https://github.com/google/ExoPlayer/issues/4016)). + * Add support for multiple #EXT-X-MAP tags in a media playlist + ([#4164](https://github.com/google/ExoPlayer/issues/4182)). + * Fix seeking in live streams + ([#4187](https://github.com/google/ExoPlayer/issues/4187)). +* IMA extension: + * Allow setting the ad media load timeout + ([#3691](https://github.com/google/ExoPlayer/issues/3691)). + * Expose ad load errors via `MediaSourceEventListener` on `AdsMediaSource`, + and allow setting an ad event listener on `ImaAdsLoader`. Deprecate the + `AdsMediaSource.EventListener`. +* Add `AnalyticsListener` interface which can be registered in + `SimpleExoPlayer` to receive detailed metadata for each ExoPlayer event. +* Optimize seeking in FMP4 by enabling seeking to the nearest sync sample within + a fragment. This benefits standalone FMP4 playbacks, DASH and SmoothStreaming. +* Updated default max buffer length in `DefaultLoadControl`. +* Fix ClearKey decryption error if the key contains a forward slash + ([#4075](https://github.com/google/ExoPlayer/issues/4075)). +* Fix crash when switching surface on Huawei P9 Lite + ([#4084](https://github.com/google/ExoPlayer/issues/4084)), and Philips QM163E + ([#4104](https://github.com/google/ExoPlayer/issues/4104)). +* Support ZLIB compressed PGS subtitles. +* Added `getPlaybackError` to `Player` interface. +* Moved initial bitrate estimate from `AdaptiveTrackSelection` to + `DefaultBandwidthMeter`. +* Removed default renderer time offset of 60000000 from internal player. The + actual renderer timestamp offset can be obtained by listening to + `BaseRenderer.onStreamChanged`. +* Added dependencies on checkerframework annotations for static code analysis. + +### 2.7.3 (2018-04-04) ### + +* Fix ProGuard configuration for Cast, IMA and OkHttp extensions. +* Update OkHttp extension to depend on OkHttp 3.10.0. + +### 2.7.2 (2018-03-29) ### + +* Gradle: Upgrade Gradle version from 4.1 to 4.4 so it can work with Android + Studio 3.1 ([#3708](https://github.com/google/ExoPlayer/issues/3708)). +* Match codecs starting with "mp4a" to different Audio MimeTypes + ([#3779](https://github.com/google/ExoPlayer/issues/3779)). +* Fix ANR issue on Redmi 4X and Redmi Note 4 + ([#4006](https://github.com/google/ExoPlayer/issues/4006)). +* Fix handling of zero padded strings when parsing Matroska streams + ([#4010](https://github.com/google/ExoPlayer/issues/4010)). +* Fix "Decoder input buffer too small" error when playing some FLAC streams. +* MediaSession extension: Omit fast forward and rewind actions when media is not + seekable ([#4001](https://github.com/google/ExoPlayer/issues/4001)). + +### 2.7.1 (2018-03-09) ### + +* Gradle: Replaced 'compile' (deprecated) with 'implementation' and + 'api'. This may lead to build breakage for applications upgrading from + previous version that rely on indirect dependencies of certain modules. In + such cases, application developers need to add the missing dependency to + their gradle file. You can read more about the new dependency configurations + [here](https://developer.android.com/studio/build/gradle-plugin-3-0-0-migration.html#new_configurations). +* HlsMediaSource: Make HLS periods start at zero instead of the epoch. + Applications that rely on HLS timelines having a period starting at + the epoch will need to update their handling of HLS timelines. The program + date time is still available via the informational + `Timeline.Window.windowStartTimeMs` field + ([#3865](https://github.com/google/ExoPlayer/issues/3865), + [#3888](https://github.com/google/ExoPlayer/issues/3888)). +* Enable seeking in MP4 streams where duration is set incorrectly in the track + header ([#3926](https://github.com/google/ExoPlayer/issues/3926)). +* Video: Force rendering a frame periodically in `MediaCodecVideoRenderer` and + `LibvpxVideoRenderer`, even if it is late. + +### 2.7.0 (2018-02-19) ### + +* Player interface: + * Add optional parameter to `stop` to reset the player when stopping. + * Add a reason to `EventListener.onTimelineChanged` to distinguish between + initial preparation, reset and dynamic updates. + * Add `Player.DISCONTINUITY_REASON_AD_INSERTION` to the possible reasons + reported in `Eventlistener.onPositionDiscontinuity` to distinguish + transitions to and from ads within one period from transitions between + periods. + * Replaced `ExoPlayer.sendMessages` with `ExoPlayer.createMessage` to allow + more customization of the message. Now supports setting a message delivery + playback position and/or a delivery handler + ([#2189](https://github.com/google/ExoPlayer/issues/2189)). + * Add `Player.VideoComponent`, `Player.TextComponent` and + `Player.MetadataComponent` interfaces that define optional video, text and + metadata output functionality. New `getVideoComponent`, `getTextComponent` + and `getMetadataComponent` methods provide access to this functionality. +* Add `ExoPlayer.setSeekParameters` for controlling how seek operations are + performed. The `SeekParameters` class contains defaults for exact seeking and + seeking to the closest sync points before, either side or after specified seek + positions. `SeekParameters` are not currently supported when playing HLS + streams. +* DefaultTrackSelector: + * Replace `DefaultTrackSelector.Parameters` copy methods with a builder. + * Support disabling of individual text track selection flags. +* Buffering: + * Allow a back-buffer of media to be retained behind the current playback + position, for fast backward seeking. The back-buffer can be configured by + custom `LoadControl` implementations. + * Add ability for `SequenceableLoader` to re-evaluate its buffer and discard + buffered media so that it can be re-buffered in a different quality. + * Allow more flexible loading strategy when playing media containing multiple + sub-streams, by allowing injection of custom `CompositeSequenceableLoader` + factories through `DashMediaSource.Factory`, `HlsMediaSource.Factory`, + `SsMediaSource.Factory`, and `MergingMediaSource`. + * Play out existing buffer before retrying for progressive live streams + ([#1606](https://github.com/google/ExoPlayer/issues/1606)). +* UI: + * Generalized player and control views to allow them to bind with any + `Player`, and renamed them to `PlayerView` and `PlayerControlView` + respectively. + * Made `PlayerView` automatically apply video rotation when configured to use + `TextureView` ([#91](https://github.com/google/ExoPlayer/issues/91)). + * Made `PlayerView` play button behave correctly when the player is ended + ([#3689](https://github.com/google/ExoPlayer/issues/3689)), and call a + `PlaybackPreparer` when the player is idle. +* DRM: Optimistically attempt playback of DRM protected content that does not + declare scheme specific init data in the manifest. If playback of clear + samples without keys is allowed, delay DRM session error propagation until + keys are actually needed + ([#3630](https://github.com/google/ExoPlayer/issues/3630)). +* DASH: + * Support in-band Emsg events targeting the player with scheme id + `urn:mpeg:dash:event:2012` and scheme values "1", "2" and "3". + * Support EventStream elements in DASH manifests. +* HLS: + * Add opt-in support for chunkless preparation in HLS. This allows an + HLS source to finish preparation without downloading any chunks, which can + significantly reduce initial buffering time + ([#3149](https://github.com/google/ExoPlayer/issues/3149)). More details + can be found + [here](https://medium.com/google-exoplayer/faster-hls-preparation-f6611aa15ea6). + * Fail if unable to sync with the Transport Stream, rather than entering + stuck in an indefinite buffering state. + * Fix mime type propagation + ([#3653](https://github.com/google/ExoPlayer/issues/3653)). + * Fix ID3 context reuse across segment format changes + ([#3622](https://github.com/google/ExoPlayer/issues/3622)). + * Use long for media sequence numbers + ([#3747](https://github.com/google/ExoPlayer/issues/3747)) + * Add initial support for the EXT-X-GAP tag. +* Audio: + * Support TrueHD passthrough for rechunked samples in Matroska files + ([#2147](https://github.com/google/ExoPlayer/issues/2147)). + * Support resampling 24-bit and 32-bit integer to 32-bit float for high + resolution output in `DefaultAudioSink` + ([#3635](https://github.com/google/ExoPlayer/pull/3635)). +* Captions: + * Basic support for PGS subtitles + ([#3008](https://github.com/google/ExoPlayer/issues/3008)). + * Fix handling of CEA-608 captions where multiple buffers have the same + presentation timestamp + ([#3782](https://github.com/google/ExoPlayer/issues/3782)). +* Caching: + * Fix cache corruption issue + ([#3762](https://github.com/google/ExoPlayer/issues/3762)). + * Implement periodic check in `CacheDataSource` to see whether it's possible + to switch to reading/writing the cache having initially bypassed it. +* IMA extension: + * Fix the player getting stuck when an ad group fails to load + ([#3584](https://github.com/google/ExoPlayer/issues/3584)). + * Work around loadAd not being called beore the LOADED AdEvent arrives + ([#3552](https://github.com/google/ExoPlayer/issues/3552)). + * Handle asset mismatch errors + ([#3801](https://github.com/google/ExoPlayer/issues/3801)). + * Add support for playing non-Extractor content MediaSources in + the IMA demo app + ([#3676](https://github.com/google/ExoPlayer/issues/3676)). + * Fix handling of ad tags where ad groups are out of order + ([#3716](https://github.com/google/ExoPlayer/issues/3716)). + * Fix handling of ad tags with only preroll/postroll ad groups + ([#3715](https://github.com/google/ExoPlayer/issues/3715)). + * Propagate ad media preparation errors to IMA so that the ads can be + skipped. + * Handle exceptions in IMA callbacks so that can be logged less verbosely. +* New Cast extension. Simplifies toggling between local and Cast playbacks. +* `EventLogger` moved from the demo app into the core library. +* Fix ANR issue on the Huawei P8 Lite, Huawei Y6II, Moto C+, Meizu M5C, + Lenovo K4 Note and Sony Xperia E5. + ([#3724](https://github.com/google/ExoPlayer/issues/3724), + [#3835](https://github.com/google/ExoPlayer/issues/3835)). +* Fix potential NPE when removing media sources from a + DynamicConcatenatingMediaSource + ([#3796](https://github.com/google/ExoPlayer/issues/3796)). +* Check `sys.display-size` on Philips ATVs + ([#3807](https://github.com/google/ExoPlayer/issues/3807)). +* Release `Extractor`s on the loading thread to avoid potentially leaking + resources when the playback thread has quit by the time the loading task has + completed. +* ID3: Better handle malformed ID3 data + ([#3792](https://github.com/google/ExoPlayer/issues/3792). +* Support 14-bit mode and little endianness in DTS PES packets + ([#3340](https://github.com/google/ExoPlayer/issues/3340)). +* Demo app: Add ability to download not DRM protected content. + +### 2.6.1 (2017-12-15) ### + +* Add factories to `ExtractorMediaSource`, `HlsMediaSource`, `SsMediaSource`, + `DashMediaSource` and `SingleSampleMediaSource`. +* Use the same listener `MediaSourceEventListener` for all MediaSource + implementations. +* IMA extension: + * Support non-ExtractorMediaSource ads + ([#3302](https://github.com/google/ExoPlayer/issues/3302)). + * Skip ads before the ad preceding the player's initial seek position + ([#3527](https://github.com/google/ExoPlayer/issues/3527)). + * Fix ad loading when there is no preroll. + * Add an option to turn off hiding controls during ad playback + ([#3532](https://github.com/google/ExoPlayer/issues/3532)). + * Support specifying an ads response instead of an ad tag + ([#3548](https://github.com/google/ExoPlayer/issues/3548)). + * Support overriding the ad load timeout + ([#3556](https://github.com/google/ExoPlayer/issues/3556)). +* DASH: Support time zone designators in ISO8601 UTCTiming elements + ([#3524](https://github.com/google/ExoPlayer/issues/3524)). +* Audio: + * Support 32-bit PCM float output from `DefaultAudioSink`, and add an option + to use this with `FfmpegAudioRenderer`. + * Add support for extracting 32-bit WAVE files + ([#3379](https://github.com/google/ExoPlayer/issues/3379)). + * Support extraction and decoding of Dolby Atmos + ([#2465](https://github.com/google/ExoPlayer/issues/2465)). + * Fix handling of playback parameter changes while paused when followed by a + seek. +* SimpleExoPlayer: Allow multiple audio and video debug listeners. +* DefaultTrackSelector: Support undefined language text track selection when the + preferred language is not available + ([#2980](https://github.com/google/ExoPlayer/issues/2980)). +* Add options to `DefaultLoadControl` to set maximum buffer size in bytes and + to choose whether size or time constraints are prioritized. +* Use surfaceless context for secure `DummySurface`, if available + ([#3558](https://github.com/google/ExoPlayer/issues/3558)). +* FLV: Fix playback of live streams that do not contain an audio track + ([#3188](https://github.com/google/ExoPlayer/issues/3188)). +* CEA-608: Fix handling of row count changes in roll-up mode + ([#3513](https://github.com/google/ExoPlayer/issues/3513)). +* Prevent period transitions when seeking to the end of a period when paused + ([#2439](https://github.com/google/ExoPlayer/issues/2439)). + +### 2.6.0 (2017-11-03) ### + +* Removed "r" prefix from versions. This release is "2.6.0", not "r2.6.0". +* New `Player.DefaultEventListener` abstract class can be extended to avoid + having to implement all methods defined by `Player.EventListener`. +* Added a reason to `EventListener.onPositionDiscontinuity` + ([#3252](https://github.com/google/ExoPlayer/issues/3252)). +* New `setShuffleModeEnabled` method for enabling shuffled playback. +* SimpleExoPlayer: Support for multiple video, text and metadata outputs. +* Support for `Renderer`s that don't consume any media + ([#3212](https://github.com/google/ExoPlayer/issues/3212)). +* Fix reporting of internal position discontinuities via + `Player.onPositionDiscontinuity`. `DISCONTINUITY_REASON_SEEK_ADJUSTMENT` is + added to disambiguate position adjustments during seeks from other types of + internal position discontinuity. +* Fix potential `IndexOutOfBoundsException` when calling `ExoPlayer.getDuration` + ([#3362](https://github.com/google/ExoPlayer/issues/3362)). +* Fix playbacks involving looping, concatenation and ads getting stuck when + media contains tracks with uneven durations + ([#1874](https://github.com/google/ExoPlayer/issues/1874)). +* Fix issue with `ContentDataSource` when reading from certain `ContentProvider` + implementations ([#3426](https://github.com/google/ExoPlayer/issues/3426)). +* Better playback experience when the video decoder cannot keep up, by skipping + to key-frames. This is particularly relevant for variable speed playbacks. +* Allow `SingleSampleMediaSource` to suppress load errors + ([#3140](https://github.com/google/ExoPlayer/issues/3140)). +* `DynamicConcatenatingMediaSource`: Allow specifying a callback to be invoked + after a dynamic playlist modification has been applied + ([#3407](https://github.com/google/ExoPlayer/issues/3407)). +* Audio: New `AudioSink` interface allows customization of audio output path. +* Offline: Added `Downloader` implementations for DASH, HLS, SmoothStreaming + and progressive streams. +* Track selection: + * Fixed adaptive track selection logic for live playbacks + ([#3017](https://github.com/google/ExoPlayer/issues/3017)). + * Added ability to select the lowest bitrate tracks. +* DASH: + * Don't crash when a malformed or unexpected manifest update occurs + ([#2795](https://github.com/google/ExoPlayer/issues/2795)). +* HLS: + * Support for Widevine protected FMP4 variants. + * Support CEA-608 in FMP4 variants. + * Support extractor injection + ([#2748](https://github.com/google/ExoPlayer/issues/2748)). +* DRM: + * Improved compatibility with ClearKey content + ([#3138](https://github.com/google/ExoPlayer/issues/3138)). + * Support multiple PSSH boxes of the same type. + * Retry initial provisioning and key requests if they fail + * Fix incorrect parsing of non-CENC sinf boxes. +* IMA extension: + * Expose `AdsLoader` via getter + ([#3322](https://github.com/google/ExoPlayer/issues/3322)). + * Handle `setPlayWhenReady` calls during ad playbacks + ([#3303](https://github.com/google/ExoPlayer/issues/3303)). + * Ignore seeks if an ad is playing + ([#3309](https://github.com/google/ExoPlayer/issues/3309)). + * Improve robustness of `ImaAdsLoader` in case content is not paused between + content to ad transitions + ([#3430](https://github.com/google/ExoPlayer/issues/3430)). +* UI: + * Allow specifying a `Drawable` for the `TimeBar` scrubber + ([#3337](https://github.com/google/ExoPlayer/issues/3337)). + * Allow multiple listeners on `TimeBar` + ([#3406](https://github.com/google/ExoPlayer/issues/3406)). +* New Leanback extension: Simplifies binding Exoplayer to Leanback UI + components. +* Unit tests moved to Robolectric. +* Misc bugfixes. + +### r2.5.4 (2017-10-19) ### + +* Remove unnecessary media playlist fetches during playback of live HLS streams. +* Add the ability to inject a HLS playlist parser through `HlsMediaSource`. +* Fix potential `IndexOutOfBoundsException` when using `ImaMediaSource` + ([#3334](https://github.com/google/ExoPlayer/issues/3334)). +* Fix an issue parsing MP4 content containing non-CENC sinf boxes. +* Fix memory leak when seeking with repeated periods. +* Fix playback position when `ExoPlayer.prepare` is called with `resetPosition` + set to false. +* Ignore MP4 edit lists that seem invalid + ([#3351](https://github.com/google/ExoPlayer/issues/3351)). +* Add extractor flag for ignoring all MP4 edit lists + ([#3358](https://github.com/google/ExoPlayer/issues/3358)). +* Improve extensibility by exposing public constructors for + `FrameworkMediaCrypto` and by making `DefaultDashChunkSource.getNextChunk` + non-final. + +### r2.5.3 (2017-09-20) ### + +* IMA extension: Support skipping of skippable ads on AndroidTV and other + non-touch devices ([#3258](https://github.com/google/ExoPlayer/issues/3258)). +* HLS: Fix broken WebVTT captions when PTS wraps around + ([#2928](https://github.com/google/ExoPlayer/issues/2928)). +* Captions: Fix issues rendering CEA-608 captions + ([#3250](https://github.com/google/ExoPlayer/issues/3250)). +* Workaround broken AAC decoders on Galaxy S6 + ([#3249](https://github.com/google/ExoPlayer/issues/3249)). +* Caching: Fix infinite loop when cache eviction fails + ([#3260](https://github.com/google/ExoPlayer/issues/3260)). +* Caching: Force use of BouncyCastle on JellyBean to fix decryption issue + ([#2755](https://github.com/google/ExoPlayer/issues/2755)). + +### r2.5.2 (2017-09-11) ### + +* IMA extension: Fix issue where ad playback could end prematurely for some + content types ([#3180](https://github.com/google/ExoPlayer/issues/3180)). +* RTMP extension: Fix SIGABRT on fast RTMP stream restart + ([#3156](https://github.com/google/ExoPlayer/issues/3156)). +* UI: Allow app to manually specify ad markers + ([#3184](https://github.com/google/ExoPlayer/issues/3184)). +* DASH: Expose segment indices to subclasses of DefaultDashChunkSource + ([#3037](https://github.com/google/ExoPlayer/issues/3037)). +* Captions: Added robustness against malformed WebVTT captions + ([#3228](https://github.com/google/ExoPlayer/issues/3228)). +* DRM: Support forcing a specific license URL. +* Fix playback error when seeking in media loaded through content:// URIs + ([#3216](https://github.com/google/ExoPlayer/issues/3216)). +* Fix issue playing MP4s in which the last atom specifies a size of zero + ([#3191](https://github.com/google/ExoPlayer/issues/3191)). +* Workaround playback failures on some Xiaomi devices + ([#3171](https://github.com/google/ExoPlayer/issues/3171)). +* Workaround SIGSEGV issue on some devices when setting and swapping surface for + secure playbacks ([#3215](https://github.com/google/ExoPlayer/issues/3215)). +* Workaround for Nexus 7 issue when swapping output surface + ([#3236](https://github.com/google/ExoPlayer/issues/3236)). +* Workaround for SimpleExoPlayerView's surface not being hidden properly + ([#3160](https://github.com/google/ExoPlayer/issues/3160)). + +### r2.5.1 (2017-08-08) ### + +* Fix an issue that could cause the reported playback position to stop advancing + in some cases. +* Fix an issue where a Surface could be released whilst still in use by the + player. + +### r2.5.0 (2017-08-07) ### + +* IMA extension: Wraps the Google Interactive Media Ads (IMA) SDK to provide an + easy and seamless way of incorporating display ads into ExoPlayer playbacks. + You can read more about the IMA extension + [here](https://medium.com/google-exoplayer/playing-ads-with-exoplayer-and-ima-868dfd767ea). +* MediaSession extension: Provides an easy way to connect ExoPlayer with + MediaSessionCompat in the Android Support Library. +* RTMP extension: An extension for playing streams over RTMP. +* Build: Made it easier for application developers to depend on a local checkout + of ExoPlayer. You can learn how to do this + [here](https://medium.com/google-exoplayer/howto-2-depend-on-a-local-checkout-of-exoplayer-bcd7f8531720). +* Core playback improvements: + * Eliminated re-buffering when changing audio and text track selections during + playback of progressive streams + ([#2926](https://github.com/google/ExoPlayer/issues/2926)). + * New DynamicConcatenatingMediaSource class to support playback of dynamic + playlists. + * New ExoPlayer.setRepeatMode method for dynamic toggling of repeat mode + during playback. Use of setRepeatMode should be preferred to + LoopingMediaSource for most looping use cases. You can read more about + setRepeatMode + [here](https://medium.com/google-exoplayer/repeat-modes-in-exoplayer-19dd85f036d3). + * Eliminated jank when switching video playback from one Surface to another on + API level 23+ for unencrypted content, and on devices that support the + EGL_EXT_protected_content OpenGL extension for protected content + ([#677](https://github.com/google/ExoPlayer/issues/677)). + * Enabled ExoPlayer instantiation on background threads without Loopers. + Events from such players are delivered on the application's main thread. +* HLS improvements: + * Optimized adaptive switches for playlists that specify the + EXT-X-INDEPENDENT-SEGMENTS tag. + * Optimized in-buffer seeking + ([#551](https://github.com/google/ExoPlayer/issues/551)). + * Eliminated re-buffering when changing audio and text track selections during + playback, provided the new selection does not require switching to different + renditions ([#2718](https://github.com/google/ExoPlayer/issues/2718)). + * Exposed all media playlist tags in ExoPlayer's MediaPlaylist object. +* DASH: Support for seamless switching across streams in different AdaptationSet + elements ([#2431](https://github.com/google/ExoPlayer/issues/2431)). +* DRM: Support for additional crypto schemes (cbc1, cbcs and cens) on + API level 24+ ([#1989](https://github.com/google/ExoPlayer/issues/1989)). +* Captions: Initial support for SSA/ASS subtitles + ([#889](https://github.com/google/ExoPlayer/issues/889)). +* AndroidTV: Fixed issue where tunneled video playback would not start on some + devices ([#2985](https://github.com/google/ExoPlayer/issues/2985)). +* MPEG-TS: Fixed segmentation issue when parsing H262 + ([#2891](https://github.com/google/ExoPlayer/issues/2891)). +* Cronet extension: Support for a user-defined fallback if Cronet library is not + present. +* Fix buffer too small IllegalStateException issue affecting some composite + media playbacks ([#2900](https://github.com/google/ExoPlayer/issues/2900)). +* Misc bugfixes. + +### r2.4.4 (2017-07-19) ### * HLS/MPEG-TS: Some initial optimizations of MPEG-TS extractor performance ([#3040](https://github.com/google/ExoPlayer/issues/3040)). @@ -11,7 +1580,7 @@ * Video: Fix video dimension reporting on some devices ([#3007](https://github.com/google/ExoPlayer/issues/3007)). -### r2.4.3 ### +### r2.4.3 (2017-06-30) ### * Audio: Workaround custom audio decoders misreporting their maximum supported channel counts ([#2940](https://github.com/google/ExoPlayer/issues/2940)). @@ -29,7 +1598,7 @@ ([#2977](https://github.com/google/ExoPlayer/pull/2977)). * Misc bugfixes. -### r2.4.2 ### +### r2.4.2 (2017-06-06) ### * Stability: Work around Nexus 10 reboot when playing certain content ([#2806](https://github.com/google/ExoPlayer/issues/2806)). @@ -43,7 +1612,7 @@ ([#2871](https://github.com/google/ExoPlayer/issues/2871)). * Misc bugfixes. -### r2.4.1 ### +### r2.4.1 (2017-05-23) ### * Stability: Avoid OutOfMemoryError in extractors when parsing malformed media ([#2780](https://github.com/google/ExoPlayer/issues/2780)). @@ -66,7 +1635,7 @@ ([#2824](https://github.com/google/ExoPlayer/issues/2824)). * Misc bugfixes. -### r2.4.0 ### +### r2.4.0 (2017-04-25) ### * New modular library structure. You can read more about depending on individual library modules @@ -99,7 +1668,7 @@ * FLAC extension: Enabled 64 bit targets. * Misc bugfixes. -### r2.3.1 ### +### r2.3.1 (2017-03-23) ### * Fix NPE enabling WebVTT subtitles in DASH streams ([#2596](https://github.com/google/ExoPlayer/issues/2596)). @@ -108,7 +1677,7 @@ * Minor fix for CEA-708 decoder ([#2595](https://github.com/google/ExoPlayer/issues/2595)). -### r2.3.0 ### +### r2.3.0 (2017-03-16) ### * GVR extension: Wraps the Google VR Audio SDK to provide spatial audio rendering. You can read more about the GVR extension @@ -155,7 +1724,7 @@ ([#2427](https://github.com/google/ExoPlayer/issues/2427)). * Misc bugfixes. -### r2.2.0 ### +### r2.2.0 (2017-01-30) ### * Demo app: Automatic recovery from BehindLiveWindowException, plus improved handling of pausing and resuming live streams @@ -218,7 +1787,7 @@ [#2264](https://github.com/google/ExoPlayer/issues/2264) and [#2290](https://github.com/google/ExoPlayer/issues/2290). -### r2.1.1 ### +### r2.1.1 (2016-12-20) ### * Fix some subtitle types (e.g. WebVTT) being displayed out of sync ([#2208](https://github.com/google/ExoPlayer/issues/2208)). @@ -228,7 +1797,7 @@ * Fix issue where playbacks could get stuck in the initial buffering state if over 1MB of data needs to be read to initialize the playback. -### r2.1.0 ### +### r2.1.0 (2016-12-14) ### * HLS: Support for seeking in live streams ([#87](https://github.com/google/ExoPlayer/issues/87)). @@ -249,7 +1818,7 @@ [here](https://medium.com/google-exoplayer/customizing-exoplayers-ui-components-728cf55ee07a#.9ewjg7avi). * Robustness improvements when handling MediaSource timeline changes and MediaPeriod transitions. -* EIA608: Support for caption styling and positioning. +* CEA-608: Support for caption styling and positioning. * MPEG-TS: Improved support: * Support injection of custom TS payload readers. * Support injection of custom section payload readers. @@ -282,12 +1851,12 @@ ([#2145](https://github.com/google/ExoPlayer/issues/2145)). * Misc bugfixes. -### r2.0.4 ### +### r2.0.4 (2016-10-20) ### * Fix crash on Jellybean devices when using playback controls ([#1965](https://github.com/google/ExoPlayer/issues/1965)). -### r2.0.3 ### +### r2.0.3 (2016-10-17) ### * Fixed NullPointerException in ExtractorMediaSource ([#1914](https://github.com/google/ExoPlayer/issues/1914)). @@ -304,7 +1873,7 @@ * Improvements to Cronet network stack extension. * Misc bug fixes. -### r2.0.2 ### +### r2.0.2 (2016-10-06) ### * Fixes for MergingMediaSource and sideloaded subtitles. ([#1882](https://github.com/google/ExoPlayer/issues/1882), @@ -315,7 +1884,7 @@ * Initial support for fragmented MP4 in HLS. * Misc bug fixes and minor features. -### r2.0.1 ### +### r2.0.1 (2016-09-30) ### * Fix playback of short duration content ([#1837](https://github.com/google/ExoPlayer/issues/1837)). @@ -324,7 +1893,7 @@ * Fix live stream buffering (out of memory) issue ([#1825](https://github.com/google/ExoPlayer/issues/1825)). -### r2.0.0 ### +### r2.0.0 (2016-09-14) ### ExoPlayer 2.x is a major iteration of the library. It includes significant API and architectural changes, new features and many bug fixes. You can read about @@ -493,8 +2062,8 @@ V2 release. (#801). * MP3: Fix playback of some streams when stream length is unknown. * ID3: Support multiple frames of the same type in a single tag. -* EIA608: Correctly handle repeated control characters, fixing an issue in which - captions would immediately disappear. +* CEA-608: Correctly handle repeated control characters, fixing an issue in + which captions would immediately disappear. * AVC3: Fix decoder failures on some MediaTek devices in the case where the first buffer fed to the decoder does not start with SPS/PPS NAL units. * Misc bug fixes. diff --git a/build.gradle b/build.gradle index a4ae1f175e..a4823b94ee 100644 --- a/build.gradle +++ b/build.gradle @@ -13,43 +13,22 @@ // limitations under the License. buildscript { repositories { + google() jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:2.3.1' - classpath 'com.novoda:bintray-release:0.4.0' - } - // Workaround for the following test coverage issue. Remove when fixed: - // https://code.google.com/p/android/issues/detail?id=226070 - configurations.all { - resolutionStrategy { - force 'org.jacoco:org.jacoco.report:0.7.4.201502262128' - force 'org.jacoco:org.jacoco.core:0.7.4.201502262128' - } + classpath 'com.android.tools.build:gradle:3.5.1' + classpath 'com.novoda:bintray-release:0.9.1' + classpath 'com.google.android.gms:strict-version-matcher-plugin:1.2.0' } } allprojects { repositories { + google() jcenter() } project.ext { - // Important: ExoPlayer specifies a minSdkVersion of 9 because various - // components provided by the library may be of use on older devices. - // However, please note that the core media playback functionality - // provided by the library requires API level 16 or greater. - minSdkVersion = 9 - compileSdkVersion = 25 - targetSdkVersion = 25 - buildToolsVersion = '25' - testSupportLibraryVersion = '0.5' - supportLibraryVersion = '25.3.1' - dexmakerVersion = '1.2' - mockitoVersion = '1.9.5' - releaseRepoName = getBintrayRepo() - releaseUserOrg = 'google' - releaseGroupId = 'com.google.android.exoplayer' - releaseVersion = 'r2.4.4' - releaseWebsite = 'https://github.com/google/ExoPlayer' + exoplayerPublishEnabled = false } if (it.hasProperty('externalBuildDir')) { if (!new File(externalBuildDir).isAbsolute()) { @@ -57,12 +36,7 @@ allprojects { } buildDir = "${externalBuildDir}/${project.name}" } -} - -def getBintrayRepo() { - boolean publicRepo = hasProperty('publicRepo') && - property('publicRepo').toBoolean() - return publicRepo ? 'exoplayer' : 'exoplayer-test' + group = 'com.google.android.exoplayer' } apply from: 'javadoc_combined.gradle' diff --git a/constants.gradle b/constants.gradle new file mode 100644 index 0000000000..f81a6b4210 --- /dev/null +++ b/constants.gradle @@ -0,0 +1,43 @@ +// Copyright (C) 2017 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +project.ext { + // ExoPlayer version and version code. + releaseVersion = '2.11.2' + releaseVersionCode = 2011002 + minSdkVersion = 16 + appTargetSdkVersion = 29 + targetSdkVersion = 28 // TODO: Bump once b/143232359 is resolved + compileSdkVersion = 29 + dexmakerVersion = '2.21.0' + junitVersion = '4.13-rc-2' + guavaVersion = '23.5-android' + mockitoVersion = '2.25.0' + robolectricVersion = '4.3.1' + checkerframeworkVersion = '2.5.0' + jsr305Version = '3.0.2' + kotlinAnnotationsVersion = '1.3.31' + androidxAnnotationVersion = '1.1.0' + androidxAppCompatVersion = '1.1.0' + androidxCollectionVersion = '1.1.0' + androidxMediaVersion = '1.0.1' + androidxTestCoreVersion = '1.2.0' + androidxTestJUnitVersion = '1.1.1' + androidxTestRunnerVersion = '1.2.0' + androidxTestRulesVersion = '1.2.0' + truthVersion = '1.0' + modulePrefix = ':' + if (gradle.ext.has('exoplayerModulePrefix')) { + modulePrefix += gradle.ext.exoplayerModulePrefix + } +} diff --git a/core_settings.gradle b/core_settings.gradle new file mode 100644 index 0000000000..2c6ddbda78 --- /dev/null +++ b/core_settings.gradle @@ -0,0 +1,68 @@ +// Copyright (C) 2017 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +def rootDir = gradle.ext.exoplayerRoot +def modulePrefix = ':' +if (gradle.ext.has('exoplayerModulePrefix')) { + modulePrefix += gradle.ext.exoplayerModulePrefix +} + +include modulePrefix + 'library' +include modulePrefix + 'library-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 + 'extension-av1' +include modulePrefix + 'extension-ffmpeg' +include modulePrefix + 'extension-flac' +include modulePrefix + 'extension-gvr' +include modulePrefix + 'extension-ima' +include modulePrefix + 'extension-cast' +include modulePrefix + 'extension-cronet' +include modulePrefix + 'extension-mediasession' +include modulePrefix + 'extension-okhttp' +include modulePrefix + 'extension-opus' +include modulePrefix + 'extension-vp9' +include modulePrefix + 'extension-rtmp' +include modulePrefix + 'extension-leanback' +include modulePrefix + 'extension-jobdispatcher' +include modulePrefix + 'extension-workmanager' + +project(modulePrefix + 'library').projectDir = new File(rootDir, 'library/all') +project(modulePrefix + 'library-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 + 'extension-av1').projectDir = new File(rootDir, 'extensions/av1') +project(modulePrefix + 'extension-ffmpeg').projectDir = new File(rootDir, 'extensions/ffmpeg') +project(modulePrefix + 'extension-flac').projectDir = new File(rootDir, 'extensions/flac') +project(modulePrefix + 'extension-gvr').projectDir = new File(rootDir, 'extensions/gvr') +project(modulePrefix + 'extension-ima').projectDir = new File(rootDir, 'extensions/ima') +project(modulePrefix + 'extension-cast').projectDir = new File(rootDir, 'extensions/cast') +project(modulePrefix + 'extension-cronet').projectDir = new File(rootDir, 'extensions/cronet') +project(modulePrefix + 'extension-mediasession').projectDir = new File(rootDir, 'extensions/mediasession') +project(modulePrefix + 'extension-okhttp').projectDir = new File(rootDir, 'extensions/okhttp') +project(modulePrefix + 'extension-opus').projectDir = new File(rootDir, 'extensions/opus') +project(modulePrefix + 'extension-vp9').projectDir = new File(rootDir, 'extensions/vp9') +project(modulePrefix + 'extension-rtmp').projectDir = new File(rootDir, 'extensions/rtmp') +project(modulePrefix + 'extension-leanback').projectDir = new File(rootDir, 'extensions/leanback') +project(modulePrefix + 'extension-jobdispatcher').projectDir = new File(rootDir, 'extensions/jobdispatcher') +project(modulePrefix + 'extension-workmanager').projectDir = new File(rootDir, 'extensions/workmanager') diff --git a/demo/src/main/assets/media.exolist.json b/demo/src/main/assets/media.exolist.json deleted file mode 100644 index 814c89a45b..0000000000 --- a/demo/src/main/assets/media.exolist.json +++ /dev/null @@ -1,456 +0,0 @@ -[ - { - "name": "YouTube DASH", - "samples": [ - { - "name": "Google Glass (MP4,H264)", - "uri": "http://www.youtube.com/api/manifest/dash/id/bf5bb2419360daf1/source/youtube?as=fmp4_audio_clear,fmp4_sd_hd_clear&sparams=ip,ipbits,expire,source,id,as&ip=0.0.0.0&ipbits=0&expire=19000000000&signature=51AF5F39AB0CEC3E5497CD9C900EBFEAECCCB5C7.8506521BFC350652163895D4C26DEE124209AA9E&key=ik0", - "extension": "mpd" - }, - { - "name": "Google Play (MP4,H264)", - "uri": "http://www.youtube.com/api/manifest/dash/id/3aa39fa2cc27967f/source/youtube?as=fmp4_audio_clear,fmp4_sd_hd_clear&sparams=ip,ipbits,expire,source,id,as&ip=0.0.0.0&ipbits=0&expire=19000000000&signature=A2716F75795F5D2AF0E88962FFCD10DB79384F29.84308FF04844498CE6FBCE4731507882B8307798&key=ik0", - "extension": "mpd" - }, - { - "name": "Google Glass (WebM,VP9)", - "uri": "http://www.youtube.com/api/manifest/dash/id/bf5bb2419360daf1/source/youtube?as=fmp4_audio_clear,webm2_sd_hd_clear&sparams=ip,ipbits,expire,source,id,as&ip=0.0.0.0&ipbits=0&expire=19000000000&signature=249B04F79E984D7F86B4D8DB48AE6FAF41C17AB3.7B9F0EC0505E1566E59B8E488E9419F253DDF413&key=ik0", - "extension": "mpd" - }, - { - "name": "Google Play (WebM,VP9)", - "uri": "http://www.youtube.com/api/manifest/dash/id/3aa39fa2cc27967f/source/youtube?as=fmp4_audio_clear,webm2_sd_hd_clear&sparams=ip,ipbits,expire,source,id,as&ip=0.0.0.0&ipbits=0&expire=19000000000&signature=B1C2A74783AC1CC4865EB312D7DD2D48230CC9FD.BD153B9882175F1F94BFE5141A5482313EA38E8D&key=ik0", - "extension": "mpd" - } - ] - }, - { - "name": "Widevine DASH Policy Tests (GTS)", - "samples": [ - { - "name": "WV: HDCP not specified", - "uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd", - "drm_scheme": "widevine", - "drm_license_url": "https://proxy.uat.widevine.com/proxy?video_id=d286538032258a1c&provider=widevine_test" - }, - { - "name": "WV: HDCP not required", - "uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd", - "drm_scheme": "widevine", - "drm_license_url": "https://proxy.uat.widevine.com/proxy?video_id=48fcc369939ac96c&provider=widevine_test" - }, - { - "name": "WV: HDCP required", - "uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd", - "drm_scheme": "widevine", - "drm_license_url": "https://proxy.uat.widevine.com/proxy?video_id=e06c39f1151da3df&provider=widevine_test" - }, - { - "name": "WV: Secure video path required (MP4,H264)", - "uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd", - "drm_scheme": "widevine", - "drm_license_url": "https://proxy.uat.widevine.com/proxy?video_id=0894c7c8719b28a0&provider=widevine_test" - }, - { - "name": "WV: Secure video path required (WebM,VP9)", - "uri": "https://storage.googleapis.com/wvmedia/cenc/vp9/tears/tears.mpd", - "drm_scheme": "widevine", - "drm_license_url": "https://proxy.uat.widevine.com/proxy?video_id=0894c7c8719b28a0&provider=widevine_test" - }, - { - "name": "WV: Secure video path required (MP4,H265)", - "uri": "https://storage.googleapis.com/wvmedia/cenc/hevc/tears/tears.mpd", - "drm_scheme": "widevine", - "drm_license_url": "https://proxy.uat.widevine.com/proxy?video_id=0894c7c8719b28a0&provider=widevine_test" - }, - { - "name": "WV: HDCP + secure video path required", - "uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd", - "drm_scheme": "widevine", - "drm_license_url": "https://proxy.uat.widevine.com/proxy?video_id=efd045b1eb61888a&provider=widevine_test" - }, - { - "name": "WV: 30s license duration (fails at ~30s)", - "uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd", - "drm_scheme": "widevine", - "drm_license_url": "https://proxy.uat.widevine.com/proxy?video_id=f9a34cab7b05881a&provider=widevine_test" - } - ] - }, - { - "name": "Widevine HDCP Capabilities Tests", - "samples": [ - { - "name": "WV: HDCP: None (not required)", - "uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd", - "drm_scheme": "widevine", - "drm_license_url": "https://proxy.uat.widevine.com/proxy?video_id=HDCP_None&provider=widevine_test" - }, - { - "name": "WV: HDCP: 1.0 required", - "uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd", - "drm_scheme": "widevine", - "drm_license_url": "https://proxy.uat.widevine.com/proxy?video_id=HDCP_V1&provider=widevine_test" - }, - { - "name": "WV: HDCP: 2.0 required", - "uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd", - "drm_scheme": "widevine", - "drm_license_url": "https://proxy.uat.widevine.com/proxy?video_id=HDCP_V2&provider=widevine_test" - }, - { - "name": "WV: HDCP: 2.1 required", - "uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd", - "drm_scheme": "widevine", - "drm_license_url": "https://proxy.uat.widevine.com/proxy?video_id=HDCP_V2_1&provider=widevine_test" - }, - { - "name": "WV: HDCP: 2.2 required", - "uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd", - "drm_scheme": "widevine", - "drm_license_url": "https://proxy.uat.widevine.com/proxy?video_id=HDCP_V2_2&provider=widevine_test" - }, - { - "name": "WV: HDCP: No digital output", - "uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd", - "drm_scheme": "widevine", - "drm_license_url": "https://proxy.uat.widevine.com/proxy?video_id=HDCP_NO_DIGTAL_OUTPUT&provider=widevine_test" - } - ] - }, - { - "name": "Widevine DASH: MP4,H264", - "samples": [ - { - "name": "WV: Clear SD & HD (MP4,H264)", - "uri": "https://storage.googleapis.com/wvmedia/clear/h264/tears/tears.mpd" - }, - { - "name": "WV: Clear SD (MP4,H264)", - "uri": "https://storage.googleapis.com/wvmedia/clear/h264/tears/tears_sd.mpd" - }, - { - "name": "WV: Clear HD (MP4,H264)", - "uri": "https://storage.googleapis.com/wvmedia/clear/h264/tears/tears_hd.mpd" - }, - { - "name": "WV: Clear UHD (MP4,H264)", - "uri": "https://storage.googleapis.com/wvmedia/clear/h264/tears/tears_uhd.mpd" - }, - { - "name": "WV: Secure SD & HD (MP4,H264)", - "uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd", - "drm_scheme": "widevine", - "drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test" - }, - { - "name": "WV: Secure SD (MP4,H264)", - "uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears_sd.mpd", - "drm_scheme": "widevine", - "drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test" - }, - { - "name": "WV: Secure HD (MP4,H264)", - "uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears_hd.mpd", - "drm_scheme": "widevine", - "drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test" - }, - { - "name": "WV: Secure UHD (MP4,H264)", - "uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears_uhd.mpd", - "drm_scheme": "widevine", - "drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test" - } - ] - }, - { - "name": "Widevine DASH: WebM,VP9", - "samples": [ - { - "name": "WV: Clear SD & HD (WebM,VP9)", - "uri": "https://storage.googleapis.com/wvmedia/clear/vp9/tears/tears.mpd" - }, - { - "name": "WV: Clear SD (WebM,VP9)", - "uri": "https://storage.googleapis.com/wvmedia/clear/vp9/tears/tears_sd.mpd" - }, - { - "name": "WV: Clear HD (WebM,VP9)", - "uri": "https://storage.googleapis.com/wvmedia/clear/vp9/tears/tears_hd.mpd" - }, - { - "name": "WV: Clear UHD (WebM,VP9)", - "uri": "https://storage.googleapis.com/wvmedia/clear/vp9/tears/tears_uhd.mpd" - }, - { - "name": "WV: Secure Fullsample SD & HD (WebM,VP9)", - "uri": "https://storage.googleapis.com/wvmedia/cenc/vp9/tears/tears.mpd", - "drm_scheme": "widevine", - "drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test" - }, - { - "name": "WV: Secure Fullsample SD (WebM,VP9)", - "uri": "https://storage.googleapis.com/wvmedia/cenc/vp9/tears/tears_sd.mpd", - "drm_scheme": "widevine", - "drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test" - }, - { - "name": "WV: Secure Fullsample HD (WebM,VP9)", - "uri": "https://storage.googleapis.com/wvmedia/cenc/vp9/tears/tears_hd.mpd", - "drm_scheme": "widevine", - "drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test" - }, - { - "name": "WV: Secure Fullsample UHD (WebM,VP9)", - "uri": "https://storage.googleapis.com/wvmedia/cenc/vp9/tears/tears_uhd.mpd", - "drm_scheme": "widevine", - "drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test" - }, - { - "name": "WV: Secure Subsample SD & HD (WebM,VP9)", - "uri": "https://storage.googleapis.com/wvmedia/cenc/vp9/subsample/24fps/tears/tears.mpd", - "drm_scheme": "widevine", - "drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test" - }, - { - "name": "WV: Secure Subsample SD (WebM,VP9)", - "uri": "https://storage.googleapis.com/wvmedia/cenc/vp9/subsample/24fps/tears/tears_sd.mpd", - "drm_scheme": "widevine", - "drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test" - }, - { - "name": "WV: Secure Subsample HD (WebM,VP9)", - "uri": "https://storage.googleapis.com/wvmedia/cenc/vp9/subsample/24fps/tears/tears_hd.mpd", - "drm_scheme": "widevine", - "drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test" - }, - { - "name": "WV: Secure Subsample UHD (WebM,VP9)", - "uri": "https://storage.googleapis.com/wvmedia/cenc/vp9/subsample/24fps/tears/tears_uhd.mpd", - "drm_scheme": "widevine", - "drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test" - } - ] - }, - { - "name": "Widevine DASH: MP4,H265", - "samples": [ - { - "name": "WV: Clear SD & HD (MP4,H265)", - "uri": "https://storage.googleapis.com/wvmedia/clear/hevc/tears/tears.mpd" - }, - { - "name": "WV: Clear SD (MP4,H265)", - "uri": "https://storage.googleapis.com/wvmedia/clear/hevc/tears/tears_sd.mpd" - }, - { - "name": "WV: Clear HD (MP4,H265)", - "uri": "https://storage.googleapis.com/wvmedia/clear/hevc/tears/tears_hd.mpd" - }, - { - "name": "WV: Clear UHD (MP4,H265)", - "uri": "https://storage.googleapis.com/wvmedia/clear/hevc/tears/tears_uhd.mpd" - }, - { - "name": "WV: Secure SD & HD (MP4,H265)", - "uri": "https://storage.googleapis.com/wvmedia/cenc/hevc/tears/tears.mpd", - "drm_scheme": "widevine", - "drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test" - }, - { - "name": "WV: Secure SD (MP4,H265)", - "uri": "https://storage.googleapis.com/wvmedia/cenc/hevc/tears/tears_sd.mpd", - "drm_scheme": "widevine", - "drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test" - }, - { - "name": "WV: Secure HD (MP4,H265)", - "uri": "https://storage.googleapis.com/wvmedia/cenc/hevc/tears/tears_hd.mpd", - "drm_scheme": "widevine", - "drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test" - }, - { - "name": "WV: Secure UHD (MP4,H265)", - "uri": "https://storage.googleapis.com/wvmedia/cenc/hevc/tears/tears_uhd.mpd", - "drm_scheme": "widevine", - "drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test" - } - ] - }, - { - "name": "ClearKey DASH", - "samples": [ - { - "name": "Big Buck Bunny (CENC ClearKey)", - "uri": "http://html5.cablelabs.com:8100/cenc/ck/dash.mpd", - "extension": "mpd", - "drm_scheme": "cenc", - "drm_license_url": "https://wasabeef.jp/demos/cenc-ck-dash.json" - } - ] - }, - { - "name": "SmoothStreaming", - "samples": [ - { - "name": "Super speed", - "uri": "http://playready.directtaps.net/smoothstreaming/SSWSS720H264/SuperSpeedway_720.ism" - }, - { - "name": "Super speed (PlayReady)", - "uri": "http://playready.directtaps.net/smoothstreaming/SSWSS720H264PR/SuperSpeedway_720.ism", - "drm_scheme": "playready" - } - ] - }, - { - "name": "HLS", - "samples": [ - { - "name": "Apple 4x3 basic stream", - "uri": "https://devimages.apple.com.edgekey.net/streaming/examples/bipbop_4x3/bipbop_4x3_variant.m3u8" - }, - { - "name": "Apple 16x9 basic stream", - "uri": "https://devimages.apple.com.edgekey.net/streaming/examples/bipbop_16x9/bipbop_16x9_variant.m3u8" - }, - { - "name": "Apple master playlist advanced (TS)", - "uri": "https://tungsten.aaplimg.com/VOD/bipbop_adv_example_v2/master.m3u8" - }, - { - "name": "Apple master playlist advanced (fMP4)", - "uri": "https://tungsten.aaplimg.com/VOD/bipbop_adv_fmp4_example/master.m3u8" - }, - { - "name": "Apple TS media playlist", - "uri": "https://devimages.apple.com.edgekey.net/streaming/examples/bipbop_4x3/gear1/prog_index.m3u8" - }, - { - "name": "Apple AAC media playlist", - "uri": "https://devimages.apple.com.edgekey.net/streaming/examples/bipbop_4x3/gear0/prog_index.m3u8" - }, - { - "name": "Apple ID3 metadata", - "uri": "http://devimages.apple.com/samplecode/adDemo/ad.m3u8" - } - ] - }, - { - "name": "Misc", - "samples": [ - { - "name": "Dizzy", - "uri": "http://html5demos.com/assets/dizzy.mp4" - }, - { - "name": "Apple AAC 10s", - "uri": "https://devimages.apple.com.edgekey.net/streaming/examples/bipbop_4x3/gear0/fileSequence0.aac" - }, - { - "name": "Apple TS 10s", - "uri": "https://devimages.apple.com.edgekey.net/streaming/examples/bipbop_4x3/gear1/fileSequence0.ts" - }, - { - "name": "Android screens (Matroska)", - "uri": "http://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv" - }, - { - "name": "Big Buck Bunny (MP4 Video)", - "uri": "http://redirector.c.youtube.com/videoplayback?id=604ed5ce52eda7ee&itag=22&source=youtube&sparams=ip,ipbits,expire,source,id&ip=0.0.0.0&ipbits=0&expire=19000000000&signature=513F28C7FDCBEC60A66C86C9A393556C99DC47FB.04C88036EEE12565A1ED864A875A58F15D8B5300&key=ik0" - }, - { - "name": "Screens 360P (WebM,VP9,No Audio)", - "uri": "https://storage.googleapis.com/exoplayer-test-media-1/gen-3/screens/dash-vod-single-segment/video-vp9-360.webm" - }, - { - "name": "Screens 480p (FMP4,H264,No Audio)", - "uri": "https://storage.googleapis.com/exoplayer-test-media-1/gen-3/screens/dash-vod-single-segment/video-avc-baseline-480.mp4" - }, - { - "name": "Screens 1080p (FMP4,H264, No Audio)", - "uri": "https://storage.googleapis.com/exoplayer-test-media-1/gen-3/screens/dash-vod-single-segment/video-137.mp4" - }, - { - "name": "Screens (FMP4,AAC Audio)", - "uri": "https://storage.googleapis.com/exoplayer-test-media-1/gen-3/screens/dash-vod-single-segment/audio-141.mp4" - }, - { - "name": "Google Play (MP3 Audio)", - "uri": "http://storage.googleapis.com/exoplayer-test-media-0/play.mp3" - }, - { - "name": "Google Play (Ogg/Vorbis Audio)", - "uri": "https://storage.googleapis.com/exoplayer-test-media-1/ogg/play.ogg" - }, - { - "name": "Google Glass (WebM Video with Vorbis Audio)", - "uri": "http://demos.webmproject.org/exoplayer/glass_vp9_vorbis.webm" - }, - { - "name": "Google Glass (VP9 in MP4/ISO-BMFF)", - "uri": "http://demos.webmproject.org/exoplayer/glass.mp4" - }, - { - "name": "Google Glass DASH - VP9 and Opus", - "uri": "http://demos.webmproject.org/dash/201410/vp9_glass/manifest_vp9_opus.mpd" - }, - { - "name": "Big Buck Bunny (FLV Video)", - "uri": "http://vod.leasewebcdn.com/bbb.flv?ri=1024&rs=150&start=0" - } - ] - }, - { - "name": "Playlists", - "samples": [ - { - "name": "Cats -> Dogs", - "playlist": [ - { - "uri": "http://html5demos.com/assets/dizzy.mp4" - }, - { - "uri": "http://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv" - } - ] - }, - { - "name": "Audio -> Video -> Audio", - "playlist": [ - { - "uri": "https://storage.googleapis.com/exoplayer-test-media-1/gen-3/screens/dash-vod-single-segment/audio-141.mp4" - }, - { - "uri": "http://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv" - }, - { - "uri": "https://storage.googleapis.com/exoplayer-test-media-1/gen-3/screens/dash-vod-single-segment/audio-141.mp4" - } - ] - }, - { - "name": "Clear -> Enc -> Clear -> Enc -> Enc", - "drm_scheme": "widevine", - "drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test", - "playlist": [ - { - "uri": "http://html5demos.com/assets/dizzy.mp4" - }, - { - "uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears_sd.mpd" - }, - { - "uri": "http://html5demos.com/assets/dizzy.mp4" - }, - { - "uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears_sd.mpd" - }, - { - "uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears_sd.mpd" - } - ] - } - ] - } -] diff --git a/demo/src/main/java/com/google/android/exoplayer2/demo/DemoApplication.java b/demo/src/main/java/com/google/android/exoplayer2/demo/DemoApplication.java deleted file mode 100644 index b5db4c018d..0000000000 --- a/demo/src/main/java/com/google/android/exoplayer2/demo/DemoApplication.java +++ /dev/null @@ -1,52 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.google.android.exoplayer2.demo; - -import android.app.Application; -import com.google.android.exoplayer2.upstream.DataSource; -import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter; -import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory; -import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory; -import com.google.android.exoplayer2.upstream.HttpDataSource; -import com.google.android.exoplayer2.util.Util; - -/** - * Placeholder application to facilitate overriding Application methods for debugging and testing. - */ -public class DemoApplication extends Application { - - protected String userAgent; - - @Override - public void onCreate() { - super.onCreate(); - userAgent = Util.getUserAgent(this, "ExoPlayerDemo"); - } - - public DataSource.Factory buildDataSourceFactory(DefaultBandwidthMeter bandwidthMeter) { - return new DefaultDataSourceFactory(this, bandwidthMeter, - buildHttpDataSourceFactory(bandwidthMeter)); - } - - public HttpDataSource.Factory buildHttpDataSourceFactory(DefaultBandwidthMeter bandwidthMeter) { - return new DefaultHttpDataSourceFactory(userAgent, bandwidthMeter); - } - - public boolean useExtensionRenderers() { - return BuildConfig.FLAVOR.equals("withExtensions"); - } - -} diff --git a/demo/src/main/java/com/google/android/exoplayer2/demo/DemoUtil.java b/demo/src/main/java/com/google/android/exoplayer2/demo/DemoUtil.java deleted file mode 100644 index f9e9c34158..0000000000 --- a/demo/src/main/java/com/google/android/exoplayer2/demo/DemoUtil.java +++ /dev/null @@ -1,86 +0,0 @@ -/* - * Copyright (C) 2017 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.google.android.exoplayer2.demo; - -import android.text.TextUtils; -import com.google.android.exoplayer2.Format; -import com.google.android.exoplayer2.util.MimeTypes; -import java.util.Locale; - -/** - * Utility methods for demo application. - */ -/*package*/ final class DemoUtil { - - /** - * Builds a track name for display. - * - * @param format {@link Format} of the track. - * @return a generated name specific to the track. - */ - public static String buildTrackName(Format format) { - String trackName; - if (MimeTypes.isVideo(format.sampleMimeType)) { - trackName = joinWithSeparator(joinWithSeparator(joinWithSeparator( - buildResolutionString(format), buildBitrateString(format)), buildTrackIdString(format)), - buildSampleMimeTypeString(format)); - } else if (MimeTypes.isAudio(format.sampleMimeType)) { - trackName = joinWithSeparator(joinWithSeparator(joinWithSeparator(joinWithSeparator( - buildLanguageString(format), buildAudioPropertyString(format)), - buildBitrateString(format)), buildTrackIdString(format)), - buildSampleMimeTypeString(format)); - } else { - trackName = joinWithSeparator(joinWithSeparator(joinWithSeparator(buildLanguageString(format), - buildBitrateString(format)), buildTrackIdString(format)), - buildSampleMimeTypeString(format)); - } - return trackName.length() == 0 ? "unknown" : trackName; - } - - private static String buildResolutionString(Format format) { - return format.width == Format.NO_VALUE || format.height == Format.NO_VALUE - ? "" : format.width + "x" + format.height; - } - - private static String buildAudioPropertyString(Format format) { - return format.channelCount == Format.NO_VALUE || format.sampleRate == Format.NO_VALUE - ? "" : format.channelCount + "ch, " + format.sampleRate + "Hz"; - } - - private static String buildLanguageString(Format format) { - return TextUtils.isEmpty(format.language) || "und".equals(format.language) ? "" - : format.language; - } - - private static String buildBitrateString(Format format) { - return format.bitrate == Format.NO_VALUE ? "" - : String.format(Locale.US, "%.2fMbit", format.bitrate / 1000000f); - } - - private static String joinWithSeparator(String first, String second) { - return first.length() == 0 ? second : (second.length() == 0 ? first : first + ", " + second); - } - - private static String buildTrackIdString(Format format) { - return format.id == null ? "" : ("id:" + format.id); - } - - private static String buildSampleMimeTypeString(Format format) { - return format.sampleMimeType == null ? "" : format.sampleMimeType; - } - - private DemoUtil() {} -} diff --git a/demo/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java b/demo/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java deleted file mode 100644 index 953021fe6f..0000000000 --- a/demo/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java +++ /dev/null @@ -1,464 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.google.android.exoplayer2.demo; - -import android.os.SystemClock; -import android.util.Log; -import android.view.Surface; -import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.ExoPlaybackException; -import com.google.android.exoplayer2.ExoPlayer; -import com.google.android.exoplayer2.Format; -import com.google.android.exoplayer2.PlaybackParameters; -import com.google.android.exoplayer2.RendererCapabilities; -import com.google.android.exoplayer2.Timeline; -import com.google.android.exoplayer2.audio.AudioRendererEventListener; -import com.google.android.exoplayer2.decoder.DecoderCounters; -import com.google.android.exoplayer2.drm.DefaultDrmSessionManager; -import com.google.android.exoplayer2.metadata.Metadata; -import com.google.android.exoplayer2.metadata.MetadataRenderer; -import com.google.android.exoplayer2.metadata.emsg.EventMessage; -import com.google.android.exoplayer2.metadata.id3.ApicFrame; -import com.google.android.exoplayer2.metadata.id3.CommentFrame; -import com.google.android.exoplayer2.metadata.id3.GeobFrame; -import com.google.android.exoplayer2.metadata.id3.Id3Frame; -import com.google.android.exoplayer2.metadata.id3.PrivFrame; -import com.google.android.exoplayer2.metadata.id3.TextInformationFrame; -import com.google.android.exoplayer2.metadata.id3.UrlLinkFrame; -import com.google.android.exoplayer2.source.AdaptiveMediaSourceEventListener; -import com.google.android.exoplayer2.source.ExtractorMediaSource; -import com.google.android.exoplayer2.source.TrackGroup; -import com.google.android.exoplayer2.source.TrackGroupArray; -import com.google.android.exoplayer2.trackselection.MappingTrackSelector; -import com.google.android.exoplayer2.trackselection.MappingTrackSelector.MappedTrackInfo; -import com.google.android.exoplayer2.trackselection.TrackSelection; -import com.google.android.exoplayer2.trackselection.TrackSelectionArray; -import com.google.android.exoplayer2.upstream.DataSpec; -import com.google.android.exoplayer2.video.VideoRendererEventListener; -import java.io.IOException; -import java.text.NumberFormat; -import java.util.Locale; - -/** - * Logs player events using {@link Log}. - */ -/* package */ final class EventLogger implements ExoPlayer.EventListener, - AudioRendererEventListener, VideoRendererEventListener, AdaptiveMediaSourceEventListener, - ExtractorMediaSource.EventListener, DefaultDrmSessionManager.EventListener, - MetadataRenderer.Output { - - private static final String TAG = "EventLogger"; - private static final int MAX_TIMELINE_ITEM_LINES = 3; - private static final NumberFormat TIME_FORMAT; - static { - TIME_FORMAT = NumberFormat.getInstance(Locale.US); - TIME_FORMAT.setMinimumFractionDigits(2); - TIME_FORMAT.setMaximumFractionDigits(2); - TIME_FORMAT.setGroupingUsed(false); - } - - private final MappingTrackSelector trackSelector; - private final Timeline.Window window; - private final Timeline.Period period; - private final long startTimeMs; - - public EventLogger(MappingTrackSelector trackSelector) { - this.trackSelector = trackSelector; - window = new Timeline.Window(); - period = new Timeline.Period(); - startTimeMs = SystemClock.elapsedRealtime(); - } - - // ExoPlayer.EventListener - - @Override - public void onLoadingChanged(boolean isLoading) { - Log.d(TAG, "loading [" + isLoading + "]"); - } - - @Override - public void onPlayerStateChanged(boolean playWhenReady, int state) { - Log.d(TAG, "state [" + getSessionTimeString() + ", " + playWhenReady + ", " - + getStateString(state) + "]"); - } - - @Override - public void onPositionDiscontinuity() { - Log.d(TAG, "positionDiscontinuity"); - } - - @Override - public void onPlaybackParametersChanged(PlaybackParameters playbackParameters) { - Log.d(TAG, "playbackParameters " + String.format( - "[speed=%.2f, pitch=%.2f]", playbackParameters.speed, playbackParameters.pitch)); - } - - @Override - public void onTimelineChanged(Timeline timeline, Object manifest) { - int periodCount = timeline.getPeriodCount(); - int windowCount = timeline.getWindowCount(); - Log.d(TAG, "sourceInfo [periodCount=" + periodCount + ", windowCount=" + windowCount); - for (int i = 0; i < Math.min(periodCount, MAX_TIMELINE_ITEM_LINES); i++) { - timeline.getPeriod(i, period); - Log.d(TAG, " " + "period [" + getTimeString(period.getDurationMs()) + "]"); - } - if (periodCount > MAX_TIMELINE_ITEM_LINES) { - Log.d(TAG, " ..."); - } - for (int i = 0; i < Math.min(windowCount, MAX_TIMELINE_ITEM_LINES); i++) { - timeline.getWindow(i, window); - Log.d(TAG, " " + "window [" + getTimeString(window.getDurationMs()) + ", " - + window.isSeekable + ", " + window.isDynamic + "]"); - } - if (windowCount > MAX_TIMELINE_ITEM_LINES) { - Log.d(TAG, " ..."); - } - Log.d(TAG, "]"); - } - - @Override - public void onPlayerError(ExoPlaybackException e) { - Log.e(TAG, "playerFailed [" + getSessionTimeString() + "]", e); - } - - @Override - public void onTracksChanged(TrackGroupArray ignored, TrackSelectionArray trackSelections) { - MappedTrackInfo mappedTrackInfo = trackSelector.getCurrentMappedTrackInfo(); - if (mappedTrackInfo == null) { - Log.d(TAG, "Tracks []"); - return; - } - Log.d(TAG, "Tracks ["); - // Log tracks associated to renderers. - for (int rendererIndex = 0; rendererIndex < mappedTrackInfo.length; rendererIndex++) { - TrackGroupArray rendererTrackGroups = mappedTrackInfo.getTrackGroups(rendererIndex); - TrackSelection trackSelection = trackSelections.get(rendererIndex); - if (rendererTrackGroups.length > 0) { - Log.d(TAG, " Renderer:" + rendererIndex + " ["); - for (int groupIndex = 0; groupIndex < rendererTrackGroups.length; groupIndex++) { - TrackGroup trackGroup = rendererTrackGroups.get(groupIndex); - String adaptiveSupport = getAdaptiveSupportString(trackGroup.length, - mappedTrackInfo.getAdaptiveSupport(rendererIndex, groupIndex, false)); - Log.d(TAG, " Group:" + groupIndex + ", adaptive_supported=" + adaptiveSupport + " ["); - for (int trackIndex = 0; trackIndex < trackGroup.length; trackIndex++) { - String status = getTrackStatusString(trackSelection, trackGroup, trackIndex); - String formatSupport = getFormatSupportString( - mappedTrackInfo.getTrackFormatSupport(rendererIndex, groupIndex, trackIndex)); - Log.d(TAG, " " + status + " Track:" + trackIndex + ", " - + Format.toLogString(trackGroup.getFormat(trackIndex)) - + ", supported=" + formatSupport); - } - Log.d(TAG, " ]"); - } - // Log metadata for at most one of the tracks selected for the renderer. - if (trackSelection != null) { - for (int selectionIndex = 0; selectionIndex < trackSelection.length(); selectionIndex++) { - Metadata metadata = trackSelection.getFormat(selectionIndex).metadata; - if (metadata != null) { - Log.d(TAG, " Metadata ["); - printMetadata(metadata, " "); - Log.d(TAG, " ]"); - break; - } - } - } - Log.d(TAG, " ]"); - } - } - // Log tracks not associated with a renderer. - TrackGroupArray unassociatedTrackGroups = mappedTrackInfo.getUnassociatedTrackGroups(); - if (unassociatedTrackGroups.length > 0) { - Log.d(TAG, " Renderer:None ["); - for (int groupIndex = 0; groupIndex < unassociatedTrackGroups.length; groupIndex++) { - Log.d(TAG, " Group:" + groupIndex + " ["); - TrackGroup trackGroup = unassociatedTrackGroups.get(groupIndex); - for (int trackIndex = 0; trackIndex < trackGroup.length; trackIndex++) { - String status = getTrackStatusString(false); - String formatSupport = getFormatSupportString( - RendererCapabilities.FORMAT_UNSUPPORTED_TYPE); - Log.d(TAG, " " + status + " Track:" + trackIndex + ", " - + Format.toLogString(trackGroup.getFormat(trackIndex)) - + ", supported=" + formatSupport); - } - Log.d(TAG, " ]"); - } - Log.d(TAG, " ]"); - } - Log.d(TAG, "]"); - } - - // MetadataRenderer.Output - - @Override - public void onMetadata(Metadata metadata) { - Log.d(TAG, "onMetadata ["); - printMetadata(metadata, " "); - Log.d(TAG, "]"); - } - - // AudioRendererEventListener - - @Override - public void onAudioEnabled(DecoderCounters counters) { - Log.d(TAG, "audioEnabled [" + getSessionTimeString() + "]"); - } - - @Override - public void onAudioSessionId(int audioSessionId) { - Log.d(TAG, "audioSessionId [" + audioSessionId + "]"); - } - - @Override - public void onAudioDecoderInitialized(String decoderName, long elapsedRealtimeMs, - long initializationDurationMs) { - Log.d(TAG, "audioDecoderInitialized [" + getSessionTimeString() + ", " + decoderName + "]"); - } - - @Override - public void onAudioInputFormatChanged(Format format) { - Log.d(TAG, "audioFormatChanged [" + getSessionTimeString() + ", " + Format.toLogString(format) - + "]"); - } - - @Override - public void onAudioDisabled(DecoderCounters counters) { - Log.d(TAG, "audioDisabled [" + getSessionTimeString() + "]"); - } - - @Override - public void onAudioTrackUnderrun(int bufferSize, long bufferSizeMs, long elapsedSinceLastFeedMs) { - printInternalError("audioTrackUnderrun [" + bufferSize + ", " + bufferSizeMs + ", " - + elapsedSinceLastFeedMs + "]", null); - } - - // VideoRendererEventListener - - @Override - public void onVideoEnabled(DecoderCounters counters) { - Log.d(TAG, "videoEnabled [" + getSessionTimeString() + "]"); - } - - @Override - public void onVideoDecoderInitialized(String decoderName, long elapsedRealtimeMs, - long initializationDurationMs) { - Log.d(TAG, "videoDecoderInitialized [" + getSessionTimeString() + ", " + decoderName + "]"); - } - - @Override - public void onVideoInputFormatChanged(Format format) { - Log.d(TAG, "videoFormatChanged [" + getSessionTimeString() + ", " + Format.toLogString(format) - + "]"); - } - - @Override - public void onVideoDisabled(DecoderCounters counters) { - Log.d(TAG, "videoDisabled [" + getSessionTimeString() + "]"); - } - - @Override - public void onDroppedFrames(int count, long elapsed) { - Log.d(TAG, "droppedFrames [" + getSessionTimeString() + ", " + count + "]"); - } - - @Override - public void onVideoSizeChanged(int width, int height, int unappliedRotationDegrees, - float pixelWidthHeightRatio) { - // Do nothing. - } - - @Override - public void onRenderedFirstFrame(Surface surface) { - Log.d(TAG, "renderedFirstFrame [" + surface + "]"); - } - - // DefaultDrmSessionManager.EventListener - - @Override - public void onDrmSessionManagerError(Exception e) { - printInternalError("drmSessionManagerError", e); - } - - @Override - public void onDrmKeysRestored() { - Log.d(TAG, "drmKeysRestored [" + getSessionTimeString() + "]"); - } - - @Override - public void onDrmKeysRemoved() { - Log.d(TAG, "drmKeysRemoved [" + getSessionTimeString() + "]"); - } - - @Override - public void onDrmKeysLoaded() { - Log.d(TAG, "drmKeysLoaded [" + getSessionTimeString() + "]"); - } - - // ExtractorMediaSource.EventListener - - @Override - public void onLoadError(IOException error) { - printInternalError("loadError", error); - } - - // AdaptiveMediaSourceEventListener - - @Override - public void onLoadStarted(DataSpec dataSpec, int dataType, int trackType, Format trackFormat, - int trackSelectionReason, Object trackSelectionData, long mediaStartTimeMs, - long mediaEndTimeMs, long elapsedRealtimeMs) { - // Do nothing. - } - - @Override - public void onLoadError(DataSpec dataSpec, int dataType, int trackType, Format trackFormat, - int trackSelectionReason, Object trackSelectionData, long mediaStartTimeMs, - long mediaEndTimeMs, long elapsedRealtimeMs, long loadDurationMs, long bytesLoaded, - IOException error, boolean wasCanceled) { - printInternalError("loadError", error); - } - - @Override - public void onLoadCanceled(DataSpec dataSpec, int dataType, int trackType, Format trackFormat, - int trackSelectionReason, Object trackSelectionData, long mediaStartTimeMs, - long mediaEndTimeMs, long elapsedRealtimeMs, long loadDurationMs, long bytesLoaded) { - // Do nothing. - } - - @Override - public void onLoadCompleted(DataSpec dataSpec, int dataType, int trackType, Format trackFormat, - int trackSelectionReason, Object trackSelectionData, long mediaStartTimeMs, - long mediaEndTimeMs, long elapsedRealtimeMs, long loadDurationMs, long bytesLoaded) { - // Do nothing. - } - - @Override - public void onUpstreamDiscarded(int trackType, long mediaStartTimeMs, long mediaEndTimeMs) { - // Do nothing. - } - - @Override - public void onDownstreamFormatChanged(int trackType, Format trackFormat, int trackSelectionReason, - Object trackSelectionData, long mediaTimeMs) { - // Do nothing. - } - - // Internal methods - - private void printInternalError(String type, Exception e) { - Log.e(TAG, "internalError [" + getSessionTimeString() + ", " + type + "]", e); - } - - private void printMetadata(Metadata metadata, String prefix) { - for (int i = 0; i < metadata.length(); i++) { - Metadata.Entry entry = metadata.get(i); - if (entry instanceof TextInformationFrame) { - TextInformationFrame textInformationFrame = (TextInformationFrame) entry; - Log.d(TAG, prefix + String.format("%s: value=%s", textInformationFrame.id, - textInformationFrame.value)); - } else if (entry instanceof UrlLinkFrame) { - UrlLinkFrame urlLinkFrame = (UrlLinkFrame) entry; - Log.d(TAG, prefix + String.format("%s: url=%s", urlLinkFrame.id, urlLinkFrame.url)); - } else if (entry instanceof PrivFrame) { - PrivFrame privFrame = (PrivFrame) entry; - Log.d(TAG, prefix + String.format("%s: owner=%s", privFrame.id, privFrame.owner)); - } else if (entry instanceof GeobFrame) { - GeobFrame geobFrame = (GeobFrame) entry; - Log.d(TAG, prefix + String.format("%s: mimeType=%s, filename=%s, description=%s", - geobFrame.id, geobFrame.mimeType, geobFrame.filename, geobFrame.description)); - } else if (entry instanceof ApicFrame) { - ApicFrame apicFrame = (ApicFrame) entry; - Log.d(TAG, prefix + String.format("%s: mimeType=%s, description=%s", - apicFrame.id, apicFrame.mimeType, apicFrame.description)); - } else if (entry instanceof CommentFrame) { - CommentFrame commentFrame = (CommentFrame) entry; - Log.d(TAG, prefix + String.format("%s: language=%s, description=%s", commentFrame.id, - commentFrame.language, commentFrame.description)); - } else if (entry instanceof Id3Frame) { - Id3Frame id3Frame = (Id3Frame) entry; - Log.d(TAG, prefix + String.format("%s", id3Frame.id)); - } else if (entry instanceof EventMessage) { - EventMessage eventMessage = (EventMessage) entry; - Log.d(TAG, prefix + String.format("EMSG: scheme=%s, id=%d, value=%s", - eventMessage.schemeIdUri, eventMessage.id, eventMessage.value)); - } - } - } - - private String getSessionTimeString() { - return getTimeString(SystemClock.elapsedRealtime() - startTimeMs); - } - - private static String getTimeString(long timeMs) { - return timeMs == C.TIME_UNSET ? "?" : TIME_FORMAT.format((timeMs) / 1000f); - } - - private static String getStateString(int state) { - switch (state) { - case ExoPlayer.STATE_BUFFERING: - return "B"; - case ExoPlayer.STATE_ENDED: - return "E"; - case ExoPlayer.STATE_IDLE: - return "I"; - case ExoPlayer.STATE_READY: - return "R"; - default: - return "?"; - } - } - - private static String getFormatSupportString(int formatSupport) { - switch (formatSupport) { - case RendererCapabilities.FORMAT_HANDLED: - return "YES"; - case RendererCapabilities.FORMAT_EXCEEDS_CAPABILITIES: - return "NO_EXCEEDS_CAPABILITIES"; - case RendererCapabilities.FORMAT_UNSUPPORTED_SUBTYPE: - return "NO_UNSUPPORTED_TYPE"; - case RendererCapabilities.FORMAT_UNSUPPORTED_TYPE: - return "NO"; - default: - return "?"; - } - } - - private static String getAdaptiveSupportString(int trackCount, int adaptiveSupport) { - if (trackCount < 2) { - return "N/A"; - } - switch (adaptiveSupport) { - case RendererCapabilities.ADAPTIVE_SEAMLESS: - return "YES"; - case RendererCapabilities.ADAPTIVE_NOT_SEAMLESS: - return "YES_NOT_SEAMLESS"; - case RendererCapabilities.ADAPTIVE_NOT_SUPPORTED: - return "NO"; - default: - return "?"; - } - } - - private static String getTrackStatusString(TrackSelection selection, TrackGroup group, - int trackIndex) { - return getTrackStatusString(selection != null && selection.getTrackGroup() == group - && selection.indexOf(trackIndex) != C.INDEX_UNSET); - } - - private static String getTrackStatusString(boolean enabled) { - return enabled ? "[X]" : "[ ]"; - } - -} diff --git a/demo/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java b/demo/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java deleted file mode 100644 index d0703f3496..0000000000 --- a/demo/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java +++ /dev/null @@ -1,574 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.google.android.exoplayer2.demo; - -import android.app.Activity; -import android.content.Intent; -import android.content.pm.PackageManager; -import android.net.Uri; -import android.os.Bundle; -import android.os.Handler; -import android.support.annotation.NonNull; -import android.text.TextUtils; -import android.view.KeyEvent; -import android.view.View; -import android.view.View.OnClickListener; -import android.widget.Button; -import android.widget.LinearLayout; -import android.widget.TextView; -import android.widget.Toast; -import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.DefaultRenderersFactory; -import com.google.android.exoplayer2.ExoPlaybackException; -import com.google.android.exoplayer2.ExoPlayer; -import com.google.android.exoplayer2.ExoPlayerFactory; -import com.google.android.exoplayer2.PlaybackParameters; -import com.google.android.exoplayer2.SimpleExoPlayer; -import com.google.android.exoplayer2.Timeline; -import com.google.android.exoplayer2.drm.DefaultDrmSessionManager; -import com.google.android.exoplayer2.drm.DrmSessionManager; -import com.google.android.exoplayer2.drm.FrameworkMediaCrypto; -import com.google.android.exoplayer2.drm.FrameworkMediaDrm; -import com.google.android.exoplayer2.drm.HttpMediaDrmCallback; -import com.google.android.exoplayer2.drm.UnsupportedDrmException; -import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory; -import com.google.android.exoplayer2.mediacodec.MediaCodecRenderer.DecoderInitializationException; -import com.google.android.exoplayer2.mediacodec.MediaCodecUtil.DecoderQueryException; -import com.google.android.exoplayer2.source.BehindLiveWindowException; -import com.google.android.exoplayer2.source.ConcatenatingMediaSource; -import com.google.android.exoplayer2.source.ExtractorMediaSource; -import com.google.android.exoplayer2.source.MediaSource; -import com.google.android.exoplayer2.source.TrackGroupArray; -import com.google.android.exoplayer2.source.dash.DashMediaSource; -import com.google.android.exoplayer2.source.dash.DefaultDashChunkSource; -import com.google.android.exoplayer2.source.hls.HlsMediaSource; -import com.google.android.exoplayer2.source.smoothstreaming.DefaultSsChunkSource; -import com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource; -import com.google.android.exoplayer2.trackselection.AdaptiveTrackSelection; -import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; -import com.google.android.exoplayer2.trackselection.MappingTrackSelector.MappedTrackInfo; -import com.google.android.exoplayer2.trackselection.TrackSelection; -import com.google.android.exoplayer2.trackselection.TrackSelectionArray; -import com.google.android.exoplayer2.ui.DebugTextViewHelper; -import com.google.android.exoplayer2.ui.PlaybackControlView; -import com.google.android.exoplayer2.ui.SimpleExoPlayerView; -import com.google.android.exoplayer2.upstream.DataSource; -import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter; -import com.google.android.exoplayer2.upstream.HttpDataSource; -import com.google.android.exoplayer2.util.Util; -import java.net.CookieHandler; -import java.net.CookieManager; -import java.net.CookiePolicy; -import java.util.UUID; - -/** - * An activity that plays media using {@link SimpleExoPlayer}. - */ -public class PlayerActivity extends Activity implements OnClickListener, ExoPlayer.EventListener, - PlaybackControlView.VisibilityListener { - - public static final String DRM_SCHEME_UUID_EXTRA = "drm_scheme_uuid"; - public static final String DRM_LICENSE_URL = "drm_license_url"; - public static final String DRM_KEY_REQUEST_PROPERTIES = "drm_key_request_properties"; - public static final String PREFER_EXTENSION_DECODERS = "prefer_extension_decoders"; - - public static final String ACTION_VIEW = "com.google.android.exoplayer.demo.action.VIEW"; - public static final String EXTENSION_EXTRA = "extension"; - - public static final String ACTION_VIEW_LIST = - "com.google.android.exoplayer.demo.action.VIEW_LIST"; - public static final String URI_LIST_EXTRA = "uri_list"; - public static final String EXTENSION_LIST_EXTRA = "extension_list"; - - private static final DefaultBandwidthMeter BANDWIDTH_METER = new DefaultBandwidthMeter(); - private static final CookieManager DEFAULT_COOKIE_MANAGER; - static { - DEFAULT_COOKIE_MANAGER = new CookieManager(); - DEFAULT_COOKIE_MANAGER.setCookiePolicy(CookiePolicy.ACCEPT_ORIGINAL_SERVER); - } - - private Handler mainHandler; - private EventLogger eventLogger; - private SimpleExoPlayerView simpleExoPlayerView; - private LinearLayout debugRootView; - private TextView debugTextView; - private Button retryButton; - - private DataSource.Factory mediaDataSourceFactory; - private SimpleExoPlayer player; - private DefaultTrackSelector trackSelector; - private TrackSelectionHelper trackSelectionHelper; - private DebugTextViewHelper debugViewHelper; - private boolean needRetrySource; - private TrackGroupArray lastSeenTrackGroupArray; - - private boolean shouldAutoPlay; - private int resumeWindow; - private long resumePosition; - - // Activity lifecycle - - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - shouldAutoPlay = true; - clearResumePosition(); - mediaDataSourceFactory = buildDataSourceFactory(true); - mainHandler = new Handler(); - if (CookieHandler.getDefault() != DEFAULT_COOKIE_MANAGER) { - CookieHandler.setDefault(DEFAULT_COOKIE_MANAGER); - } - - setContentView(R.layout.player_activity); - View rootView = findViewById(R.id.root); - rootView.setOnClickListener(this); - debugRootView = (LinearLayout) findViewById(R.id.controls_root); - debugTextView = (TextView) findViewById(R.id.debug_text_view); - retryButton = (Button) findViewById(R.id.retry_button); - retryButton.setOnClickListener(this); - - simpleExoPlayerView = (SimpleExoPlayerView) findViewById(R.id.player_view); - simpleExoPlayerView.setControllerVisibilityListener(this); - simpleExoPlayerView.requestFocus(); - } - - @Override - public void onNewIntent(Intent intent) { - releasePlayer(); - shouldAutoPlay = true; - clearResumePosition(); - setIntent(intent); - } - - @Override - public void onStart() { - super.onStart(); - if (Util.SDK_INT > 23) { - initializePlayer(); - } - } - - @Override - public void onResume() { - super.onResume(); - if ((Util.SDK_INT <= 23 || player == null)) { - initializePlayer(); - } - } - - @Override - public void onPause() { - super.onPause(); - if (Util.SDK_INT <= 23) { - releasePlayer(); - } - } - - @Override - public void onStop() { - super.onStop(); - if (Util.SDK_INT > 23) { - releasePlayer(); - } - } - - @Override - public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, - @NonNull int[] grantResults) { - if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { - initializePlayer(); - } else { - showToast(R.string.storage_permission_denied); - finish(); - } - } - - // Activity input - - @Override - public boolean dispatchKeyEvent(KeyEvent event) { - // Show the controls on any key event. - simpleExoPlayerView.showController(); - // If the event was not handled then see if the player view can handle it as a media key event. - return super.dispatchKeyEvent(event) || simpleExoPlayerView.dispatchMediaKeyEvent(event); - } - - // OnClickListener methods - - @Override - public void onClick(View view) { - if (view == retryButton) { - initializePlayer(); - } else if (view.getParent() == debugRootView) { - MappedTrackInfo mappedTrackInfo = trackSelector.getCurrentMappedTrackInfo(); - if (mappedTrackInfo != null) { - trackSelectionHelper.showSelectionDialog(this, ((Button) view).getText(), - trackSelector.getCurrentMappedTrackInfo(), (int) view.getTag()); - } - } - } - - // PlaybackControlView.VisibilityListener implementation - - @Override - public void onVisibilityChange(int visibility) { - debugRootView.setVisibility(visibility); - } - - // Internal methods - - private void initializePlayer() { - Intent intent = getIntent(); - boolean needNewPlayer = player == null; - if (needNewPlayer) { - TrackSelection.Factory adaptiveTrackSelectionFactory = - new AdaptiveTrackSelection.Factory(BANDWIDTH_METER); - trackSelector = new DefaultTrackSelector(adaptiveTrackSelectionFactory); - trackSelectionHelper = new TrackSelectionHelper(trackSelector, adaptiveTrackSelectionFactory); - lastSeenTrackGroupArray = null; - eventLogger = new EventLogger(trackSelector); - - UUID drmSchemeUuid = intent.hasExtra(DRM_SCHEME_UUID_EXTRA) - ? UUID.fromString(intent.getStringExtra(DRM_SCHEME_UUID_EXTRA)) : null; - DrmSessionManager drmSessionManager = null; - if (drmSchemeUuid != null) { - String drmLicenseUrl = intent.getStringExtra(DRM_LICENSE_URL); - String[] keyRequestPropertiesArray = intent.getStringArrayExtra(DRM_KEY_REQUEST_PROPERTIES); - try { - drmSessionManager = buildDrmSessionManager(drmSchemeUuid, drmLicenseUrl, - keyRequestPropertiesArray); - } catch (UnsupportedDrmException e) { - int errorStringId = Util.SDK_INT < 18 ? R.string.error_drm_not_supported - : (e.reason == UnsupportedDrmException.REASON_UNSUPPORTED_SCHEME - ? R.string.error_drm_unsupported_scheme : R.string.error_drm_unknown); - showToast(errorStringId); - return; - } - } - - boolean preferExtensionDecoders = intent.getBooleanExtra(PREFER_EXTENSION_DECODERS, false); - @DefaultRenderersFactory.ExtensionRendererMode int extensionRendererMode = - ((DemoApplication) getApplication()).useExtensionRenderers() - ? (preferExtensionDecoders ? DefaultRenderersFactory.EXTENSION_RENDERER_MODE_PREFER - : DefaultRenderersFactory.EXTENSION_RENDERER_MODE_ON) - : DefaultRenderersFactory.EXTENSION_RENDERER_MODE_OFF; - DefaultRenderersFactory renderersFactory = new DefaultRenderersFactory(this, - drmSessionManager, extensionRendererMode); - - player = ExoPlayerFactory.newSimpleInstance(renderersFactory, trackSelector); - player.addListener(this); - player.addListener(eventLogger); - player.setAudioDebugListener(eventLogger); - player.setVideoDebugListener(eventLogger); - player.setMetadataOutput(eventLogger); - - simpleExoPlayerView.setPlayer(player); - player.setPlayWhenReady(shouldAutoPlay); - debugViewHelper = new DebugTextViewHelper(player, debugTextView); - debugViewHelper.start(); - } - if (needNewPlayer || needRetrySource) { - String action = intent.getAction(); - Uri[] uris; - String[] extensions; - if (ACTION_VIEW.equals(action)) { - uris = new Uri[] {intent.getData()}; - extensions = new String[] {intent.getStringExtra(EXTENSION_EXTRA)}; - } else if (ACTION_VIEW_LIST.equals(action)) { - String[] uriStrings = intent.getStringArrayExtra(URI_LIST_EXTRA); - uris = new Uri[uriStrings.length]; - for (int i = 0; i < uriStrings.length; i++) { - uris[i] = Uri.parse(uriStrings[i]); - } - extensions = intent.getStringArrayExtra(EXTENSION_LIST_EXTRA); - if (extensions == null) { - extensions = new String[uriStrings.length]; - } - } else { - showToast(getString(R.string.unexpected_intent_action, action)); - return; - } - if (Util.maybeRequestReadExternalStoragePermission(this, uris)) { - // The player will be reinitialized if the permission is granted. - return; - } - MediaSource[] mediaSources = new MediaSource[uris.length]; - for (int i = 0; i < uris.length; i++) { - mediaSources[i] = buildMediaSource(uris[i], extensions[i]); - } - MediaSource mediaSource = mediaSources.length == 1 ? mediaSources[0] - : new ConcatenatingMediaSource(mediaSources); - boolean haveResumePosition = resumeWindow != C.INDEX_UNSET; - if (haveResumePosition) { - player.seekTo(resumeWindow, resumePosition); - } - player.prepare(mediaSource, !haveResumePosition, false); - needRetrySource = false; - updateButtonVisibilities(); - } - } - - private MediaSource buildMediaSource(Uri uri, String overrideExtension) { - int type = TextUtils.isEmpty(overrideExtension) ? Util.inferContentType(uri) - : Util.inferContentType("." + overrideExtension); - switch (type) { - case C.TYPE_SS: - return new SsMediaSource(uri, buildDataSourceFactory(false), - new DefaultSsChunkSource.Factory(mediaDataSourceFactory), mainHandler, eventLogger); - case C.TYPE_DASH: - return new DashMediaSource(uri, buildDataSourceFactory(false), - new DefaultDashChunkSource.Factory(mediaDataSourceFactory), mainHandler, eventLogger); - case C.TYPE_HLS: - return new HlsMediaSource(uri, mediaDataSourceFactory, mainHandler, eventLogger); - case C.TYPE_OTHER: - return new ExtractorMediaSource(uri, mediaDataSourceFactory, new DefaultExtractorsFactory(), - mainHandler, eventLogger); - default: { - throw new IllegalStateException("Unsupported type: " + type); - } - } - } - - private DrmSessionManager buildDrmSessionManager(UUID uuid, - String licenseUrl, String[] keyRequestPropertiesArray) throws UnsupportedDrmException { - if (Util.SDK_INT < 18) { - return null; - } - HttpMediaDrmCallback drmCallback = new HttpMediaDrmCallback(licenseUrl, - buildHttpDataSourceFactory(false)); - if (keyRequestPropertiesArray != null) { - for (int i = 0; i < keyRequestPropertiesArray.length - 1; i += 2) { - drmCallback.setKeyRequestProperty(keyRequestPropertiesArray[i], - keyRequestPropertiesArray[i + 1]); - } - } - return new DefaultDrmSessionManager<>(uuid, - FrameworkMediaDrm.newInstance(uuid), drmCallback, null, mainHandler, eventLogger); - } - - private void releasePlayer() { - if (player != null) { - debugViewHelper.stop(); - debugViewHelper = null; - shouldAutoPlay = player.getPlayWhenReady(); - updateResumePosition(); - player.release(); - player = null; - trackSelector = null; - trackSelectionHelper = null; - eventLogger = null; - } - } - - private void updateResumePosition() { - resumeWindow = player.getCurrentWindowIndex(); - resumePosition = player.isCurrentWindowSeekable() ? Math.max(0, player.getCurrentPosition()) - : C.TIME_UNSET; - } - - private void clearResumePosition() { - resumeWindow = C.INDEX_UNSET; - resumePosition = C.TIME_UNSET; - } - - /** - * Returns a new DataSource factory. - * - * @param useBandwidthMeter Whether to set {@link #BANDWIDTH_METER} as a listener to the new - * DataSource factory. - * @return A new DataSource factory. - */ - private DataSource.Factory buildDataSourceFactory(boolean useBandwidthMeter) { - return ((DemoApplication) getApplication()) - .buildDataSourceFactory(useBandwidthMeter ? BANDWIDTH_METER : null); - } - - /** - * Returns a new HttpDataSource factory. - * - * @param useBandwidthMeter Whether to set {@link #BANDWIDTH_METER} as a listener to the new - * DataSource factory. - * @return A new HttpDataSource factory. - */ - private HttpDataSource.Factory buildHttpDataSourceFactory(boolean useBandwidthMeter) { - return ((DemoApplication) getApplication()) - .buildHttpDataSourceFactory(useBandwidthMeter ? BANDWIDTH_METER : null); - } - - // ExoPlayer.EventListener implementation - - @Override - public void onLoadingChanged(boolean isLoading) { - // Do nothing. - } - - @Override - public void onPlayerStateChanged(boolean playWhenReady, int playbackState) { - if (playbackState == ExoPlayer.STATE_ENDED) { - showControls(); - } - updateButtonVisibilities(); - } - - @Override - public void onPositionDiscontinuity() { - if (needRetrySource) { - // This will only occur if the user has performed a seek whilst in the error state. Update the - // resume position so that if the user then retries, playback will resume from the position to - // which they seeked. - updateResumePosition(); - } - } - - @Override - public void onPlaybackParametersChanged(PlaybackParameters playbackParameters) { - // Do nothing. - } - - @Override - public void onTimelineChanged(Timeline timeline, Object manifest) { - // Do nothing. - } - - @Override - public void onPlayerError(ExoPlaybackException e) { - String errorString = null; - if (e.type == ExoPlaybackException.TYPE_RENDERER) { - Exception cause = e.getRendererException(); - if (cause instanceof DecoderInitializationException) { - // Special case for decoder initialization failures. - DecoderInitializationException decoderInitializationException = - (DecoderInitializationException) cause; - if (decoderInitializationException.decoderName == null) { - if (decoderInitializationException.getCause() instanceof DecoderQueryException) { - errorString = getString(R.string.error_querying_decoders); - } else if (decoderInitializationException.secureDecoderRequired) { - errorString = getString(R.string.error_no_secure_decoder, - decoderInitializationException.mimeType); - } else { - errorString = getString(R.string.error_no_decoder, - decoderInitializationException.mimeType); - } - } else { - errorString = getString(R.string.error_instantiating_decoder, - decoderInitializationException.decoderName); - } - } - } - if (errorString != null) { - showToast(errorString); - } - needRetrySource = true; - if (isBehindLiveWindow(e)) { - clearResumePosition(); - initializePlayer(); - } else { - updateResumePosition(); - updateButtonVisibilities(); - showControls(); - } - } - - @Override - @SuppressWarnings("ReferenceEquality") - public void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray trackSelections) { - updateButtonVisibilities(); - if (trackGroups != lastSeenTrackGroupArray) { - MappedTrackInfo mappedTrackInfo = trackSelector.getCurrentMappedTrackInfo(); - if (mappedTrackInfo != null) { - if (mappedTrackInfo.getTrackTypeRendererSupport(C.TRACK_TYPE_VIDEO) - == MappedTrackInfo.RENDERER_SUPPORT_UNSUPPORTED_TRACKS) { - showToast(R.string.error_unsupported_video); - } - if (mappedTrackInfo.getTrackTypeRendererSupport(C.TRACK_TYPE_AUDIO) - == MappedTrackInfo.RENDERER_SUPPORT_UNSUPPORTED_TRACKS) { - showToast(R.string.error_unsupported_audio); - } - } - lastSeenTrackGroupArray = trackGroups; - } - } - - // User controls - - private void updateButtonVisibilities() { - debugRootView.removeAllViews(); - - retryButton.setVisibility(needRetrySource ? View.VISIBLE : View.GONE); - debugRootView.addView(retryButton); - - if (player == null) { - return; - } - - MappedTrackInfo mappedTrackInfo = trackSelector.getCurrentMappedTrackInfo(); - if (mappedTrackInfo == null) { - return; - } - - for (int i = 0; i < mappedTrackInfo.length; i++) { - TrackGroupArray trackGroups = mappedTrackInfo.getTrackGroups(i); - if (trackGroups.length != 0) { - Button button = new Button(this); - int label; - switch (player.getRendererType(i)) { - case C.TRACK_TYPE_AUDIO: - label = R.string.audio; - break; - case C.TRACK_TYPE_VIDEO: - label = R.string.video; - break; - case C.TRACK_TYPE_TEXT: - label = R.string.text; - break; - default: - continue; - } - button.setText(label); - button.setTag(i); - button.setOnClickListener(this); - debugRootView.addView(button, debugRootView.getChildCount() - 1); - } - } - } - - private void showControls() { - debugRootView.setVisibility(View.VISIBLE); - } - - private void showToast(int messageId) { - showToast(getString(messageId)); - } - - private void showToast(String message) { - Toast.makeText(getApplicationContext(), message, Toast.LENGTH_LONG).show(); - } - - private static boolean isBehindLiveWindow(ExoPlaybackException e) { - if (e.type != ExoPlaybackException.TYPE_SOURCE) { - return false; - } - Throwable cause = e.getSourceException(); - while (cause != null) { - if (cause instanceof BehindLiveWindowException) { - return true; - } - cause = cause.getCause(); - } - return false; - } - -} diff --git a/demo/src/main/java/com/google/android/exoplayer2/demo/SampleChooserActivity.java b/demo/src/main/java/com/google/android/exoplayer2/demo/SampleChooserActivity.java deleted file mode 100644 index 081ad190b5..0000000000 --- a/demo/src/main/java/com/google/android/exoplayer2/demo/SampleChooserActivity.java +++ /dev/null @@ -1,451 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.google.android.exoplayer2.demo; - -import android.app.Activity; -import android.content.Context; -import android.content.Intent; -import android.content.res.AssetManager; -import android.net.Uri; -import android.os.AsyncTask; -import android.os.Bundle; -import android.util.JsonReader; -import android.util.Log; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.BaseExpandableListAdapter; -import android.widget.ExpandableListView; -import android.widget.ExpandableListView.OnChildClickListener; -import android.widget.TextView; -import android.widget.Toast; -import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.ParserException; -import com.google.android.exoplayer2.upstream.DataSource; -import com.google.android.exoplayer2.upstream.DataSourceInputStream; -import com.google.android.exoplayer2.upstream.DataSpec; -import com.google.android.exoplayer2.upstream.DefaultDataSource; -import com.google.android.exoplayer2.util.Assertions; -import com.google.android.exoplayer2.util.Util; -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import java.util.UUID; - -/** - * An activity for selecting from a list of samples. - */ -public class SampleChooserActivity extends Activity { - - private static final String TAG = "SampleChooserActivity"; - - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.sample_chooser_activity); - Intent intent = getIntent(); - String dataUri = intent.getDataString(); - String[] uris; - if (dataUri != null) { - uris = new String[] {dataUri}; - } else { - ArrayList uriList = new ArrayList<>(); - AssetManager assetManager = getAssets(); - try { - for (String asset : assetManager.list("")) { - if (asset.endsWith(".exolist.json")) { - uriList.add("asset:///" + asset); - } - } - } catch (IOException e) { - Toast.makeText(getApplicationContext(), R.string.sample_list_load_error, Toast.LENGTH_LONG) - .show(); - } - uris = new String[uriList.size()]; - uriList.toArray(uris); - Arrays.sort(uris); - } - SampleListLoader loaderTask = new SampleListLoader(); - loaderTask.execute(uris); - } - - private void onSampleGroups(final List groups, boolean sawError) { - if (sawError) { - Toast.makeText(getApplicationContext(), R.string.sample_list_load_error, Toast.LENGTH_LONG) - .show(); - } - ExpandableListView sampleList = (ExpandableListView) findViewById(R.id.sample_list); - sampleList.setAdapter(new SampleAdapter(this, groups)); - sampleList.setOnChildClickListener(new OnChildClickListener() { - @Override - public boolean onChildClick(ExpandableListView parent, View view, int groupPosition, - int childPosition, long id) { - onSampleSelected(groups.get(groupPosition).samples.get(childPosition)); - return true; - } - }); - } - - private void onSampleSelected(Sample sample) { - startActivity(sample.buildIntent(this)); - } - - private final class SampleListLoader extends AsyncTask> { - - private boolean sawError; - - @Override - protected List doInBackground(String... uris) { - List result = new ArrayList<>(); - Context context = getApplicationContext(); - String userAgent = Util.getUserAgent(context, "ExoPlayerDemo"); - DataSource dataSource = new DefaultDataSource(context, null, userAgent, false); - for (String uri : uris) { - DataSpec dataSpec = new DataSpec(Uri.parse(uri)); - InputStream inputStream = new DataSourceInputStream(dataSource, dataSpec); - try { - readSampleGroups(new JsonReader(new InputStreamReader(inputStream, "UTF-8")), result); - } catch (Exception e) { - Log.e(TAG, "Error loading sample list: " + uri, e); - sawError = true; - } finally { - Util.closeQuietly(dataSource); - } - } - return result; - } - - @Override - protected void onPostExecute(List result) { - onSampleGroups(result, sawError); - } - - private void readSampleGroups(JsonReader reader, List groups) throws IOException { - reader.beginArray(); - while (reader.hasNext()) { - readSampleGroup(reader, groups); - } - reader.endArray(); - } - - private void readSampleGroup(JsonReader reader, List groups) throws IOException { - String groupName = ""; - ArrayList samples = new ArrayList<>(); - - reader.beginObject(); - while (reader.hasNext()) { - String name = reader.nextName(); - switch (name) { - case "name": - groupName = reader.nextString(); - break; - case "samples": - reader.beginArray(); - while (reader.hasNext()) { - samples.add(readEntry(reader, false)); - } - reader.endArray(); - break; - case "_comment": - reader.nextString(); // Ignore. - break; - default: - throw new ParserException("Unsupported name: " + name); - } - } - reader.endObject(); - - SampleGroup group = getGroup(groupName, groups); - group.samples.addAll(samples); - } - - private Sample readEntry(JsonReader reader, boolean insidePlaylist) throws IOException { - String sampleName = null; - String uri = null; - String extension = null; - UUID drmUuid = null; - String drmLicenseUrl = null; - String[] drmKeyRequestProperties = null; - boolean preferExtensionDecoders = false; - ArrayList playlistSamples = null; - - reader.beginObject(); - while (reader.hasNext()) { - String name = reader.nextName(); - switch (name) { - case "name": - sampleName = reader.nextString(); - break; - case "uri": - uri = reader.nextString(); - break; - case "extension": - extension = reader.nextString(); - break; - case "drm_scheme": - Assertions.checkState(!insidePlaylist, "Invalid attribute on nested item: drm_scheme"); - drmUuid = getDrmUuid(reader.nextString()); - break; - case "drm_license_url": - Assertions.checkState(!insidePlaylist, - "Invalid attribute on nested item: drm_license_url"); - drmLicenseUrl = reader.nextString(); - break; - case "drm_key_request_properties": - Assertions.checkState(!insidePlaylist, - "Invalid attribute on nested item: drm_key_request_properties"); - ArrayList drmKeyRequestPropertiesList = new ArrayList<>(); - reader.beginObject(); - while (reader.hasNext()) { - drmKeyRequestPropertiesList.add(reader.nextName()); - drmKeyRequestPropertiesList.add(reader.nextString()); - } - reader.endObject(); - drmKeyRequestProperties = drmKeyRequestPropertiesList.toArray(new String[0]); - break; - case "prefer_extension_decoders": - Assertions.checkState(!insidePlaylist, - "Invalid attribute on nested item: prefer_extension_decoders"); - preferExtensionDecoders = reader.nextBoolean(); - break; - case "playlist": - Assertions.checkState(!insidePlaylist, "Invalid nesting of playlists"); - playlistSamples = new ArrayList<>(); - reader.beginArray(); - while (reader.hasNext()) { - playlistSamples.add((UriSample) readEntry(reader, true)); - } - reader.endArray(); - break; - default: - throw new ParserException("Unsupported attribute name: " + name); - } - } - reader.endObject(); - - if (playlistSamples != null) { - UriSample[] playlistSamplesArray = playlistSamples.toArray( - new UriSample[playlistSamples.size()]); - return new PlaylistSample(sampleName, drmUuid, drmLicenseUrl, drmKeyRequestProperties, - preferExtensionDecoders, playlistSamplesArray); - } else { - return new UriSample(sampleName, drmUuid, drmLicenseUrl, drmKeyRequestProperties, - preferExtensionDecoders, uri, extension); - } - } - - private SampleGroup getGroup(String groupName, List groups) { - for (int i = 0; i < groups.size(); i++) { - if (Util.areEqual(groupName, groups.get(i).title)) { - return groups.get(i); - } - } - SampleGroup group = new SampleGroup(groupName); - groups.add(group); - return group; - } - - private UUID getDrmUuid(String typeString) throws ParserException { - switch (Util.toLowerInvariant(typeString)) { - case "widevine": - return C.WIDEVINE_UUID; - case "playready": - return C.PLAYREADY_UUID; - case "cenc": - return C.CLEARKEY_UUID; - default: - try { - return UUID.fromString(typeString); - } catch (RuntimeException e) { - throw new ParserException("Unsupported drm type: " + typeString); - } - } - } - - } - - private static final class SampleAdapter extends BaseExpandableListAdapter { - - private final Context context; - private final List sampleGroups; - - public SampleAdapter(Context context, List sampleGroups) { - this.context = context; - this.sampleGroups = sampleGroups; - } - - @Override - public Sample getChild(int groupPosition, int childPosition) { - return getGroup(groupPosition).samples.get(childPosition); - } - - @Override - public long getChildId(int groupPosition, int childPosition) { - return childPosition; - } - - @Override - public View getChildView(int groupPosition, int childPosition, boolean isLastChild, - View convertView, ViewGroup parent) { - View view = convertView; - if (view == null) { - view = LayoutInflater.from(context).inflate(android.R.layout.simple_list_item_1, parent, - false); - } - ((TextView) view).setText(getChild(groupPosition, childPosition).name); - return view; - } - - @Override - public int getChildrenCount(int groupPosition) { - return getGroup(groupPosition).samples.size(); - } - - @Override - public SampleGroup getGroup(int groupPosition) { - return sampleGroups.get(groupPosition); - } - - @Override - public long getGroupId(int groupPosition) { - return groupPosition; - } - - @Override - public View getGroupView(int groupPosition, boolean isExpanded, View convertView, - ViewGroup parent) { - View view = convertView; - if (view == null) { - view = LayoutInflater.from(context).inflate(android.R.layout.simple_expandable_list_item_1, - parent, false); - } - ((TextView) view).setText(getGroup(groupPosition).title); - return view; - } - - @Override - public int getGroupCount() { - return sampleGroups.size(); - } - - @Override - public boolean hasStableIds() { - return false; - } - - @Override - public boolean isChildSelectable(int groupPosition, int childPosition) { - return true; - } - - } - - private static final class SampleGroup { - - public final String title; - public final List samples; - - public SampleGroup(String title) { - this.title = title; - this.samples = new ArrayList<>(); - } - - } - - private abstract static class Sample { - - public final String name; - public final boolean preferExtensionDecoders; - public final UUID drmSchemeUuid; - public final String drmLicenseUrl; - public final String[] drmKeyRequestProperties; - - public Sample(String name, UUID drmSchemeUuid, String drmLicenseUrl, - String[] drmKeyRequestProperties, boolean preferExtensionDecoders) { - this.name = name; - this.drmSchemeUuid = drmSchemeUuid; - this.drmLicenseUrl = drmLicenseUrl; - this.drmKeyRequestProperties = drmKeyRequestProperties; - this.preferExtensionDecoders = preferExtensionDecoders; - } - - public Intent buildIntent(Context context) { - Intent intent = new Intent(context, PlayerActivity.class); - intent.putExtra(PlayerActivity.PREFER_EXTENSION_DECODERS, preferExtensionDecoders); - if (drmSchemeUuid != null) { - intent.putExtra(PlayerActivity.DRM_SCHEME_UUID_EXTRA, drmSchemeUuid.toString()); - intent.putExtra(PlayerActivity.DRM_LICENSE_URL, drmLicenseUrl); - intent.putExtra(PlayerActivity.DRM_KEY_REQUEST_PROPERTIES, drmKeyRequestProperties); - } - return intent; - } - - } - - private static final class UriSample extends Sample { - - public final String uri; - public final String extension; - - public UriSample(String name, UUID drmSchemeUuid, String drmLicenseUrl, - String[] drmKeyRequestProperties, boolean preferExtensionDecoders, String uri, - String extension) { - super(name, drmSchemeUuid, drmLicenseUrl, drmKeyRequestProperties, preferExtensionDecoders); - this.uri = uri; - this.extension = extension; - } - - @Override - public Intent buildIntent(Context context) { - return super.buildIntent(context) - .setData(Uri.parse(uri)) - .putExtra(PlayerActivity.EXTENSION_EXTRA, extension) - .setAction(PlayerActivity.ACTION_VIEW); - } - - } - - private static final class PlaylistSample extends Sample { - - public final UriSample[] children; - - public PlaylistSample(String name, UUID drmSchemeUuid, String drmLicenseUrl, - String[] drmKeyRequestProperties, boolean preferExtensionDecoders, - UriSample... children) { - super(name, drmSchemeUuid, drmLicenseUrl, drmKeyRequestProperties, preferExtensionDecoders); - this.children = children; - } - - @Override - public Intent buildIntent(Context context) { - String[] uris = new String[children.length]; - String[] extensions = new String[children.length]; - for (int i = 0; i < children.length; i++) { - uris[i] = children[i].uri; - extensions[i] = children[i].extension; - } - return super.buildIntent(context) - .putExtra(PlayerActivity.URI_LIST_EXTRA, uris) - .putExtra(PlayerActivity.EXTENSION_LIST_EXTRA, extensions) - .setAction(PlayerActivity.ACTION_VIEW_LIST); - } - - } - -} diff --git a/demo/src/main/java/com/google/android/exoplayer2/demo/TrackSelectionHelper.java b/demo/src/main/java/com/google/android/exoplayer2/demo/TrackSelectionHelper.java deleted file mode 100644 index fb7217f8fd..0000000000 --- a/demo/src/main/java/com/google/android/exoplayer2/demo/TrackSelectionHelper.java +++ /dev/null @@ -1,290 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.google.android.exoplayer2.demo; - -import android.annotation.SuppressLint; -import android.app.Activity; -import android.app.AlertDialog; -import android.content.Context; -import android.content.DialogInterface; -import android.content.res.TypedArray; -import android.util.Pair; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.CheckedTextView; -import com.google.android.exoplayer2.RendererCapabilities; -import com.google.android.exoplayer2.source.TrackGroup; -import com.google.android.exoplayer2.source.TrackGroupArray; -import com.google.android.exoplayer2.trackselection.FixedTrackSelection; -import com.google.android.exoplayer2.trackselection.MappingTrackSelector; -import com.google.android.exoplayer2.trackselection.MappingTrackSelector.MappedTrackInfo; -import com.google.android.exoplayer2.trackselection.MappingTrackSelector.SelectionOverride; -import com.google.android.exoplayer2.trackselection.RandomTrackSelection; -import com.google.android.exoplayer2.trackselection.TrackSelection; -import java.util.Arrays; - -/** - * Helper class for displaying track selection dialogs. - */ -/* package */ final class TrackSelectionHelper implements View.OnClickListener, - DialogInterface.OnClickListener { - - private static final TrackSelection.Factory FIXED_FACTORY = new FixedTrackSelection.Factory(); - private static final TrackSelection.Factory RANDOM_FACTORY = new RandomTrackSelection.Factory(); - - private final MappingTrackSelector selector; - private final TrackSelection.Factory adaptiveTrackSelectionFactory; - - private MappedTrackInfo trackInfo; - private int rendererIndex; - private TrackGroupArray trackGroups; - private boolean[] trackGroupsAdaptive; - private boolean isDisabled; - private SelectionOverride override; - - private CheckedTextView disableView; - private CheckedTextView defaultView; - private CheckedTextView enableRandomAdaptationView; - private CheckedTextView[][] trackViews; - - /** - * @param selector The track selector. - * @param adaptiveTrackSelectionFactory A factory for adaptive {@link TrackSelection}s, or null - * if the selection helper should not support adaptive tracks. - */ - public TrackSelectionHelper(MappingTrackSelector selector, - TrackSelection.Factory adaptiveTrackSelectionFactory) { - this.selector = selector; - this.adaptiveTrackSelectionFactory = adaptiveTrackSelectionFactory; - } - - /** - * Shows the selection dialog for a given renderer. - * - * @param activity The parent activity. - * @param title The dialog's title. - * @param trackInfo The current track information. - * @param rendererIndex The index of the renderer. - */ - public void showSelectionDialog(Activity activity, CharSequence title, MappedTrackInfo trackInfo, - int rendererIndex) { - this.trackInfo = trackInfo; - this.rendererIndex = rendererIndex; - - trackGroups = trackInfo.getTrackGroups(rendererIndex); - trackGroupsAdaptive = new boolean[trackGroups.length]; - for (int i = 0; i < trackGroups.length; i++) { - trackGroupsAdaptive[i] = adaptiveTrackSelectionFactory != null - && trackInfo.getAdaptiveSupport(rendererIndex, i, false) - != RendererCapabilities.ADAPTIVE_NOT_SUPPORTED - && trackGroups.get(i).length > 1; - } - isDisabled = selector.getRendererDisabled(rendererIndex); - override = selector.getSelectionOverride(rendererIndex, trackGroups); - - AlertDialog.Builder builder = new AlertDialog.Builder(activity); - builder.setTitle(title) - .setView(buildView(builder.getContext())) - .setPositiveButton(android.R.string.ok, this) - .setNegativeButton(android.R.string.cancel, null) - .create() - .show(); - } - - @SuppressLint("InflateParams") - private View buildView(Context context) { - LayoutInflater inflater = LayoutInflater.from(context); - View view = inflater.inflate(R.layout.track_selection_dialog, null); - ViewGroup root = (ViewGroup) view.findViewById(R.id.root); - - TypedArray attributeArray = context.getTheme().obtainStyledAttributes( - new int[] {android.R.attr.selectableItemBackground}); - int selectableItemBackgroundResourceId = attributeArray.getResourceId(0, 0); - attributeArray.recycle(); - - // View for disabling the renderer. - disableView = (CheckedTextView) inflater.inflate( - android.R.layout.simple_list_item_single_choice, root, false); - disableView.setBackgroundResource(selectableItemBackgroundResourceId); - disableView.setText(R.string.selection_disabled); - disableView.setFocusable(true); - disableView.setOnClickListener(this); - root.addView(disableView); - - // View for clearing the override to allow the selector to use its default selection logic. - defaultView = (CheckedTextView) inflater.inflate( - android.R.layout.simple_list_item_single_choice, root, false); - defaultView.setBackgroundResource(selectableItemBackgroundResourceId); - defaultView.setText(R.string.selection_default); - defaultView.setFocusable(true); - defaultView.setOnClickListener(this); - root.addView(inflater.inflate(R.layout.list_divider, root, false)); - root.addView(defaultView); - - // Per-track views. - boolean haveAdaptiveTracks = false; - trackViews = new CheckedTextView[trackGroups.length][]; - for (int groupIndex = 0; groupIndex < trackGroups.length; groupIndex++) { - TrackGroup group = trackGroups.get(groupIndex); - boolean groupIsAdaptive = trackGroupsAdaptive[groupIndex]; - haveAdaptiveTracks |= groupIsAdaptive; - trackViews[groupIndex] = new CheckedTextView[group.length]; - for (int trackIndex = 0; trackIndex < group.length; trackIndex++) { - if (trackIndex == 0) { - root.addView(inflater.inflate(R.layout.list_divider, root, false)); - } - int trackViewLayoutId = groupIsAdaptive ? android.R.layout.simple_list_item_multiple_choice - : android.R.layout.simple_list_item_single_choice; - CheckedTextView trackView = (CheckedTextView) inflater.inflate( - trackViewLayoutId, root, false); - trackView.setBackgroundResource(selectableItemBackgroundResourceId); - trackView.setText(DemoUtil.buildTrackName(group.getFormat(trackIndex))); - if (trackInfo.getTrackFormatSupport(rendererIndex, groupIndex, trackIndex) - == RendererCapabilities.FORMAT_HANDLED) { - trackView.setFocusable(true); - trackView.setTag(Pair.create(groupIndex, trackIndex)); - trackView.setOnClickListener(this); - } else { - trackView.setFocusable(false); - trackView.setEnabled(false); - } - trackViews[groupIndex][trackIndex] = trackView; - root.addView(trackView); - } - } - - if (haveAdaptiveTracks) { - // View for using random adaptation. - enableRandomAdaptationView = (CheckedTextView) inflater.inflate( - android.R.layout.simple_list_item_multiple_choice, root, false); - enableRandomAdaptationView.setBackgroundResource(selectableItemBackgroundResourceId); - enableRandomAdaptationView.setText(R.string.enable_random_adaptation); - enableRandomAdaptationView.setOnClickListener(this); - root.addView(inflater.inflate(R.layout.list_divider, root, false)); - root.addView(enableRandomAdaptationView); - } - - updateViews(); - return view; - } - - private void updateViews() { - disableView.setChecked(isDisabled); - defaultView.setChecked(!isDisabled && override == null); - for (int i = 0; i < trackViews.length; i++) { - for (int j = 0; j < trackViews[i].length; j++) { - trackViews[i][j].setChecked(override != null && override.groupIndex == i - && override.containsTrack(j)); - } - } - if (enableRandomAdaptationView != null) { - boolean enableView = !isDisabled && override != null && override.length > 1; - enableRandomAdaptationView.setEnabled(enableView); - enableRandomAdaptationView.setFocusable(enableView); - if (enableView) { - enableRandomAdaptationView.setChecked(!isDisabled - && override.factory instanceof RandomTrackSelection.Factory); - } - } - } - - // DialogInterface.OnClickListener - - @Override - public void onClick(DialogInterface dialog, int which) { - selector.setRendererDisabled(rendererIndex, isDisabled); - if (override != null) { - selector.setSelectionOverride(rendererIndex, trackGroups, override); - } else { - selector.clearSelectionOverrides(rendererIndex); - } - } - - // View.OnClickListener - - @Override - public void onClick(View view) { - if (view == disableView) { - isDisabled = true; - override = null; - } else if (view == defaultView) { - isDisabled = false; - override = null; - } else if (view == enableRandomAdaptationView) { - setOverride(override.groupIndex, override.tracks, !enableRandomAdaptationView.isChecked()); - } else { - isDisabled = false; - @SuppressWarnings("unchecked") - Pair tag = (Pair) view.getTag(); - int groupIndex = tag.first; - int trackIndex = tag.second; - if (!trackGroupsAdaptive[groupIndex] || override == null - || override.groupIndex != groupIndex) { - override = new SelectionOverride(FIXED_FACTORY, groupIndex, trackIndex); - } else { - // The group being modified is adaptive and we already have a non-null override. - boolean isEnabled = ((CheckedTextView) view).isChecked(); - int overrideLength = override.length; - if (isEnabled) { - // Remove the track from the override. - if (overrideLength == 1) { - // The last track is being removed, so the override becomes empty. - override = null; - isDisabled = true; - } else { - setOverride(groupIndex, getTracksRemoving(override, trackIndex), - enableRandomAdaptationView.isChecked()); - } - } else { - // Add the track to the override. - setOverride(groupIndex, getTracksAdding(override, trackIndex), - enableRandomAdaptationView.isChecked()); - } - } - } - // Update the views with the new state. - updateViews(); - } - - private void setOverride(int group, int[] tracks, boolean enableRandomAdaptation) { - TrackSelection.Factory factory = tracks.length == 1 ? FIXED_FACTORY - : (enableRandomAdaptation ? RANDOM_FACTORY : adaptiveTrackSelectionFactory); - override = new SelectionOverride(factory, group, tracks); - } - - // Track array manipulation. - - private static int[] getTracksAdding(SelectionOverride override, int addedTrack) { - int[] tracks = override.tracks; - tracks = Arrays.copyOf(tracks, tracks.length + 1); - tracks[tracks.length - 1] = addedTrack; - return tracks; - } - - private static int[] getTracksRemoving(SelectionOverride override, int removedTrack) { - int[] tracks = new int[override.length - 1]; - int trackCount = 0; - for (int i = 0; i < tracks.length + 1; i++) { - int track = override.tracks[i]; - if (track != removedTrack) { - tracks[trackCount++] = track; - } - } - return tracks; - } - -} diff --git a/demo/src/main/res/drawable-xhdpi/ic_banner.png b/demo/src/main/res/drawable-xhdpi/ic_banner.png deleted file mode 100644 index 520d83cc3b..0000000000 Binary files a/demo/src/main/res/drawable-xhdpi/ic_banner.png and /dev/null differ diff --git a/demo/src/main/res/mipmap-hdpi/ic_launcher.png b/demo/src/main/res/mipmap-hdpi/ic_launcher.png deleted file mode 100644 index 6e8b5499de..0000000000 Binary files a/demo/src/main/res/mipmap-hdpi/ic_launcher.png and /dev/null differ diff --git a/demo/src/main/res/mipmap-mdpi/ic_launcher.png b/demo/src/main/res/mipmap-mdpi/ic_launcher.png deleted file mode 100644 index 26fe2f0782..0000000000 Binary files a/demo/src/main/res/mipmap-mdpi/ic_launcher.png and /dev/null differ diff --git a/demo/src/main/res/mipmap-xhdpi/ic_launcher.png b/demo/src/main/res/mipmap-xhdpi/ic_launcher.png deleted file mode 100644 index d3251491ce..0000000000 Binary files a/demo/src/main/res/mipmap-xhdpi/ic_launcher.png and /dev/null differ diff --git a/demo/src/main/res/mipmap-xxhdpi/ic_launcher.png b/demo/src/main/res/mipmap-xxhdpi/ic_launcher.png deleted file mode 100644 index b5a12d35f3..0000000000 Binary files a/demo/src/main/res/mipmap-xxhdpi/ic_launcher.png and /dev/null differ diff --git a/demo/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/demo/src/main/res/mipmap-xxxhdpi/ic_launcher.png deleted file mode 100644 index 9c26192c32..0000000000 Binary files a/demo/src/main/res/mipmap-xxxhdpi/ic_launcher.png and /dev/null differ diff --git a/demos/README.md b/demos/README.md new file mode 100644 index 0000000000..7e62249db1 --- /dev/null +++ b/demos/README.md @@ -0,0 +1,4 @@ +# ExoPlayer demos # + +This directory contains applications that demonstrate how to use ExoPlayer. +Browse the individual demos and their READMEs to learn more. diff --git a/demos/cast/README.md b/demos/cast/README.md new file mode 100644 index 0000000000..2c68a5277a --- /dev/null +++ b/demos/cast/README.md @@ -0,0 +1,4 @@ +# Cast demo application # + +This folder contains a demo application that showcases ExoPlayer integration +with Google Cast. diff --git a/demos/cast/build.gradle b/demos/cast/build.gradle new file mode 100644 index 0000000000..f9228e4b79 --- /dev/null +++ b/demos/cast/build.gradle @@ -0,0 +1,64 @@ +// Copyright (C) 2017 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +apply from: '../../constants.gradle' +apply plugin: 'com.android.application' + +android { + compileSdkVersion project.ext.compileSdkVersion + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + defaultConfig { + versionName project.ext.releaseVersion + versionCode project.ext.releaseVersionCode + minSdkVersion project.ext.minSdkVersion + targetSdkVersion project.ext.appTargetSdkVersion + } + + buildTypes { + release { + shrinkResources true + minifyEnabled true + proguardFiles = [ + "proguard-rules.txt", + getDefaultProguardFile('proguard-android.txt') + ] + } + debug { + jniDebuggable = true + } + } + + lintOptions { + // The demo app isn't indexed and doesn't have translations. + disable 'GoogleAppIndexingWarning','MissingTranslation' + } +} + +dependencies { + implementation project(modulePrefix + 'library-core') + implementation project(modulePrefix + 'library-dash') + implementation project(modulePrefix + 'library-hls') + implementation project(modulePrefix + 'library-smoothstreaming') + implementation project(modulePrefix + 'library-ui') + implementation project(modulePrefix + 'extension-cast') + implementation 'androidx.appcompat:appcompat:' + androidxAppCompatVersion + implementation 'androidx.recyclerview:recyclerview:1.0.0' + implementation 'com.google.android.material:material:1.0.0' +} + +apply plugin: 'com.google.android.gms.strict-version-matcher-plugin' diff --git a/demos/cast/proguard-rules.txt b/demos/cast/proguard-rules.txt new file mode 100644 index 0000000000..e6bf2dd3bf --- /dev/null +++ b/demos/cast/proguard-rules.txt @@ -0,0 +1,6 @@ +# Proguard rules specific to the Cast demo app. + +# Accessed via menu.xml +-keep class androidx.mediarouter.app.MediaRouteActionProvider { + *; +} diff --git a/demos/cast/src/main/AndroidManifest.xml b/demos/cast/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..dbfdd833f6 --- /dev/null +++ b/demos/cast/src/main/AndroidManifest.xml @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/DemoUtil.java b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/DemoUtil.java new file mode 100644 index 0000000000..dacdbfe616 --- /dev/null +++ b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/DemoUtil.java @@ -0,0 +1,104 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2.castdemo; + +import android.net.Uri; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.ext.cast.MediaItem; +import com.google.android.exoplayer2.ext.cast.MediaItem.DrmConfiguration; +import com.google.android.exoplayer2.util.MimeTypes; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** Utility methods and constants for the Cast demo application. */ +/* package */ final class DemoUtil { + + public static final String MIME_TYPE_DASH = MimeTypes.APPLICATION_MPD; + public static final String MIME_TYPE_HLS = MimeTypes.APPLICATION_M3U8; + public static final String MIME_TYPE_SS = MimeTypes.APPLICATION_SS; + public static final String MIME_TYPE_VIDEO_MP4 = MimeTypes.VIDEO_MP4; + + /** The list of samples available in the cast demo app. */ + public static final List SAMPLES; + + static { + ArrayList samples = new ArrayList<>(); + + // Clear content. + samples.add( + new MediaItem.Builder() + .setUri("https://storage.googleapis.com/wvmedia/clear/h264/tears/tears.mpd") + .setTitle("Clear DASH: Tears") + .setMimeType(MIME_TYPE_DASH) + .build()); + samples.add( + new MediaItem.Builder() + .setUri("https://storage.googleapis.com/shaka-demo-assets/angel-one-hls/hls.m3u8") + .setTitle("Clear HLS: Angel one") + .setMimeType(MIME_TYPE_HLS) + .build()); + samples.add( + new MediaItem.Builder() + .setUri("https://html5demos.com/assets/dizzy.mp4") + .setTitle("Clear MP4: Dizzy") + .setMimeType(MIME_TYPE_VIDEO_MP4) + .build()); + + // DRM content. + samples.add( + new MediaItem.Builder() + .setUri(Uri.parse("https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd")) + .setTitle("Widevine DASH cenc: Tears") + .setMimeType(MIME_TYPE_DASH) + .setDrmConfiguration( + new DrmConfiguration( + C.WIDEVINE_UUID, + Uri.parse("https://proxy.uat.widevine.com/proxy?provider=widevine_test"), + Collections.emptyMap())) + .build()); + samples.add( + new MediaItem.Builder() + .setUri( + Uri.parse( + "https://storage.googleapis.com/wvmedia/cbc1/h264/tears/tears_aes_cbc1.mpd")) + .setTitle("Widevine DASH cbc1: Tears") + .setMimeType(MIME_TYPE_DASH) + .setDrmConfiguration( + new DrmConfiguration( + C.WIDEVINE_UUID, + Uri.parse("https://proxy.uat.widevine.com/proxy?provider=widevine_test"), + Collections.emptyMap())) + .build()); + samples.add( + new MediaItem.Builder() + .setUri( + Uri.parse( + "https://storage.googleapis.com/wvmedia/cbcs/h264/tears/tears_aes_cbcs.mpd")) + .setTitle("Widevine DASH cbcs: Tears") + .setMimeType(MIME_TYPE_DASH) + .setDrmConfiguration( + new DrmConfiguration( + C.WIDEVINE_UUID, + Uri.parse("https://proxy.uat.widevine.com/proxy?provider=widevine_test"), + Collections.emptyMap())) + .build()); + + SAMPLES = Collections.unmodifiableList(samples); + } + + private DemoUtil() {} +} diff --git a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/MainActivity.java b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/MainActivity.java new file mode 100644 index 0000000000..0c5b5037f5 --- /dev/null +++ b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/MainActivity.java @@ -0,0 +1,311 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2.castdemo; + +import android.content.Context; +import android.os.Bundle; +import android.view.KeyEvent; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.View; +import android.view.View.OnClickListener; +import android.view.ViewGroup; +import android.widget.ArrayAdapter; +import android.widget.ListView; +import android.widget.TextView; +import android.widget.Toast; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.app.AlertDialog; +import androidx.appcompat.app.AppCompatActivity; +import androidx.core.graphics.ColorUtils; +import androidx.recyclerview.widget.ItemTouchHelper; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; +import androidx.recyclerview.widget.RecyclerView.ViewHolder; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.SimpleExoPlayer; +import com.google.android.exoplayer2.ext.cast.MediaItem; +import com.google.android.exoplayer2.ui.PlayerControlView; +import com.google.android.exoplayer2.ui.PlayerView; +import com.google.android.gms.cast.framework.CastButtonFactory; +import com.google.android.gms.cast.framework.CastContext; +import com.google.android.gms.dynamite.DynamiteModule; + +/** + * An activity that plays video using {@link SimpleExoPlayer} and supports casting using ExoPlayer's + * Cast extension. + */ +public class MainActivity extends AppCompatActivity + implements OnClickListener, PlayerManager.Listener { + + private PlayerView localPlayerView; + private PlayerControlView castControlView; + private PlayerManager playerManager; + private RecyclerView mediaQueueList; + private MediaQueueListAdapter mediaQueueListAdapter; + private CastContext castContext; + + // Activity lifecycle methods. + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + // Getting the cast context later than onStart can cause device discovery not to take place. + try { + castContext = CastContext.getSharedInstance(this); + } catch (RuntimeException e) { + Throwable cause = e.getCause(); + while (cause != null) { + if (cause instanceof DynamiteModule.LoadingException) { + setContentView(R.layout.cast_context_error); + return; + } + cause = cause.getCause(); + } + // Unknown error. We propagate it. + throw e; + } + + setContentView(R.layout.main_activity); + + localPlayerView = findViewById(R.id.local_player_view); + localPlayerView.requestFocus(); + + castControlView = findViewById(R.id.cast_control_view); + + mediaQueueList = findViewById(R.id.sample_list); + ItemTouchHelper helper = new ItemTouchHelper(new RecyclerViewCallback()); + helper.attachToRecyclerView(mediaQueueList); + mediaQueueList.setLayoutManager(new LinearLayoutManager(this)); + mediaQueueList.setHasFixedSize(true); + mediaQueueListAdapter = new MediaQueueListAdapter(); + + findViewById(R.id.add_sample_button).setOnClickListener(this); + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + super.onCreateOptionsMenu(menu); + getMenuInflater().inflate(R.menu.menu, menu); + CastButtonFactory.setUpMediaRouteButton(this, menu, R.id.media_route_menu_item); + return true; + } + + @Override + public void onResume() { + super.onResume(); + if (castContext == null) { + // There is no Cast context to work with. Do nothing. + return; + } + playerManager = + new PlayerManager( + /* listener= */ this, + localPlayerView, + castControlView, + /* context= */ this, + castContext); + mediaQueueList.setAdapter(mediaQueueListAdapter); + } + + @Override + public void onPause() { + super.onPause(); + if (castContext == null) { + // Nothing to release. + return; + } + mediaQueueListAdapter.notifyItemRangeRemoved(0, mediaQueueListAdapter.getItemCount()); + mediaQueueList.setAdapter(null); + playerManager.release(); + playerManager = null; + } + + // Activity input. + + @Override + public boolean dispatchKeyEvent(KeyEvent event) { + // If the event was not handled then see if the player view can handle it. + return super.dispatchKeyEvent(event) || playerManager.dispatchKeyEvent(event); + } + + @Override + public void onClick(View view) { + new AlertDialog.Builder(this) + .setTitle(R.string.add_samples) + .setView(buildSampleListView()) + .setPositiveButton(android.R.string.ok, null) + .create() + .show(); + } + + // PlayerManager.Listener implementation. + + @Override + public void onQueuePositionChanged(int previousIndex, int newIndex) { + if (previousIndex != C.INDEX_UNSET) { + mediaQueueListAdapter.notifyItemChanged(previousIndex); + } + if (newIndex != C.INDEX_UNSET) { + mediaQueueListAdapter.notifyItemChanged(newIndex); + } + } + + @Override + public void onUnsupportedTrack(int trackType) { + if (trackType == C.TRACK_TYPE_AUDIO) { + showToast(R.string.error_unsupported_audio); + } else if (trackType == C.TRACK_TYPE_VIDEO) { + showToast(R.string.error_unsupported_video); + } else { + // Do nothing. + } + } + + // Internal methods. + + private void showToast(int messageId) { + Toast.makeText(getApplicationContext(), messageId, Toast.LENGTH_LONG).show(); + } + + private View buildSampleListView() { + View dialogList = getLayoutInflater().inflate(R.layout.sample_list, null); + ListView sampleList = dialogList.findViewById(R.id.sample_list); + sampleList.setAdapter(new SampleListAdapter(this)); + sampleList.setOnItemClickListener( + (parent, view, position, id) -> { + playerManager.addItem(DemoUtil.SAMPLES.get(position)); + mediaQueueListAdapter.notifyItemInserted(playerManager.getMediaQueueSize() - 1); + }); + return dialogList; + } + + // Internal classes. + + private class MediaQueueListAdapter extends RecyclerView.Adapter { + + @Override + public QueueItemViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { + TextView v = (TextView) LayoutInflater.from(parent.getContext()) + .inflate(android.R.layout.simple_list_item_1, parent, false); + return new QueueItemViewHolder(v); + } + + @Override + public void onBindViewHolder(QueueItemViewHolder holder, int position) { + holder.item = playerManager.getItem(position); + TextView view = holder.textView; + view.setText(holder.item.title); + // TODO: Solve coloring using the theme's ColorStateList. + view.setTextColor( + ColorUtils.setAlphaComponent( + view.getCurrentTextColor(), + position == playerManager.getCurrentItemIndex() ? 255 : 100)); + } + + @Override + public int getItemCount() { + return playerManager.getMediaQueueSize(); + } + + } + + private class RecyclerViewCallback extends ItemTouchHelper.SimpleCallback { + + private int draggingFromPosition; + private int draggingToPosition; + + public RecyclerViewCallback() { + super(ItemTouchHelper.UP | ItemTouchHelper.DOWN, ItemTouchHelper.START | ItemTouchHelper.END); + draggingFromPosition = C.INDEX_UNSET; + draggingToPosition = C.INDEX_UNSET; + } + + @Override + public boolean onMove(RecyclerView list, RecyclerView.ViewHolder origin, + RecyclerView.ViewHolder target) { + int fromPosition = origin.getAdapterPosition(); + int toPosition = target.getAdapterPosition(); + if (draggingFromPosition == C.INDEX_UNSET) { + // A drag has started, but changes to the media queue will be reflected in clearView(). + draggingFromPosition = fromPosition; + } + draggingToPosition = toPosition; + mediaQueueListAdapter.notifyItemMoved(fromPosition, toPosition); + return true; + } + + @Override + public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) { + int position = viewHolder.getAdapterPosition(); + QueueItemViewHolder queueItemHolder = (QueueItemViewHolder) viewHolder; + if (playerManager.removeItem(queueItemHolder.item)) { + mediaQueueListAdapter.notifyItemRemoved(position); + // Update whichever item took its place, in case it became the new selected item. + mediaQueueListAdapter.notifyItemChanged(position); + } + } + + @Override + public void clearView(RecyclerView recyclerView, ViewHolder viewHolder) { + super.clearView(recyclerView, viewHolder); + if (draggingFromPosition != C.INDEX_UNSET) { + QueueItemViewHolder queueItemHolder = (QueueItemViewHolder) viewHolder; + // A drag has ended. We reflect the media queue change in the player. + if (!playerManager.moveItem(queueItemHolder.item, draggingToPosition)) { + // The move failed. The entire sequence of onMove calls since the drag started needs to be + // invalidated. + mediaQueueListAdapter.notifyDataSetChanged(); + } + } + draggingFromPosition = C.INDEX_UNSET; + draggingToPosition = C.INDEX_UNSET; + } + } + + private class QueueItemViewHolder extends RecyclerView.ViewHolder implements OnClickListener { + + public final TextView textView; + public MediaItem item; + + public QueueItemViewHolder(TextView textView) { + super(textView); + this.textView = textView; + textView.setOnClickListener(this); + } + + @Override + public void onClick(View v) { + playerManager.selectQueueItem(getAdapterPosition()); + } + } + + private static final class SampleListAdapter extends ArrayAdapter { + + public SampleListAdapter(Context context) { + super(context, android.R.layout.simple_list_item_1, DemoUtil.SAMPLES); + } + + @NonNull + @Override + public View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) { + View view = super.getView(position, convertView, parent); + ((TextView) view).setText(getItem(position).title); + return view; + } + } +} diff --git a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/PlayerManager.java b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/PlayerManager.java new file mode 100644 index 0000000000..85104e0d18 --- /dev/null +++ b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/PlayerManager.java @@ -0,0 +1,459 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2.castdemo; + +import android.content.Context; +import android.net.Uri; +import android.view.KeyEvent; +import android.view.View; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.Player; +import com.google.android.exoplayer2.Player.DiscontinuityReason; +import com.google.android.exoplayer2.Player.EventListener; +import com.google.android.exoplayer2.Player.TimelineChangeReason; +import com.google.android.exoplayer2.SimpleExoPlayer; +import com.google.android.exoplayer2.Timeline; +import com.google.android.exoplayer2.Timeline.Period; +import com.google.android.exoplayer2.drm.DefaultDrmSessionManager; +import com.google.android.exoplayer2.drm.DrmSessionManager; +import com.google.android.exoplayer2.drm.ExoMediaCrypto; +import com.google.android.exoplayer2.drm.FrameworkMediaDrm; +import com.google.android.exoplayer2.drm.HttpMediaDrmCallback; +import com.google.android.exoplayer2.ext.cast.CastPlayer; +import com.google.android.exoplayer2.ext.cast.DefaultMediaItemConverter; +import com.google.android.exoplayer2.ext.cast.MediaItem; +import com.google.android.exoplayer2.ext.cast.MediaItemConverter; +import com.google.android.exoplayer2.ext.cast.SessionAvailabilityListener; +import com.google.android.exoplayer2.source.ConcatenatingMediaSource; +import com.google.android.exoplayer2.source.MediaSource; +import com.google.android.exoplayer2.source.ProgressiveMediaSource; +import com.google.android.exoplayer2.source.TrackGroupArray; +import com.google.android.exoplayer2.source.dash.DashMediaSource; +import com.google.android.exoplayer2.source.hls.HlsMediaSource; +import com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource; +import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; +import com.google.android.exoplayer2.trackselection.MappingTrackSelector; +import com.google.android.exoplayer2.trackselection.TrackSelectionArray; +import com.google.android.exoplayer2.ui.PlayerControlView; +import com.google.android.exoplayer2.ui.PlayerView; +import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory; +import com.google.android.exoplayer2.util.Util; +import com.google.android.gms.cast.MediaQueueItem; +import com.google.android.gms.cast.framework.CastContext; +import java.util.ArrayList; +import java.util.Map; + +/** Manages players and an internal media queue for the demo app. */ +/* package */ class PlayerManager implements EventListener, SessionAvailabilityListener { + + /** Listener for events. */ + interface Listener { + + /** Called when the currently played item of the media queue changes. */ + void onQueuePositionChanged(int previousIndex, int newIndex); + + /** + * Called when a track of type {@code trackType} is not supported by the player. + * + * @param trackType One of the {@link C}{@code .TRACK_TYPE_*} constants. + */ + void onUnsupportedTrack(int trackType); + } + + private static final String USER_AGENT = "ExoCastDemoPlayer"; + private static final DefaultHttpDataSourceFactory DATA_SOURCE_FACTORY = + new DefaultHttpDataSourceFactory(USER_AGENT); + + private final PlayerView localPlayerView; + private final PlayerControlView castControlView; + private final DefaultTrackSelector trackSelector; + private final SimpleExoPlayer exoPlayer; + private final CastPlayer castPlayer; + private final ArrayList mediaQueue; + private final Listener listener; + private final ConcatenatingMediaSource concatenatingMediaSource; + private final MediaItemConverter mediaItemConverter; + + private TrackGroupArray lastSeenTrackGroupArray; + private int currentItemIndex; + private Player currentPlayer; + + /** + * Creates a new manager for {@link SimpleExoPlayer} and {@link CastPlayer}. + * + * @param listener A {@link Listener} for queue position changes. + * @param localPlayerView The {@link PlayerView} for local playback. + * @param castControlView The {@link PlayerControlView} to control remote playback. + * @param context A {@link Context}. + * @param castContext The {@link CastContext}. + */ + public PlayerManager( + Listener listener, + PlayerView localPlayerView, + PlayerControlView castControlView, + Context context, + CastContext castContext) { + this.listener = listener; + this.localPlayerView = localPlayerView; + this.castControlView = castControlView; + mediaQueue = new ArrayList<>(); + currentItemIndex = C.INDEX_UNSET; + concatenatingMediaSource = new ConcatenatingMediaSource(); + mediaItemConverter = new DefaultMediaItemConverter(); + + trackSelector = new DefaultTrackSelector(context); + exoPlayer = new SimpleExoPlayer.Builder(context).setTrackSelector(trackSelector).build(); + exoPlayer.addListener(this); + localPlayerView.setPlayer(exoPlayer); + + castPlayer = new CastPlayer(castContext); + castPlayer.addListener(this); + castPlayer.setSessionAvailabilityListener(this); + castControlView.setPlayer(castPlayer); + + setCurrentPlayer(castPlayer.isCastSessionAvailable() ? castPlayer : exoPlayer); + } + + // Queue manipulation methods. + + /** + * Plays a specified queue item in the current player. + * + * @param itemIndex The index of the item to play. + */ + public void selectQueueItem(int itemIndex) { + setCurrentItem(itemIndex, C.TIME_UNSET, true); + } + + /** Returns the index of the currently played item. */ + public int getCurrentItemIndex() { + return currentItemIndex; + } + + /** + * Appends {@code item} to the media queue. + * + * @param item The {@link MediaItem} to append. + */ + public void addItem(MediaItem item) { + mediaQueue.add(item); + concatenatingMediaSource.addMediaSource(buildMediaSource(item)); + if (currentPlayer == castPlayer) { + castPlayer.addItems(mediaItemConverter.toMediaQueueItem(item)); + } + } + + /** Returns the size of the media queue. */ + public int getMediaQueueSize() { + return mediaQueue.size(); + } + + /** + * Returns the item at the given index in the media queue. + * + * @param position The index of the item. + * @return The item at the given index in the media queue. + */ + public MediaItem getItem(int position) { + return mediaQueue.get(position); + } + + /** + * Removes the item at the given index from the media queue. + * + * @param item The item to remove. + * @return Whether the removal was successful. + */ + public boolean removeItem(MediaItem item) { + int itemIndex = mediaQueue.indexOf(item); + if (itemIndex == -1) { + return false; + } + concatenatingMediaSource.removeMediaSource(itemIndex); + if (currentPlayer == castPlayer) { + if (castPlayer.getPlaybackState() != Player.STATE_IDLE) { + Timeline castTimeline = castPlayer.getCurrentTimeline(); + if (castTimeline.getPeriodCount() <= itemIndex) { + return false; + } + castPlayer.removeItem((int) castTimeline.getPeriod(itemIndex, new Period()).id); + } + } + mediaQueue.remove(itemIndex); + if (itemIndex == currentItemIndex && itemIndex == mediaQueue.size()) { + maybeSetCurrentItemAndNotify(C.INDEX_UNSET); + } else if (itemIndex < currentItemIndex) { + maybeSetCurrentItemAndNotify(currentItemIndex - 1); + } + return true; + } + + /** + * Moves an item within the queue. + * + * @param item The item to move. + * @param toIndex The target index of the item in the queue. + * @return Whether the item move was successful. + */ + public boolean moveItem(MediaItem item, int toIndex) { + int fromIndex = mediaQueue.indexOf(item); + if (fromIndex == -1) { + return false; + } + // Player update. + concatenatingMediaSource.moveMediaSource(fromIndex, toIndex); + if (currentPlayer == castPlayer && castPlayer.getPlaybackState() != Player.STATE_IDLE) { + Timeline castTimeline = castPlayer.getCurrentTimeline(); + int periodCount = castTimeline.getPeriodCount(); + if (periodCount <= fromIndex || periodCount <= toIndex) { + return false; + } + int elementId = (int) castTimeline.getPeriod(fromIndex, new Period()).id; + castPlayer.moveItem(elementId, toIndex); + } + + mediaQueue.add(toIndex, mediaQueue.remove(fromIndex)); + + // Index update. + if (fromIndex == currentItemIndex) { + maybeSetCurrentItemAndNotify(toIndex); + } else if (fromIndex < currentItemIndex && toIndex >= currentItemIndex) { + maybeSetCurrentItemAndNotify(currentItemIndex - 1); + } else if (fromIndex > currentItemIndex && toIndex <= currentItemIndex) { + maybeSetCurrentItemAndNotify(currentItemIndex + 1); + } + + return true; + } + + /** + * Dispatches a given {@link KeyEvent} to the corresponding view of the current player. + * + * @param event The {@link KeyEvent}. + * @return Whether the event was handled by the target view. + */ + public boolean dispatchKeyEvent(KeyEvent event) { + if (currentPlayer == exoPlayer) { + return localPlayerView.dispatchKeyEvent(event); + } else /* currentPlayer == castPlayer */ { + return castControlView.dispatchKeyEvent(event); + } + } + + /** Releases the manager and the players that it holds. */ + public void release() { + currentItemIndex = C.INDEX_UNSET; + mediaQueue.clear(); + concatenatingMediaSource.clear(); + castPlayer.setSessionAvailabilityListener(null); + castPlayer.release(); + localPlayerView.setPlayer(null); + exoPlayer.release(); + } + + // Player.EventListener implementation. + + @Override + public void onPlayerStateChanged(boolean playWhenReady, @Player.State int playbackState) { + updateCurrentItemIndex(); + } + + @Override + public void onPositionDiscontinuity(@DiscontinuityReason int reason) { + updateCurrentItemIndex(); + } + + @Override + public void onTimelineChanged(Timeline timeline, @TimelineChangeReason int reason) { + updateCurrentItemIndex(); + } + + @Override + public void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray trackSelections) { + if (currentPlayer == exoPlayer && trackGroups != lastSeenTrackGroupArray) { + MappingTrackSelector.MappedTrackInfo mappedTrackInfo = + trackSelector.getCurrentMappedTrackInfo(); + if (mappedTrackInfo != null) { + if (mappedTrackInfo.getTypeSupport(C.TRACK_TYPE_VIDEO) + == MappingTrackSelector.MappedTrackInfo.RENDERER_SUPPORT_UNSUPPORTED_TRACKS) { + listener.onUnsupportedTrack(C.TRACK_TYPE_VIDEO); + } + if (mappedTrackInfo.getTypeSupport(C.TRACK_TYPE_AUDIO) + == MappingTrackSelector.MappedTrackInfo.RENDERER_SUPPORT_UNSUPPORTED_TRACKS) { + listener.onUnsupportedTrack(C.TRACK_TYPE_AUDIO); + } + } + lastSeenTrackGroupArray = trackGroups; + } + } + + // CastPlayer.SessionAvailabilityListener implementation. + + @Override + public void onCastSessionAvailable() { + setCurrentPlayer(castPlayer); + } + + @Override + public void onCastSessionUnavailable() { + setCurrentPlayer(exoPlayer); + } + + // Internal methods. + + private void updateCurrentItemIndex() { + int playbackState = currentPlayer.getPlaybackState(); + maybeSetCurrentItemAndNotify( + playbackState != Player.STATE_IDLE && playbackState != Player.STATE_ENDED + ? currentPlayer.getCurrentWindowIndex() + : C.INDEX_UNSET); + } + + private void setCurrentPlayer(Player currentPlayer) { + if (this.currentPlayer == currentPlayer) { + return; + } + + // View management. + if (currentPlayer == exoPlayer) { + localPlayerView.setVisibility(View.VISIBLE); + castControlView.hide(); + } else /* currentPlayer == castPlayer */ { + localPlayerView.setVisibility(View.GONE); + castControlView.show(); + } + + // Player state management. + long playbackPositionMs = C.TIME_UNSET; + int windowIndex = C.INDEX_UNSET; + boolean playWhenReady = false; + + Player previousPlayer = this.currentPlayer; + if (previousPlayer != null) { + // Save state from the previous player. + int playbackState = previousPlayer.getPlaybackState(); + if (playbackState != Player.STATE_ENDED) { + playbackPositionMs = previousPlayer.getCurrentPosition(); + playWhenReady = previousPlayer.getPlayWhenReady(); + windowIndex = previousPlayer.getCurrentWindowIndex(); + if (windowIndex != currentItemIndex) { + playbackPositionMs = C.TIME_UNSET; + windowIndex = currentItemIndex; + } + } + previousPlayer.stop(true); + } + + this.currentPlayer = currentPlayer; + + // Media queue management. + if (currentPlayer == exoPlayer) { + exoPlayer.prepare(concatenatingMediaSource); + } + + // Playback transition. + if (windowIndex != C.INDEX_UNSET) { + setCurrentItem(windowIndex, playbackPositionMs, playWhenReady); + } + } + + /** + * Starts playback of the item at the given position. + * + * @param itemIndex The index of the item to play. + * @param positionMs The position at which playback should start. + * @param playWhenReady Whether the player should proceed when ready to do so. + */ + private void setCurrentItem(int itemIndex, long positionMs, boolean playWhenReady) { + maybeSetCurrentItemAndNotify(itemIndex); + if (currentPlayer == castPlayer && castPlayer.getCurrentTimeline().isEmpty()) { + MediaQueueItem[] items = new MediaQueueItem[mediaQueue.size()]; + for (int i = 0; i < items.length; i++) { + items[i] = mediaItemConverter.toMediaQueueItem(mediaQueue.get(i)); + } + castPlayer.loadItems(items, itemIndex, positionMs, Player.REPEAT_MODE_OFF); + } else { + currentPlayer.seekTo(itemIndex, positionMs); + currentPlayer.setPlayWhenReady(playWhenReady); + } + } + + private void maybeSetCurrentItemAndNotify(int currentItemIndex) { + if (this.currentItemIndex != currentItemIndex) { + int oldIndex = this.currentItemIndex; + this.currentItemIndex = currentItemIndex; + listener.onQueuePositionChanged(oldIndex, currentItemIndex); + } + } + + private MediaSource buildMediaSource(MediaItem item) { + Uri uri = item.uri; + String mimeType = item.mimeType; + if (mimeType == null) { + throw new IllegalArgumentException("mimeType is required"); + } + + DrmSessionManager drmSessionManager = + DrmSessionManager.getDummyDrmSessionManager(); + MediaItem.DrmConfiguration drmConfiguration = item.drmConfiguration; + if (drmConfiguration != null && Util.SDK_INT >= 18) { + String licenseServerUrl = + drmConfiguration.licenseUri != null ? drmConfiguration.licenseUri.toString() : ""; + HttpMediaDrmCallback drmCallback = + new HttpMediaDrmCallback(licenseServerUrl, DATA_SOURCE_FACTORY); + for (Map.Entry requestHeader : drmConfiguration.requestHeaders.entrySet()) { + drmCallback.setKeyRequestProperty(requestHeader.getKey(), requestHeader.getValue()); + } + drmSessionManager = + new DefaultDrmSessionManager.Builder() + .setMultiSession(/* multiSession= */ true) + .setUuidAndExoMediaDrmProvider( + drmConfiguration.uuid, FrameworkMediaDrm.DEFAULT_PROVIDER) + .build(drmCallback); + } + + MediaSource createdMediaSource; + switch (mimeType) { + case DemoUtil.MIME_TYPE_SS: + createdMediaSource = + new SsMediaSource.Factory(DATA_SOURCE_FACTORY) + .setDrmSessionManager(drmSessionManager) + .createMediaSource(uri); + break; + case DemoUtil.MIME_TYPE_DASH: + createdMediaSource = + new DashMediaSource.Factory(DATA_SOURCE_FACTORY) + .setDrmSessionManager(drmSessionManager) + .createMediaSource(uri); + break; + case DemoUtil.MIME_TYPE_HLS: + createdMediaSource = + new HlsMediaSource.Factory(DATA_SOURCE_FACTORY) + .setDrmSessionManager(drmSessionManager) + .createMediaSource(uri); + break; + case DemoUtil.MIME_TYPE_VIDEO_MP4: + createdMediaSource = + new ProgressiveMediaSource.Factory(DATA_SOURCE_FACTORY) + .setDrmSessionManager(drmSessionManager) + .createMediaSource(uri); + break; + default: + throw new IllegalArgumentException("mimeType is unsupported: " + mimeType); + } + return createdMediaSource; + } +} diff --git a/demos/cast/src/main/res/drawable/ic_plus.xml b/demos/cast/src/main/res/drawable/ic_plus.xml new file mode 100644 index 0000000000..5a5a5154c9 --- /dev/null +++ b/demos/cast/src/main/res/drawable/ic_plus.xml @@ -0,0 +1,24 @@ + + + + + diff --git a/demos/cast/src/main/res/layout/cast_context_error.xml b/demos/cast/src/main/res/layout/cast_context_error.xml new file mode 100644 index 0000000000..0b3fdb63d2 --- /dev/null +++ b/demos/cast/src/main/res/layout/cast_context_error.xml @@ -0,0 +1,22 @@ + + + diff --git a/demos/cast/src/main/res/layout/main_activity.xml b/demos/cast/src/main/res/layout/main_activity.xml new file mode 100644 index 0000000000..71dbcdcd9c --- /dev/null +++ b/demos/cast/src/main/res/layout/main_activity.xml @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + + + + diff --git a/demos/cast/src/main/res/layout/sample_list.xml b/demos/cast/src/main/res/layout/sample_list.xml new file mode 100644 index 0000000000..183c74eb3a --- /dev/null +++ b/demos/cast/src/main/res/layout/sample_list.xml @@ -0,0 +1,25 @@ + + + + + + + diff --git a/demos/cast/src/main/res/menu/menu.xml b/demos/cast/src/main/res/menu/menu.xml new file mode 100644 index 0000000000..95419adf3c --- /dev/null +++ b/demos/cast/src/main/res/menu/menu.xml @@ -0,0 +1,25 @@ + + + + + + + diff --git a/demos/cast/src/main/res/mipmap-hdpi/ic_launcher.png b/demos/cast/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000000..52e8dc93d9 Binary files /dev/null and b/demos/cast/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/demos/cast/src/main/res/mipmap-mdpi/ic_launcher.png b/demos/cast/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000000..b55576eff3 Binary files /dev/null and b/demos/cast/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/demos/cast/src/main/res/mipmap-xhdpi/ic_launcher.png b/demos/cast/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000000..ca84d6a60e Binary files /dev/null and b/demos/cast/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/demos/cast/src/main/res/mipmap-xxhdpi/ic_launcher.png b/demos/cast/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000000..27ab9b1054 Binary files /dev/null and b/demos/cast/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/demos/cast/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/demos/cast/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000000..d1eb9b78cf Binary files /dev/null and b/demos/cast/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/demos/cast/src/main/res/values/strings.xml b/demos/cast/src/main/res/values/strings.xml new file mode 100644 index 0000000000..69f0691630 --- /dev/null +++ b/demos/cast/src/main/res/values/strings.xml @@ -0,0 +1,31 @@ + + + + + + Exo Cast Demo + + Cast + + Add samples + + Failed to get Cast context. Try updating Google Play Services and restart the app. + + Media includes video tracks, but none are playable by this device + + Media includes audio tracks, but none are playable by this device + + diff --git a/demo/README.md b/demos/main/README.md similarity index 58% rename from demo/README.md rename to demos/main/README.md index ca37392623..bdb04e5ba8 100644 --- a/demo/README.md +++ b/demos/main/README.md @@ -1,5 +1,5 @@ -# Demo application # +# ExoPlayer main demo # -This folder contains a demo application that uses ExoPlayer to play a number +This is the main ExoPlayer demo application. It uses ExoPlayer to play a number of test streams. It can be used as a starting point or reference project when developing other applications that make use of the ExoPlayer library. diff --git a/demos/main/build.gradle b/demos/main/build.gradle new file mode 100644 index 0000000000..ab47b6de81 --- /dev/null +++ b/demos/main/build.gradle @@ -0,0 +1,82 @@ +// Copyright (C) 2016 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +apply from: '../../constants.gradle' +apply plugin: 'com.android.application' + +android { + compileSdkVersion project.ext.compileSdkVersion + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + defaultConfig { + versionName project.ext.releaseVersion + versionCode project.ext.releaseVersionCode + minSdkVersion project.ext.minSdkVersion + targetSdkVersion project.ext.appTargetSdkVersion + } + + buildTypes { + release { + shrinkResources true + minifyEnabled true + proguardFiles = [ + "proguard-rules.txt", + getDefaultProguardFile('proguard-android.txt') + ] + } + debug { + jniDebuggable = true + } + } + + lintOptions { + // The demo app isn't indexed, doesn't have translations, and has a + // banner for AndroidTV that's only in xhdpi density. + disable 'GoogleAppIndexingWarning','MissingTranslation','IconDensities' + } + + flavorDimensions "extensions" + + productFlavors { + noExtensions { + dimension "extensions" + } + withExtensions { + dimension "extensions" + } + } +} + +dependencies { + implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion + implementation 'androidx.appcompat:appcompat:' + androidxAppCompatVersion + implementation 'com.google.android.material:material:1.0.0' + implementation project(modulePrefix + 'library-core') + implementation project(modulePrefix + 'library-dash') + implementation project(modulePrefix + 'library-hls') + implementation project(modulePrefix + 'library-smoothstreaming') + implementation project(modulePrefix + 'library-ui') + withExtensionsImplementation project(path: modulePrefix + 'extension-av1') + withExtensionsImplementation project(path: modulePrefix + 'extension-ffmpeg') + withExtensionsImplementation project(path: modulePrefix + 'extension-flac') + withExtensionsImplementation project(path: modulePrefix + 'extension-ima') + withExtensionsImplementation project(path: modulePrefix + 'extension-opus') + withExtensionsImplementation project(path: modulePrefix + 'extension-vp9') + withExtensionsImplementation project(path: modulePrefix + 'extension-rtmp') +} + +apply plugin: 'com.google.android.gms.strict-version-matcher-plugin' diff --git a/demos/main/proguard-rules.txt b/demos/main/proguard-rules.txt new file mode 100644 index 0000000000..cd201892ab --- /dev/null +++ b/demos/main/proguard-rules.txt @@ -0,0 +1,7 @@ +# Proguard rules specific to the main demo app. + +# Constructor accessed via reflection in PlayerActivity +-dontnote com.google.android.exoplayer2.ext.ima.ImaAdsLoader +-keepclassmembers class com.google.android.exoplayer2.ext.ima.ImaAdsLoader { + (android.content.Context, android.net.Uri); +} diff --git a/demo/src/main/AndroidManifest.xml b/demos/main/src/main/AndroidManifest.xml similarity index 74% rename from demo/src/main/AndroidManifest.xml rename to demos/main/src/main/AndroidManifest.xml index afcddccac9..0240a377ac 100644 --- a/demo/src/main/AndroidManifest.xml +++ b/demos/main/src/main/AndroidManifest.xml @@ -15,15 +15,18 @@ --> + xmlns:tools="http://schemas.android.com/tools" + package="com.google.android.exoplayer2.demo"> + + + + - + + android:requestLegacyExternalStorage="true" + android:name="com.google.android.exoplayer2.demo.DemoApplication" + tools:ignore="UnusedAttribute"> + android:label="@string/application_name" + android:theme="@style/Theme.AppCompat"> @@ -75,6 +81,18 @@ + + + + + + + + + diff --git a/demos/main/src/main/assets/media.exolist.json b/demos/main/src/main/assets/media.exolist.json new file mode 100644 index 0000000000..06f063b1c1 --- /dev/null +++ b/demos/main/src/main/assets/media.exolist.json @@ -0,0 +1,620 @@ +[ + { + "name": "YouTube DASH", + "samples": [ + { + "name": "Google Glass (MP4,H264)", + "uri": "https://www.youtube.com/api/manifest/dash/id/bf5bb2419360daf1/source/youtube?as=fmp4_audio_clear,fmp4_sd_hd_clear&sparams=ip,ipbits,expire,source,id,as&ip=0.0.0.0&ipbits=0&expire=19000000000&signature=51AF5F39AB0CEC3E5497CD9C900EBFEAECCCB5C7.8506521BFC350652163895D4C26DEE124209AA9E&key=ik0", + "extension": "mpd" + }, + { + "name": "Google Play (MP4,H264)", + "uri": "https://www.youtube.com/api/manifest/dash/id/3aa39fa2cc27967f/source/youtube?as=fmp4_audio_clear,fmp4_sd_hd_clear&sparams=ip,ipbits,expire,source,id,as&ip=0.0.0.0&ipbits=0&expire=19000000000&signature=A2716F75795F5D2AF0E88962FFCD10DB79384F29.84308FF04844498CE6FBCE4731507882B8307798&key=ik0", + "extension": "mpd" + }, + { + "name": "Google Glass (WebM,VP9)", + "uri": "https://www.youtube.com/api/manifest/dash/id/bf5bb2419360daf1/source/youtube?as=fmp4_audio_clear,webm2_sd_hd_clear&sparams=ip,ipbits,expire,source,id,as&ip=0.0.0.0&ipbits=0&expire=19000000000&signature=249B04F79E984D7F86B4D8DB48AE6FAF41C17AB3.7B9F0EC0505E1566E59B8E488E9419F253DDF413&key=ik0", + "extension": "mpd" + }, + { + "name": "Google Play (WebM,VP9)", + "uri": "https://www.youtube.com/api/manifest/dash/id/3aa39fa2cc27967f/source/youtube?as=fmp4_audio_clear,webm2_sd_hd_clear&sparams=ip,ipbits,expire,source,id,as&ip=0.0.0.0&ipbits=0&expire=19000000000&signature=B1C2A74783AC1CC4865EB312D7DD2D48230CC9FD.BD153B9882175F1F94BFE5141A5482313EA38E8D&key=ik0", + "extension": "mpd" + } + ] + }, + { + "name": "Widevine DASH Policy Tests (GTS)", + "samples": [ + { + "name": "WV: HDCP not specified", + "uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd", + "drm_scheme": "widevine", + "drm_license_url": "https://proxy.uat.widevine.com/proxy?video_id=d286538032258a1c&provider=widevine_test" + }, + { + "name": "WV: HDCP not required", + "uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd", + "drm_scheme": "widevine", + "drm_license_url": "https://proxy.uat.widevine.com/proxy?video_id=48fcc369939ac96c&provider=widevine_test" + }, + { + "name": "WV: HDCP required", + "uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd", + "drm_scheme": "widevine", + "drm_license_url": "https://proxy.uat.widevine.com/proxy?video_id=e06c39f1151da3df&provider=widevine_test" + }, + { + "name": "WV: Secure video path required (MP4,H264)", + "uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd", + "drm_scheme": "widevine", + "drm_license_url": "https://proxy.uat.widevine.com/proxy?video_id=0894c7c8719b28a0&provider=widevine_test" + }, + { + "name": "WV: Secure video path required (WebM,VP9)", + "uri": "https://storage.googleapis.com/wvmedia/cenc/vp9/tears/tears.mpd", + "drm_scheme": "widevine", + "drm_license_url": "https://proxy.uat.widevine.com/proxy?video_id=0894c7c8719b28a0&provider=widevine_test" + }, + { + "name": "WV: Secure video path required (MP4,H265)", + "uri": "https://storage.googleapis.com/wvmedia/cenc/hevc/tears/tears.mpd", + "drm_scheme": "widevine", + "drm_license_url": "https://proxy.uat.widevine.com/proxy?video_id=0894c7c8719b28a0&provider=widevine_test" + }, + { + "name": "WV: HDCP + secure video path required", + "uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd", + "drm_scheme": "widevine", + "drm_license_url": "https://proxy.uat.widevine.com/proxy?video_id=efd045b1eb61888a&provider=widevine_test" + }, + { + "name": "WV: 30s license duration (fails at ~30s)", + "uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd", + "drm_scheme": "widevine", + "drm_license_url": "https://proxy.uat.widevine.com/proxy?video_id=f9a34cab7b05881a&provider=widevine_test" + } + ] + }, + { + "name": "Widevine HDCP Capabilities Tests", + "samples": [ + { + "name": "WV: HDCP: None (not required)", + "uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd", + "drm_scheme": "widevine", + "drm_license_url": "https://proxy.uat.widevine.com/proxy?video_id=HDCP_None&provider=widevine_test" + }, + { + "name": "WV: HDCP: 1.0 required", + "uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd", + "drm_scheme": "widevine", + "drm_license_url": "https://proxy.uat.widevine.com/proxy?video_id=HDCP_V1&provider=widevine_test" + }, + { + "name": "WV: HDCP: 2.0 required", + "uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd", + "drm_scheme": "widevine", + "drm_license_url": "https://proxy.uat.widevine.com/proxy?video_id=HDCP_V2&provider=widevine_test" + }, + { + "name": "WV: HDCP: 2.1 required", + "uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd", + "drm_scheme": "widevine", + "drm_license_url": "https://proxy.uat.widevine.com/proxy?video_id=HDCP_V2_1&provider=widevine_test" + }, + { + "name": "WV: HDCP: 2.2 required", + "uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd", + "drm_scheme": "widevine", + "drm_license_url": "https://proxy.uat.widevine.com/proxy?video_id=HDCP_V2_2&provider=widevine_test" + }, + { + "name": "WV: HDCP: No digital output", + "uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd", + "drm_scheme": "widevine", + "drm_license_url": "https://proxy.uat.widevine.com/proxy?video_id=HDCP_NO_DIGTAL_OUTPUT&provider=widevine_test" + } + ] + }, + { + "name": "Widevine DASH: MP4,H264", + "samples": [ + { + "name": "WV: Clear SD & HD (MP4,H264)", + "uri": "https://storage.googleapis.com/wvmedia/clear/h264/tears/tears.mpd" + }, + { + "name": "WV: Clear SD (MP4,H264)", + "uri": "https://storage.googleapis.com/wvmedia/clear/h264/tears/tears_sd.mpd" + }, + { + "name": "WV: Clear HD (MP4,H264)", + "uri": "https://storage.googleapis.com/wvmedia/clear/h264/tears/tears_hd.mpd" + }, + { + "name": "WV: Clear UHD (MP4,H264)", + "uri": "https://storage.googleapis.com/wvmedia/clear/h264/tears/tears_uhd.mpd" + }, + { + "name": "WV: Secure SD & HD (cenc,MP4,H264)", + "uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd", + "drm_scheme": "widevine", + "drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test" + }, + { + "name": "WV: Secure SD (cenc,MP4,H264)", + "uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears_sd.mpd", + "drm_scheme": "widevine", + "drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test" + }, + { + "name": "WV: Secure HD (cenc,MP4,H264)", + "uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears_hd.mpd", + "drm_scheme": "widevine", + "drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test" + }, + { + "name": "WV: Secure UHD (cenc,MP4,H264)", + "uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears_uhd.mpd", + "drm_scheme": "widevine", + "drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test" + }, + { + "name": "WV: Secure SD & HD (cbc1,MP4,H264)", + "uri": "https://storage.googleapis.com/wvmedia/cbc1/h264/tears/tears_aes_cbc1.mpd", + "drm_scheme": "widevine", + "drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test" + }, + { + "name": "WV: Secure SD (cbc1,MP4,H264)", + "uri": "https://storage.googleapis.com/wvmedia/cbc1/h264/tears/tears_aes_cbc1_sd.mpd", + "drm_scheme": "widevine", + "drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test" + }, + { + "name": "WV: Secure HD (cbc1,MP4,H264)", + "uri": "https://storage.googleapis.com/wvmedia/cbc1/h264/tears/tears_aes_cbc1_hd.mpd", + "drm_scheme": "widevine", + "drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test" + }, + { + "name": "WV: Secure UHD (cbc1,MP4,H264)", + "uri": "https://storage.googleapis.com/wvmedia/cbc1/h264/tears/tears_aes_cbc1_uhd.mpd", + "drm_scheme": "widevine", + "drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test" + }, + { + "name": "WV: Secure SD & HD (cbcs,MP4,H264)", + "uri": "https://storage.googleapis.com/wvmedia/cbcs/h264/tears/tears_aes_cbcs.mpd", + "drm_scheme": "widevine", + "drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test" + }, + { + "name": "WV: Secure SD (cbcs,MP4,H264)", + "uri": "https://storage.googleapis.com/wvmedia/cbcs/h264/tears/tears_aes_cbcs_sd.mpd", + "drm_scheme": "widevine", + "drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test" + }, + { + "name": "WV: Secure HD (cbcs,MP4,H264)", + "uri": "https://storage.googleapis.com/wvmedia/cbcs/h264/tears/tears_aes_cbcs_hd.mpd", + "drm_scheme": "widevine", + "drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test" + }, + { + "name": "WV: Secure UHD (cbcs,MP4,H264)", + "uri": "https://storage.googleapis.com/wvmedia/cbcs/h264/tears/tears_aes_cbcs_uhd.mpd", + "drm_scheme": "widevine", + "drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test" + }, + { + "name": "WV: Secure and Clear SD & HD (cenc,MP4,H264)", + "uri": "https://storage.googleapis.com/exoplayer-test-media-1/widevine/tears_enc_clear_enc.mpd", + "drm_scheme": "widevine", + "drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test", + "drm_session_for_clear_types": ["audio", "video"] + } + ] + }, + { + "name": "Widevine DASH: WebM,VP9", + "samples": [ + { + "name": "WV: Clear SD & HD (WebM,VP9)", + "uri": "https://storage.googleapis.com/wvmedia/clear/vp9/tears/tears.mpd" + }, + { + "name": "WV: Clear SD (WebM,VP9)", + "uri": "https://storage.googleapis.com/wvmedia/clear/vp9/tears/tears_sd.mpd" + }, + { + "name": "WV: Clear HD (WebM,VP9)", + "uri": "https://storage.googleapis.com/wvmedia/clear/vp9/tears/tears_hd.mpd" + }, + { + "name": "WV: Clear UHD (WebM,VP9)", + "uri": "https://storage.googleapis.com/wvmedia/clear/vp9/tears/tears_uhd.mpd" + }, + { + "name": "WV: Secure Fullsample SD & HD (WebM,VP9)", + "uri": "https://storage.googleapis.com/wvmedia/cenc/vp9/tears/tears.mpd", + "drm_scheme": "widevine", + "drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test" + }, + { + "name": "WV: Secure Fullsample SD (WebM,VP9)", + "uri": "https://storage.googleapis.com/wvmedia/cenc/vp9/tears/tears_sd.mpd", + "drm_scheme": "widevine", + "drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test" + }, + { + "name": "WV: Secure Fullsample HD (WebM,VP9)", + "uri": "https://storage.googleapis.com/wvmedia/cenc/vp9/tears/tears_hd.mpd", + "drm_scheme": "widevine", + "drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test" + }, + { + "name": "WV: Secure Fullsample UHD (WebM,VP9)", + "uri": "https://storage.googleapis.com/wvmedia/cenc/vp9/tears/tears_uhd.mpd", + "drm_scheme": "widevine", + "drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test" + }, + { + "name": "WV: Secure Subsample SD & HD (WebM,VP9)", + "uri": "https://storage.googleapis.com/wvmedia/cenc/vp9/subsample/24fps/tears/tears.mpd", + "drm_scheme": "widevine", + "drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test" + }, + { + "name": "WV: Secure Subsample SD (WebM,VP9)", + "uri": "https://storage.googleapis.com/wvmedia/cenc/vp9/subsample/24fps/tears/tears_sd.mpd", + "drm_scheme": "widevine", + "drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test" + }, + { + "name": "WV: Secure Subsample HD (WebM,VP9)", + "uri": "https://storage.googleapis.com/wvmedia/cenc/vp9/subsample/24fps/tears/tears_hd.mpd", + "drm_scheme": "widevine", + "drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test" + }, + { + "name": "WV: Secure Subsample UHD (WebM,VP9)", + "uri": "https://storage.googleapis.com/wvmedia/cenc/vp9/subsample/24fps/tears/tears_uhd.mpd", + "drm_scheme": "widevine", + "drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test" + } + ] + }, + { + "name": "Widevine DASH: MP4,H265", + "samples": [ + { + "name": "WV: Clear SD & HD (MP4,H265)", + "uri": "https://storage.googleapis.com/wvmedia/clear/hevc/tears/tears.mpd" + }, + { + "name": "WV: Clear SD (MP4,H265)", + "uri": "https://storage.googleapis.com/wvmedia/clear/hevc/tears/tears_sd.mpd" + }, + { + "name": "WV: Clear HD (MP4,H265)", + "uri": "https://storage.googleapis.com/wvmedia/clear/hevc/tears/tears_hd.mpd" + }, + { + "name": "WV: Clear UHD (MP4,H265)", + "uri": "https://storage.googleapis.com/wvmedia/clear/hevc/tears/tears_uhd.mpd" + }, + { + "name": "WV: Secure SD & HD (MP4,H265)", + "uri": "https://storage.googleapis.com/wvmedia/cenc/hevc/tears/tears.mpd", + "drm_scheme": "widevine", + "drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test" + }, + { + "name": "WV: Secure SD (MP4,H265)", + "uri": "https://storage.googleapis.com/wvmedia/cenc/hevc/tears/tears_sd.mpd", + "drm_scheme": "widevine", + "drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test" + }, + { + "name": "WV: Secure HD (MP4,H265)", + "uri": "https://storage.googleapis.com/wvmedia/cenc/hevc/tears/tears_hd.mpd", + "drm_scheme": "widevine", + "drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test" + }, + { + "name": "WV: Secure UHD (MP4,H265)", + "uri": "https://storage.googleapis.com/wvmedia/cenc/hevc/tears/tears_uhd.mpd", + "drm_scheme": "widevine", + "drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test" + } + ] + }, + { + "name": "SmoothStreaming", + "samples": [ + { + "name": "Super speed", + "uri": "https://playready.directtaps.net/smoothstreaming/SSWSS720H264/SuperSpeedway_720.ism/Manifest" + }, + { + "name": "Super speed (PlayReady)", + "uri": "https://playready.directtaps.net/smoothstreaming/SSWSS720H264PR/SuperSpeedway_720.ism/Manifest", + "drm_scheme": "playready" + } + ] + }, + { + "name": "HLS", + "samples": [ + { + "name": "Apple 4x3 basic stream", + "uri": "https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_4x3/bipbop_4x3_variant.m3u8" + }, + { + "name": "Apple 16x9 basic stream", + "uri": "https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_16x9/bipbop_16x9_variant.m3u8" + }, + { + "name": "Apple master playlist advanced (TS)", + "uri": "https://devstreaming-cdn.apple.com/videos/streaming/examples/img_bipbop_adv_example_ts/master.m3u8" + }, + { + "name": "Apple master playlist advanced (fMP4)", + "uri": "https://devstreaming-cdn.apple.com/videos/streaming/examples/img_bipbop_adv_example_fmp4/master.m3u8" + }, + { + "name": "Apple TS media playlist", + "uri": "https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_4x3/gear1/prog_index.m3u8" + }, + { + "name": "Apple AAC media playlist", + "uri": "https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_4x3/gear0/prog_index.m3u8" + } + ] + }, + { + "name": "Misc", + "samples": [ + { + "name": "Dizzy (MP4)", + "uri": "https://html5demos.com/assets/dizzy.mp4" + }, + { + "name": "Apple 10s (AAC)", + "uri": "https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_4x3/gear0/fileSequence0.aac" + }, + { + "name": "Apple 10s (TS)", + "uri": "https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_4x3/gear1/fileSequence0.ts" + }, + { + "name": "Android screens (MKV)", + "uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv" + }, + { + "name": "Screens 360p video (WebM,VP9)", + "uri": "https://storage.googleapis.com/exoplayer-test-media-1/gen-3/screens/dash-vod-single-segment/video-vp9-360.webm" + }, + { + "name": "Screens 480p video (FMP4,H264)", + "uri": "https://storage.googleapis.com/exoplayer-test-media-1/gen-3/screens/dash-vod-single-segment/video-avc-baseline-480.mp4" + }, + { + "name": "Screens 1080p video (FMP4,H264)", + "uri": "https://storage.googleapis.com/exoplayer-test-media-1/gen-3/screens/dash-vod-single-segment/video-137.mp4" + }, + { + "name": "Screens audio (FMP4)", + "uri": "https://storage.googleapis.com/exoplayer-test-media-1/gen-3/screens/dash-vod-single-segment/audio-141.mp4" + }, + { + "name": "Google Play (MP3)", + "uri": "https://storage.googleapis.com/exoplayer-test-media-0/play.mp3" + }, + { + "name": "Google Play (Ogg/Vorbis)", + "uri": "https://storage.googleapis.com/exoplayer-test-media-1/ogg/play.ogg" + }, + { + "name": "Google Play (FLAC)", + "uri": "https://storage.googleapis.com/exoplayer-test-media-1/flac/play.flac" + }, + { + "name": "Big Buck Bunny video (FLV)", + "uri": "https://vod.leasewebcdn.com/bbb.flv?ri=1024&rs=150&start=0" + }, + { + "name": "Big Buck Bunny 480p video (MP4,AV1)", + "uri": "https://storage.googleapis.com/downloads.webmproject.org/av1/exoplayer/bbb-av1-480p.mp4" + } + ] + }, + { + "name": "Playlists", + "samples": [ + { + "name": "Cats -> Dogs", + "playlist": [ + { + "uri": "https://html5demos.com/assets/dizzy.mp4" + }, + { + "uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv" + } + ] + }, + { + "name": "Audio -> Video -> Audio", + "playlist": [ + { + "uri": "https://storage.googleapis.com/exoplayer-test-media-1/gen-3/screens/dash-vod-single-segment/audio-141.mp4" + }, + { + "uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv" + }, + { + "uri": "https://storage.googleapis.com/exoplayer-test-media-1/gen-3/screens/dash-vod-single-segment/audio-141.mp4" + } + ] + }, + { + "name": "Clear -> Enc -> Clear -> Enc -> Enc", + "playlist": [ + { + "uri": "https://html5demos.com/assets/dizzy.mp4" + }, + { + "uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears_sd.mpd", + "drm_scheme": "widevine", + "drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test" + }, + { + "uri": "https://html5demos.com/assets/dizzy.mp4" + }, + { + "uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears_sd.mpd", + "drm_scheme": "widevine", + "drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test" + }, + { + "uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears_sd.mpd", + "drm_scheme": "widevine", + "drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test" + } + ] + } + ] + }, + { + "name": "IMA sample ad tags", + "samples": [ + { + "name": "Single inline linear", + "uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv", + "ad_tag_uri": "https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/single_ad_samples&ciu_szs=300x250&impl=s&gdfp_req=1&env=vp&output=vast&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ct%3Dlinear&correlator=" + }, + { + "name": "Single skippable inline", + "uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv", + "ad_tag_uri": "https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/single_ad_samples&ciu_szs=300x250&impl=s&gdfp_req=1&env=vp&output=vast&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ct%3Dskippablelinear&correlator=" + }, + { + "name": "Single redirect linear", + "uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv", + "ad_tag_uri": "https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/single_ad_samples&ciu_szs=300x250&impl=s&gdfp_req=1&env=vp&output=vast&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ct%3Dredirectlinear&correlator=" + }, + { + "name": "Single redirect error", + "uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv", + "ad_tag_uri": "https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/single_ad_samples&ciu_szs=300x250&impl=s&gdfp_req=1&env=vp&output=vast&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ct%3Dredirecterror&nofb=1&correlator=" + }, + { + "name": "Single redirect broken (fallback)", + "uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv", + "ad_tag_uri": "https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/single_ad_samples&ciu_szs=300x250&impl=s&gdfp_req=1&env=vp&output=vast&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ct%3Dredirecterror&correlator=" + }, + { + "name": "VMAP pre-roll", + "uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv", + "ad_tag_uri": "https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/ad_rule_samples&ciu_szs=300x250&ad_rule=1&impl=s&gdfp_req=1&env=vp&output=vmap&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ar%3Dpreonly&cmsid=496&vid=short_onecue&correlator=" + }, + { + "name": "VMAP pre-roll + bumper", + "uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv", + "ad_tag_uri": "https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/ad_rule_samples&ciu_szs=300x250&ad_rule=1&impl=s&gdfp_req=1&env=vp&output=vmap&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ar%3Dpreonlybumper&cmsid=496&vid=short_onecue&correlator=" + }, + { + "name": "VMAP post-roll", + "uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv", + "ad_tag_uri": "https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/ad_rule_samples&ciu_szs=300x250&ad_rule=1&impl=s&gdfp_req=1&env=vp&output=vmap&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ar%3Dpostonly&cmsid=496&vid=short_onecue&correlator=" + }, + { + "name": "VMAP post-roll + bumper", + "uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv", + "ad_tag_uri": "https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/ad_rule_samples&ciu_szs=300x250&ad_rule=1&impl=s&gdfp_req=1&env=vp&output=vmap&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ar%3Dpostonlybumper&cmsid=496&vid=short_onecue&correlator=" + }, + { + "name": "VMAP pre-, mid- and post-rolls, single ads", + "uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv", + "ad_tag_uri": "https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/ad_rule_samples&ciu_szs=300x250&ad_rule=1&impl=s&gdfp_req=1&env=vp&output=vmap&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ar%3Dpremidpost&cmsid=496&vid=short_onecue&correlator=" + }, + { + "name": "VMAP pre-roll single ad, mid-roll standard pod with 3 ads, post-roll single ad", + "uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv", + "ad_tag_uri": "https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/ad_rule_samples&ciu_szs=300x250&ad_rule=1&impl=s&gdfp_req=1&env=vp&output=vmap&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ar%3Dpremidpostpod&cmsid=496&vid=short_onecue&correlator=" + }, + { + "name": "VMAP pre-roll single ad, mid-roll optimized pod with 3 ads, post-roll single ad", + "uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv", + "ad_tag_uri": "https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/ad_rule_samples&ciu_szs=300x250&ad_rule=1&impl=s&gdfp_req=1&env=vp&output=vmap&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ar%3Dpremidpostoptimizedpod&cmsid=496&vid=short_onecue&correlator=" + }, + { + "name": "VMAP pre-roll single ad, mid-roll standard pod with 3 ads, post-roll single ad (bumpers around all ad breaks)", + "uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv", + "ad_tag_uri": "https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/ad_rule_samples&ciu_szs=300x250&ad_rule=1&impl=s&gdfp_req=1&env=vp&output=vmap&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ar%3Dpremidpostpodbumper&cmsid=496&vid=short_onecue&correlator=" + }, + { + "name": "VMAP pre-roll single ad, mid-roll optimized pod with 3 ads, post-roll single ad (bumpers around all ad breaks)", + "uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv", + "ad_tag_uri": "https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/ad_rule_samples&ciu_szs=300x250&ad_rule=1&impl=s&gdfp_req=1&env=vp&output=vmap&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ar%3Dpremidpostoptimizedpodbumper&cmsid=496&vid=short_onecue&correlator=" + }, + { + "name": "VMAP pre-roll single ad, mid-roll standard pods with 5 ads every 10 seconds for 1:40, post-roll single ad", + "uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv", + "ad_tag_uri": "https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/ad_rule_samples&ciu_szs=300x250&ad_rule=1&impl=s&gdfp_req=1&env=vp&output=vmap&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ar%3Dpremidpostlongpod&cmsid=496&vid=short_tencue&correlator=" + }, + { + "name": "VMAP empty midroll", + "uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv", + "ad_tag_uri": "https://vastsynthesizer.appspot.com/empty-midroll" + }, + { + "name": "VMAP full, empty, full midrolls", + "uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv", + "ad_tag_uri": "https://vastsynthesizer.appspot.com/empty-midroll-2" + } + ] + }, + { + "name": "360", + "samples": [ + { + "name": "Congo (360 top-bottom stereo)", + "uri": "https://storage.googleapis.com/exoplayer-test-media-1/360/congo.mp4", + "spherical_stereo_mode": "top_bottom" + }, + { + "name": "Sphericalv2 (180 top-bottom stereo)", + "uri": "https://storage.googleapis.com/exoplayer-test-media-1/360/sphericalv2.mp4", + "spherical_stereo_mode": "top_bottom" + }, + { + "name": "Iceland (360 top-bottom stereo ts)", + "uri": "https://storage.googleapis.com/exoplayer-test-media-1/360/iceland0.ts", + "spherical_stereo_mode": "top_bottom" + } + ] + }, + { + "name": "Subtitles", + "samples": [ + { + "name": "TTML", + "uri": "https://html5demos.com/assets/dizzy.mp4", + "subtitle_uri": "https://storage.googleapis.com/exoplayer-test-media-1/ttml/netflix_ttml_sample.xml", + "subtitle_mime_type": "application/ttml+xml", + "subtitle_language": "en" + }, + { + "name": "SSA/ASS position & alignment", + "uri": "https://storage.googleapis.com/exoplayer-test-media-1/gen-3/screens/dash-vod-single-segment/video-avc-baseline-480.mp4", + "subtitle_uri": "https://storage.googleapis.com/exoplayer-test-media-1/ssa/test-subs-position.ass", + "subtitle_mime_type": "text/x-ssa", + "subtitle_language": "en" + } + ] + } +] diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/DemoApplication.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/DemoApplication.java new file mode 100644 index 0000000000..d83d7076c5 --- /dev/null +++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/DemoApplication.java @@ -0,0 +1,173 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2.demo; + +import android.app.Application; +import com.google.android.exoplayer2.DefaultRenderersFactory; +import com.google.android.exoplayer2.RenderersFactory; +import com.google.android.exoplayer2.database.DatabaseProvider; +import com.google.android.exoplayer2.database.ExoDatabaseProvider; +import com.google.android.exoplayer2.offline.ActionFileUpgradeUtil; +import com.google.android.exoplayer2.offline.DefaultDownloadIndex; +import com.google.android.exoplayer2.offline.DefaultDownloaderFactory; +import com.google.android.exoplayer2.offline.DownloadManager; +import com.google.android.exoplayer2.offline.DownloaderConstructorHelper; +import com.google.android.exoplayer2.upstream.DataSource; +import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory; +import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory; +import com.google.android.exoplayer2.upstream.FileDataSource; +import com.google.android.exoplayer2.upstream.HttpDataSource; +import com.google.android.exoplayer2.upstream.cache.Cache; +import com.google.android.exoplayer2.upstream.cache.CacheDataSource; +import com.google.android.exoplayer2.upstream.cache.CacheDataSourceFactory; +import com.google.android.exoplayer2.upstream.cache.NoOpCacheEvictor; +import com.google.android.exoplayer2.upstream.cache.SimpleCache; +import com.google.android.exoplayer2.util.Log; +import com.google.android.exoplayer2.util.Util; +import java.io.File; +import java.io.IOException; + +/** + * Placeholder application to facilitate overriding Application methods for debugging and testing. + */ +public class DemoApplication extends Application { + + private static final String TAG = "DemoApplication"; + private static final String DOWNLOAD_ACTION_FILE = "actions"; + private static final String DOWNLOAD_TRACKER_ACTION_FILE = "tracked_actions"; + private static final String DOWNLOAD_CONTENT_DIRECTORY = "downloads"; + + protected String userAgent; + + private DatabaseProvider databaseProvider; + private File downloadDirectory; + private Cache downloadCache; + private DownloadManager downloadManager; + private DownloadTracker downloadTracker; + + @Override + public void onCreate() { + super.onCreate(); + userAgent = Util.getUserAgent(this, "ExoPlayerDemo"); + } + + /** Returns a {@link DataSource.Factory}. */ + public DataSource.Factory buildDataSourceFactory() { + DefaultDataSourceFactory upstreamFactory = + new DefaultDataSourceFactory(this, buildHttpDataSourceFactory()); + return buildReadOnlyCacheDataSource(upstreamFactory, getDownloadCache()); + } + + /** Returns a {@link HttpDataSource.Factory}. */ + public HttpDataSource.Factory buildHttpDataSourceFactory() { + return new DefaultHttpDataSourceFactory(userAgent); + } + + /** Returns whether extension renderers should be used. */ + public boolean useExtensionRenderers() { + return "withExtensions".equals(BuildConfig.FLAVOR); + } + + public RenderersFactory buildRenderersFactory(boolean preferExtensionRenderer) { + @DefaultRenderersFactory.ExtensionRendererMode + int extensionRendererMode = + useExtensionRenderers() + ? (preferExtensionRenderer + ? DefaultRenderersFactory.EXTENSION_RENDERER_MODE_PREFER + : DefaultRenderersFactory.EXTENSION_RENDERER_MODE_ON) + : DefaultRenderersFactory.EXTENSION_RENDERER_MODE_OFF; + return new DefaultRenderersFactory(/* context= */ this) + .setExtensionRendererMode(extensionRendererMode); + } + + public DownloadManager getDownloadManager() { + initDownloadManager(); + return downloadManager; + } + + public DownloadTracker getDownloadTracker() { + initDownloadManager(); + return downloadTracker; + } + + protected synchronized Cache getDownloadCache() { + if (downloadCache == null) { + File downloadContentDirectory = new File(getDownloadDirectory(), DOWNLOAD_CONTENT_DIRECTORY); + downloadCache = + new SimpleCache(downloadContentDirectory, new NoOpCacheEvictor(), getDatabaseProvider()); + } + return downloadCache; + } + + private synchronized void initDownloadManager() { + if (downloadManager == null) { + DefaultDownloadIndex downloadIndex = new DefaultDownloadIndex(getDatabaseProvider()); + upgradeActionFile( + DOWNLOAD_ACTION_FILE, downloadIndex, /* addNewDownloadsAsCompleted= */ false); + upgradeActionFile( + DOWNLOAD_TRACKER_ACTION_FILE, downloadIndex, /* addNewDownloadsAsCompleted= */ true); + DownloaderConstructorHelper downloaderConstructorHelper = + new DownloaderConstructorHelper(getDownloadCache(), buildHttpDataSourceFactory()); + downloadManager = + new DownloadManager( + this, downloadIndex, new DefaultDownloaderFactory(downloaderConstructorHelper)); + downloadTracker = + new DownloadTracker(/* context= */ this, buildDataSourceFactory(), downloadManager); + } + } + + private void upgradeActionFile( + String fileName, DefaultDownloadIndex downloadIndex, boolean addNewDownloadsAsCompleted) { + try { + ActionFileUpgradeUtil.upgradeAndDelete( + new File(getDownloadDirectory(), fileName), + /* downloadIdProvider= */ null, + downloadIndex, + /* deleteOnFailure= */ true, + addNewDownloadsAsCompleted); + } catch (IOException e) { + Log.e(TAG, "Failed to upgrade action file: " + fileName, e); + } + } + + private DatabaseProvider getDatabaseProvider() { + if (databaseProvider == null) { + databaseProvider = new ExoDatabaseProvider(this); + } + return databaseProvider; + } + + private File getDownloadDirectory() { + if (downloadDirectory == null) { + downloadDirectory = getExternalFilesDir(null); + if (downloadDirectory == null) { + downloadDirectory = getFilesDir(); + } + } + return downloadDirectory; + } + + protected static CacheDataSourceFactory buildReadOnlyCacheDataSource( + DataSource.Factory upstreamFactory, Cache cache) { + return new CacheDataSourceFactory( + cache, + upstreamFactory, + new FileDataSource.Factory(), + /* cacheWriteDataSinkFactory= */ null, + CacheDataSource.FLAG_IGNORE_CACHE_ON_ERROR, + /* eventListener= */ null); + } +} diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/DemoDownloadService.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/DemoDownloadService.java new file mode 100644 index 0000000000..be2863d4eb --- /dev/null +++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/DemoDownloadService.java @@ -0,0 +1,116 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2.demo; + +import android.app.Notification; +import android.content.Context; +import com.google.android.exoplayer2.offline.Download; +import com.google.android.exoplayer2.offline.DownloadManager; +import com.google.android.exoplayer2.offline.DownloadService; +import com.google.android.exoplayer2.scheduler.PlatformScheduler; +import com.google.android.exoplayer2.ui.DownloadNotificationHelper; +import com.google.android.exoplayer2.util.NotificationUtil; +import com.google.android.exoplayer2.util.Util; +import java.util.List; + +/** A service for downloading media. */ +public class DemoDownloadService extends DownloadService { + + private static final String CHANNEL_ID = "download_channel"; + private static final int JOB_ID = 1; + private static final int FOREGROUND_NOTIFICATION_ID = 1; + + private DownloadNotificationHelper notificationHelper; + + public DemoDownloadService() { + super( + FOREGROUND_NOTIFICATION_ID, + DEFAULT_FOREGROUND_NOTIFICATION_UPDATE_INTERVAL, + CHANNEL_ID, + R.string.exo_download_notification_channel_name, + /* channelDescriptionResourceId= */ 0); + } + + @Override + public void onCreate() { + super.onCreate(); + notificationHelper = new DownloadNotificationHelper(this, CHANNEL_ID); + } + + @Override + protected DownloadManager getDownloadManager() { + DownloadManager downloadManager = ((DemoApplication) getApplication()).getDownloadManager(); + // This will only happen once, because getDownloadManager is guaranteed to be called only once + // in the life cycle of the process. + downloadManager.addListener( + new TerminalStateNotificationHelper( + this, notificationHelper, FOREGROUND_NOTIFICATION_ID + 1)); + return downloadManager; + } + + @Override + protected PlatformScheduler getScheduler() { + return Util.SDK_INT >= 21 ? new PlatformScheduler(this, JOB_ID) : null; + } + + @Override + protected Notification getForegroundNotification(List downloads) { + return notificationHelper.buildProgressNotification( + 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 manager, Download download) { + Notification notification; + if (download.state == Download.STATE_COMPLETED) { + notification = + notificationHelper.buildDownloadCompletedNotification( + R.drawable.ic_download_done, + /* contentIntent= */ null, + Util.fromUtf8Bytes(download.request.data)); + } else if (download.state == Download.STATE_FAILED) { + notification = + notificationHelper.buildDownloadFailedNotification( + R.drawable.ic_download_done, + /* contentIntent= */ null, + Util.fromUtf8Bytes(download.request.data)); + } else { + return; + } + NotificationUtil.setNotification(context, nextNotificationId++, notification); + } + } +} diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/DownloadTracker.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/DownloadTracker.java new file mode 100644 index 0000000000..143eda93df --- /dev/null +++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/DownloadTracker.java @@ -0,0 +1,274 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2.demo; + +import android.content.Context; +import android.content.DialogInterface; +import android.net.Uri; +import android.widget.Toast; +import androidx.annotation.Nullable; +import androidx.fragment.app.FragmentManager; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.RenderersFactory; +import com.google.android.exoplayer2.offline.Download; +import com.google.android.exoplayer2.offline.DownloadCursor; +import com.google.android.exoplayer2.offline.DownloadHelper; +import com.google.android.exoplayer2.offline.DownloadIndex; +import com.google.android.exoplayer2.offline.DownloadManager; +import com.google.android.exoplayer2.offline.DownloadRequest; +import com.google.android.exoplayer2.offline.DownloadService; +import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; +import com.google.android.exoplayer2.trackselection.MappingTrackSelector.MappedTrackInfo; +import com.google.android.exoplayer2.upstream.DataSource; +import com.google.android.exoplayer2.util.Log; +import com.google.android.exoplayer2.util.Util; +import java.io.IOException; +import java.util.HashMap; +import java.util.concurrent.CopyOnWriteArraySet; + +/** Tracks media that has been downloaded. */ +public class DownloadTracker { + + /** Listens for changes in the tracked downloads. */ + public interface Listener { + + /** Called when the tracked downloads changed. */ + void onDownloadsChanged(); + } + + private static final String TAG = "DownloadTracker"; + + private final Context context; + private final DataSource.Factory dataSourceFactory; + private final CopyOnWriteArraySet listeners; + private final HashMap downloads; + private final DownloadIndex downloadIndex; + private final DefaultTrackSelector.Parameters trackSelectorParameters; + + @Nullable private StartDownloadDialogHelper startDownloadDialogHelper; + + public DownloadTracker( + Context context, DataSource.Factory dataSourceFactory, DownloadManager downloadManager) { + this.context = context.getApplicationContext(); + this.dataSourceFactory = dataSourceFactory; + listeners = new CopyOnWriteArraySet<>(); + downloads = new HashMap<>(); + downloadIndex = downloadManager.getDownloadIndex(); + trackSelectorParameters = DownloadHelper.getDefaultTrackSelectorParameters(context); + downloadManager.addListener(new DownloadManagerListener()); + loadDownloads(); + } + + public void addListener(Listener listener) { + listeners.add(listener); + } + + public void removeListener(Listener listener) { + listeners.remove(listener); + } + + public boolean isDownloaded(Uri uri) { + Download download = downloads.get(uri); + return download != null && download.state != Download.STATE_FAILED; + } + + public DownloadRequest getDownloadRequest(Uri uri) { + Download download = downloads.get(uri); + return download != null && download.state != Download.STATE_FAILED ? download.request : null; + } + + public void toggleDownload( + FragmentManager fragmentManager, + String name, + Uri uri, + String extension, + RenderersFactory renderersFactory) { + Download download = downloads.get(uri); + if (download != null) { + DownloadService.sendRemoveDownload( + context, DemoDownloadService.class, download.request.id, /* foreground= */ false); + } else { + if (startDownloadDialogHelper != null) { + startDownloadDialogHelper.release(); + } + startDownloadDialogHelper = + new StartDownloadDialogHelper( + fragmentManager, getDownloadHelper(uri, extension, renderersFactory), name); + } + } + + private void loadDownloads() { + try (DownloadCursor loadedDownloads = downloadIndex.getDownloads()) { + while (loadedDownloads.moveToNext()) { + Download download = loadedDownloads.getDownload(); + downloads.put(download.request.uri, download); + } + } catch (IOException e) { + Log.w(TAG, "Failed to query downloads", e); + } + } + + private DownloadHelper getDownloadHelper( + Uri uri, String extension, RenderersFactory renderersFactory) { + int type = Util.inferContentType(uri, extension); + switch (type) { + case C.TYPE_DASH: + return DownloadHelper.forDash(context, uri, dataSourceFactory, renderersFactory); + case C.TYPE_SS: + return DownloadHelper.forSmoothStreaming(context, uri, dataSourceFactory, renderersFactory); + case C.TYPE_HLS: + return DownloadHelper.forHls(context, uri, dataSourceFactory, renderersFactory); + case C.TYPE_OTHER: + return DownloadHelper.forProgressive(context, uri); + default: + throw new IllegalStateException("Unsupported type: " + type); + } + } + + private class DownloadManagerListener implements DownloadManager.Listener { + + @Override + public void onDownloadChanged(DownloadManager downloadManager, Download download) { + downloads.put(download.request.uri, download); + for (Listener listener : listeners) { + listener.onDownloadsChanged(); + } + } + + @Override + public void onDownloadRemoved(DownloadManager downloadManager, Download download) { + downloads.remove(download.request.uri); + for (Listener listener : listeners) { + listener.onDownloadsChanged(); + } + } + } + + private final class StartDownloadDialogHelper + implements DownloadHelper.Callback, + DialogInterface.OnClickListener, + DialogInterface.OnDismissListener { + + private final FragmentManager fragmentManager; + private final DownloadHelper downloadHelper; + private final String name; + + private TrackSelectionDialog trackSelectionDialog; + private MappedTrackInfo mappedTrackInfo; + + public StartDownloadDialogHelper( + FragmentManager fragmentManager, DownloadHelper downloadHelper, String name) { + this.fragmentManager = fragmentManager; + this.downloadHelper = downloadHelper; + this.name = name; + downloadHelper.prepare(this); + } + + public void release() { + downloadHelper.release(); + if (trackSelectionDialog != null) { + trackSelectionDialog.dismiss(); + } + } + + // DownloadHelper.Callback implementation. + + @Override + public void onPrepared(DownloadHelper helper) { + if (helper.getPeriodCount() == 0) { + Log.d(TAG, "No periods found. Downloading entire stream."); + startDownload(); + downloadHelper.release(); + return; + } + mappedTrackInfo = downloadHelper.getMappedTrackInfo(/* periodIndex= */ 0); + if (!TrackSelectionDialog.willHaveContent(mappedTrackInfo)) { + Log.d(TAG, "No dialog content. Downloading entire stream."); + startDownload(); + downloadHelper.release(); + return; + } + trackSelectionDialog = + TrackSelectionDialog.createForMappedTrackInfoAndParameters( + /* titleId= */ R.string.exo_download_description, + mappedTrackInfo, + trackSelectorParameters, + /* allowAdaptiveSelections =*/ false, + /* allowMultipleOverrides= */ true, + /* onClickListener= */ this, + /* onDismissListener= */ this); + trackSelectionDialog.show(fragmentManager, /* tag= */ null); + } + + @Override + public void onPrepareError(DownloadHelper helper, IOException e) { + Toast.makeText(context, R.string.download_start_error, Toast.LENGTH_LONG).show(); + Log.e( + TAG, + e instanceof DownloadHelper.LiveContentUnsupportedException + ? "Downloading live content unsupported" + : "Failed to start download", + e); + } + + // DialogInterface.OnClickListener implementation. + + @Override + public void onClick(DialogInterface dialog, int which) { + for (int periodIndex = 0; periodIndex < downloadHelper.getPeriodCount(); periodIndex++) { + downloadHelper.clearTrackSelections(periodIndex); + for (int i = 0; i < mappedTrackInfo.getRendererCount(); i++) { + if (!trackSelectionDialog.getIsDisabled(/* rendererIndex= */ i)) { + downloadHelper.addTrackSelectionForSingleRenderer( + periodIndex, + /* rendererIndex= */ i, + trackSelectorParameters, + trackSelectionDialog.getOverrides(/* rendererIndex= */ i)); + } + } + } + DownloadRequest downloadRequest = buildDownloadRequest(); + if (downloadRequest.streamKeys.isEmpty()) { + // All tracks were deselected in the dialog. Don't start the download. + return; + } + startDownload(downloadRequest); + } + + // DialogInterface.OnDismissListener implementation. + + @Override + public void onDismiss(DialogInterface dialogInterface) { + trackSelectionDialog = null; + downloadHelper.release(); + } + + // Internal methods. + + private void startDownload() { + startDownload(buildDownloadRequest()); + } + + private void startDownload(DownloadRequest downloadRequest) { + DownloadService.sendAddDownload( + context, DemoDownloadService.class, downloadRequest, /* foreground= */ false); + } + + private DownloadRequest buildDownloadRequest() { + return downloadHelper.getDownloadRequest(Util.getUtf8Bytes(name)); + } + } +} diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java new file mode 100644 index 0000000000..6aa56341f5 --- /dev/null +++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java @@ -0,0 +1,764 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2.demo; + +import android.content.Intent; +import android.content.pm.PackageManager; +import android.media.MediaDrm; +import android.net.Uri; +import android.os.Bundle; +import android.util.Pair; +import android.view.KeyEvent; +import android.view.View; +import android.view.View.OnClickListener; +import android.widget.Button; +import android.widget.LinearLayout; +import android.widget.TextView; +import android.widget.Toast; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.app.AppCompatActivity; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.C.ContentType; +import com.google.android.exoplayer2.ExoPlaybackException; +import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.PlaybackPreparer; +import com.google.android.exoplayer2.Player; +import com.google.android.exoplayer2.RenderersFactory; +import com.google.android.exoplayer2.SimpleExoPlayer; +import com.google.android.exoplayer2.demo.Sample.UriSample; +import com.google.android.exoplayer2.drm.DefaultDrmSessionManager; +import com.google.android.exoplayer2.drm.DrmSessionManager; +import com.google.android.exoplayer2.drm.ExoMediaCrypto; +import com.google.android.exoplayer2.drm.FrameworkMediaDrm; +import com.google.android.exoplayer2.drm.HttpMediaDrmCallback; +import com.google.android.exoplayer2.drm.MediaDrmCallback; +import com.google.android.exoplayer2.mediacodec.MediaCodecRenderer.DecoderInitializationException; +import com.google.android.exoplayer2.mediacodec.MediaCodecUtil.DecoderQueryException; +import com.google.android.exoplayer2.offline.DownloadHelper; +import com.google.android.exoplayer2.offline.DownloadRequest; +import com.google.android.exoplayer2.source.BehindLiveWindowException; +import com.google.android.exoplayer2.source.MediaSource; +import com.google.android.exoplayer2.source.MediaSourceFactory; +import com.google.android.exoplayer2.source.MergingMediaSource; +import com.google.android.exoplayer2.source.ProgressiveMediaSource; +import com.google.android.exoplayer2.source.SingleSampleMediaSource; +import com.google.android.exoplayer2.source.TrackGroupArray; +import com.google.android.exoplayer2.source.ads.AdsLoader; +import com.google.android.exoplayer2.source.ads.AdsMediaSource; +import com.google.android.exoplayer2.source.dash.DashMediaSource; +import com.google.android.exoplayer2.source.hls.HlsMediaSource; +import com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource; +import com.google.android.exoplayer2.trackselection.AdaptiveTrackSelection; +import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; +import com.google.android.exoplayer2.trackselection.MappingTrackSelector.MappedTrackInfo; +import com.google.android.exoplayer2.trackselection.RandomTrackSelection; +import com.google.android.exoplayer2.trackselection.TrackSelection; +import com.google.android.exoplayer2.trackselection.TrackSelectionArray; +import com.google.android.exoplayer2.ui.DebugTextViewHelper; +import com.google.android.exoplayer2.ui.PlayerControlView; +import com.google.android.exoplayer2.ui.PlayerView; +import com.google.android.exoplayer2.ui.spherical.SphericalGLSurfaceView; +import com.google.android.exoplayer2.upstream.DataSource; +import com.google.android.exoplayer2.upstream.HttpDataSource; +import com.google.android.exoplayer2.util.ErrorMessageProvider; +import com.google.android.exoplayer2.util.EventLogger; +import com.google.android.exoplayer2.util.Util; +import java.lang.reflect.Constructor; +import java.net.CookieHandler; +import java.net.CookieManager; +import java.net.CookiePolicy; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** An activity that plays media using {@link SimpleExoPlayer}. */ +public class PlayerActivity extends AppCompatActivity + implements OnClickListener, PlaybackPreparer, PlayerControlView.VisibilityListener { + + // Activity extras. + + public static final String SPHERICAL_STEREO_MODE_EXTRA = "spherical_stereo_mode"; + public static final String SPHERICAL_STEREO_MODE_MONO = "mono"; + public static final String SPHERICAL_STEREO_MODE_TOP_BOTTOM = "top_bottom"; + public static final String SPHERICAL_STEREO_MODE_LEFT_RIGHT = "left_right"; + + // Actions. + + public static final String ACTION_VIEW = "com.google.android.exoplayer.demo.action.VIEW"; + public static final String ACTION_VIEW_LIST = + "com.google.android.exoplayer.demo.action.VIEW_LIST"; + + // Player configuration extras. + + public static final String ABR_ALGORITHM_EXTRA = "abr_algorithm"; + public static final String ABR_ALGORITHM_DEFAULT = "default"; + public static final String ABR_ALGORITHM_RANDOM = "random"; + + // Media item configuration extras. + + public static final String URI_EXTRA = "uri"; + public static final String EXTENSION_EXTRA = "extension"; + public static final String IS_LIVE_EXTRA = "is_live"; + + public static final String DRM_SCHEME_EXTRA = "drm_scheme"; + public static final String DRM_LICENSE_URL_EXTRA = "drm_license_url"; + public static final String DRM_KEY_REQUEST_PROPERTIES_EXTRA = "drm_key_request_properties"; + public static final String DRM_SESSION_FOR_CLEAR_TYPES_EXTRA = "drm_session_for_clear_types"; + public static final String DRM_MULTI_SESSION_EXTRA = "drm_multi_session"; + public static final String PREFER_EXTENSION_DECODERS_EXTRA = "prefer_extension_decoders"; + public static final String TUNNELING_EXTRA = "tunneling"; + public static final String AD_TAG_URI_EXTRA = "ad_tag_uri"; + public static final String SUBTITLE_URI_EXTRA = "subtitle_uri"; + public static final String SUBTITLE_MIME_TYPE_EXTRA = "subtitle_mime_type"; + public static final String SUBTITLE_LANGUAGE_EXTRA = "subtitle_language"; + // For backwards compatibility only. + public static final String DRM_SCHEME_UUID_EXTRA = "drm_scheme_uuid"; + + // Saved instance state keys. + + private static final String KEY_TRACK_SELECTOR_PARAMETERS = "track_selector_parameters"; + private static final String KEY_WINDOW = "window"; + private static final String KEY_POSITION = "position"; + private static final String KEY_AUTO_PLAY = "auto_play"; + + private static final CookieManager DEFAULT_COOKIE_MANAGER; + static { + DEFAULT_COOKIE_MANAGER = new CookieManager(); + DEFAULT_COOKIE_MANAGER.setCookiePolicy(CookiePolicy.ACCEPT_ORIGINAL_SERVER); + } + + private PlayerView playerView; + private LinearLayout debugRootView; + private Button selectTracksButton; + private TextView debugTextView; + private boolean isShowingTrackSelectionDialog; + + private DataSource.Factory dataSourceFactory; + private SimpleExoPlayer player; + private List mediaSources; + private DefaultTrackSelector trackSelector; + private DefaultTrackSelector.Parameters trackSelectorParameters; + private DebugTextViewHelper debugViewHelper; + private TrackGroupArray lastSeenTrackGroupArray; + + private boolean startAutoPlay; + private int startWindow; + private long startPosition; + + // Fields used only for ad playback. The ads loader is loaded via reflection. + + private AdsLoader adsLoader; + private Uri loadedAdTagUri; + + // Activity lifecycle + + @Override + public void onCreate(Bundle savedInstanceState) { + Intent intent = getIntent(); + String sphericalStereoMode = intent.getStringExtra(SPHERICAL_STEREO_MODE_EXTRA); + if (sphericalStereoMode != null) { + setTheme(R.style.PlayerTheme_Spherical); + } + super.onCreate(savedInstanceState); + dataSourceFactory = buildDataSourceFactory(); + if (CookieHandler.getDefault() != DEFAULT_COOKIE_MANAGER) { + CookieHandler.setDefault(DEFAULT_COOKIE_MANAGER); + } + + setContentView(R.layout.player_activity); + debugRootView = findViewById(R.id.controls_root); + debugTextView = findViewById(R.id.debug_text_view); + selectTracksButton = findViewById(R.id.select_tracks_button); + selectTracksButton.setOnClickListener(this); + + playerView = findViewById(R.id.player_view); + playerView.setControllerVisibilityListener(this); + playerView.setErrorMessageProvider(new PlayerErrorMessageProvider()); + playerView.requestFocus(); + if (sphericalStereoMode != null) { + int stereoMode; + if (SPHERICAL_STEREO_MODE_MONO.equals(sphericalStereoMode)) { + stereoMode = C.STEREO_MODE_MONO; + } else if (SPHERICAL_STEREO_MODE_TOP_BOTTOM.equals(sphericalStereoMode)) { + stereoMode = C.STEREO_MODE_TOP_BOTTOM; + } else if (SPHERICAL_STEREO_MODE_LEFT_RIGHT.equals(sphericalStereoMode)) { + stereoMode = C.STEREO_MODE_LEFT_RIGHT; + } else { + showToast(R.string.error_unrecognized_stereo_mode); + finish(); + return; + } + ((SphericalGLSurfaceView) playerView.getVideoSurfaceView()).setDefaultStereoMode(stereoMode); + } + + if (savedInstanceState != null) { + trackSelectorParameters = savedInstanceState.getParcelable(KEY_TRACK_SELECTOR_PARAMETERS); + startAutoPlay = savedInstanceState.getBoolean(KEY_AUTO_PLAY); + startWindow = savedInstanceState.getInt(KEY_WINDOW); + startPosition = savedInstanceState.getLong(KEY_POSITION); + } else { + DefaultTrackSelector.ParametersBuilder builder = + new DefaultTrackSelector.ParametersBuilder(/* context= */ this); + boolean tunneling = intent.getBooleanExtra(TUNNELING_EXTRA, false); + if (Util.SDK_INT >= 21 && tunneling) { + builder.setTunnelingAudioSessionId(C.generateAudioSessionIdV21(/* context= */ this)); + } + trackSelectorParameters = builder.build(); + clearStartPosition(); + } + } + + @Override + public void onNewIntent(Intent intent) { + super.onNewIntent(intent); + releasePlayer(); + releaseAdsLoader(); + clearStartPosition(); + setIntent(intent); + } + + @Override + public void onStart() { + super.onStart(); + if (Util.SDK_INT > 23) { + initializePlayer(); + if (playerView != null) { + playerView.onResume(); + } + } + } + + @Override + public void onResume() { + super.onResume(); + if (Util.SDK_INT <= 23 || player == null) { + initializePlayer(); + if (playerView != null) { + playerView.onResume(); + } + } + } + + @Override + public void onPause() { + super.onPause(); + if (Util.SDK_INT <= 23) { + if (playerView != null) { + playerView.onPause(); + } + releasePlayer(); + } + } + + @Override + public void onStop() { + super.onStop(); + if (Util.SDK_INT > 23) { + if (playerView != null) { + playerView.onPause(); + } + releasePlayer(); + } + } + + @Override + public void onDestroy() { + super.onDestroy(); + releaseAdsLoader(); + } + + @Override + public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, + @NonNull int[] grantResults) { + if (grantResults.length == 0) { + // Empty results are triggered if a permission is requested while another request was already + // pending and can be safely ignored in this case. + return; + } + if (grantResults[0] == PackageManager.PERMISSION_GRANTED) { + initializePlayer(); + } else { + showToast(R.string.storage_permission_denied); + finish(); + } + } + + @Override + public void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + updateTrackSelectorParameters(); + updateStartPosition(); + outState.putParcelable(KEY_TRACK_SELECTOR_PARAMETERS, trackSelectorParameters); + outState.putBoolean(KEY_AUTO_PLAY, startAutoPlay); + outState.putInt(KEY_WINDOW, startWindow); + outState.putLong(KEY_POSITION, startPosition); + } + + // Activity input + + @Override + public boolean dispatchKeyEvent(KeyEvent event) { + // See whether the player view wants to handle media or DPAD keys events. + return playerView.dispatchKeyEvent(event) || super.dispatchKeyEvent(event); + } + + // OnClickListener methods + + @Override + public void onClick(View view) { + if (view == selectTracksButton + && !isShowingTrackSelectionDialog + && TrackSelectionDialog.willHaveContent(trackSelector)) { + isShowingTrackSelectionDialog = true; + TrackSelectionDialog trackSelectionDialog = + TrackSelectionDialog.createForTrackSelector( + trackSelector, + /* onDismissListener= */ dismissedDialog -> isShowingTrackSelectionDialog = false); + trackSelectionDialog.show(getSupportFragmentManager(), /* tag= */ null); + } + } + + // PlaybackControlView.PlaybackPreparer implementation + + @Override + public void preparePlayback() { + player.retry(); + } + + // PlaybackControlView.VisibilityListener implementation + + @Override + public void onVisibilityChange(int visibility) { + debugRootView.setVisibility(visibility); + } + + // Internal methods + + private void initializePlayer() { + if (player == null) { + Intent intent = getIntent(); + mediaSources = createTopLevelMediaSources(intent); + if (mediaSources.isEmpty()) { + return; + } + TrackSelection.Factory trackSelectionFactory; + String abrAlgorithm = intent.getStringExtra(ABR_ALGORITHM_EXTRA); + if (abrAlgorithm == null || ABR_ALGORITHM_DEFAULT.equals(abrAlgorithm)) { + trackSelectionFactory = new AdaptiveTrackSelection.Factory(); + } else if (ABR_ALGORITHM_RANDOM.equals(abrAlgorithm)) { + trackSelectionFactory = new RandomTrackSelection.Factory(); + } else { + showToast(R.string.error_unrecognized_abr_algorithm); + finish(); + return; + } + + boolean preferExtensionDecoders = + intent.getBooleanExtra(PREFER_EXTENSION_DECODERS_EXTRA, false); + RenderersFactory renderersFactory = + ((DemoApplication) getApplication()).buildRenderersFactory(preferExtensionDecoders); + + trackSelector = new DefaultTrackSelector(/* context= */ this, trackSelectionFactory); + trackSelector.setParameters(trackSelectorParameters); + lastSeenTrackGroupArray = null; + + player = + new SimpleExoPlayer.Builder(/* context= */ this, renderersFactory) + .setTrackSelector(trackSelector) + .build(); + player.addListener(new PlayerEventListener()); + player.setPlayWhenReady(startAutoPlay); + player.addAnalyticsListener(new EventLogger(trackSelector)); + playerView.setPlayer(player); + playerView.setPlaybackPreparer(this); + debugViewHelper = new DebugTextViewHelper(player, debugTextView); + debugViewHelper.start(); + if (adsLoader != null) { + adsLoader.setPlayer(player); + } + } + boolean haveStartPosition = startWindow != C.INDEX_UNSET; + if (haveStartPosition) { + player.seekTo(startWindow, startPosition); + } + player.setMediaSources(mediaSources, /* resetPosition= */ !haveStartPosition); + player.prepare(); + updateButtonVisibility(); + } + + private List createTopLevelMediaSources(Intent intent) { + String action = intent.getAction(); + boolean actionIsListView = ACTION_VIEW_LIST.equals(action); + if (!actionIsListView && !ACTION_VIEW.equals(action)) { + showToast(getString(R.string.unexpected_intent_action, action)); + finish(); + return Collections.emptyList(); + } + + Sample intentAsSample = Sample.createFromIntent(intent); + UriSample[] samples = + intentAsSample instanceof Sample.PlaylistSample + ? ((Sample.PlaylistSample) intentAsSample).children + : new UriSample[] {(UriSample) intentAsSample}; + + boolean seenAdsTagUri = false; + for (UriSample sample : samples) { + seenAdsTagUri |= sample.adTagUri != null; + if (!Util.checkCleartextTrafficPermitted(sample.uri)) { + showToast(R.string.error_cleartext_not_permitted); + return Collections.emptyList(); + } + if (Util.maybeRequestReadExternalStoragePermission(/* activity= */ this, sample.uri)) { + // The player will be reinitialized if the permission is granted. + return Collections.emptyList(); + } + } + + List mediaSources = new ArrayList<>(); + for (UriSample sample : samples) { + MediaSource mediaSource = createLeafMediaSource(sample); + if (mediaSource == null) { + continue; + } + Sample.SubtitleInfo subtitleInfo = sample.subtitleInfo; + if (subtitleInfo != null) { + if (Util.maybeRequestReadExternalStoragePermission( + /* activity= */ this, subtitleInfo.uri)) { + // The player will be reinitialized if the permission is granted. + return Collections.emptyList(); + } + Format subtitleFormat = + Format.createTextSampleFormat( + /* id= */ null, + subtitleInfo.mimeType, + C.SELECTION_FLAG_DEFAULT, + subtitleInfo.language); + MediaSource subtitleMediaSource = + new SingleSampleMediaSource.Factory(dataSourceFactory) + .createMediaSource(subtitleInfo.uri, subtitleFormat, C.TIME_UNSET); + mediaSource = new MergingMediaSource(mediaSource, subtitleMediaSource); + } + mediaSources.add(mediaSource); + } + if (seenAdsTagUri && mediaSources.size() == 1) { + Uri adTagUri = samples[0].adTagUri; + if (!adTagUri.equals(loadedAdTagUri)) { + releaseAdsLoader(); + loadedAdTagUri = adTagUri; + } + MediaSource adsMediaSource = createAdsMediaSource(mediaSources.get(0), adTagUri); + if (adsMediaSource != null) { + mediaSources.set(0, adsMediaSource); + } else { + showToast(R.string.ima_not_loaded); + } + } else if (seenAdsTagUri && mediaSources.size() > 1) { + showToast(R.string.unsupported_ads_in_concatenation); + releaseAdsLoader(); + } else { + releaseAdsLoader(); + } + + return mediaSources; + } + + @Nullable + private MediaSource createLeafMediaSource(UriSample parameters) { + Sample.DrmInfo drmInfo = parameters.drmInfo; + int errorStringId = R.string.error_drm_unknown; + DrmSessionManager drmSessionManager = null; + if (drmInfo == null) { + drmSessionManager = DrmSessionManager.getDummyDrmSessionManager(); + } else if (Util.SDK_INT < 18) { + errorStringId = R.string.error_drm_unsupported_before_api_18; + } else if (!MediaDrm.isCryptoSchemeSupported(drmInfo.drmScheme)) { + errorStringId = R.string.error_drm_unsupported_scheme; + } else { + MediaDrmCallback mediaDrmCallback = + createMediaDrmCallback(drmInfo.drmLicenseUrl, drmInfo.drmKeyRequestProperties); + drmSessionManager = + new DefaultDrmSessionManager.Builder() + .setUuidAndExoMediaDrmProvider(drmInfo.drmScheme, FrameworkMediaDrm.DEFAULT_PROVIDER) + .setMultiSession(drmInfo.drmMultiSession) + .setUseDrmSessionsForClearContent(drmInfo.drmSessionForClearTypes) + .build(mediaDrmCallback); + } + + if (drmSessionManager == null) { + showToast(errorStringId); + finish(); + return null; + } + + DownloadRequest downloadRequest = + ((DemoApplication) getApplication()) + .getDownloadTracker() + .getDownloadRequest(parameters.uri); + if (downloadRequest != null) { + return DownloadHelper.createMediaSource(downloadRequest, dataSourceFactory); + } + return createLeafMediaSource(parameters.uri, parameters.extension, drmSessionManager); + } + + private MediaSource createLeafMediaSource( + Uri uri, String extension, DrmSessionManager drmSessionManager) { + @ContentType int type = Util.inferContentType(uri, extension); + switch (type) { + case C.TYPE_DASH: + return new DashMediaSource.Factory(dataSourceFactory) + .setDrmSessionManager(drmSessionManager) + .createMediaSource(uri); + case C.TYPE_SS: + return new SsMediaSource.Factory(dataSourceFactory) + .setDrmSessionManager(drmSessionManager) + .createMediaSource(uri); + case C.TYPE_HLS: + return new HlsMediaSource.Factory(dataSourceFactory) + .setDrmSessionManager(drmSessionManager) + .createMediaSource(uri); + case C.TYPE_OTHER: + return new ProgressiveMediaSource.Factory(dataSourceFactory) + .setDrmSessionManager(drmSessionManager) + .createMediaSource(uri); + default: + throw new IllegalStateException("Unsupported type: " + type); + } + } + + private HttpMediaDrmCallback createMediaDrmCallback( + String licenseUrl, String[] keyRequestPropertiesArray) { + HttpDataSource.Factory licenseDataSourceFactory = + ((DemoApplication) getApplication()).buildHttpDataSourceFactory(); + HttpMediaDrmCallback drmCallback = + new HttpMediaDrmCallback(licenseUrl, licenseDataSourceFactory); + if (keyRequestPropertiesArray != null) { + for (int i = 0; i < keyRequestPropertiesArray.length - 1; i += 2) { + drmCallback.setKeyRequestProperty(keyRequestPropertiesArray[i], + keyRequestPropertiesArray[i + 1]); + } + } + return drmCallback; + } + + private void releasePlayer() { + if (player != null) { + updateTrackSelectorParameters(); + updateStartPosition(); + debugViewHelper.stop(); + debugViewHelper = null; + player.release(); + player = null; + mediaSources = null; + trackSelector = null; + } + if (adsLoader != null) { + adsLoader.setPlayer(null); + } + } + + private void releaseAdsLoader() { + if (adsLoader != null) { + adsLoader.release(); + adsLoader = null; + loadedAdTagUri = null; + playerView.getOverlayFrameLayout().removeAllViews(); + } + } + + private void updateTrackSelectorParameters() { + if (trackSelector != null) { + trackSelectorParameters = trackSelector.getParameters(); + } + } + + private void updateStartPosition() { + if (player != null) { + startAutoPlay = player.getPlayWhenReady(); + startWindow = player.getCurrentWindowIndex(); + startPosition = Math.max(0, player.getContentPosition()); + } + } + + private void clearStartPosition() { + startAutoPlay = true; + startWindow = C.INDEX_UNSET; + startPosition = C.TIME_UNSET; + } + + /** Returns a new DataSource factory. */ + private DataSource.Factory buildDataSourceFactory() { + return ((DemoApplication) getApplication()).buildDataSourceFactory(); + } + + /** Returns an ads media source, reusing the ads loader if one exists. */ + @Nullable + private MediaSource createAdsMediaSource(MediaSource mediaSource, Uri adTagUri) { + // Load the extension source using reflection so the demo app doesn't have to depend on it. + // The ads loader is reused for multiple playbacks, so that ad playback can resume. + try { + Class loaderClass = Class.forName("com.google.android.exoplayer2.ext.ima.ImaAdsLoader"); + if (adsLoader == null) { + // Full class names used so the lint rule triggers should any of the classes move. + // LINT.IfChange + Constructor loaderConstructor = + loaderClass + .asSubclass(AdsLoader.class) + .getConstructor(android.content.Context.class, android.net.Uri.class); + // LINT.ThenChange(../../../../../../../../proguard-rules.txt) + adsLoader = loaderConstructor.newInstance(this, adTagUri); + } + MediaSourceFactory adMediaSourceFactory = + new MediaSourceFactory() { + + private DrmSessionManager drmSessionManager = + DrmSessionManager.getDummyDrmSessionManager(); + + @Override + public MediaSourceFactory setDrmSessionManager(DrmSessionManager drmSessionManager) { + this.drmSessionManager = drmSessionManager; + return this; + } + + @Override + public MediaSource createMediaSource(Uri uri) { + return PlayerActivity.this.createLeafMediaSource( + uri, /* extension=*/ null, drmSessionManager); + } + + @Override + public int[] getSupportedTypes() { + return new int[] {C.TYPE_DASH, C.TYPE_SS, C.TYPE_HLS, C.TYPE_OTHER}; + } + }; + return new AdsMediaSource(mediaSource, adMediaSourceFactory, adsLoader, playerView); + } catch (ClassNotFoundException e) { + // IMA extension not loaded. + return null; + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + // User controls + + private void updateButtonVisibility() { + selectTracksButton.setEnabled( + player != null && TrackSelectionDialog.willHaveContent(trackSelector)); + } + + private void showControls() { + debugRootView.setVisibility(View.VISIBLE); + } + + private void showToast(int messageId) { + showToast(getString(messageId)); + } + + private void showToast(String message) { + Toast.makeText(getApplicationContext(), message, Toast.LENGTH_LONG).show(); + } + + private static boolean isBehindLiveWindow(ExoPlaybackException e) { + if (e.type != ExoPlaybackException.TYPE_SOURCE) { + return false; + } + Throwable cause = e.getSourceException(); + while (cause != null) { + if (cause instanceof BehindLiveWindowException) { + return true; + } + cause = cause.getCause(); + } + return false; + } + + private class PlayerEventListener implements Player.EventListener { + + @Override + public void onPlayerStateChanged(boolean playWhenReady, @Player.State int playbackState) { + if (playbackState == Player.STATE_ENDED) { + showControls(); + } + updateButtonVisibility(); + } + + @Override + public void onPlayerError(ExoPlaybackException e) { + if (isBehindLiveWindow(e)) { + clearStartPosition(); + initializePlayer(); + } else { + updateButtonVisibility(); + showControls(); + } + } + + @Override + @SuppressWarnings("ReferenceEquality") + public void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray trackSelections) { + updateButtonVisibility(); + if (trackGroups != lastSeenTrackGroupArray) { + MappedTrackInfo mappedTrackInfo = trackSelector.getCurrentMappedTrackInfo(); + if (mappedTrackInfo != null) { + if (mappedTrackInfo.getTypeSupport(C.TRACK_TYPE_VIDEO) + == MappedTrackInfo.RENDERER_SUPPORT_UNSUPPORTED_TRACKS) { + showToast(R.string.error_unsupported_video); + } + if (mappedTrackInfo.getTypeSupport(C.TRACK_TYPE_AUDIO) + == MappedTrackInfo.RENDERER_SUPPORT_UNSUPPORTED_TRACKS) { + showToast(R.string.error_unsupported_audio); + } + } + lastSeenTrackGroupArray = trackGroups; + } + } + } + + private class PlayerErrorMessageProvider implements ErrorMessageProvider { + + @Override + public Pair getErrorMessage(ExoPlaybackException e) { + String errorString = getString(R.string.error_generic); + if (e.type == ExoPlaybackException.TYPE_RENDERER) { + Exception cause = e.getRendererException(); + if (cause instanceof DecoderInitializationException) { + // Special case for decoder initialization failures. + DecoderInitializationException decoderInitializationException = + (DecoderInitializationException) cause; + if (decoderInitializationException.codecInfo == null) { + if (decoderInitializationException.getCause() instanceof DecoderQueryException) { + errorString = getString(R.string.error_querying_decoders); + } else if (decoderInitializationException.secureDecoderRequired) { + errorString = + getString( + R.string.error_no_secure_decoder, decoderInitializationException.mimeType); + } else { + errorString = + getString(R.string.error_no_decoder, decoderInitializationException.mimeType); + } + } else { + errorString = + getString( + R.string.error_instantiating_decoder, + decoderInitializationException.codecInfo.name); + } + } + } + return Pair.create(0, errorString); + } + } +} diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/Sample.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/Sample.java new file mode 100644 index 0000000000..0bf0d2a80c --- /dev/null +++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/Sample.java @@ -0,0 +1,277 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2.demo; + +import static com.google.android.exoplayer2.demo.PlayerActivity.ACTION_VIEW_LIST; +import static com.google.android.exoplayer2.demo.PlayerActivity.AD_TAG_URI_EXTRA; +import static com.google.android.exoplayer2.demo.PlayerActivity.DRM_KEY_REQUEST_PROPERTIES_EXTRA; +import static com.google.android.exoplayer2.demo.PlayerActivity.DRM_LICENSE_URL_EXTRA; +import static com.google.android.exoplayer2.demo.PlayerActivity.DRM_MULTI_SESSION_EXTRA; +import static com.google.android.exoplayer2.demo.PlayerActivity.DRM_SCHEME_EXTRA; +import static com.google.android.exoplayer2.demo.PlayerActivity.DRM_SCHEME_UUID_EXTRA; +import static com.google.android.exoplayer2.demo.PlayerActivity.DRM_SESSION_FOR_CLEAR_TYPES_EXTRA; +import static com.google.android.exoplayer2.demo.PlayerActivity.EXTENSION_EXTRA; +import static com.google.android.exoplayer2.demo.PlayerActivity.IS_LIVE_EXTRA; +import static com.google.android.exoplayer2.demo.PlayerActivity.SUBTITLE_LANGUAGE_EXTRA; +import static com.google.android.exoplayer2.demo.PlayerActivity.SUBTITLE_MIME_TYPE_EXTRA; +import static com.google.android.exoplayer2.demo.PlayerActivity.SUBTITLE_URI_EXTRA; +import static com.google.android.exoplayer2.demo.PlayerActivity.URI_EXTRA; + +import android.content.Intent; +import android.net.Uri; +import androidx.annotation.Nullable; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.util.Assertions; +import com.google.android.exoplayer2.util.Util; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.UUID; + +/* package */ abstract class Sample { + + public static final class UriSample extends Sample { + + public static UriSample createFromIntent(Uri uri, Intent intent, String extrasKeySuffix) { + String extension = intent.getStringExtra(EXTENSION_EXTRA + extrasKeySuffix); + String adsTagUriString = intent.getStringExtra(AD_TAG_URI_EXTRA + extrasKeySuffix); + boolean isLive = + intent.getBooleanExtra(IS_LIVE_EXTRA + extrasKeySuffix, /* defaultValue= */ false); + Uri adTagUri = adsTagUriString != null ? Uri.parse(adsTagUriString) : null; + return new UriSample( + /* name= */ null, + uri, + extension, + isLive, + DrmInfo.createFromIntent(intent, extrasKeySuffix), + adTagUri, + /* sphericalStereoMode= */ null, + SubtitleInfo.createFromIntent(intent, extrasKeySuffix)); + } + + public final Uri uri; + public final String extension; + public final boolean isLive; + public final DrmInfo drmInfo; + public final Uri adTagUri; + @Nullable public final String sphericalStereoMode; + @Nullable SubtitleInfo subtitleInfo; + + public UriSample( + String name, + Uri uri, + String extension, + boolean isLive, + DrmInfo drmInfo, + Uri adTagUri, + @Nullable String sphericalStereoMode, + @Nullable SubtitleInfo subtitleInfo) { + super(name); + this.uri = uri; + this.extension = extension; + this.isLive = isLive; + this.drmInfo = drmInfo; + this.adTagUri = adTagUri; + this.sphericalStereoMode = sphericalStereoMode; + this.subtitleInfo = subtitleInfo; + } + + @Override + public void addToIntent(Intent intent) { + intent.setAction(PlayerActivity.ACTION_VIEW).setData(uri); + intent.putExtra(PlayerActivity.IS_LIVE_EXTRA, isLive); + intent.putExtra(PlayerActivity.SPHERICAL_STEREO_MODE_EXTRA, sphericalStereoMode); + addPlayerConfigToIntent(intent, /* extrasKeySuffix= */ ""); + } + + public void addToPlaylistIntent(Intent intent, String extrasKeySuffix) { + intent.putExtra(PlayerActivity.URI_EXTRA + extrasKeySuffix, uri.toString()); + intent.putExtra(PlayerActivity.IS_LIVE_EXTRA + extrasKeySuffix, isLive); + addPlayerConfigToIntent(intent, extrasKeySuffix); + } + + private void addPlayerConfigToIntent(Intent intent, String extrasKeySuffix) { + intent + .putExtra(EXTENSION_EXTRA + extrasKeySuffix, extension) + .putExtra( + AD_TAG_URI_EXTRA + extrasKeySuffix, adTagUri != null ? adTagUri.toString() : null); + if (drmInfo != null) { + drmInfo.addToIntent(intent, extrasKeySuffix); + } + if (subtitleInfo != null) { + subtitleInfo.addToIntent(intent, extrasKeySuffix); + } + } + } + + public static final class PlaylistSample extends Sample { + + public final UriSample[] children; + + public PlaylistSample(String name, UriSample... children) { + super(name); + this.children = children; + } + + @Override + public void addToIntent(Intent intent) { + intent.setAction(PlayerActivity.ACTION_VIEW_LIST); + for (int i = 0; i < children.length; i++) { + children[i].addToPlaylistIntent(intent, /* extrasKeySuffix= */ "_" + i); + } + } + } + + public static final class DrmInfo { + + public static DrmInfo createFromIntent(Intent intent, String extrasKeySuffix) { + String schemeKey = DRM_SCHEME_EXTRA + extrasKeySuffix; + String schemeUuidKey = DRM_SCHEME_UUID_EXTRA + extrasKeySuffix; + if (!intent.hasExtra(schemeKey) && !intent.hasExtra(schemeUuidKey)) { + return null; + } + String drmSchemeExtra = + intent.hasExtra(schemeKey) + ? intent.getStringExtra(schemeKey) + : intent.getStringExtra(schemeUuidKey); + UUID drmScheme = Util.getDrmUuid(drmSchemeExtra); + String drmLicenseUrl = intent.getStringExtra(DRM_LICENSE_URL_EXTRA + extrasKeySuffix); + String[] keyRequestPropertiesArray = + intent.getStringArrayExtra(DRM_KEY_REQUEST_PROPERTIES_EXTRA + extrasKeySuffix); + String[] drmSessionForClearTypesExtra = + intent.getStringArrayExtra(DRM_SESSION_FOR_CLEAR_TYPES_EXTRA + extrasKeySuffix); + int[] drmSessionForClearTypes = toTrackTypeArray(drmSessionForClearTypesExtra); + boolean drmMultiSession = + intent.getBooleanExtra(DRM_MULTI_SESSION_EXTRA + extrasKeySuffix, false); + return new DrmInfo( + drmScheme, + drmLicenseUrl, + keyRequestPropertiesArray, + drmSessionForClearTypes, + drmMultiSession); + } + + public final UUID drmScheme; + public final String drmLicenseUrl; + public final String[] drmKeyRequestProperties; + public final int[] drmSessionForClearTypes; + public final boolean drmMultiSession; + + public DrmInfo( + UUID drmScheme, + String drmLicenseUrl, + String[] drmKeyRequestProperties, + int[] drmSessionForClearTypes, + boolean drmMultiSession) { + this.drmScheme = drmScheme; + this.drmLicenseUrl = drmLicenseUrl; + this.drmKeyRequestProperties = drmKeyRequestProperties; + this.drmSessionForClearTypes = drmSessionForClearTypes; + this.drmMultiSession = drmMultiSession; + } + + public void addToIntent(Intent intent, String extrasKeySuffix) { + Assertions.checkNotNull(intent); + intent.putExtra(DRM_SCHEME_EXTRA + extrasKeySuffix, drmScheme.toString()); + intent.putExtra(DRM_LICENSE_URL_EXTRA + extrasKeySuffix, drmLicenseUrl); + intent.putExtra(DRM_KEY_REQUEST_PROPERTIES_EXTRA + extrasKeySuffix, drmKeyRequestProperties); + ArrayList typeStrings = new ArrayList<>(); + for (int type : drmSessionForClearTypes) { + // Only audio and video are supported. + typeStrings.add(type == C.TRACK_TYPE_AUDIO ? "audio" : "video"); + } + intent.putExtra( + DRM_SESSION_FOR_CLEAR_TYPES_EXTRA + extrasKeySuffix, typeStrings.toArray(new String[0])); + intent.putExtra(DRM_MULTI_SESSION_EXTRA + extrasKeySuffix, drmMultiSession); + } + } + + public static final class SubtitleInfo { + + @Nullable + public static SubtitleInfo createFromIntent(Intent intent, String extrasKeySuffix) { + if (!intent.hasExtra(SUBTITLE_URI_EXTRA + extrasKeySuffix)) { + return null; + } + return new SubtitleInfo( + Uri.parse(intent.getStringExtra(SUBTITLE_URI_EXTRA + extrasKeySuffix)), + intent.getStringExtra(SUBTITLE_MIME_TYPE_EXTRA + extrasKeySuffix), + intent.getStringExtra(SUBTITLE_LANGUAGE_EXTRA + extrasKeySuffix)); + } + + public final Uri uri; + public final String mimeType; + @Nullable public final String language; + + public SubtitleInfo(Uri uri, String mimeType, @Nullable String language) { + this.uri = Assertions.checkNotNull(uri); + this.mimeType = Assertions.checkNotNull(mimeType); + this.language = language; + } + + public void addToIntent(Intent intent, String extrasKeySuffix) { + intent.putExtra(SUBTITLE_URI_EXTRA + extrasKeySuffix, uri.toString()); + intent.putExtra(SUBTITLE_MIME_TYPE_EXTRA + extrasKeySuffix, mimeType); + intent.putExtra(SUBTITLE_LANGUAGE_EXTRA + extrasKeySuffix, language); + } + } + + public static int[] toTrackTypeArray(@Nullable String[] trackTypeStringsArray) { + if (trackTypeStringsArray == null) { + return new int[0]; + } + HashSet trackTypes = new HashSet<>(); + for (String trackTypeString : trackTypeStringsArray) { + switch (Util.toLowerInvariant(trackTypeString)) { + case "audio": + trackTypes.add(C.TRACK_TYPE_AUDIO); + break; + case "video": + trackTypes.add(C.TRACK_TYPE_VIDEO); + break; + default: + throw new IllegalArgumentException("Invalid track type: " + trackTypeString); + } + } + return Util.toArray(new ArrayList<>(trackTypes)); + } + + public static Sample createFromIntent(Intent intent) { + if (ACTION_VIEW_LIST.equals(intent.getAction())) { + ArrayList intentUris = new ArrayList<>(); + int index = 0; + while (intent.hasExtra(URI_EXTRA + "_" + index)) { + intentUris.add(intent.getStringExtra(URI_EXTRA + "_" + index)); + index++; + } + UriSample[] children = new UriSample[intentUris.size()]; + for (int i = 0; i < children.length; i++) { + Uri uri = Uri.parse(intentUris.get(i)); + children[i] = UriSample.createFromIntent(uri, intent, /* extrasKeySuffix= */ "_" + i); + } + return new PlaylistSample(/* name= */ null, children); + } else { + return UriSample.createFromIntent(intent.getData(), intent, /* extrasKeySuffix= */ ""); + } + } + + @Nullable public final String name; + + public Sample(String name) { + this.name = name; + } + + public abstract void addToIntent(Intent intent); +} diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/SampleChooserActivity.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/SampleChooserActivity.java new file mode 100644 index 0000000000..66bf4bad5a --- /dev/null +++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/SampleChooserActivity.java @@ -0,0 +1,553 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2.demo; + +import android.content.Context; +import android.content.Intent; +import android.content.res.AssetManager; +import android.net.Uri; +import android.os.AsyncTask; +import android.os.Bundle; +import android.util.JsonReader; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.View.OnClickListener; +import android.view.ViewGroup; +import android.widget.BaseExpandableListAdapter; +import android.widget.ExpandableListView; +import android.widget.ExpandableListView.OnChildClickListener; +import android.widget.ImageButton; +import android.widget.TextView; +import android.widget.Toast; +import androidx.annotation.Nullable; +import androidx.appcompat.app.AppCompatActivity; +import com.google.android.exoplayer2.ParserException; +import com.google.android.exoplayer2.RenderersFactory; +import com.google.android.exoplayer2.demo.Sample.DrmInfo; +import com.google.android.exoplayer2.demo.Sample.PlaylistSample; +import com.google.android.exoplayer2.demo.Sample.UriSample; +import com.google.android.exoplayer2.offline.DownloadService; +import com.google.android.exoplayer2.upstream.DataSource; +import com.google.android.exoplayer2.upstream.DataSourceInputStream; +import com.google.android.exoplayer2.upstream.DataSpec; +import com.google.android.exoplayer2.upstream.DefaultDataSource; +import com.google.android.exoplayer2.util.Assertions; +import com.google.android.exoplayer2.util.Log; +import com.google.android.exoplayer2.util.Util; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +/** An activity for selecting from a list of media samples. */ +public class SampleChooserActivity extends AppCompatActivity + implements DownloadTracker.Listener, OnChildClickListener { + + private static final String TAG = "SampleChooserActivity"; + + private boolean useExtensionRenderers; + private DownloadTracker downloadTracker; + private SampleAdapter sampleAdapter; + private MenuItem preferExtensionDecodersMenuItem; + private MenuItem randomAbrMenuItem; + private MenuItem tunnelingMenuItem; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.sample_chooser_activity); + sampleAdapter = new SampleAdapter(); + ExpandableListView sampleListView = findViewById(R.id.sample_list); + sampleListView.setAdapter(sampleAdapter); + sampleListView.setOnChildClickListener(this); + + Intent intent = getIntent(); + String dataUri = intent.getDataString(); + String[] uris; + if (dataUri != null) { + uris = new String[] {dataUri}; + } else { + ArrayList uriList = new ArrayList<>(); + AssetManager assetManager = getAssets(); + try { + for (String asset : assetManager.list("")) { + if (asset.endsWith(".exolist.json")) { + uriList.add("asset:///" + asset); + } + } + } catch (IOException e) { + Toast.makeText(getApplicationContext(), R.string.sample_list_load_error, Toast.LENGTH_LONG) + .show(); + } + uris = new String[uriList.size()]; + uriList.toArray(uris); + Arrays.sort(uris); + } + + DemoApplication application = (DemoApplication) getApplication(); + useExtensionRenderers = application.useExtensionRenderers(); + downloadTracker = application.getDownloadTracker(); + SampleListLoader loaderTask = new SampleListLoader(); + loaderTask.execute(uris); + + // Start the download service if it should be running but it's not currently. + // Starting the service in the foreground causes notification flicker if there is no scheduled + // action. Starting it in the background throws an exception if the app is in the background too + // (e.g. if device screen is locked). + try { + DownloadService.start(this, DemoDownloadService.class); + } catch (IllegalStateException e) { + DownloadService.startForeground(this, DemoDownloadService.class); + } + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + MenuInflater inflater = getMenuInflater(); + inflater.inflate(R.menu.sample_chooser_menu, menu); + preferExtensionDecodersMenuItem = menu.findItem(R.id.prefer_extension_decoders); + preferExtensionDecodersMenuItem.setVisible(useExtensionRenderers); + randomAbrMenuItem = menu.findItem(R.id.random_abr); + tunnelingMenuItem = menu.findItem(R.id.tunneling); + if (Util.SDK_INT < 21) { + tunnelingMenuItem.setEnabled(false); + } + return true; + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + item.setChecked(!item.isChecked()); + return true; + } + + @Override + public void onStart() { + super.onStart(); + downloadTracker.addListener(this); + sampleAdapter.notifyDataSetChanged(); + } + + @Override + public void onStop() { + downloadTracker.removeListener(this); + super.onStop(); + } + + @Override + public void onDownloadsChanged() { + sampleAdapter.notifyDataSetChanged(); + } + + private void onSampleGroups(final List groups, boolean sawError) { + if (sawError) { + Toast.makeText(getApplicationContext(), R.string.sample_list_load_error, Toast.LENGTH_LONG) + .show(); + } + sampleAdapter.setSampleGroups(groups); + } + + @Override + public boolean onChildClick( + ExpandableListView parent, View view, int groupPosition, int childPosition, long id) { + Sample sample = (Sample) view.getTag(); + Intent intent = new Intent(this, PlayerActivity.class); + intent.putExtra( + PlayerActivity.PREFER_EXTENSION_DECODERS_EXTRA, + isNonNullAndChecked(preferExtensionDecodersMenuItem)); + String abrAlgorithm = + isNonNullAndChecked(randomAbrMenuItem) + ? PlayerActivity.ABR_ALGORITHM_RANDOM + : PlayerActivity.ABR_ALGORITHM_DEFAULT; + intent.putExtra(PlayerActivity.ABR_ALGORITHM_EXTRA, abrAlgorithm); + intent.putExtra(PlayerActivity.TUNNELING_EXTRA, isNonNullAndChecked(tunnelingMenuItem)); + sample.addToIntent(intent); + startActivity(intent); + return true; + } + + private void onSampleDownloadButtonClicked(Sample sample) { + int downloadUnsupportedStringId = getDownloadUnsupportedStringId(sample); + if (downloadUnsupportedStringId != 0) { + Toast.makeText(getApplicationContext(), downloadUnsupportedStringId, Toast.LENGTH_LONG) + .show(); + } else { + UriSample uriSample = (UriSample) sample; + RenderersFactory renderersFactory = + ((DemoApplication) getApplication()) + .buildRenderersFactory(isNonNullAndChecked(preferExtensionDecodersMenuItem)); + downloadTracker.toggleDownload( + getSupportFragmentManager(), + sample.name, + uriSample.uri, + uriSample.extension, + renderersFactory); + } + } + + private int getDownloadUnsupportedStringId(Sample sample) { + if (sample instanceof PlaylistSample) { + return R.string.download_playlist_unsupported; + } + UriSample uriSample = (UriSample) sample; + if (uriSample.drmInfo != null) { + return R.string.download_drm_unsupported; + } + if (uriSample.isLive) { + return R.string.download_live_unsupported; + } + if (uriSample.adTagUri != null) { + return R.string.download_ads_unsupported; + } + String scheme = uriSample.uri.getScheme(); + if (!("http".equals(scheme) || "https".equals(scheme))) { + return R.string.download_scheme_unsupported; + } + return 0; + } + + private static boolean isNonNullAndChecked(@Nullable MenuItem menuItem) { + // Temporary workaround for layouts that do not inflate the options menu. + return menuItem != null && menuItem.isChecked(); + } + + private final class SampleListLoader extends AsyncTask> { + + private boolean sawError; + + @Override + protected List doInBackground(String... uris) { + List result = new ArrayList<>(); + Context context = getApplicationContext(); + String userAgent = Util.getUserAgent(context, "ExoPlayerDemo"); + DataSource dataSource = + new DefaultDataSource(context, userAgent, /* allowCrossProtocolRedirects= */ false); + for (String uri : uris) { + DataSpec dataSpec = new DataSpec(Uri.parse(uri)); + InputStream inputStream = new DataSourceInputStream(dataSource, dataSpec); + try { + readSampleGroups(new JsonReader(new InputStreamReader(inputStream, "UTF-8")), result); + } catch (Exception e) { + Log.e(TAG, "Error loading sample list: " + uri, e); + sawError = true; + } finally { + Util.closeQuietly(dataSource); + } + } + return result; + } + + @Override + protected void onPostExecute(List result) { + onSampleGroups(result, sawError); + } + + private void readSampleGroups(JsonReader reader, List groups) throws IOException { + reader.beginArray(); + while (reader.hasNext()) { + readSampleGroup(reader, groups); + } + reader.endArray(); + } + + private void readSampleGroup(JsonReader reader, List groups) throws IOException { + String groupName = ""; + ArrayList samples = new ArrayList<>(); + + reader.beginObject(); + while (reader.hasNext()) { + String name = reader.nextName(); + switch (name) { + case "name": + groupName = reader.nextString(); + break; + case "samples": + reader.beginArray(); + while (reader.hasNext()) { + samples.add(readEntry(reader, false)); + } + reader.endArray(); + break; + case "_comment": + reader.nextString(); // Ignore. + break; + default: + throw new ParserException("Unsupported name: " + name); + } + } + reader.endObject(); + + SampleGroup group = getGroup(groupName, groups); + group.samples.addAll(samples); + } + + private Sample readEntry(JsonReader reader, boolean insidePlaylist) throws IOException { + String sampleName = null; + Uri uri = null; + String extension = null; + boolean isLive = false; + String drmScheme = null; + String drmLicenseUrl = null; + String[] drmKeyRequestProperties = null; + String[] drmSessionForClearTypes = null; + boolean drmMultiSession = false; + ArrayList playlistSamples = null; + String adTagUri = null; + String sphericalStereoMode = null; + List subtitleInfos = new ArrayList<>(); + Uri subtitleUri = null; + String subtitleMimeType = null; + String subtitleLanguage = null; + + reader.beginObject(); + while (reader.hasNext()) { + String name = reader.nextName(); + switch (name) { + case "name": + sampleName = reader.nextString(); + break; + case "uri": + uri = Uri.parse(reader.nextString()); + break; + case "extension": + extension = reader.nextString(); + break; + case "drm_scheme": + drmScheme = reader.nextString(); + break; + case "is_live": + isLive = reader.nextBoolean(); + break; + case "drm_license_url": + drmLicenseUrl = reader.nextString(); + break; + case "drm_key_request_properties": + ArrayList drmKeyRequestPropertiesList = new ArrayList<>(); + reader.beginObject(); + while (reader.hasNext()) { + drmKeyRequestPropertiesList.add(reader.nextName()); + drmKeyRequestPropertiesList.add(reader.nextString()); + } + reader.endObject(); + drmKeyRequestProperties = drmKeyRequestPropertiesList.toArray(new String[0]); + break; + case "drm_session_for_clear_types": + ArrayList drmSessionForClearTypesList = new ArrayList<>(); + reader.beginArray(); + while (reader.hasNext()) { + drmSessionForClearTypesList.add(reader.nextString()); + } + reader.endArray(); + drmSessionForClearTypes = drmSessionForClearTypesList.toArray(new String[0]); + break; + case "drm_multi_session": + drmMultiSession = reader.nextBoolean(); + break; + case "playlist": + Assertions.checkState(!insidePlaylist, "Invalid nesting of playlists"); + playlistSamples = new ArrayList<>(); + reader.beginArray(); + while (reader.hasNext()) { + playlistSamples.add((UriSample) readEntry(reader, /* insidePlaylist= */ true)); + } + reader.endArray(); + break; + case "ad_tag_uri": + adTagUri = reader.nextString(); + break; + case "spherical_stereo_mode": + Assertions.checkState( + !insidePlaylist, "Invalid attribute on nested item: spherical_stereo_mode"); + sphericalStereoMode = reader.nextString(); + break; + case "subtitle_uri": + subtitleUri = Uri.parse(reader.nextString()); + break; + case "subtitle_mime_type": + subtitleMimeType = reader.nextString(); + break; + case "subtitle_language": + subtitleLanguage = reader.nextString(); + break; + default: + throw new ParserException("Unsupported attribute name: " + name); + } + } + reader.endObject(); + DrmInfo drmInfo = + drmScheme == null + ? null + : new DrmInfo( + Util.getDrmUuid(drmScheme), + drmLicenseUrl, + drmKeyRequestProperties, + Sample.toTrackTypeArray(drmSessionForClearTypes), + drmMultiSession); + Sample.SubtitleInfo subtitleInfo = + subtitleUri == null + ? null + : new Sample.SubtitleInfo( + subtitleUri, + Assertions.checkNotNull( + subtitleMimeType, "subtitle_mime_type is required if subtitle_uri is set."), + subtitleLanguage); + if (playlistSamples != null) { + UriSample[] playlistSamplesArray = playlistSamples.toArray(new UriSample[0]); + return new PlaylistSample(sampleName, playlistSamplesArray); + } else { + return new UriSample( + sampleName, + uri, + extension, + isLive, + drmInfo, + adTagUri != null ? Uri.parse(adTagUri) : null, + sphericalStereoMode, + subtitleInfo); + } + } + + private SampleGroup getGroup(String groupName, List groups) { + for (int i = 0; i < groups.size(); i++) { + if (Util.areEqual(groupName, groups.get(i).title)) { + return groups.get(i); + } + } + SampleGroup group = new SampleGroup(groupName); + groups.add(group); + return group; + } + + } + + private final class SampleAdapter extends BaseExpandableListAdapter implements OnClickListener { + + private List sampleGroups; + + public SampleAdapter() { + sampleGroups = Collections.emptyList(); + } + + public void setSampleGroups(List sampleGroups) { + this.sampleGroups = sampleGroups; + notifyDataSetChanged(); + } + + @Override + public Sample getChild(int groupPosition, int childPosition) { + return getGroup(groupPosition).samples.get(childPosition); + } + + @Override + public long getChildId(int groupPosition, int childPosition) { + return childPosition; + } + + @Override + public View getChildView(int groupPosition, int childPosition, boolean isLastChild, + View convertView, ViewGroup parent) { + View view = convertView; + if (view == null) { + view = getLayoutInflater().inflate(R.layout.sample_list_item, parent, false); + View downloadButton = view.findViewById(R.id.download_button); + downloadButton.setOnClickListener(this); + downloadButton.setFocusable(false); + } + initializeChildView(view, getChild(groupPosition, childPosition)); + return view; + } + + @Override + public int getChildrenCount(int groupPosition) { + return getGroup(groupPosition).samples.size(); + } + + @Override + public SampleGroup getGroup(int groupPosition) { + return sampleGroups.get(groupPosition); + } + + @Override + public long getGroupId(int groupPosition) { + return groupPosition; + } + + @Override + public View getGroupView(int groupPosition, boolean isExpanded, View convertView, + ViewGroup parent) { + View view = convertView; + if (view == null) { + view = + getLayoutInflater() + .inflate(android.R.layout.simple_expandable_list_item_1, parent, false); + } + ((TextView) view).setText(getGroup(groupPosition).title); + return view; + } + + @Override + public int getGroupCount() { + return sampleGroups.size(); + } + + @Override + public boolean hasStableIds() { + return false; + } + + @Override + public boolean isChildSelectable(int groupPosition, int childPosition) { + return true; + } + + @Override + public void onClick(View view) { + onSampleDownloadButtonClicked((Sample) view.getTag()); + } + + private void initializeChildView(View view, Sample sample) { + view.setTag(sample); + TextView sampleTitle = view.findViewById(R.id.sample_title); + sampleTitle.setText(sample.name); + + boolean canDownload = getDownloadUnsupportedStringId(sample) == 0; + boolean isDownloaded = canDownload && downloadTracker.isDownloaded(((UriSample) sample).uri); + ImageButton downloadButton = view.findViewById(R.id.download_button); + downloadButton.setTag(sample); + downloadButton.setColorFilter( + canDownload ? (isDownloaded ? 0xFF42A5F5 : 0xFFBDBDBD) : 0xFF666666); + downloadButton.setImageResource( + isDownloaded ? R.drawable.ic_download_done : R.drawable.ic_download); + } + } + + private static final class SampleGroup { + + public final String title; + public final List samples; + + public SampleGroup(String title) { + this.title = title; + this.samples = new ArrayList<>(); + } + + } +} diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/TrackSelectionDialog.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/TrackSelectionDialog.java new file mode 100644 index 0000000000..9e8009388e --- /dev/null +++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/TrackSelectionDialog.java @@ -0,0 +1,368 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2.demo; + +import android.app.Dialog; +import android.content.DialogInterface; +import android.content.res.Resources; +import android.os.Bundle; +import android.util.SparseArray; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Button; +import androidx.annotation.Nullable; +import androidx.appcompat.app.AppCompatDialog; +import androidx.fragment.app.DialogFragment; +import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentManager; +import androidx.fragment.app.FragmentPagerAdapter; +import androidx.viewpager.widget.ViewPager; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.source.TrackGroupArray; +import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; +import com.google.android.exoplayer2.trackselection.DefaultTrackSelector.SelectionOverride; +import com.google.android.exoplayer2.trackselection.MappingTrackSelector.MappedTrackInfo; +import com.google.android.exoplayer2.ui.TrackSelectionView; +import com.google.android.exoplayer2.util.Assertions; +import com.google.android.material.tabs.TabLayout; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** Dialog to select tracks. */ +public final class TrackSelectionDialog extends DialogFragment { + + private final SparseArray tabFragments; + private final ArrayList tabTrackTypes; + + private int titleId; + private DialogInterface.OnClickListener onClickListener; + private DialogInterface.OnDismissListener onDismissListener; + + /** + * Returns whether a track selection dialog will have content to display if initialized with the + * specified {@link DefaultTrackSelector} in its current state. + */ + public static boolean willHaveContent(DefaultTrackSelector trackSelector) { + MappedTrackInfo mappedTrackInfo = trackSelector.getCurrentMappedTrackInfo(); + return mappedTrackInfo != null && willHaveContent(mappedTrackInfo); + } + + /** + * Returns whether a track selection dialog will have content to display if initialized with the + * specified {@link MappedTrackInfo}. + */ + public static boolean willHaveContent(MappedTrackInfo mappedTrackInfo) { + for (int i = 0; i < mappedTrackInfo.getRendererCount(); i++) { + if (showTabForRenderer(mappedTrackInfo, i)) { + return true; + } + } + return false; + } + + /** + * Creates a dialog for a given {@link DefaultTrackSelector}, whose parameters will be + * automatically updated when tracks are selected. + * + * @param trackSelector The {@link DefaultTrackSelector}. + * @param onDismissListener A {@link DialogInterface.OnDismissListener} to call when the dialog is + * dismissed. + */ + public static TrackSelectionDialog createForTrackSelector( + DefaultTrackSelector trackSelector, DialogInterface.OnDismissListener onDismissListener) { + MappedTrackInfo mappedTrackInfo = + Assertions.checkNotNull(trackSelector.getCurrentMappedTrackInfo()); + TrackSelectionDialog trackSelectionDialog = new TrackSelectionDialog(); + DefaultTrackSelector.Parameters parameters = trackSelector.getParameters(); + trackSelectionDialog.init( + /* titleId= */ R.string.track_selection_title, + mappedTrackInfo, + /* initialParameters = */ parameters, + /* allowAdaptiveSelections =*/ true, + /* allowMultipleOverrides= */ false, + /* onClickListener= */ (dialog, which) -> { + DefaultTrackSelector.ParametersBuilder builder = parameters.buildUpon(); + for (int i = 0; i < mappedTrackInfo.getRendererCount(); i++) { + builder + .clearSelectionOverrides(/* rendererIndex= */ i) + .setRendererDisabled( + /* rendererIndex= */ i, + trackSelectionDialog.getIsDisabled(/* rendererIndex= */ i)); + List overrides = + trackSelectionDialog.getOverrides(/* rendererIndex= */ i); + if (!overrides.isEmpty()) { + builder.setSelectionOverride( + /* rendererIndex= */ i, + mappedTrackInfo.getTrackGroups(/* rendererIndex= */ i), + overrides.get(0)); + } + } + trackSelector.setParameters(builder); + }, + onDismissListener); + return trackSelectionDialog; + } + + /** + * Creates a dialog for given {@link MappedTrackInfo} and {@link DefaultTrackSelector.Parameters}. + * + * @param titleId The resource id of the dialog title. + * @param mappedTrackInfo The {@link MappedTrackInfo} to display. + * @param initialParameters The {@link DefaultTrackSelector.Parameters} describing the initial + * track selection. + * @param allowAdaptiveSelections Whether adaptive selections (consisting of more than one track) + * can be made. + * @param allowMultipleOverrides Whether tracks from multiple track groups can be selected. + * @param onClickListener {@link DialogInterface.OnClickListener} called when tracks are selected. + * @param onDismissListener {@link DialogInterface.OnDismissListener} called when the dialog is + * dismissed. + */ + public static TrackSelectionDialog createForMappedTrackInfoAndParameters( + int titleId, + MappedTrackInfo mappedTrackInfo, + DefaultTrackSelector.Parameters initialParameters, + boolean allowAdaptiveSelections, + boolean allowMultipleOverrides, + DialogInterface.OnClickListener onClickListener, + DialogInterface.OnDismissListener onDismissListener) { + TrackSelectionDialog trackSelectionDialog = new TrackSelectionDialog(); + trackSelectionDialog.init( + titleId, + mappedTrackInfo, + initialParameters, + allowAdaptiveSelections, + allowMultipleOverrides, + onClickListener, + onDismissListener); + return trackSelectionDialog; + } + + public TrackSelectionDialog() { + tabFragments = new SparseArray<>(); + tabTrackTypes = new ArrayList<>(); + // Retain instance across activity re-creation to prevent losing access to init data. + setRetainInstance(true); + } + + private void init( + int titleId, + MappedTrackInfo mappedTrackInfo, + DefaultTrackSelector.Parameters initialParameters, + boolean allowAdaptiveSelections, + boolean allowMultipleOverrides, + DialogInterface.OnClickListener onClickListener, + DialogInterface.OnDismissListener onDismissListener) { + this.titleId = titleId; + this.onClickListener = onClickListener; + this.onDismissListener = onDismissListener; + for (int i = 0; i < mappedTrackInfo.getRendererCount(); i++) { + if (showTabForRenderer(mappedTrackInfo, i)) { + int trackType = mappedTrackInfo.getRendererType(/* rendererIndex= */ i); + TrackGroupArray trackGroupArray = mappedTrackInfo.getTrackGroups(i); + TrackSelectionViewFragment tabFragment = new TrackSelectionViewFragment(); + tabFragment.init( + mappedTrackInfo, + /* rendererIndex= */ i, + initialParameters.getRendererDisabled(/* rendererIndex= */ i), + initialParameters.getSelectionOverride(/* rendererIndex= */ i, trackGroupArray), + allowAdaptiveSelections, + allowMultipleOverrides); + tabFragments.put(i, tabFragment); + tabTrackTypes.add(trackType); + } + } + } + + /** + * Returns whether a renderer is disabled. + * + * @param rendererIndex Renderer index. + * @return Whether the renderer is disabled. + */ + public boolean getIsDisabled(int rendererIndex) { + TrackSelectionViewFragment rendererView = tabFragments.get(rendererIndex); + return rendererView != null && rendererView.isDisabled; + } + + /** + * Returns the list of selected track selection overrides for the specified renderer. There will + * be at most one override for each track group. + * + * @param rendererIndex Renderer index. + * @return The list of track selection overrides for this renderer. + */ + public List getOverrides(int rendererIndex) { + TrackSelectionViewFragment rendererView = tabFragments.get(rendererIndex); + return rendererView == null ? Collections.emptyList() : rendererView.overrides; + } + + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + // We need to own the view to let tab layout work correctly on all API levels. We can't use + // AlertDialog because it owns the view itself, so we use AppCompatDialog instead, themed using + // the AlertDialog theme overlay with force-enabled title. + AppCompatDialog dialog = + new AppCompatDialog(getActivity(), R.style.TrackSelectionDialogThemeOverlay); + dialog.setTitle(titleId); + return dialog; + } + + @Override + public void onDismiss(DialogInterface dialog) { + super.onDismiss(dialog); + onDismissListener.onDismiss(dialog); + } + + @Nullable + @Override + public View onCreateView( + LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + + View dialogView = inflater.inflate(R.layout.track_selection_dialog, container, false); + TabLayout tabLayout = dialogView.findViewById(R.id.track_selection_dialog_tab_layout); + ViewPager viewPager = dialogView.findViewById(R.id.track_selection_dialog_view_pager); + Button cancelButton = dialogView.findViewById(R.id.track_selection_dialog_cancel_button); + Button okButton = dialogView.findViewById(R.id.track_selection_dialog_ok_button); + viewPager.setAdapter(new FragmentAdapter(getChildFragmentManager())); + tabLayout.setupWithViewPager(viewPager); + tabLayout.setVisibility(tabFragments.size() > 1 ? View.VISIBLE : View.GONE); + cancelButton.setOnClickListener(view -> dismiss()); + okButton.setOnClickListener( + view -> { + onClickListener.onClick(getDialog(), DialogInterface.BUTTON_POSITIVE); + dismiss(); + }); + return dialogView; + } + + private static boolean showTabForRenderer(MappedTrackInfo mappedTrackInfo, int rendererIndex) { + TrackGroupArray trackGroupArray = mappedTrackInfo.getTrackGroups(rendererIndex); + if (trackGroupArray.length == 0) { + return false; + } + int trackType = mappedTrackInfo.getRendererType(rendererIndex); + return isSupportedTrackType(trackType); + } + + private static boolean isSupportedTrackType(int trackType) { + switch (trackType) { + case C.TRACK_TYPE_VIDEO: + case C.TRACK_TYPE_AUDIO: + case C.TRACK_TYPE_TEXT: + return true; + default: + return false; + } + } + + private static String getTrackTypeString(Resources resources, int trackType) { + switch (trackType) { + case C.TRACK_TYPE_VIDEO: + return resources.getString(R.string.exo_track_selection_title_video); + case C.TRACK_TYPE_AUDIO: + return resources.getString(R.string.exo_track_selection_title_audio); + case C.TRACK_TYPE_TEXT: + return resources.getString(R.string.exo_track_selection_title_text); + default: + throw new IllegalArgumentException(); + } + } + + private final class FragmentAdapter extends FragmentPagerAdapter { + + public FragmentAdapter(FragmentManager fragmentManager) { + super(fragmentManager); + } + + @Override + public Fragment getItem(int position) { + return tabFragments.valueAt(position); + } + + @Override + public int getCount() { + return tabFragments.size(); + } + + @Nullable + @Override + public CharSequence getPageTitle(int position) { + return getTrackTypeString(getResources(), tabTrackTypes.get(position)); + } + } + + /** Fragment to show a track selection in tab of the track selection dialog. */ + public static final class TrackSelectionViewFragment extends Fragment + implements TrackSelectionView.TrackSelectionListener { + + private MappedTrackInfo mappedTrackInfo; + private int rendererIndex; + private boolean allowAdaptiveSelections; + private boolean allowMultipleOverrides; + + /* package */ boolean isDisabled; + /* package */ List overrides; + + public TrackSelectionViewFragment() { + // Retain instance across activity re-creation to prevent losing access to init data. + setRetainInstance(true); + } + + public void init( + MappedTrackInfo mappedTrackInfo, + int rendererIndex, + boolean initialIsDisabled, + @Nullable SelectionOverride initialOverride, + boolean allowAdaptiveSelections, + boolean allowMultipleOverrides) { + this.mappedTrackInfo = mappedTrackInfo; + this.rendererIndex = rendererIndex; + this.isDisabled = initialIsDisabled; + this.overrides = + initialOverride == null + ? Collections.emptyList() + : Collections.singletonList(initialOverride); + this.allowAdaptiveSelections = allowAdaptiveSelections; + this.allowMultipleOverrides = allowMultipleOverrides; + } + + @Nullable + @Override + public View onCreateView( + LayoutInflater inflater, + @Nullable ViewGroup container, + @Nullable Bundle savedInstanceState) { + View rootView = + inflater.inflate( + R.layout.exo_track_selection_dialog, container, /* attachToRoot= */ false); + TrackSelectionView trackSelectionView = rootView.findViewById(R.id.exo_track_selection_view); + trackSelectionView.setShowDisableOption(true); + trackSelectionView.setAllowMultipleOverrides(allowMultipleOverrides); + trackSelectionView.setAllowAdaptiveSelections(allowAdaptiveSelections); + trackSelectionView.init( + mappedTrackInfo, rendererIndex, isDisabled, overrides, /* listener= */ this); + return rootView; + } + + @Override + public void onTrackSelectionChanged(boolean isDisabled, List overrides) { + this.isDisabled = isDisabled; + this.overrides = overrides; + } + } +} diff --git a/demos/main/src/main/res/drawable-hdpi/ic_download.png b/demos/main/src/main/res/drawable-hdpi/ic_download.png new file mode 100644 index 0000000000..fa3ebbb310 Binary files /dev/null and b/demos/main/src/main/res/drawable-hdpi/ic_download.png differ diff --git a/demos/main/src/main/res/drawable-hdpi/ic_download_done.png b/demos/main/src/main/res/drawable-hdpi/ic_download_done.png new file mode 100644 index 0000000000..fa0ec9dd68 Binary files /dev/null and b/demos/main/src/main/res/drawable-hdpi/ic_download_done.png differ diff --git a/demos/main/src/main/res/drawable-mdpi/ic_download.png b/demos/main/src/main/res/drawable-mdpi/ic_download.png new file mode 100644 index 0000000000..c8a2039c58 Binary files /dev/null and b/demos/main/src/main/res/drawable-mdpi/ic_download.png differ diff --git a/demos/main/src/main/res/drawable-mdpi/ic_download_done.png b/demos/main/src/main/res/drawable-mdpi/ic_download_done.png new file mode 100644 index 0000000000..08073a2a6d Binary files /dev/null and b/demos/main/src/main/res/drawable-mdpi/ic_download_done.png differ diff --git a/demos/main/src/main/res/drawable-xhdpi/ic_banner.png b/demos/main/src/main/res/drawable-xhdpi/ic_banner.png new file mode 100644 index 0000000000..09de177387 Binary files /dev/null and b/demos/main/src/main/res/drawable-xhdpi/ic_banner.png differ diff --git a/demos/main/src/main/res/drawable-xhdpi/ic_download.png b/demos/main/src/main/res/drawable-xhdpi/ic_download.png new file mode 100644 index 0000000000..671e0b3ece Binary files /dev/null and b/demos/main/src/main/res/drawable-xhdpi/ic_download.png differ diff --git a/demos/main/src/main/res/drawable-xhdpi/ic_download_done.png b/demos/main/src/main/res/drawable-xhdpi/ic_download_done.png new file mode 100644 index 0000000000..2339c0bf16 Binary files /dev/null and b/demos/main/src/main/res/drawable-xhdpi/ic_download_done.png differ diff --git a/demos/main/src/main/res/drawable-xxhdpi/ic_download.png b/demos/main/src/main/res/drawable-xxhdpi/ic_download.png new file mode 100644 index 0000000000..4e04a30198 Binary files /dev/null and b/demos/main/src/main/res/drawable-xxhdpi/ic_download.png differ diff --git a/demos/main/src/main/res/drawable-xxhdpi/ic_download_done.png b/demos/main/src/main/res/drawable-xxhdpi/ic_download_done.png new file mode 100644 index 0000000000..b631a00088 Binary files /dev/null and b/demos/main/src/main/res/drawable-xxhdpi/ic_download_done.png differ diff --git a/demos/main/src/main/res/drawable-xxxhdpi/ic_download.png b/demos/main/src/main/res/drawable-xxxhdpi/ic_download.png new file mode 100644 index 0000000000..f9bfb5edba Binary files /dev/null and b/demos/main/src/main/res/drawable-xxxhdpi/ic_download.png differ diff --git a/demos/main/src/main/res/drawable-xxxhdpi/ic_download_done.png b/demos/main/src/main/res/drawable-xxxhdpi/ic_download_done.png new file mode 100644 index 0000000000..52fe8f6990 Binary files /dev/null and b/demos/main/src/main/res/drawable-xxxhdpi/ic_download_done.png differ diff --git a/demo/src/main/res/layout/player_activity.xml b/demos/main/src/main/res/layout/player_activity.xml similarity index 89% rename from demo/src/main/res/layout/player_activity.xml rename to demos/main/src/main/res/layout/player_activity.xml index 3f8cdaa7d6..ea3de257e2 100644 --- a/demo/src/main/res/layout/player_activity.xml +++ b/demos/main/src/main/res/layout/player_activity.xml @@ -20,7 +20,7 @@ android:layout_height="match_parent" android:keepScreenOn="true"> - @@ -44,11 +44,11 @@ android:orientation="horizontal" android:visibility="gone"> -