diff --git a/.github/ISSUE_TEMPLATE/bug.md b/.github/ISSUE_TEMPLATE/bug.md new file mode 100644 index 0000000000..c0980df440 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug.md @@ -0,0 +1,62 @@ +--- +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/content_not_playing.md b/.github/ISSUE_TEMPLATE/content_not_playing.md new file mode 100644 index 0000000000..c8d4668a6a --- /dev/null +++ b/.github/ISSUE_TEMPLATE/content_not_playing.md @@ -0,0 +1,58 @@ +--- +name: Content not playing correctly +about: Issue template for a content not playing issue. +title: '' +labels: content not playing, needs triage +assignees: '' +--- + +Before filing a content issue: +------------------------------ +- Search existing issues, including issues that are closed: + https://github.com/google/ExoPlayer/issues?q=is%3Aissue +- Consult our supported formats page, which can be found at + https://exoplayer.dev/supported-formats.html. +- Learn how to create useful log output by using the EventLogger: + https://exoplayer.dev/listening-to-player-events.html#using-eventlogger +- Try playing your content in the ExoPlayer demo app. Information about the + ExoPlayer demo app can be found here: + http://exoplayer.dev/demo-application.html. + +When reporting a content issue: +----------------------------- +Fill out the sections below, leaving the headers but replacing the content. If +you're unable to provide certain information, please explain why in the relevant +section. We may close issues if they do not include sufficient information. + +### [REQUIRED] Content description +Describe the content and any specifics you expected to play but did not. This +could be the container or sample format itself or any features the stream has +and you expect to play, like 5.1 audio track, text tracks or drm systems. + +### [REQUIRED] Link to test content +Provide a JSON snippet for the demo app’s media.exolist.json file, or a link to +media that reproduces the issue. If you don't wish to post it publicly, please +submit the issue, then email the link to dev.exoplayer@gmail.com using a subject +in the format "Issue #1234", where "#1234" should be replaced with your issue +number. Provide all the metadata we'd need to play the content like drm license +urls or similar. If the content is accessible only in certain countries or +regions, please say so. + +### [REQUIRED] Version of ExoPlayer being used +Specify the absolute version number. Avoid using terms such as "latest". + +### [REQUIRED] Device(s) and version(s) of Android being used +Specify the devices and versions of Android on which you expect the content to +play. If possible, please test on multiple devices and Android versions. + +### [REQUIRED] A full bug report captured from the device +Capture a full bug report using "adb bugreport". Output from "adb logcat" or a +log snippet is NOT sufficient. Please attach the captured bug report as a file. +If you don't wish to post it publicly, please submit the issue, then email the +bug report to dev.exoplayer@gmail.com using a subject in the format +"Issue #1234", where "#1234" should be replaced with your issue number. + + diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000000..d481de33ce --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,35 @@ +--- +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..b5f40884d8 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/question.md @@ -0,0 +1,55 @@ +--- +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 db5a8c4305..4731d5ba99 100644 --- a/.gitignore +++ b/.gitignore @@ -37,6 +37,12 @@ local.properties proguard.cfg proguard-project.txt +# Bazel +bazel-bin +bazel-genfiles +bazel-out +bazel-testlogs + # Other .DS_Store cmake-build-debug @@ -66,3 +72,6 @@ extensions/cronet/jniLibs/* extensions/cronet/libs/* !extensions/cronet/libs/README.md +# Cast receiver +cast_receiver_app/external-js +cast_receiver_app/bazel-cast_receiver_app diff --git a/.hgignore b/.hgignore index f7c3656f65..36d3268005 100644 --- a/.hgignore +++ b/.hgignore @@ -44,6 +44,12 @@ local.properties proguard.cfg proguard-project.txt +# Bazel +bazel-bin +bazel-genfiles +bazel-out +bazel-testlogs + # Other .DS_Store cmake-build-debug @@ -69,3 +75,7 @@ extensions/cronet/jniLibs/* !extensions/cronet/jniLibs/README.md extensions/cronet/libs/* !extensions/cronet/libs/README.md + +# Cast receiver +cast_receiver_app/external-js +cast_receiver_app/bazel-cast_receiver_app 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 8d2f66093d..0000000000 --- a/ISSUE_TEMPLATE +++ /dev/null @@ -1,43 +0,0 @@ -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 using a subject in the format "Issue #1234". - -### 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 using a subject in the format -"Issue #1234". - diff --git a/README.md b/README.md index 13dfaddab3..a369b077f4 100644 --- a/README.md +++ b/README.md @@ -15,8 +15,8 @@ and extend, and can be updated through Play Store application updates. * 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 +[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 @@ -27,17 +27,21 @@ 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 and Google repositories +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 { - jcenter() google() + jcenter() } ``` +#### 2. Add ExoPlayer module dependencies #### + Next add a dependency in the `build.gradle` file of your app module. The following will add a dependency to the full library: @@ -45,10 +49,12 @@ following will add a dependency to the full library: implementation 'com.google.android.exoplayer:exoplayer:2.X.X' ``` -where `2.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 implementation 'com.google.android.exoplayer:exoplayer-core:2.X.X' @@ -77,6 +83,18 @@ JCenter can be found on [Bintray][]. [extensions directory]: https://github.com/google/ExoPlayer/tree/release-v2/extensions/ [Bintray]: https://bintray.com/google/exoplayer +#### 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 diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 3098bd9a76..7c934c478c 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -2,144 +2,610 @@ ### dev-v2 (not yet released) ### -* Add a flag to opt-in to automatic audio focus handling via +* Update `DefaultTrackSelector` to apply a viewport constraint for the default + display by default. +* Add `PlaybackStatsListener` to collect `PlaybackStats` for playbacks analysis + and analytics reporting (TODO: link to developer guide page/blog post). +* Add basic DRM support to the Cast demo app. +* Assume that encrypted content requires secure decoders in renderer support + checks ([#5568](https://github.com/google/ExoPlayer/issues/5568)). +* Decoders: Prefer decoders that advertise format support over ones that do not, + even if they are listed lower in the `MediaCodecList`. +* Add a workaround for broken raw audio decoding on Oppo R9 + ([#5782](https://github.com/google/ExoPlayer/issues/5782)). +* Add VR player demo. +* Wrap decoder exceptions in a new `DecoderException` class and report as + renderer error. +* Do not pass the manifest to callbacks of `Player.EventListener` and + `SourceInfoRefreshListener` anymore. Instead make it accessible through + `Player.getCurrentManifest()` and `Timeline.Window.manifest`. Also rename + `SourceInfoRefreshListener` to `MediaSourceCaller`. +* Set `compileSdkVersion` to 29 to use Android Q APIs. +* Add `enable` and `disable` methods to `MediaSource` to improve resource + management in playlists. +* Improve text selection logic to always prefer the better language matches + over other selection parameters. +* Remove `AnalyticsCollector.Factory`. Instances can be created directly and + the `Player` set later using `AnalyticsCollector.setPlayer`. +* Add `allowAudioMixedChannelCountAdaptiveness` parameter to + `DefaultTrackSelector` to allow adaptive selections of audio tracks with + different channel counts + ([#6257](https://github.com/google/ExoPlayer/issues/6257)). + +### 2.10.4 ### + +* 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 ### + +* 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 ### + +* 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 ### + +* 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 ### + +* 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. + * 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`. + * 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. +* LoadControl: Set minimum buffer for playbacks with video equal to maximum + buffer ([#2083](https://github.com/google/ExoPlayer/issues/2083)). +* 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 ### + +* 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 ### + +* 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 ### + +* 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 ### + +* 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 ### + +* 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 ### + +* 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 ### + +* 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`. -* Distribute Cronet extension via jCenter. -* Set compileSdkVersion and targetSdkVersion to 28. +* Add `ExoPlayer.retry` convenience method. * Add `AudioListener` for listening to changes in audio configuration during playback ([#3994](https://github.com/google/ExoPlayer/issues/3994)). -* Improved seeking support: +* 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)). Note that the + ([#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. - Note that the `FLAG_ENABLE_CONSTANT_BITRATE_SEEKING` flag must be set on the - extractor to enable this functionality. + 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 for all extractors + approximate seeking using a constant bitrate assumption on all extractors that support it. -* MPEG-TS: Support CEA-608/708 in H262 - ([#2565](https://github.com/google/ExoPlayer/issues/2565)). -* MediaSession extension: Allow apps to set custom errors. +* 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: - * Add support for mu-law and A-law PCM with the ffmpeg extension + * 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)). - * Add support for attaching auxiliary audio effects to the `AudioTrack`. - * Add support for seamless adaptation while playing xHE-AAC streams. -* Video: - * Add callback to `VideoListener` to notify of surface size changes. - * Scale up the initial video decoder maximum input size so playlist item - transitions with small increases in maximum sample size don't require - reinitialization ([#4510](https://github.com/google/ExoPlayer/issues/4510)). - * Propagate the end-of-stream signal directly in the renderer when using - tunneling, to fix an issue where the player would remain ready after the - stream ended. + * 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 components: + * 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`. -* Turned on Java 8 compiler support for the ExoPlayer library. Apps that depend - on ExoPlayer via its source code rather than an AAR may need to add - `compileOptions { targetCompatibility JavaVersion.VERSION_1_8 }` to their - gradle settings to ensure bytecode compatibility. -* ConcatenatingMediaSource: - * Add support for lazy preparation of playlist media sources - ([#3972](https://github.com/google/ExoPlayer/issues/3972)). - * Add support for range removal with `removeMediaSourceRange` methods. -* `BandwidthMeter` management: - * Pass `BandwidthMeter` directly to `ExoPlayerFactory` instead of - `TrackSelection.Factory` and `DataSource.Factory`. May also be omitted to - use the default bandwidth meter automatically. This change only works - correctly if the following changes are adopted for custom `BandwidthMeter`s, - `TrackSelection`s, `MediaSource`s and `DataSource`s. - * Pass `BandwidthMeter` to `TrackSelection.Factory` which should be used to - obtain bandwidth estimates. - * Add method to `BandwidthMeter` to return the `TransferListener` used to - gather bandwidth information. Also add methods to add and remove event - listeners. - * Pass `TransferListener` to `MediaSource`s to listen to media data transfers. - * Add method to `DataSource` to add `TransferListener`s. Custom `DataSource`s - directly reading data should implement `BaseDataSource` to handle the - registration correctly. Custom `DataSource`'s forwarding to other sources - should forward all calls to `addTransferListener`. - * Extend `TransferListener` with additional callback parameters. -* Error handling: - * Allow configuration of the Loader retry delay - ([#3370](https://github.com/google/ExoPlayer/issues/3370)). -* HLS: - * Add support for variable substitution - ([#4422](https://github.com/google/ExoPlayer/issues/4422)). - * Add support for PlayReady. - * Add support for alternative EXT-X-KEY tags. - * Set the bitrate on primary track sample formats - ([#3297](https://github.com/google/ExoPlayer/issues/3297)). - * Pass HTTP response headers to `HlsExtractorFactory.createExtractor`. - * Add support for EXT-X-INDEPENDENT-SEGMENTS in the master playlist. - * Support load error handling customization - ([#2981](https://github.com/google/ExoPlayer/issues/2981)). -* Fix bug when reporting buffered position for multi-period windows and add - two additional convenience methods `Player.getTotalBufferedDuration` and - `Player.getContentBufferedDuration` - ([#4023](https://github.com/google/ExoPlayer/issues/4023)). -* MediaSession extension: - * Allow apps to set custom metadata with a MediaMetadataProvider - ([#3497](https://github.com/google/ExoPlayer/issues/3497)). -* Improved performance when playing high frame-rate content, and when playing - at greater than 1x speed - ([#2777](https://github.com/google/ExoPlayer/issues/2777)). -* Allow setting the `Looper`, which is used to access the player, in - `ExoPlayerFactory` ([#4278](https://github.com/google/ExoPlayer/issues/4278)). -* Use default Deserializers if non given to DownloadManager. -* 360: - * Add monoscopic 360 surface type to PlayerView. - * Support - [VR180 video format](https://github.com/google/spatial-media/blob/master/docs/vr180.md). -* Deprecate `Player.DefaultEventListener` as selective listener overrides can - be directly made with the `Player.EventListener` interface. -* Deprecate `DefaultAnalyticsListener` as selective listener overrides can be - directly made with the `AnalyticsListener` interface. -* Add uri field to `LoadEventInfo` in `MediaSourceEventListener` or - `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` or - `AnalyticsListener` callbacks - ([#4361](https://github.com/google/ExoPlayer/issues/4361) and - [#4615](https://github.com/google/ExoPlayer/issues/4615)). +* 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)). -* Fix where transitions to clipped media sources happened too early +* 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)). -* Add `DataSpec.httpMethod` and update `HttpDataSource` implementations to - support HTTP HEAD method. Previously, only GET and POST were supported. -* Add option 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. -* IMA: +* 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)), - ([#4280](https://github.com/google/ExoPlayer/issues/4280)). + 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). + ([#4681](https://github.com/google/ExoPlayer/issues/4681)). * Fix handling of postrolls with multiple ads - ([#4710](https://github.com/google/ExoPlayer/issues/4710). + ([#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 ### -* IMA: Improve handling of consecutive empty ad groups +* 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 ### -* IMA: +* 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. @@ -182,8 +648,9 @@ ### 2.8.2 ### -* IMA: 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)). +* 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: @@ -293,18 +760,18 @@ 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]). + ([#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]). + ([#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]). + ([#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]). + ([#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. @@ -328,7 +795,7 @@ ([#4164](https://github.com/google/ExoPlayer/issues/4182)). * Fix seeking in live streams ([#4187](https://github.com/google/ExoPlayer/issues/4187)). -* IMA: +* 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`, @@ -1001,7 +1468,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. @@ -1245,8 +1712,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 a013f4fb84..1d0b459bf5 100644 --- a/build.gradle +++ b/build.gradle @@ -13,30 +13,22 @@ // limitations under the License. buildscript { repositories { - jcenter() google() + jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:3.1.4' - classpath 'com.novoda:bintray-release:0.8.1' - classpath 'com.google.android.gms:strict-version-matcher-plugin:1.0.3' - } - // 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.4.0' + classpath 'com.novoda:bintray-release:0.9' + classpath 'com.google.android.gms:strict-version-matcher-plugin:1.1.0' } } allprojects { repositories { - jcenter() google() + jcenter() } project.ext { - exoplayerPublishEnabled = true + exoplayerPublishEnabled = false } if (it.hasProperty('externalBuildDir')) { if (!new File(externalBuildDir).isAbsolute()) { @@ -44,6 +36,7 @@ allprojects { } buildDir = "${externalBuildDir}/${project.name}" } + group = 'com.google.android.exoplayer' } apply from: 'javadoc_combined.gradle' diff --git a/constants.gradle b/constants.gradle index c1776b86c4..b1c2c636c7 100644 --- a/constants.gradle +++ b/constants.gradle @@ -13,26 +13,20 @@ // limitations under the License. project.ext { // ExoPlayer version and version code. - releaseVersion = '2.8.4' - releaseVersionCode = 2804 - // Important: ExoPlayer specifies a minSdkVersion of 14 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 = 14 + releaseVersion = '2.10.4' + releaseVersionCode = 2010004 + minSdkVersion = 16 targetSdkVersion = 28 - compileSdkVersion = 28 - buildToolsVersion = '28.0.2' - testSupportLibraryVersion = '0.5' - supportLibraryVersion = '27.1.1' - dexmakerVersion = '1.2' - mockitoVersion = '1.9.5' - junitVersion = '4.12' - truthVersion = '0.39' - robolectricVersion = '3.7.1' + compileSdkVersion = 29 + dexmakerVersion = '2.21.0' + mockitoVersion = '2.25.0' + robolectricVersion = '4.3' autoValueVersion = '1.6' + autoServiceVersion = '1.0-rc4' checkerframeworkVersion = '2.5.0' - testRunnerVersion = '1.1.0-alpha3' + jsr305Version = '3.0.2' + androidXTestVersion = '1.1.0' + truthVersion = '0.44' modulePrefix = ':' if (gradle.ext.has('exoplayerModulePrefix')) { modulePrefix += gradle.ext.exoplayerModulePrefix diff --git a/core_settings.gradle b/core_settings.gradle index 4d90fa962a..3f6d58f777 100644 --- a/core_settings.gradle +++ b/core_settings.gradle @@ -24,7 +24,6 @@ include modulePrefix + 'library-hls' include modulePrefix + 'library-smoothstreaming' include modulePrefix + 'library-ui' include modulePrefix + 'testutils' -include modulePrefix + 'testutils-robolectric' include modulePrefix + 'extension-ffmpeg' include modulePrefix + 'extension-flac' include modulePrefix + 'extension-gvr' @@ -38,6 +37,7 @@ include modulePrefix + 'extension-vp9' include modulePrefix + 'extension-rtmp' include modulePrefix + 'extension-leanback' include modulePrefix + 'extension-jobdispatcher' +include modulePrefix + 'extension-workmanager' project(modulePrefix + 'library').projectDir = new File(rootDir, 'library/all') project(modulePrefix + 'library-core').projectDir = new File(rootDir, 'library/core') @@ -46,7 +46,6 @@ project(modulePrefix + 'library-hls').projectDir = new File(rootDir, 'library/hl 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 + 'testutils-robolectric').projectDir = new File(rootDir, 'testutils_robolectric') 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') @@ -60,3 +59,4 @@ project(modulePrefix + 'extension-vp9').projectDir = new File(rootDir, 'extensio 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/demos/cast/build.gradle b/demos/cast/build.gradle index 915bc10b7c..85e60f2796 100644 --- a/demos/cast/build.gradle +++ b/demos/cast/build.gradle @@ -16,7 +16,6 @@ apply plugin: 'com.android.application' android { compileSdkVersion project.ext.compileSdkVersion - buildToolsVersion project.ext.buildToolsVersion compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 @@ -26,7 +25,7 @@ android { defaultConfig { versionName project.ext.releaseVersion versionCode project.ext.releaseVersionCode - minSdkVersion 16 + minSdkVersion project.ext.minSdkVersion targetSdkVersion project.ext.targetSdkVersion } @@ -45,10 +44,9 @@ android { } lintOptions { - // The demo app does not have translations. - disable 'MissingTranslation' + // The demo app isn't indexed and doesn't have translations. + disable 'GoogleAppIndexingWarning','MissingTranslation' } - } dependencies { @@ -58,9 +56,10 @@ dependencies { implementation project(modulePrefix + 'library-smoothstreaming') implementation project(modulePrefix + 'library-ui') implementation project(modulePrefix + 'extension-cast') - implementation 'com.android.support:support-v4:' + supportLibraryVersion - implementation 'com.android.support:appcompat-v7:' + supportLibraryVersion - implementation 'com.android.support:recyclerview-v7:' + supportLibraryVersion + implementation 'com.google.android.material:material:1.0.0' + implementation 'androidx.legacy:legacy-support-v4:1.0.0' + implementation 'androidx.appcompat:appcompat:1.0.2' + implementation 'androidx.recyclerview:recyclerview:1.0.0' } apply plugin: 'com.google.android.gms.strict-version-matcher-plugin' diff --git a/demos/cast/proguard-rules.txt b/demos/cast/proguard-rules.txt index 3221818080..e6bf2dd3bf 100644 --- a/demos/cast/proguard-rules.txt +++ b/demos/cast/proguard-rules.txt @@ -1,6 +1,6 @@ # Proguard rules specific to the Cast demo app. # Accessed via menu.xml --keep class android.support.v7.app.MediaRouteActionProvider { +-keep class androidx.mediarouter.app.MediaRouteActionProvider { *; } diff --git a/demos/cast/src/main/AndroidManifest.xml b/demos/cast/src/main/AndroidManifest.xml index ae16776333..dbfdd833f6 100644 --- a/demos/cast/src/main/AndroidManifest.xml +++ b/demos/cast/src/main/AndroidManifest.xml @@ -17,13 +17,15 @@ package="com.google.android.exoplayer2.castdemo"> + + + android:value="com.google.android.exoplayer2.ext.cast.DefaultCastOptionsProvider"/> SAMPLES; - - /** - * Represents a media sample. - */ - public static final class Sample { - - /** - * The uri from which the media sample is obtained. - */ - public final String uri; - /** - * A descriptive name for the sample. - */ - public final String name; - /** - * The mime type of the media sample, as required by {@link MediaInfo#setContentType}. - */ - public final String mimeType; - - /** - * @param uri See {@link #uri}. - * @param name See {@link #name}. - * @param mimeType See {@link #mimeType}. - */ - public Sample(String uri, String name, String mimeType) { - this.uri = uri; - this.name = name; - this.mimeType = mimeType; - } - - @Override - public String toString() { - return name; - } - - } + /** The list of samples available in the cast demo app. */ + public static final List SAMPLES; static { - // App samples. - ArrayList samples = new ArrayList<>(); - samples.add(new Sample("https://storage.googleapis.com/wvmedia/clear/h264/tears/tears.mpd", - "DASH (clear,MP4,H264)", MIME_TYPE_DASH)); - samples.add(new Sample("https://commondatastorage.googleapis.com/gtv-videos-bucket/CastVideos/" - + "hls/TearsOfSteel.m3u8", "Tears of Steel (HLS)", MIME_TYPE_HLS)); - samples.add(new Sample("https://html5demos.com/assets/dizzy.mp4", "Dizzy (MP4)", - MIME_TYPE_VIDEO_MP4)); + 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 index 30968b8f85..d0e40990be 100644 --- 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 @@ -17,13 +17,15 @@ package com.google.android.exoplayer2.castdemo; import android.content.Context; import android.os.Bundle; -import android.support.v4.graphics.ColorUtils; -import android.support.v7.app.AlertDialog; -import android.support.v7.app.AppCompatActivity; -import android.support.v7.widget.LinearLayoutManager; -import android.support.v7.widget.RecyclerView; -import android.support.v7.widget.RecyclerView.ViewHolder; -import android.support.v7.widget.helper.ItemTouchHelper; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.core.graphics.ColorUtils; +import androidx.appcompat.app.AlertDialog; +import androidx.appcompat.app.AppCompatActivity; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; +import androidx.recyclerview.widget.RecyclerView.ViewHolder; +import androidx.recyclerview.widget.ItemTouchHelper; import android.view.KeyEvent; import android.view.LayoutInflater; import android.view.Menu; @@ -33,20 +35,22 @@ import android.view.ViewGroup; import android.widget.ArrayAdapter; import android.widget.ListView; import android.widget.TextView; +import android.widget.Toast; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.SimpleExoPlayer; -import com.google.android.exoplayer2.castdemo.DemoUtil.Sample; -import com.google.android.exoplayer2.ext.cast.CastPlayer; +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 {@link CastPlayer}. + * An activity that plays video using {@link SimpleExoPlayer} and supports casting using ExoPlayer's + * Cast extension. */ -public class MainActivity extends AppCompatActivity implements OnClickListener, - PlayerManager.QueuePositionListener { +public class MainActivity extends AppCompatActivity + implements OnClickListener, PlayerManager.Listener { private PlayerView localPlayerView; private PlayerControlView castControlView; @@ -61,7 +65,20 @@ public class MainActivity extends AppCompatActivity implements OnClickListener, public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); // Getting the cast context later than onStart can cause device discovery not to take place. - castContext = CastContext.getSharedInstance(this); + 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); @@ -91,9 +108,13 @@ public class MainActivity extends AppCompatActivity implements OnClickListener, @Override public void onResume() { super.onResume(); + if (castContext == null) { + // There is no Cast context to work with. Do nothing. + return; + } playerManager = - PlayerManager.createPlayerManager( - /* queuePositionListener= */ this, + new PlayerManager( + /* listener= */ this, localPlayerView, castControlView, /* context= */ this, @@ -104,9 +125,14 @@ public class MainActivity extends AppCompatActivity implements OnClickListener, @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. @@ -119,12 +145,15 @@ public class MainActivity extends AppCompatActivity implements OnClickListener, @Override public void onClick(View view) { - new AlertDialog.Builder(this).setTitle(R.string.sample_list_dialog_title) - .setView(buildSampleListView()).setPositiveButton(android.R.string.ok, null).create() + new AlertDialog.Builder(this) + .setTitle(R.string.add_samples) + .setView(buildSampleListView()) + .setPositiveButton(android.R.string.ok, null) + .create() .show(); } - // PlayerManager.QueuePositionListener implementation. + // PlayerManager.Listener implementation. @Override public void onQueuePositionChanged(int previousIndex, int newIndex) { @@ -136,8 +165,23 @@ public class MainActivity extends AppCompatActivity implements OnClickListener, } } + @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); @@ -152,23 +196,6 @@ public class MainActivity extends AppCompatActivity implements OnClickListener, // Internal classes. - private class QueueItemViewHolder extends RecyclerView.ViewHolder implements OnClickListener { - - public final TextView textView; - - public QueueItemViewHolder(TextView textView) { - super(textView); - this.textView = textView; - textView.setOnClickListener(this); - } - - @Override - public void onClick(View v) { - playerManager.selectQueueItem(getAdapterPosition()); - } - - } - private class MediaQueueListAdapter extends RecyclerView.Adapter { @Override @@ -180,11 +207,14 @@ public class MainActivity extends AppCompatActivity implements OnClickListener, @Override public void onBindViewHolder(QueueItemViewHolder holder, int position) { + holder.item = playerManager.getItem(position); TextView view = holder.textView; - view.setText(playerManager.getItem(position).name); + view.setText(holder.item.title); // TODO: Solve coloring using the theme's ColorStateList. - view.setTextColor(ColorUtils.setAlphaComponent(view.getCurrentTextColor(), - position == playerManager.getCurrentItemIndex() ? 255 : 100)); + view.setTextColor( + ColorUtils.setAlphaComponent( + view.getCurrentTextColor(), + position == playerManager.getCurrentItemIndex() ? 255 : 100)); } @Override @@ -222,8 +252,11 @@ public class MainActivity extends AppCompatActivity implements OnClickListener, @Override public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) { int position = viewHolder.getAdapterPosition(); - if (playerManager.removeItem(position)) { + 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); } } @@ -231,8 +264,9 @@ public class MainActivity extends AppCompatActivity implements OnClickListener, 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(draggingFromPosition, draggingToPosition)) { + 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(); @@ -241,15 +275,37 @@ public class MainActivity extends AppCompatActivity implements OnClickListener, draggingFromPosition = C.INDEX_UNSET; draggingToPosition = C.INDEX_UNSET; } - } - private static final class SampleListAdapter extends ArrayAdapter { + 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 index d188469de8..8b75eb0c74 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright (C) 2017 The Android Open Source Project + * Copyright (C) 2019 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,52 +17,62 @@ package com.google.android.exoplayer2.castdemo; import android.content.Context; import android.net.Uri; -import android.support.annotation.Nullable; import android.view.KeyEvent; import android.view.View; import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.DefaultRenderersFactory; import com.google.android.exoplayer2.ExoPlayerFactory; 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.RenderersFactory; import com.google.android.exoplayer2.SimpleExoPlayer; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.Timeline.Period; -import com.google.android.exoplayer2.castdemo.DemoUtil.Sample; +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.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.ExtractorMediaSource; import com.google.android.exoplayer2.source.MediaSource; +import com.google.android.exoplayer2.source.ProgressiveMediaSource; +import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.source.dash.DashMediaSource; import com.google.android.exoplayer2.source.hls.HlsMediaSource; import com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource; import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; +import com.google.android.exoplayer2.trackselection.MappingTrackSelector; +import com.google.android.exoplayer2.trackselection.TrackSelectionArray; import com.google.android.exoplayer2.ui.PlayerControlView; import com.google.android.exoplayer2.ui.PlayerView; import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory; -import com.google.android.gms.cast.MediaInfo; -import com.google.android.gms.cast.MediaMetadata; import com.google.android.gms.cast.MediaQueueItem; import com.google.android.gms.cast.framework.CastContext; import java.util.ArrayList; +import java.util.IdentityHashMap; +import java.util.Map; -/** Manages players and an internal media queue for the ExoPlayer/Cast demo app. */ -/* package */ final class PlayerManager - implements EventListener, CastPlayer.SessionAvailabilityListener { +/** Manages players and an internal media queue for the demo app. */ +/* package */ class PlayerManager implements EventListener, SessionAvailabilityListener { - /** - * Listener for changes in the media queue playback position. - */ - public interface QueuePositionListener { + /** Listener for events. */ + interface Listener { - /** - * Called when the currently played item of the media queue changes. - */ + /** 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"; @@ -71,52 +81,45 @@ import java.util.ArrayList; 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 QueuePositionListener queuePositionListener; + private final ArrayList mediaQueue; + private final Listener listener; private final ConcatenatingMediaSource concatenatingMediaSource; + private final MediaItemConverter mediaItemConverter; + private final IdentityHashMap mediaDrms; - private boolean castMediaQueueCreationPending; + private TrackGroupArray lastSeenTrackGroupArray; private int currentItemIndex; private Player currentPlayer; /** - * @param queuePositionListener A {@link QueuePositionListener} for queue position changes. + * 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 static PlayerManager createPlayerManager( - QueuePositionListener queuePositionListener, + public PlayerManager( + Listener listener, PlayerView localPlayerView, PlayerControlView castControlView, Context context, CastContext castContext) { - PlayerManager playerManager = - new PlayerManager( - queuePositionListener, localPlayerView, castControlView, context, castContext); - playerManager.init(); - return playerManager; - } - - private PlayerManager( - QueuePositionListener queuePositionListener, - PlayerView localPlayerView, - PlayerControlView castControlView, - Context context, - CastContext castContext) { - this.queuePositionListener = queuePositionListener; + this.listener = listener; this.localPlayerView = localPlayerView; this.castControlView = castControlView; mediaQueue = new ArrayList<>(); currentItemIndex = C.INDEX_UNSET; concatenatingMediaSource = new ConcatenatingMediaSource(); + mediaItemConverter = new DefaultMediaItemConverter(); + mediaDrms = new IdentityHashMap<>(); - DefaultTrackSelector trackSelector = new DefaultTrackSelector(); - RenderersFactory renderersFactory = new DefaultRenderersFactory(context); - exoPlayer = ExoPlayerFactory.newSimpleInstance(context, renderersFactory, trackSelector); + trackSelector = new DefaultTrackSelector(context); + exoPlayer = ExoPlayerFactory.newSimpleInstance(context, trackSelector); exoPlayer.addListener(this); localPlayerView.setPlayer(exoPlayer); @@ -124,6 +127,8 @@ import java.util.ArrayList; castPlayer.addListener(this); castPlayer.setSessionAvailabilityListener(this); castControlView.setPlayer(castPlayer); + + setCurrentPlayer(castPlayer.isCastSessionAvailable() ? castPlayer : exoPlayer); } // Queue manipulation methods. @@ -137,29 +142,25 @@ import java.util.ArrayList; setCurrentItem(itemIndex, C.TIME_UNSET, true); } - /** - * Returns the index of the currently played item. - */ + /** Returns the index of the currently played item. */ public int getCurrentItemIndex() { return currentItemIndex; } /** - * Appends {@code sample} to the media queue. + * Appends {@code item} to the media queue. * - * @param sample The {@link Sample} to append. + * @param item The {@link MediaItem} to append. */ - public void addItem(Sample sample) { - mediaQueue.add(sample); - concatenatingMediaSource.addMediaSource(buildMediaSource(sample)); + public void addItem(MediaItem item) { + mediaQueue.add(item); + concatenatingMediaSource.addMediaSource(buildMediaSource(item)); if (currentPlayer == castPlayer) { - castPlayer.addItems(buildMediaQueueItem(sample)); + castPlayer.addItems(mediaItemConverter.toMediaQueueItem(item)); } } - /** - * Returns the size of the media queue. - */ + /** Returns the size of the media queue. */ public int getMediaQueueSize() { return mediaQueue.size(); } @@ -170,18 +171,23 @@ import java.util.ArrayList; * @param position The index of the item. * @return The item at the given index in the media queue. */ - public Sample getItem(int position) { + public MediaItem getItem(int position) { return mediaQueue.get(position); } /** * Removes the item at the given index from the media queue. * - * @param itemIndex The index of the item to remove. + * @param item The item to remove. * @return Whether the removal was successful. */ - public boolean removeItem(int itemIndex) { - concatenatingMediaSource.removeMediaSource(itemIndex); + public boolean removeItem(MediaItem item) { + int itemIndex = mediaQueue.indexOf(item); + if (itemIndex == -1) { + return false; + } + MediaSource removedMediaSource = concatenatingMediaSource.removeMediaSource(itemIndex); + releaseMediaDrmOfMediaSource(removedMediaSource); if (currentPlayer == castPlayer) { if (castPlayer.getPlaybackState() != Player.STATE_IDLE) { Timeline castTimeline = castPlayer.getCurrentTimeline(); @@ -203,11 +209,15 @@ import java.util.ArrayList; /** * Moves an item within the queue. * - * @param fromIndex The index of the item to move. + * @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(int fromIndex, int toIndex) { + 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) { @@ -234,8 +244,6 @@ import java.util.ArrayList; return true; } - // Miscellaneous methods. - /** * Dispatches a given {@link KeyEvent} to the corresponding view of the current player. * @@ -250,13 +258,14 @@ import java.util.ArrayList; } } - /** - * Releases the manager and the players that it holds. - */ + /** Releases the manager and the players that it holds. */ public void release() { currentItemIndex = C.INDEX_UNSET; mediaQueue.clear(); concatenatingMediaSource.clear(); + for (FrameworkMediaDrm mediaDrm : mediaDrms.values()) { + mediaDrm.release(); + } castPlayer.setSessionAvailabilityListener(null); castPlayer.release(); localPlayerView.setPlayer(null); @@ -266,7 +275,7 @@ import java.util.ArrayList; // Player.EventListener implementation. @Override - public void onPlayerStateChanged(boolean playWhenReady, int playbackState) { + public void onPlayerStateChanged(boolean playWhenReady, @Player.State int playbackState) { updateCurrentItemIndex(); } @@ -276,11 +285,26 @@ import java.util.ArrayList; } @Override - public void onTimelineChanged( - Timeline timeline, @Nullable Object manifest, @TimelineChangeReason int reason) { + public void onTimelineChanged(Timeline timeline, @TimelineChangeReason int reason) { updateCurrentItemIndex(); - if (timeline.isEmpty()) { - castMediaQueueCreationPending = true; + } + + @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; } } @@ -298,15 +322,12 @@ import java.util.ArrayList; // Internal methods. - private void init() { - setCurrentPlayer(castPlayer.isCastSessionAvailable() ? castPlayer : exoPlayer); - } - private void updateCurrentItemIndex() { int playbackState = currentPlayer.getPlaybackState(); maybeSetCurrentItemAndNotify( playbackState != Player.STATE_IDLE && playbackState != Player.STATE_ENDED - ? currentPlayer.getCurrentWindowIndex() : C.INDEX_UNSET); + ? currentPlayer.getCurrentWindowIndex() + : C.INDEX_UNSET); } private void setCurrentPlayer(Player currentPlayer) { @@ -327,26 +348,26 @@ import java.util.ArrayList; long playbackPositionMs = C.TIME_UNSET; int windowIndex = C.INDEX_UNSET; boolean playWhenReady = false; - if (this.currentPlayer != null) { - int playbackState = this.currentPlayer.getPlaybackState(); + + Player previousPlayer = this.currentPlayer; + if (previousPlayer != null) { + // Save state from the previous player. + int playbackState = previousPlayer.getPlaybackState(); if (playbackState != Player.STATE_ENDED) { - playbackPositionMs = this.currentPlayer.getCurrentPosition(); - playWhenReady = this.currentPlayer.getPlayWhenReady(); - windowIndex = this.currentPlayer.getCurrentWindowIndex(); + playbackPositionMs = previousPlayer.getCurrentPosition(); + playWhenReady = previousPlayer.getPlayWhenReady(); + windowIndex = previousPlayer.getCurrentWindowIndex(); if (windowIndex != currentItemIndex) { playbackPositionMs = C.TIME_UNSET; windowIndex = currentItemIndex; } } - this.currentPlayer.stop(true); - } else { - // This is the initial setup. No need to save any state. + previousPlayer.stop(true); } this.currentPlayer = currentPlayer; // Media queue management. - castMediaQueueCreationPending = currentPlayer == castPlayer; if (currentPlayer == exoPlayer) { exoPlayer.prepare(concatenatingMediaSource); } @@ -366,12 +387,11 @@ import java.util.ArrayList; */ private void setCurrentItem(int itemIndex, long positionMs, boolean playWhenReady) { maybeSetCurrentItemAndNotify(itemIndex); - if (castMediaQueueCreationPending) { + if (currentPlayer == castPlayer && castPlayer.getCurrentTimeline().isEmpty()) { MediaQueueItem[] items = new MediaQueueItem[mediaQueue.size()]; for (int i = 0; i < items.length; i++) { - items[i] = buildMediaQueueItem(mediaQueue.get(i)); + items[i] = mediaItemConverter.toMediaQueueItem(mediaQueue.get(i)); } - castMediaQueueCreationPending = false; castPlayer.loadItems(items, itemIndex, positionMs, Player.REPEAT_MODE_OFF); } else { currentPlayer.seekTo(itemIndex, positionMs); @@ -383,34 +403,82 @@ import java.util.ArrayList; if (this.currentItemIndex != currentItemIndex) { int oldIndex = this.currentItemIndex; this.currentItemIndex = currentItemIndex; - queuePositionListener.onQueuePositionChanged(oldIndex, currentItemIndex); + listener.onQueuePositionChanged(oldIndex, currentItemIndex); } } - private static MediaSource buildMediaSource(DemoUtil.Sample sample) { - Uri uri = Uri.parse(sample.uri); - switch (sample.mimeType) { - case DemoUtil.MIME_TYPE_SS: - return new SsMediaSource.Factory(DATA_SOURCE_FACTORY).createMediaSource(uri); - case DemoUtil.MIME_TYPE_DASH: - return new DashMediaSource.Factory(DATA_SOURCE_FACTORY).createMediaSource(uri); - case DemoUtil.MIME_TYPE_HLS: - return new HlsMediaSource.Factory(DATA_SOURCE_FACTORY).createMediaSource(uri); - case DemoUtil.MIME_TYPE_VIDEO_MP4: - return new ExtractorMediaSource.Factory(DATA_SOURCE_FACTORY).createMediaSource(uri); - default: { - throw new IllegalStateException("Unsupported type: " + sample.mimeType); + private MediaSource buildMediaSource(MediaItem item) { + Uri uri = item.uri; + String mimeType = item.mimeType; + if (mimeType == null) { + throw new IllegalArgumentException("mimeType is required"); + } + + FrameworkMediaDrm mediaDrm = null; + DrmSessionManager drmSessionManager = + DrmSessionManager.getDummyDrmSessionManager(); + MediaItem.DrmConfiguration drmConfiguration = item.drmConfiguration; + if (drmConfiguration != null) { + String licenseServerUrl = + drmConfiguration.licenseUri != null ? drmConfiguration.licenseUri.toString() : ""; + HttpMediaDrmCallback drmCallback = + new HttpMediaDrmCallback(licenseServerUrl, DATA_SOURCE_FACTORY); + for (Map.Entry requestHeader : drmConfiguration.requestHeaders.entrySet()) { + drmCallback.setKeyRequestProperty(requestHeader.getKey(), requestHeader.getValue()); + } + try { + mediaDrm = FrameworkMediaDrm.newInstance(drmConfiguration.uuid); + drmSessionManager = + new DefaultDrmSessionManager<>( + drmConfiguration.uuid, + mediaDrm, + drmCallback, + /* optionalKeyRequestParameters= */ null, + /* multiSession= */ true); + } catch (UnsupportedDrmException e) { + // Do nothing. The track selector will avoid selecting the DRM protected tracks. } } + + MediaSource createdMediaSource; + switch (mimeType) { + case DemoUtil.MIME_TYPE_SS: + createdMediaSource = + new SsMediaSource.Factory(DATA_SOURCE_FACTORY) + .setDrmSessionManager(drmSessionManager) + .createMediaSource(uri); + break; + case DemoUtil.MIME_TYPE_DASH: + createdMediaSource = + new DashMediaSource.Factory(DATA_SOURCE_FACTORY) + .setDrmSessionManager(drmSessionManager) + .createMediaSource(uri); + break; + case DemoUtil.MIME_TYPE_HLS: + createdMediaSource = + new HlsMediaSource.Factory(DATA_SOURCE_FACTORY) + .setDrmSessionManager(drmSessionManager) + .createMediaSource(uri); + break; + case DemoUtil.MIME_TYPE_VIDEO_MP4: + createdMediaSource = + new ProgressiveMediaSource.Factory(DATA_SOURCE_FACTORY) + .setDrmSessionManager(drmSessionManager) + .createMediaSource(uri); + break; + default: + throw new IllegalArgumentException("mimeType is unsupported: " + mimeType); + } + if (mediaDrm != null) { + mediaDrms.put(createdMediaSource, mediaDrm); + } + return createdMediaSource; } - private static MediaQueueItem buildMediaQueueItem(DemoUtil.Sample sample) { - MediaMetadata movieMetadata = new MediaMetadata(MediaMetadata.MEDIA_TYPE_MOVIE); - movieMetadata.putString(MediaMetadata.KEY_TITLE, sample.name); - MediaInfo mediaInfo = new MediaInfo.Builder(sample.uri) - .setStreamType(MediaInfo.STREAM_TYPE_BUFFERED).setContentType(sample.mimeType) - .setMetadata(movieMetadata).build(); - return new MediaQueueItem.Builder(mediaInfo).build(); + private void releaseMediaDrmOfMediaSource(MediaSource mediaSource) { + FrameworkMediaDrm mediaDrmToRelease = mediaDrms.remove(mediaSource); + if (mediaDrmToRelease != null) { + mediaDrmToRelease.release(); + } } - } diff --git a/demos/cast/src/main/res/drawable/ic_add_circle_white_24dp.xml b/demos/cast/src/main/res/drawable/ic_plus.xml similarity index 59% rename from demos/cast/src/main/res/drawable/ic_add_circle_white_24dp.xml rename to demos/cast/src/main/res/drawable/ic_plus.xml index 5f3c8961ef..5a5a5154c9 100644 --- a/demos/cast/src/main/res/drawable/ic_add_circle_white_24dp.xml +++ b/demos/cast/src/main/res/drawable/ic_plus.xml @@ -13,8 +13,12 @@ See the License for the specific language governing permissions and limitations under the License. --> - - + + 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 index 01e48cdea7..71dbcdcd9c 100644 --- a/demos/cast/src/main/res/layout/main_activity.xml +++ b/demos/cast/src/main/res/layout/main_activity.xml @@ -19,34 +19,42 @@ android:layout_width="match_parent" android:layout_height="match_parent" android:keepScreenOn="true"> + + - + + - + android:layout_margin="16dp" + android:contentDescription="@string/add_samples"/> + + + diff --git a/demos/cast/src/main/res/layout/sample_list.xml b/demos/cast/src/main/res/layout/sample_list.xml index 910db9e058..183c74eb3a 100644 --- a/demos/cast/src/main/res/layout/sample_list.xml +++ b/demos/cast/src/main/res/layout/sample_list.xml @@ -14,7 +14,7 @@ limitations under the License. --> diff --git a/demos/cast/src/main/res/values/strings.xml b/demos/cast/src/main/res/values/strings.xml index 3505c40400..69f0691630 100644 --- a/demos/cast/src/main/res/values/strings.xml +++ b/demos/cast/src/main/res/values/strings.xml @@ -20,6 +20,12 @@ Cast - Add samples + Add samples + + Failed to get Cast context. Try updating Google Play Services and restart the app. + + Media includes video tracks, but none are playable by this device + + Media includes audio tracks, but none are playable by this device diff --git a/demos/gvr/README.md b/demos/gvr/README.md new file mode 100644 index 0000000000..8cc52c5f10 --- /dev/null +++ b/demos/gvr/README.md @@ -0,0 +1,4 @@ +# ExoPlayer VR player demo # + +This folder contains a demo application that showcases 360 video playback using +ExoPlayer GVR extension. diff --git a/demos/gvr/build.gradle b/demos/gvr/build.gradle new file mode 100644 index 0000000000..37d8fbbb99 --- /dev/null +++ b/demos/gvr/build.gradle @@ -0,0 +1,59 @@ +// Copyright (C) 2019 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +apply from: '../../constants.gradle' +apply plugin: 'com.android.application' + +android { + compileSdkVersion project.ext.compileSdkVersion + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + defaultConfig { + versionName project.ext.releaseVersion + versionCode project.ext.releaseVersionCode + minSdkVersion 19 + targetSdkVersion project.ext.targetSdkVersion + } + + buildTypes { + release { + shrinkResources true + minifyEnabled true + proguardFiles getDefaultProguardFile('proguard-android.txt') + } + debug { + jniDebuggable = true + } + } + + lintOptions { + // The demo app isn't indexed and doesn't have translations. + disable 'GoogleAppIndexingWarning','MissingTranslation' + } +} + +dependencies { + implementation project(modulePrefix + 'library-core') + implementation project(modulePrefix + 'library-ui') + implementation project(modulePrefix + 'library-dash') + implementation project(modulePrefix + 'library-hls') + implementation project(modulePrefix + 'library-smoothstreaming') + implementation project(modulePrefix + 'extension-gvr') + implementation 'androidx.annotation:annotation:1.1.0' +} + +apply plugin: 'com.google.android.gms.strict-version-matcher-plugin' diff --git a/demos/gvr/src/main/AndroidManifest.xml b/demos/gvr/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..8545787064 --- /dev/null +++ b/demos/gvr/src/main/AndroidManifest.xml @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/demos/gvr/src/main/java/com/google/android/exoplayer2/gvrdemo/PlayerActivity.java b/demos/gvr/src/main/java/com/google/android/exoplayer2/gvrdemo/PlayerActivity.java new file mode 100644 index 0000000000..059f26b374 --- /dev/null +++ b/demos/gvr/src/main/java/com/google/android/exoplayer2/gvrdemo/PlayerActivity.java @@ -0,0 +1,242 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2.gvrdemo; + +import android.content.Intent; +import android.net.Uri; +import android.os.Bundle; +import androidx.annotation.Nullable; +import android.widget.Toast; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.C.ContentType; +import com.google.android.exoplayer2.DefaultRenderersFactory; +import com.google.android.exoplayer2.ExoPlaybackException; +import com.google.android.exoplayer2.ExoPlayerFactory; +import com.google.android.exoplayer2.PlaybackPreparer; +import com.google.android.exoplayer2.Player; +import com.google.android.exoplayer2.SimpleExoPlayer; +import com.google.android.exoplayer2.ext.gvr.GvrPlayerActivity; +import com.google.android.exoplayer2.source.MediaSource; +import com.google.android.exoplayer2.source.ProgressiveMediaSource; +import com.google.android.exoplayer2.source.TrackGroupArray; +import com.google.android.exoplayer2.source.dash.DashMediaSource; +import com.google.android.exoplayer2.source.hls.HlsMediaSource; +import com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource; +import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; +import com.google.android.exoplayer2.trackselection.MappingTrackSelector.MappedTrackInfo; +import com.google.android.exoplayer2.trackselection.TrackSelectionArray; +import com.google.android.exoplayer2.upstream.DataSource; +import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory; +import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory; +import com.google.android.exoplayer2.util.EventLogger; +import com.google.android.exoplayer2.util.Util; + +/** An activity that plays media using {@link SimpleExoPlayer}. */ +public class PlayerActivity extends GvrPlayerActivity implements PlaybackPreparer { + + public static final String EXTENSION_EXTRA = "extension"; + + public static final String SPHERICAL_STEREO_MODE_EXTRA = "spherical_stereo_mode"; + public static final String SPHERICAL_STEREO_MODE_MONO = "mono"; + public static final String SPHERICAL_STEREO_MODE_TOP_BOTTOM = "top_bottom"; + public static final String SPHERICAL_STEREO_MODE_LEFT_RIGHT = "left_right"; + + private DataSource.Factory dataSourceFactory; + private SimpleExoPlayer player; + private MediaSource mediaSource; + private DefaultTrackSelector trackSelector; + private TrackGroupArray lastSeenTrackGroupArray; + + private boolean startAutoPlay; + private int startWindow; + private long startPosition; + + // Activity lifecycle + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + String userAgent = Util.getUserAgent(this, "ExoPlayerDemo"); + dataSourceFactory = + new DefaultDataSourceFactory(this, new DefaultHttpDataSourceFactory(userAgent)); + + String sphericalStereoMode = getIntent().getStringExtra(SPHERICAL_STEREO_MODE_EXTRA); + if (sphericalStereoMode != null) { + int stereoMode; + if (SPHERICAL_STEREO_MODE_MONO.equals(sphericalStereoMode)) { + stereoMode = C.STEREO_MODE_MONO; + } else if (SPHERICAL_STEREO_MODE_TOP_BOTTOM.equals(sphericalStereoMode)) { + stereoMode = C.STEREO_MODE_TOP_BOTTOM; + } else if (SPHERICAL_STEREO_MODE_LEFT_RIGHT.equals(sphericalStereoMode)) { + stereoMode = C.STEREO_MODE_LEFT_RIGHT; + } else { + showToast(R.string.error_unrecognized_stereo_mode); + finish(); + return; + } + setDefaultStereoMode(stereoMode); + } + + clearStartPosition(); + } + + @Override + public void onResume() { + super.onResume(); + if (Util.SDK_INT <= 23 || player == null) { + initializePlayer(); + } + } + + @Override + public void onPause() { + super.onPause(); + if (Util.SDK_INT <= 23) { + releasePlayer(); + } + } + + @Override + public void onDestroy() { + super.onDestroy(); + } + + // PlaybackControlView.PlaybackPreparer implementation + + @Override + public void preparePlayback() { + initializePlayer(); + } + + // Internal methods + + private void initializePlayer() { + if (player == null) { + Intent intent = getIntent(); + Uri uri = intent.getData(); + if (!Util.checkCleartextTrafficPermitted(uri)) { + showToast(R.string.error_cleartext_not_permitted); + return; + } + + DefaultRenderersFactory renderersFactory = new DefaultRenderersFactory(this); + + trackSelector = new DefaultTrackSelector(/* context= */ this); + lastSeenTrackGroupArray = null; + + player = + ExoPlayerFactory.newSimpleInstance(/* context= */ this, renderersFactory, trackSelector); + player.addListener(new PlayerEventListener()); + player.setPlayWhenReady(startAutoPlay); + player.addAnalyticsListener(new EventLogger(trackSelector)); + setPlayer(player); + + mediaSource = buildMediaSource(uri, intent.getStringExtra(EXTENSION_EXTRA)); + } + boolean haveStartPosition = startWindow != C.INDEX_UNSET; + if (haveStartPosition) { + player.seekTo(startWindow, startPosition); + } + player.prepare(mediaSource, !haveStartPosition, false); + } + + private MediaSource buildMediaSource(Uri uri, @Nullable String overrideExtension) { + @ContentType int type = Util.inferContentType(uri, overrideExtension); + switch (type) { + case C.TYPE_DASH: + return new DashMediaSource.Factory(dataSourceFactory).createMediaSource(uri); + case C.TYPE_SS: + return new SsMediaSource.Factory(dataSourceFactory).createMediaSource(uri); + case C.TYPE_HLS: + return new HlsMediaSource.Factory(dataSourceFactory).createMediaSource(uri); + case C.TYPE_OTHER: + return new ProgressiveMediaSource.Factory(dataSourceFactory).createMediaSource(uri); + default: + throw new IllegalStateException("Unsupported type: " + type); + } + } + + private void releasePlayer() { + if (player != null) { + updateStartPosition(); + player.release(); + player = null; + mediaSource = null; + trackSelector = null; + } + } + + private void updateStartPosition() { + if (player != null) { + startAutoPlay = player.getPlayWhenReady(); + startWindow = player.getCurrentWindowIndex(); + startPosition = Math.max(0, player.getContentPosition()); + } + } + + private void clearStartPosition() { + startAutoPlay = true; + startWindow = C.INDEX_UNSET; + startPosition = C.TIME_UNSET; + } + + private void showToast(int messageId) { + showToast(getString(messageId)); + } + + private void showToast(String message) { + Toast.makeText(getApplicationContext(), message, Toast.LENGTH_LONG).show(); + } + + private class PlayerEventListener implements Player.EventListener { + + @Override + public void onPlayerStateChanged(boolean playWhenReady, int playbackState) {} + + @Override + public void onPositionDiscontinuity(@Player.DiscontinuityReason int reason) { + if (player.getPlaybackError() != null) { + // The user has performed a seek whilst in the error state. Update the resume position so + // that if the user then retries, playback resumes from the position to which they seeked. + updateStartPosition(); + } + } + + @Override + public void onPlayerError(ExoPlaybackException e) { + updateStartPosition(); + } + + @Override + @SuppressWarnings("ReferenceEquality") + public void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray trackSelections) { + if (trackGroups != lastSeenTrackGroupArray) { + MappedTrackInfo mappedTrackInfo = trackSelector.getCurrentMappedTrackInfo(); + if (mappedTrackInfo != null) { + if (mappedTrackInfo.getTypeSupport(C.TRACK_TYPE_VIDEO) + == MappedTrackInfo.RENDERER_SUPPORT_UNSUPPORTED_TRACKS) { + showToast(R.string.error_unsupported_video); + } + if (mappedTrackInfo.getTypeSupport(C.TRACK_TYPE_AUDIO) + == MappedTrackInfo.RENDERER_SUPPORT_UNSUPPORTED_TRACKS) { + showToast(R.string.error_unsupported_audio); + } + } + lastSeenTrackGroupArray = trackGroups; + } + } + } +} diff --git a/demos/gvr/src/main/java/com/google/android/exoplayer2/gvrdemo/SampleChooserActivity.java b/demos/gvr/src/main/java/com/google/android/exoplayer2/gvrdemo/SampleChooserActivity.java new file mode 100644 index 0000000000..1ddf5c1517 --- /dev/null +++ b/demos/gvr/src/main/java/com/google/android/exoplayer2/gvrdemo/SampleChooserActivity.java @@ -0,0 +1,133 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2.gvrdemo; + +import static com.google.android.exoplayer2.gvrdemo.PlayerActivity.SPHERICAL_STEREO_MODE_LEFT_RIGHT; +import static com.google.android.exoplayer2.gvrdemo.PlayerActivity.SPHERICAL_STEREO_MODE_MONO; +import static com.google.android.exoplayer2.gvrdemo.PlayerActivity.SPHERICAL_STEREO_MODE_TOP_BOTTOM; + +import android.app.Activity; +import android.content.Context; +import android.content.Intent; +import android.net.Uri; +import android.os.Bundle; +import android.widget.ArrayAdapter; +import android.widget.ListView; + +/** An activity for selecting from a list of media samples. */ +public class SampleChooserActivity extends Activity { + + private final Sample[] samples = + new Sample[] { + new Sample( + "Congo (360 top-bottom stereo)", + "https://storage.googleapis.com/exoplayer-test-media-1/360/congo.mp4", + SPHERICAL_STEREO_MODE_TOP_BOTTOM), + new Sample( + "Sphericalv2 (180 top-bottom stereo)", + "https://storage.googleapis.com/exoplayer-test-media-1/360/sphericalv2.mp4", + SPHERICAL_STEREO_MODE_TOP_BOTTOM), + new Sample( + "Iceland (360 top-bottom stereo ts)", + "https://storage.googleapis.com/exoplayer-test-media-1/360/iceland0.ts", + SPHERICAL_STEREO_MODE_TOP_BOTTOM), + new Sample( + "Camera motion metadata test", + "https://storage.googleapis.com/exoplayer-test-media-internal-" + + "63834241aced7884c2544af1a3452e01/vr180/synthetic_with_camm.mp4", + SPHERICAL_STEREO_MODE_TOP_BOTTOM), + new Sample( + "actual_camera_cat", + "https://storage.googleapis.com/exoplayer-test-media-internal-" + + "63834241aced7884c2544af1a3452e01/vr180/actual_camera_cat.mp4", + SPHERICAL_STEREO_MODE_TOP_BOTTOM), + new Sample( + "johnny_stitched", + "https://storage.googleapis.com/exoplayer-test-media-internal-" + + "63834241aced7884c2544af1a3452e01/vr180/johnny_stitched.mp4", + SPHERICAL_STEREO_MODE_TOP_BOTTOM), + new Sample( + "lenovo_birds.vr", + "https://storage.googleapis.com/exoplayer-test-media-internal-" + + "63834241aced7884c2544af1a3452e01/vr180/lenovo_birds.vr.mp4", + SPHERICAL_STEREO_MODE_TOP_BOTTOM), + new Sample( + "mono_v1_sample", + "https://storage.googleapis.com/exoplayer-test-media-internal-" + + "63834241aced7884c2544af1a3452e01/vr180/mono_v1_sample.mp4", + SPHERICAL_STEREO_MODE_MONO), + new Sample( + "not_vr180_actually_shot_with_moto_mod", + "https://storage.googleapis.com/exoplayer-test-media-internal-" + + "63834241aced7884c2544af1a3452e01/vr180/" + + "not_vr180_actually_shot_with_moto_mod.mp4", + SPHERICAL_STEREO_MODE_TOP_BOTTOM), + new Sample( + "stereo_v1_sample", + "https://storage.googleapis.com/exoplayer-test-media-internal-" + + "63834241aced7884c2544af1a3452e01/vr180/stereo_v1_sample.mp4", + SPHERICAL_STEREO_MODE_TOP_BOTTOM), + new Sample( + "yi_giraffes.vr", + "https://storage.googleapis.com/exoplayer-test-media-internal-" + + "63834241aced7884c2544af1a3452e01/vr180/yi_giraffes.vr.mp4", + SPHERICAL_STEREO_MODE_TOP_BOTTOM), + }; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.sample_chooser_activity); + ListView sampleListView = findViewById(R.id.sample_list); + sampleListView.setAdapter( + new ArrayAdapter<>(this, android.R.layout.simple_list_item_1, samples)); + sampleListView.setOnItemClickListener( + (parent, view, position, id) -> + startActivity( + samples[position].buildIntent(/* context= */ SampleChooserActivity.this))); + } + + private static final class Sample { + public final String name; + public final String uri; + public final String extension; + public final String sphericalStereoMode; + + public Sample(String name, String uri, String sphericalStereoMode) { + this(name, uri, sphericalStereoMode, null); + } + + public Sample(String name, String uri, String sphericalStereoMode, String extension) { + this.name = name; + this.uri = uri; + this.extension = extension; + this.sphericalStereoMode = sphericalStereoMode; + } + + public Intent buildIntent(Context context) { + Intent intent = new Intent(context, PlayerActivity.class); + return intent + .setData(Uri.parse(uri)) + .putExtra(PlayerActivity.EXTENSION_EXTRA, extension) + .putExtra(PlayerActivity.SPHERICAL_STEREO_MODE_EXTRA, sphericalStereoMode); + } + + @Override + public String toString() { + return name; + } + } +} diff --git a/demos/gvr/src/main/res/layout/sample_chooser_activity.xml b/demos/gvr/src/main/res/layout/sample_chooser_activity.xml new file mode 100644 index 0000000000..ce520e70e4 --- /dev/null +++ b/demos/gvr/src/main/res/layout/sample_chooser_activity.xml @@ -0,0 +1,25 @@ + + + + + + + diff --git a/demos/gvr/src/main/res/mipmap-hdpi/ic_launcher.png b/demos/gvr/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000000..adaa93220e Binary files /dev/null and b/demos/gvr/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/demos/gvr/src/main/res/mipmap-mdpi/ic_launcher.png b/demos/gvr/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000000..9b6f7d5e80 Binary files /dev/null and b/demos/gvr/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/demos/gvr/src/main/res/mipmap-xhdpi/ic_launcher.png b/demos/gvr/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000000..2101026c9f Binary files /dev/null and b/demos/gvr/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/demos/gvr/src/main/res/mipmap-xxhdpi/ic_launcher.png b/demos/gvr/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000000..223ec8bd11 Binary files /dev/null and b/demos/gvr/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/demos/gvr/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/demos/gvr/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000000..698ed68c42 Binary files /dev/null and b/demos/gvr/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/demos/gvr/src/main/res/values/strings.xml b/demos/gvr/src/main/res/values/strings.xml new file mode 100644 index 0000000000..08feccb398 --- /dev/null +++ b/demos/gvr/src/main/res/values/strings.xml @@ -0,0 +1,28 @@ + + + + + ExoPlayer VR Demo + + Cleartext traffic not permitted + + Unrecognized stereo mode + + Media includes video tracks, but none are playable by this device + + Media includes audio tracks, but none are playable by this device + + diff --git a/demos/ima/build.gradle b/demos/ima/build.gradle index 33cca6ef46..124555d9b5 100644 --- a/demos/ima/build.gradle +++ b/demos/ima/build.gradle @@ -16,7 +16,6 @@ apply plugin: 'com.android.application' android { compileSdkVersion project.ext.compileSdkVersion - buildToolsVersion project.ext.buildToolsVersion compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 @@ -26,7 +25,7 @@ android { defaultConfig { versionName project.ext.releaseVersion versionCode project.ext.releaseVersionCode - minSdkVersion 16 + minSdkVersion project.ext.minSdkVersion targetSdkVersion project.ext.targetSdkVersion } @@ -42,8 +41,8 @@ android { } lintOptions { - // The demo app does not have translations. - disable 'MissingTranslation' + // The demo app isn't indexed and doesn't have translations. + disable 'GoogleAppIndexingWarning','MissingTranslation' } } @@ -54,7 +53,7 @@ dependencies { implementation project(modulePrefix + 'library-hls') implementation project(modulePrefix + 'library-smoothstreaming') implementation project(modulePrefix + 'extension-ima') - implementation 'com.android.support:support-annotations:' + supportLibraryVersion + implementation 'androidx.annotation:annotation:1.1.0' } apply plugin: 'com.google.android.gms.strict-version-matcher-plugin' diff --git a/demos/ima/src/main/AndroidManifest.xml b/demos/ima/src/main/AndroidManifest.xml index 50ad0c1b54..85439018fd 100644 --- a/demos/ima/src/main/AndroidManifest.xml +++ b/demos/ima/src/main/AndroidManifest.xml @@ -17,6 +17,7 @@ package="com.google.android.exoplayer2.imademo"> + Exo IMA Demo - + diff --git a/demos/main/build.gradle b/demos/main/build.gradle index c516ba297f..f58389d9d4 100644 --- a/demos/main/build.gradle +++ b/demos/main/build.gradle @@ -16,7 +16,6 @@ apply plugin: 'com.android.application' android { compileSdkVersion project.ext.compileSdkVersion - buildToolsVersion project.ext.buildToolsVersion compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 @@ -26,7 +25,7 @@ android { defaultConfig { versionName project.ext.releaseVersion versionCode project.ext.releaseVersionCode - minSdkVersion 16 + minSdkVersion project.ext.minSdkVersion targetSdkVersion project.ext.targetSdkVersion } @@ -45,8 +44,9 @@ android { } lintOptions { - // The demo app does not have translations. - disable 'MissingTranslation' + // 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" @@ -62,7 +62,10 @@ android { } dependencies { - implementation 'com.android.support:support-annotations:' + supportLibraryVersion + implementation 'androidx.annotation:annotation:1.1.0' + implementation 'androidx.viewpager:viewpager:1.0.0' + implementation 'androidx.fragment:fragment:1.0.0' + implementation 'com.google.android.material:material:1.0.0' implementation project(modulePrefix + 'library-core') implementation project(modulePrefix + 'library-dash') implementation project(modulePrefix + 'library-hls') diff --git a/demos/main/src/main/AndroidManifest.xml b/demos/main/src/main/AndroidManifest.xml index e80e37688d..355ba43405 100644 --- a/demos/main/src/main/AndroidManifest.xml +++ b/demos/main/src/main/AndroidManifest.xml @@ -15,6 +15,7 @@ --> @@ -33,11 +34,13 @@ android:banner="@drawable/ic_banner" android:largeHeap="true" android:allowBackup="false" - android:name="com.google.android.exoplayer2.demo.DemoApplication"> + android:name="com.google.android.exoplayer2.demo.DemoApplication" + tools:ignore="UnusedAttribute"> + android:label="@string/application_name" + android:theme="@style/Theme.AppCompat"> diff --git a/demos/main/src/main/assets/media.exolist.json b/demos/main/src/main/assets/media.exolist.json index 732bb5f4f4..bcb3ef4ad1 100644 --- a/demos/main/src/main/assets/media.exolist.json +++ b/demos/main/src/main/assets/media.exolist.json @@ -330,11 +330,11 @@ "samples": [ { "name": "Super speed", - "uri": "https://playready.directtaps.net/smoothstreaming/SSWSS720H264/SuperSpeedway_720.ism" + "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", + "uri": "https://playready.directtaps.net/smoothstreaming/SSWSS720H264PR/SuperSpeedway_720.ism/Manifest", "drm_scheme": "playready" } ] @@ -352,11 +352,11 @@ }, { "name": "Apple master playlist advanced (TS)", - "uri": "https://tungsten.aaplimg.com/VOD/bipbop_adv_example_v2/master.m3u8" + "uri": "https://devstreaming-cdn.apple.com/videos/streaming/examples/img_bipbop_adv_example_ts/master.m3u8" }, { "name": "Apple master playlist advanced (fMP4)", - "uri": "https://tungsten.aaplimg.com/VOD/bipbop_adv_fmp4_example/master.m3u8" + "uri": "https://devstreaming-cdn.apple.com/videos/streaming/examples/img_bipbop_adv_example_fmp4/master.m3u8" }, { "name": "Apple TS media playlist", 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 index ac8be7dc16..6985d42b36 100644 --- 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 @@ -16,6 +16,13 @@ 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; @@ -28,21 +35,24 @@ 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"; - private static final int MAX_SIMULTANEOUS_DOWNLOADS = 2; protected String userAgent; + private DatabaseProvider databaseProvider; private File downloadDirectory; private Cache downloadCache; private DownloadManager downloadManager; @@ -71,6 +81,18 @@ public class DemoApplication extends Application { 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; @@ -81,31 +103,51 @@ public class DemoApplication extends Application { 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( - downloaderConstructorHelper, - MAX_SIMULTANEOUS_DOWNLOADS, - DownloadManager.DEFAULT_MIN_RETRY_COUNT, - new File(getDownloadDirectory(), DOWNLOAD_ACTION_FILE)); + this, downloadIndex, new DefaultDownloaderFactory(downloaderConstructorHelper)); downloadTracker = - new DownloadTracker( - /* context= */ this, - buildDataSourceFactory(), - new File(getDownloadDirectory(), DOWNLOAD_TRACKER_ACTION_FILE)); - downloadManager.addListener(downloadTracker); + new DownloadTracker(/* context= */ this, buildDataSourceFactory(), downloadManager); } } - private synchronized Cache getDownloadCache() { - if (downloadCache == null) { - File downloadContentDirectory = new File(getDownloadDirectory(), DOWNLOAD_CONTENT_DIRECTORY); - downloadCache = new SimpleCache(downloadContentDirectory, new NoOpCacheEvictor()); + 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); } - return downloadCache; + } + + private DatabaseProvider getDatabaseProvider() { + if (databaseProvider == null) { + databaseProvider = new ExoDatabaseProvider(this); + } + return databaseProvider; } private File getDownloadDirectory() { @@ -118,8 +160,8 @@ public class DemoApplication extends Application { return downloadDirectory; } - private static CacheDataSourceFactory buildReadOnlyCacheDataSource( - DefaultDataSourceFactory upstreamFactory, Cache cache) { + protected static CacheDataSourceFactory buildReadOnlyCacheDataSource( + DataSource.Factory upstreamFactory, Cache cache) { return new CacheDataSourceFactory( cache, upstreamFactory, 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 index 7d1ab16ce4..c3909dfe46 100644 --- 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 @@ -16,13 +16,14 @@ package com.google.android.exoplayer2.demo; import android.app.Notification; +import com.google.android.exoplayer2.offline.Download; import com.google.android.exoplayer2.offline.DownloadManager; -import com.google.android.exoplayer2.offline.DownloadManager.TaskState; import com.google.android.exoplayer2.offline.DownloadService; import com.google.android.exoplayer2.scheduler.PlatformScheduler; -import com.google.android.exoplayer2.ui.DownloadNotificationUtil; +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 { @@ -31,12 +32,24 @@ public class DemoDownloadService extends DownloadService { private static final int JOB_ID = 1; private static final int FOREGROUND_NOTIFICATION_ID = 1; + private static int nextNotificationId = FOREGROUND_NOTIFICATION_ID + 1; + + private DownloadNotificationHelper notificationHelper; + public DemoDownloadService() { super( FOREGROUND_NOTIFICATION_ID, DEFAULT_FOREGROUND_NOTIFICATION_UPDATE_INTERVAL, CHANNEL_ID, - R.string.exo_download_notification_channel_name); + R.string.exo_download_notification_channel_name, + /* channelDescriptionResourceId= */ 0); + nextNotificationId = FOREGROUND_NOTIFICATION_ID + 1; + } + + @Override + public void onCreate() { + super.onCreate(); + notificationHelper = new DownloadNotificationHelper(this, CHANNEL_ID); } @Override @@ -50,40 +63,29 @@ public class DemoDownloadService extends DownloadService { } @Override - protected Notification getForegroundNotification(TaskState[] taskStates) { - return DownloadNotificationUtil.buildProgressNotification( - /* context= */ this, - R.drawable.exo_controls_play, - CHANNEL_ID, - /* contentIntent= */ null, - /* message= */ null, - taskStates); + protected Notification getForegroundNotification(List downloads) { + return notificationHelper.buildProgressNotification( + R.drawable.ic_download, /* contentIntent= */ null, /* message= */ null, downloads); } @Override - protected void onTaskStateChanged(TaskState taskState) { - if (taskState.action.isRemoveAction) { + protected void onDownloadChanged(Download download) { + Notification notification; + if (download.state == Download.STATE_COMPLETED) { + notification = + notificationHelper.buildDownloadCompletedNotification( + R.drawable.ic_download_done, + /* contentIntent= */ null, + Util.fromUtf8Bytes(download.request.data)); + } else if (download.state == Download.STATE_FAILED) { + notification = + notificationHelper.buildDownloadFailedNotification( + R.drawable.ic_download_done, + /* contentIntent= */ null, + Util.fromUtf8Bytes(download.request.data)); + } else { return; } - Notification notification = null; - if (taskState.state == TaskState.STATE_COMPLETED) { - notification = - DownloadNotificationUtil.buildDownloadCompletedNotification( - /* context= */ this, - R.drawable.exo_controls_play, - CHANNEL_ID, - /* contentIntent= */ null, - Util.fromUtf8Bytes(taskState.action.data)); - } else if (taskState.state == TaskState.STATE_FAILED) { - notification = - DownloadNotificationUtil.buildDownloadFailedNotification( - /* context= */ this, - R.drawable.exo_controls_play, - CHANNEL_ID, - /* contentIntent= */ null, - Util.fromUtf8Bytes(taskState.action.data)); - } - int notificationId = FOREGROUND_NOTIFICATION_ID + 1 + taskState.taskId; - NotificationUtil.setNotification(this, notificationId, notification); + NotificationUtil.setNotification(this, nextNotificationId++, notification); } } diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/DownloadTracker.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/DownloadTracker.java index be2dec71d5..839ed304bd 100644 --- 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 @@ -15,54 +15,32 @@ */ package com.google.android.exoplayer2.demo; -import android.app.Activity; -import android.app.AlertDialog; import android.content.Context; import android.content.DialogInterface; import android.net.Uri; -import android.os.Handler; -import android.os.HandlerThread; -import android.util.Log; -import android.view.LayoutInflater; -import android.view.View; -import android.widget.ArrayAdapter; -import android.widget.ListView; +import androidx.annotation.Nullable; +import androidx.fragment.app.FragmentManager; import android.widget.Toast; import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.offline.ActionFile; -import com.google.android.exoplayer2.offline.DownloadAction; +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.DownloadManager.TaskState; +import com.google.android.exoplayer2.offline.DownloadRequest; import com.google.android.exoplayer2.offline.DownloadService; -import com.google.android.exoplayer2.offline.ProgressiveDownloadHelper; -import com.google.android.exoplayer2.offline.StreamKey; -import com.google.android.exoplayer2.offline.TrackKey; -import com.google.android.exoplayer2.source.TrackGroup; -import com.google.android.exoplayer2.source.TrackGroupArray; -import com.google.android.exoplayer2.source.dash.offline.DashDownloadHelper; -import com.google.android.exoplayer2.source.hls.offline.HlsDownloadHelper; -import com.google.android.exoplayer2.source.smoothstreaming.offline.SsDownloadHelper; -import com.google.android.exoplayer2.ui.DefaultTrackNameProvider; -import com.google.android.exoplayer2.ui.TrackNameProvider; +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.File; import java.io.IOException; -import java.util.ArrayList; -import java.util.Collections; import java.util.HashMap; -import java.util.List; import java.util.concurrent.CopyOnWriteArraySet; -/** - * Tracks media that has been downloaded. - * - *

Tracked downloads are persisted using an {@link ActionFile}, however in a real application - * it's expected that state will be stored directly in the application's media database, so that it - * can be queried efficiently together with other information about the media. - */ -public class DownloadTracker implements DownloadManager.Listener { +/** Tracks media that has been downloaded. */ +public class DownloadTracker { /** Listens for changes in the tracked downloads. */ public interface Listener { @@ -75,28 +53,23 @@ public class DownloadTracker implements DownloadManager.Listener { private final Context context; private final DataSource.Factory dataSourceFactory; - private final TrackNameProvider trackNameProvider; private final CopyOnWriteArraySet listeners; - private final HashMap trackedDownloadStates; - private final ActionFile actionFile; - private final Handler actionFileWriteHandler; + private final HashMap downloads; + private final DownloadIndex downloadIndex; + private final DefaultTrackSelector.Parameters trackSelectorParameters; + + @Nullable private StartDownloadDialogHelper startDownloadDialogHelper; public DownloadTracker( - Context context, - DataSource.Factory dataSourceFactory, - File actionFile, - DownloadAction.Deserializer... deserializers) { + Context context, DataSource.Factory dataSourceFactory, DownloadManager downloadManager) { this.context = context.getApplicationContext(); this.dataSourceFactory = dataSourceFactory; - this.actionFile = new ActionFile(actionFile); - trackNameProvider = new DefaultTrackNameProvider(context.getResources()); listeners = new CopyOnWriteArraySet<>(); - trackedDownloadStates = new HashMap<>(); - HandlerThread actionFileWriteThread = new HandlerThread("DownloadTracker"); - actionFileWriteThread.start(); - actionFileWriteHandler = new Handler(actionFileWriteThread.getLooper()); - loadTrackedActions( - deserializers.length > 0 ? deserializers : DownloadAction.getDefaultDeserializers()); + downloads = new HashMap<>(); + downloadIndex = downloadManager.getDownloadIndex(); + trackSelectorParameters = DownloadHelper.getDefaultTrackSelectorParameters(context); + downloadManager.addListener(new DownloadManagerListener()); + loadDownloads(); } public void addListener(Listener listener) { @@ -108,191 +81,189 @@ public class DownloadTracker implements DownloadManager.Listener { } public boolean isDownloaded(Uri uri) { - return trackedDownloadStates.containsKey(uri); + Download download = downloads.get(uri); + return download != null && download.state != Download.STATE_FAILED; } - @SuppressWarnings("unchecked") - public List getOfflineStreamKeys(Uri uri) { - if (!trackedDownloadStates.containsKey(uri)) { - return Collections.emptyList(); - } - return trackedDownloadStates.get(uri).getKeys(); + public DownloadRequest getDownloadRequest(Uri uri) { + Download download = downloads.get(uri); + return download != null && download.state != Download.STATE_FAILED ? download.request : null; } - public void toggleDownload(Activity activity, String name, Uri uri, String extension) { - if (isDownloaded(uri)) { - DownloadAction removeAction = - getDownloadHelper(uri, extension).getRemoveAction(Util.getUtf8Bytes(name)); - startServiceWithAction(removeAction); + 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 { - StartDownloadDialogHelper helper = - new StartDownloadDialogHelper(activity, getDownloadHelper(uri, extension), name); - helper.prepare(); - } - } - - // DownloadManager.Listener - - @Override - public void onInitialized(DownloadManager downloadManager) { - // Do nothing. - } - - @Override - public void onTaskStateChanged(DownloadManager downloadManager, TaskState taskState) { - DownloadAction action = taskState.action; - Uri uri = action.uri; - if ((action.isRemoveAction && taskState.state == TaskState.STATE_COMPLETED) - || (!action.isRemoveAction && taskState.state == TaskState.STATE_FAILED)) { - // A download has been removed, or has failed. Stop tracking it. - if (trackedDownloadStates.remove(uri) != null) { - handleTrackedDownloadStatesChanged(); + if (startDownloadDialogHelper != null) { + startDownloadDialogHelper.release(); } + startDownloadDialogHelper = + new StartDownloadDialogHelper( + fragmentManager, getDownloadHelper(uri, extension, renderersFactory), name); } } - @Override - public void onIdle(DownloadManager downloadManager) { - // Do nothing. - } - - // Internal methods - - private void loadTrackedActions(DownloadAction.Deserializer[] deserializers) { - try { - DownloadAction[] allActions = actionFile.load(deserializers); - for (DownloadAction action : allActions) { - trackedDownloadStates.put(action.uri, action); + 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.e(TAG, "Failed to load tracked actions", e); + Log.w(TAG, "Failed to query downloads", e); } } - private void handleTrackedDownloadStatesChanged() { - for (Listener listener : listeners) { - listener.onDownloadsChanged(); - } - final DownloadAction[] actions = trackedDownloadStates.values().toArray(new DownloadAction[0]); - actionFileWriteHandler.post( - () -> { - try { - actionFile.store(actions); - } catch (IOException e) { - Log.e(TAG, "Failed to store tracked actions", e); - } - }); - } - - private void startDownload(DownloadAction action) { - if (trackedDownloadStates.containsKey(action.uri)) { - // This content is already being downloaded. Do nothing. - return; - } - trackedDownloadStates.put(action.uri, action); - handleTrackedDownloadStatesChanged(); - startServiceWithAction(action); - } - - private void startServiceWithAction(DownloadAction action) { - DownloadService.startWithAction(context, DemoDownloadService.class, action, false); - } - - private DownloadHelper getDownloadHelper(Uri uri, String extension) { + private DownloadHelper getDownloadHelper( + Uri uri, String extension, RenderersFactory renderersFactory) { int type = Util.inferContentType(uri, extension); switch (type) { case C.TYPE_DASH: - return new DashDownloadHelper(uri, dataSourceFactory); + return DownloadHelper.forDash(context, uri, dataSourceFactory, renderersFactory); case C.TYPE_SS: - return new SsDownloadHelper(uri, dataSourceFactory); + return DownloadHelper.forSmoothStreaming(context, uri, dataSourceFactory, renderersFactory); case C.TYPE_HLS: - return new HlsDownloadHelper(uri, dataSourceFactory); + return DownloadHelper.forHls(context, uri, dataSourceFactory, renderersFactory); case C.TYPE_OTHER: - return new ProgressiveDownloadHelper(uri); + return DownloadHelper.forProgressive(context, uri); default: throw new IllegalStateException("Unsupported type: " + type); } } - private final class StartDownloadDialogHelper - implements DownloadHelper.Callback, DialogInterface.OnClickListener { + private class DownloadManagerListener implements DownloadManager.Listener { - private final DownloadHelper downloadHelper; - private final String name; - - private final AlertDialog.Builder builder; - private final View dialogView; - private final List trackKeys; - private final ArrayAdapter trackTitles; - private final ListView representationList; - - public StartDownloadDialogHelper( - Activity activity, DownloadHelper downloadHelper, String name) { - this.downloadHelper = downloadHelper; - this.name = name; - builder = - new AlertDialog.Builder(activity) - .setTitle(R.string.exo_download_description) - .setPositiveButton(android.R.string.ok, this) - .setNegativeButton(android.R.string.cancel, null); - - // Inflate with the builder's context to ensure the correct style is used. - LayoutInflater dialogInflater = LayoutInflater.from(builder.getContext()); - dialogView = dialogInflater.inflate(R.layout.start_download_dialog, null); - - trackKeys = new ArrayList<>(); - trackTitles = - new ArrayAdapter<>( - builder.getContext(), android.R.layout.simple_list_item_multiple_choice); - representationList = dialogView.findViewById(R.id.representation_list); - representationList.setChoiceMode(ListView.CHOICE_MODE_MULTIPLE); - representationList.setAdapter(trackTitles); - } - - public void prepare() { - downloadHelper.prepare(this); + @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) { - for (int i = 0; i < downloadHelper.getPeriodCount(); i++) { - TrackGroupArray trackGroups = downloadHelper.getTrackGroups(i); - for (int j = 0; j < trackGroups.length; j++) { - TrackGroup trackGroup = trackGroups.get(j); - for (int k = 0; k < trackGroup.length; k++) { - trackKeys.add(new TrackKey(i, j, k)); - trackTitles.add(trackNameProvider.getTrackName(trackGroup.getFormat(k))); - } - } + if (helper.getPeriodCount() == 0) { + Log.d(TAG, "No periods found. Downloading entire stream."); + startDownload(); + downloadHelper.release(); + return; } - if (!trackKeys.isEmpty()) { - builder.setView(dialogView); + mappedTrackInfo = downloadHelper.getMappedTrackInfo(/* periodIndex= */ 0); + if (!TrackSelectionDialog.willHaveContent(mappedTrackInfo)) { + Log.d(TAG, "No dialog content. Downloading entire stream."); + startDownload(); + downloadHelper.release(); + return; } - builder.create().show(); + 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.getApplicationContext(), R.string.download_start_error, Toast.LENGTH_LONG) - .show(); + Toast.makeText(context, R.string.download_start_error, Toast.LENGTH_LONG).show(); Log.e(TAG, "Failed to start download", e); } + // DialogInterface.OnClickListener implementation. + @Override public void onClick(DialogInterface dialog, int which) { - ArrayList selectedTrackKeys = new ArrayList<>(); - for (int i = 0; i < representationList.getChildCount(); i++) { - if (representationList.isItemChecked(i)) { - selectedTrackKeys.add(trackKeys.get(i)); + 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)); + } } } - if (!selectedTrackKeys.isEmpty() || trackKeys.isEmpty()) { - // We have selected keys, or we're dealing with single stream content. - DownloadAction downloadAction = - downloadHelper.getDownloadAction(Util.getUtf8Bytes(name), selectedTrackKeys); - startDownload(downloadAction); + 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 index ffa9bafa4f..1e231dd45e 100644 --- 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 @@ -15,54 +15,51 @@ */ package com.google.android.exoplayer2.demo; -import android.app.Activity; -import android.app.AlertDialog; import android.content.Intent; import android.content.pm.PackageManager; import android.net.Uri; import android.os.Bundle; -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.app.AppCompatActivity; import android.util.Pair; import android.view.KeyEvent; import android.view.View; import android.view.View.OnClickListener; -import android.view.ViewGroup; import android.widget.Button; -import android.widget.FrameLayout; import android.widget.LinearLayout; import android.widget.TextView; import android.widget.Toast; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C.ContentType; -import com.google.android.exoplayer2.DefaultRenderersFactory; import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.ExoPlayerFactory; import com.google.android.exoplayer2.PlaybackPreparer; import com.google.android.exoplayer2.Player; +import com.google.android.exoplayer2.RenderersFactory; import com.google.android.exoplayer2.SimpleExoPlayer; +import com.google.android.exoplayer2.demo.Sample.UriSample; import com.google.android.exoplayer2.drm.DefaultDrmSessionManager; +import com.google.android.exoplayer2.drm.DrmSessionManager; import com.google.android.exoplayer2.drm.FrameworkMediaCrypto; import com.google.android.exoplayer2.drm.FrameworkMediaDrm; import com.google.android.exoplayer2.drm.HttpMediaDrmCallback; import com.google.android.exoplayer2.drm.UnsupportedDrmException; import com.google.android.exoplayer2.mediacodec.MediaCodecRenderer.DecoderInitializationException; import com.google.android.exoplayer2.mediacodec.MediaCodecUtil.DecoderQueryException; -import com.google.android.exoplayer2.offline.FilteringManifestParser; -import com.google.android.exoplayer2.offline.StreamKey; +import com.google.android.exoplayer2.offline.DownloadHelper; +import com.google.android.exoplayer2.offline.DownloadRequest; import com.google.android.exoplayer2.source.BehindLiveWindowException; import com.google.android.exoplayer2.source.ConcatenatingMediaSource; -import com.google.android.exoplayer2.source.ExtractorMediaSource; import com.google.android.exoplayer2.source.MediaSource; +import com.google.android.exoplayer2.source.MediaSourceFactory; +import com.google.android.exoplayer2.source.ProgressiveMediaSource; import com.google.android.exoplayer2.source.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.dash.manifest.DashManifestParser; import com.google.android.exoplayer2.source.hls.HlsMediaSource; -import com.google.android.exoplayer2.source.hls.playlist.DefaultHlsPlaylistParserFactory; import com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource; -import com.google.android.exoplayer2.source.smoothstreaming.manifest.SsManifestParser; import com.google.android.exoplayer2.trackselection.AdaptiveTrackSelection; import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; import com.google.android.exoplayer2.trackselection.MappingTrackSelector.MappedTrackInfo; @@ -72,7 +69,6 @@ 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.TrackSelectionView; import com.google.android.exoplayer2.ui.spherical.SphericalSurfaceView; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.HttpDataSource; @@ -83,42 +79,48 @@ import java.lang.reflect.Constructor; import java.net.CookieHandler; import java.net.CookieManager; import java.net.CookiePolicy; -import java.util.List; +import java.util.ArrayList; import java.util.UUID; /** An activity that plays media using {@link SimpleExoPlayer}. */ -public class PlayerActivity extends Activity +public class PlayerActivity extends AppCompatActivity implements OnClickListener, PlaybackPreparer, PlayerControlView.VisibilityListener { - public static final String DRM_SCHEME_EXTRA = "drm_scheme"; - public static final String DRM_LICENSE_URL_EXTRA = "drm_license_url"; - public static final String DRM_KEY_REQUEST_PROPERTIES_EXTRA = "drm_key_request_properties"; - public static final String DRM_MULTI_SESSION_EXTRA = "drm_multi_session"; - public static final String PREFER_EXTENSION_DECODERS_EXTRA = "prefer_extension_decoders"; - - public static final String 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"; - - public static final String AD_TAG_URI_EXTRA = "ad_tag_uri"; - - 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"; + // Activity extras. public static final String SPHERICAL_STEREO_MODE_EXTRA = "spherical_stereo_mode"; public static final String SPHERICAL_STEREO_MODE_MONO = "mono"; public static final String SPHERICAL_STEREO_MODE_TOP_BOTTOM = "top_bottom"; public static final String SPHERICAL_STEREO_MODE_LEFT_RIGHT = "left_right"; + // Actions. + + public static final String ACTION_VIEW = "com.google.android.exoplayer.demo.action.VIEW"; + public static final String ACTION_VIEW_LIST = + "com.google.android.exoplayer.demo.action.VIEW_LIST"; + + // Player configuration extras. + + public static final String ABR_ALGORITHM_EXTRA = "abr_algorithm"; + public static final String ABR_ALGORITHM_DEFAULT = "default"; + public static final String ABR_ALGORITHM_RANDOM = "random"; + + // Media item configuration extras. + + public static final String URI_EXTRA = "uri"; + public static final String EXTENSION_EXTRA = "extension"; + + public static final String DRM_SCHEME_EXTRA = "drm_scheme"; + public static final String DRM_LICENSE_URL_EXTRA = "drm_license_url"; + public static final String DRM_KEY_REQUEST_PROPERTIES_EXTRA = "drm_key_request_properties"; + public static final String DRM_MULTI_SESSION_EXTRA = "drm_multi_session"; + public static final String PREFER_EXTENSION_DECODERS_EXTRA = "prefer_extension_decoders"; + public static final String AD_TAG_URI_EXTRA = "ad_tag_uri"; // For backwards compatibility only. - private static final String DRM_SCHEME_UUID_EXTRA = "drm_scheme_uuid"; + 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"; @@ -130,13 +132,16 @@ public class PlayerActivity extends Activity DEFAULT_COOKIE_MANAGER.setCookiePolicy(CookiePolicy.ACCEPT_ORIGINAL_SERVER); } + private final ArrayList mediaDrms; + private PlayerView playerView; private LinearLayout debugRootView; + private Button selectTracksButton; private TextView debugTextView; + private boolean isShowingTrackSelectionDialog; private DataSource.Factory dataSourceFactory; private SimpleExoPlayer player; - private FrameworkMediaDrm mediaDrm; private MediaSource mediaSource; private DefaultTrackSelector trackSelector; private DefaultTrackSelector.Parameters trackSelectorParameters; @@ -151,13 +156,17 @@ public class PlayerActivity extends Activity private AdsLoader adsLoader; private Uri loadedAdTagUri; - private ViewGroup adUiViewGroup; + + public PlayerActivity() { + mediaDrms = new ArrayList<>(); + } // Activity lifecycle @Override public void onCreate(Bundle savedInstanceState) { - String sphericalStereoMode = getIntent().getStringExtra(SPHERICAL_STEREO_MODE_EXTRA); + Intent intent = getIntent(); + String sphericalStereoMode = intent.getStringExtra(SPHERICAL_STEREO_MODE_EXTRA); if (sphericalStereoMode != null) { setTheme(R.style.PlayerTheme_Spherical); } @@ -168,10 +177,10 @@ public class PlayerActivity extends Activity } setContentView(R.layout.player_activity); - View rootView = findViewById(R.id.root); - rootView.setOnClickListener(this); 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); @@ -199,13 +208,14 @@ public class PlayerActivity extends Activity startWindow = savedInstanceState.getInt(KEY_WINDOW); startPosition = savedInstanceState.getLong(KEY_POSITION); } else { - trackSelectorParameters = new DefaultTrackSelector.ParametersBuilder().build(); + trackSelectorParameters = DefaultTrackSelector.Parameters.getDefaults(/* context= */ this); clearStartPosition(); } } @Override public void onNewIntent(Intent intent) { + super.onNewIntent(intent); releasePlayer(); releaseAdsLoader(); clearStartPosition(); @@ -280,6 +290,7 @@ public class PlayerActivity extends Activity @Override public void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); updateTrackSelectorParameters(); updateStartPosition(); outState.putParcelable(KEY_TRACK_SELECTOR_PARAMETERS, trackSelectorParameters); @@ -300,23 +311,15 @@ public class PlayerActivity extends Activity @Override public void onClick(View view) { - if (view.getParent() == debugRootView) { - MappedTrackInfo mappedTrackInfo = trackSelector.getCurrentMappedTrackInfo(); - if (mappedTrackInfo != null) { - CharSequence title = ((Button) view).getText(); - int rendererIndex = (int) view.getTag(); - int rendererType = mappedTrackInfo.getRendererType(rendererIndex); - boolean allowAdaptiveSelections = - rendererType == C.TRACK_TYPE_VIDEO - || (rendererType == C.TRACK_TYPE_AUDIO - && mappedTrackInfo.getTypeSupport(C.TRACK_TYPE_VIDEO) - == MappedTrackInfo.RENDERER_SUPPORT_NO_TRACKS); - Pair dialogPair = - TrackSelectionView.getDialog(this, title, trackSelector, rendererIndex); - dialogPair.second.setShowDisableOption(true); - dialogPair.second.setAllowAdaptiveSelections(allowAdaptiveSelections); - dialogPair.first.show(); - } + if (view == selectTracksButton + && !isShowingTrackSelectionDialog + && TrackSelectionDialog.willHaveContent(trackSelector)) { + isShowingTrackSelectionDialog = true; + TrackSelectionDialog trackSelectionDialog = + TrackSelectionDialog.createForTrackSelector( + trackSelector, + /* onDismissListener= */ dismissedDialog -> isShowingTrackSelectionDialog = false); + trackSelectionDialog.show(getSupportFragmentManager(), /* tag= */ null); } } @@ -324,7 +327,7 @@ public class PlayerActivity extends Activity @Override public void preparePlayback() { - initializePlayer(); + player.retry(); } // PlaybackControlView.VisibilityListener implementation @@ -339,67 +342,11 @@ public class PlayerActivity extends Activity private void initializePlayer() { if (player == null) { Intent intent = getIntent(); - 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)); - finish(); - return; - } - if (!Util.checkCleartextTrafficPermitted(uris)) { - showToast(R.string.error_cleartext_not_permitted); - return; - } - if (Util.maybeRequestReadExternalStoragePermission(/* activity= */ this, uris)) { - // The player will be reinitialized if the permission is granted. - return; - } - DefaultDrmSessionManager drmSessionManager = null; - if (intent.hasExtra(DRM_SCHEME_EXTRA) || intent.hasExtra(DRM_SCHEME_UUID_EXTRA)) { - String drmLicenseUrl = intent.getStringExtra(DRM_LICENSE_URL_EXTRA); - String[] keyRequestPropertiesArray = - intent.getStringArrayExtra(DRM_KEY_REQUEST_PROPERTIES_EXTRA); - boolean multiSession = intent.getBooleanExtra(DRM_MULTI_SESSION_EXTRA, false); - int errorStringId = R.string.error_drm_unknown; - if (Util.SDK_INT < 18) { - errorStringId = R.string.error_drm_not_supported; - } else { - try { - String drmSchemeExtra = intent.hasExtra(DRM_SCHEME_EXTRA) ? DRM_SCHEME_EXTRA - : DRM_SCHEME_UUID_EXTRA; - UUID drmSchemeUuid = Util.getDrmUuid(intent.getStringExtra(drmSchemeExtra)); - if (drmSchemeUuid == null) { - errorStringId = R.string.error_drm_unsupported_scheme; - } else { - drmSessionManager = - buildDrmSessionManagerV18( - drmSchemeUuid, drmLicenseUrl, keyRequestPropertiesArray, multiSession); - } - } catch (UnsupportedDrmException e) { - errorStringId = e.reason == UnsupportedDrmException.REASON_UNSUPPORTED_SCHEME - ? R.string.error_drm_unsupported_scheme : R.string.error_drm_unknown; - } - } - if (drmSessionManager == null) { - showToast(errorStringId); - finish(); - return; - } + releaseMediaDrms(); + mediaSource = createTopLevelMediaSource(intent); + if (mediaSource == null) { + return; } TrackSelection.Factory trackSelectionFactory; @@ -416,21 +363,15 @@ public class PlayerActivity extends Activity boolean preferExtensionDecoders = intent.getBooleanExtra(PREFER_EXTENSION_DECODERS_EXTRA, 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, extensionRendererMode); + RenderersFactory renderersFactory = + ((DemoApplication) getApplication()).buildRenderersFactory(preferExtensionDecoders); - trackSelector = new DefaultTrackSelector(trackSelectionFactory); + trackSelector = new DefaultTrackSelector(/* context= */ this, trackSelectionFactory); trackSelector.setParameters(trackSelectorParameters); lastSeenTrackGroupArray = null; player = - ExoPlayerFactory.newSimpleInstance( - /* context= */ this, renderersFactory, trackSelector, drmSessionManager); + ExoPlayerFactory.newSimpleInstance(/* context= */ this, renderersFactory, trackSelector); player.addListener(new PlayerEventListener()); player.setPlayWhenReady(startAutoPlay); player.addAnalyticsListener(new EventLogger(trackSelector)); @@ -438,28 +379,8 @@ public class PlayerActivity extends Activity playerView.setPlaybackPreparer(this); debugViewHelper = new DebugTextViewHelper(player, debugTextView); debugViewHelper.start(); - - MediaSource[] mediaSources = new MediaSource[uris.length]; - for (int i = 0; i < uris.length; i++) { - mediaSources[i] = buildMediaSource(uris[i], extensions[i]); - } - mediaSource = - mediaSources.length == 1 ? mediaSources[0] : new ConcatenatingMediaSource(mediaSources); - String adTagUriString = intent.getStringExtra(AD_TAG_URI_EXTRA); - if (adTagUriString != null) { - Uri adTagUri = Uri.parse(adTagUriString); - if (!adTagUri.equals(loadedAdTagUri)) { - releaseAdsLoader(); - loadedAdTagUri = adTagUri; - } - MediaSource adsMediaSource = createAdsMediaSource(mediaSource, Uri.parse(adTagUriString)); - if (adsMediaSource != null) { - mediaSource = adsMediaSource; - } else { - showToast(R.string.ima_not_loaded); - } - } else { - releaseAdsLoader(); + if (adsLoader != null) { + adsLoader.setPlayer(player); } } boolean haveStartPosition = startWindow != C.INDEX_UNSET; @@ -467,44 +388,138 @@ public class PlayerActivity extends Activity player.seekTo(startWindow, startPosition); } player.prepare(mediaSource, !haveStartPosition, false); - updateButtonVisibilities(); + updateButtonVisibility(); } - private MediaSource buildMediaSource(Uri uri) { - return buildMediaSource(uri, null); + @Nullable + private MediaSource createTopLevelMediaSource(Intent intent) { + String action = intent.getAction(); + boolean actionIsListView = ACTION_VIEW_LIST.equals(action); + if (!actionIsListView && !ACTION_VIEW.equals(action)) { + showToast(getString(R.string.unexpected_intent_action, action)); + finish(); + return null; + } + + Sample intentAsSample = Sample.createFromIntent(intent); + UriSample[] samples = + intentAsSample instanceof Sample.PlaylistSample + ? ((Sample.PlaylistSample) intentAsSample).children + : new UriSample[] {(UriSample) intentAsSample}; + + boolean seenAdsTagUri = false; + for (UriSample sample : samples) { + seenAdsTagUri |= sample.adTagUri != null; + if (!Util.checkCleartextTrafficPermitted(sample.uri)) { + showToast(R.string.error_cleartext_not_permitted); + return null; + } + if (Util.maybeRequestReadExternalStoragePermission(/* activity= */ this, sample.uri)) { + // The player will be reinitialized if the permission is granted. + return null; + } + } + + MediaSource[] mediaSources = new MediaSource[samples.length]; + for (int i = 0; i < samples.length; i++) { + mediaSources[i] = createLeafMediaSource(samples[i]); + } + MediaSource mediaSource = + mediaSources.length == 1 ? mediaSources[0] : new ConcatenatingMediaSource(mediaSources); + + if (seenAdsTagUri) { + Uri adTagUri = samples[0].adTagUri; + if (actionIsListView) { + showToast(R.string.unsupported_ads_in_concatenation); + } else { + if (!adTagUri.equals(loadedAdTagUri)) { + releaseAdsLoader(); + loadedAdTagUri = adTagUri; + } + MediaSource adsMediaSource = createAdsMediaSource(mediaSource, adTagUri); + if (adsMediaSource != null) { + mediaSource = adsMediaSource; + } else { + showToast(R.string.ima_not_loaded); + } + } + } else { + releaseAdsLoader(); + } + + return mediaSource; } - @SuppressWarnings("unchecked") - private MediaSource buildMediaSource(Uri uri, @Nullable String overrideExtension) { - @ContentType int type = Util.inferContentType(uri, overrideExtension); + private MediaSource createLeafMediaSource(UriSample parameters) { + DrmSessionManager drmSessionManager = null; + Sample.DrmInfo drmInfo = parameters.drmInfo; + if (drmInfo != null) { + int errorStringId = R.string.error_drm_unknown; + if (Util.SDK_INT < 18) { + errorStringId = R.string.error_drm_not_supported; + } else { + try { + if (drmInfo.drmScheme == null) { + errorStringId = R.string.error_drm_unsupported_scheme; + } else { + drmSessionManager = + buildDrmSessionManagerV18( + drmInfo.drmScheme, + drmInfo.drmLicenseUrl, + drmInfo.drmKeyRequestProperties, + drmInfo.drmMultiSession); + } + } catch (UnsupportedDrmException e) { + errorStringId = + e.reason == UnsupportedDrmException.REASON_UNSUPPORTED_SCHEME + ? R.string.error_drm_unsupported_scheme + : R.string.error_drm_unknown; + } + } + if (drmSessionManager == null) { + showToast(errorStringId); + finish(); + return null; + } + } else { + drmSessionManager = DrmSessionManager.getDummyDrmSessionManager(); + } + + 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) - .setManifestParser( - new FilteringManifestParser<>(new DashManifestParser(), getOfflineStreamKeys(uri))) + .setDrmSessionManager(drmSessionManager) .createMediaSource(uri); case C.TYPE_SS: return new SsMediaSource.Factory(dataSourceFactory) - .setManifestParser( - new FilteringManifestParser<>(new SsManifestParser(), getOfflineStreamKeys(uri))) + .setDrmSessionManager(drmSessionManager) .createMediaSource(uri); case C.TYPE_HLS: return new HlsMediaSource.Factory(dataSourceFactory) - .setPlaylistParserFactory( - new DefaultHlsPlaylistParserFactory(getOfflineStreamKeys(uri))) + .setDrmSessionManager(drmSessionManager) .createMediaSource(uri); case C.TYPE_OTHER: - return new ExtractorMediaSource.Factory(dataSourceFactory).createMediaSource(uri); - default: { + return new ProgressiveMediaSource.Factory(dataSourceFactory) + .setDrmSessionManager(drmSessionManager) + .createMediaSource(uri); + default: throw new IllegalStateException("Unsupported type: " + type); - } } } - private List getOfflineStreamKeys(Uri uri) { - return ((DemoApplication) getApplication()).getDownloadTracker().getOfflineStreamKeys(uri); - } - private DefaultDrmSessionManager buildDrmSessionManagerV18( UUID uuid, String licenseUrl, String[] keyRequestPropertiesArray, boolean multiSession) throws UnsupportedDrmException { @@ -518,8 +533,9 @@ public class PlayerActivity extends Activity keyRequestPropertiesArray[i + 1]); } } - releaseMediaDrm(); - mediaDrm = FrameworkMediaDrm.newInstance(uuid); + + FrameworkMediaDrm mediaDrm = FrameworkMediaDrm.newInstance(uuid); + mediaDrms.add(mediaDrm); return new DefaultDrmSessionManager<>(uuid, mediaDrm, drmCallback, null, multiSession); } @@ -534,14 +550,17 @@ public class PlayerActivity extends Activity mediaSource = null; trackSelector = null; } - releaseMediaDrm(); + if (adsLoader != null) { + adsLoader.setPlayer(null); + } + releaseMediaDrms(); } - private void releaseMediaDrm() { - if (mediaDrm != null) { + private void releaseMediaDrms() { + for (FrameworkMediaDrm mediaDrm : mediaDrms) { mediaDrm.release(); - mediaDrm = null; } + mediaDrms.clear(); } private void releaseAdsLoader() { @@ -579,7 +598,8 @@ public class PlayerActivity extends Activity } /** Returns an ads media source, reusing the ads loader if one exists. */ - private @Nullable MediaSource createAdsMediaSource(MediaSource mediaSource, Uri adTagUri) { + @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 { @@ -593,15 +613,13 @@ public class PlayerActivity extends Activity .getConstructor(android.content.Context.class, android.net.Uri.class); // LINT.ThenChange(../../../../../../../../proguard-rules.txt) adsLoader = loaderConstructor.newInstance(this, adTagUri); - adUiViewGroup = new FrameLayout(this); - // The demo app has a non-null overlay frame layout. - playerView.getOverlayFrameLayout().addView(adUiViewGroup); } - AdsMediaSource.MediaSourceFactory adMediaSourceFactory = - new AdsMediaSource.MediaSourceFactory() { + MediaSourceFactory adMediaSourceFactory = + new MediaSourceFactory() { @Override public MediaSource createMediaSource(Uri uri) { - return PlayerActivity.this.buildMediaSource(uri); + return PlayerActivity.this.createLeafMediaSource( + uri, /* extension=*/ null, DrmSessionManager.getDummyDrmSessionManager()); } @Override @@ -609,7 +627,7 @@ public class PlayerActivity extends Activity return new int[] {C.TYPE_DASH, C.TYPE_SS, C.TYPE_HLS, C.TYPE_OTHER}; } }; - return new AdsMediaSource(mediaSource, adMediaSourceFactory, adsLoader, adUiViewGroup); + return new AdsMediaSource(mediaSource, adMediaSourceFactory, adsLoader, playerView); } catch (ClassNotFoundException e) { // IMA extension not loaded. return null; @@ -620,41 +638,9 @@ public class PlayerActivity extends Activity // User controls - private void updateButtonVisibilities() { - debugRootView.removeAllViews(); - if (player == null) { - return; - } - - MappedTrackInfo mappedTrackInfo = trackSelector.getCurrentMappedTrackInfo(); - if (mappedTrackInfo == null) { - return; - } - - for (int i = 0; i < mappedTrackInfo.getRendererCount(); 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.exo_track_selection_title_audio; - break; - case C.TRACK_TYPE_VIDEO: - label = R.string.exo_track_selection_title_video; - break; - case C.TRACK_TYPE_TEXT: - label = R.string.exo_track_selection_title_text; - break; - default: - continue; - } - button.setText(label); - button.setTag(i); - button.setOnClickListener(this); - debugRootView.addView(button); - } - } + private void updateButtonVisibility() { + selectTracksButton.setEnabled( + player != null && TrackSelectionDialog.willHaveContent(trackSelector)); } private void showControls() { @@ -686,20 +672,11 @@ public class PlayerActivity extends Activity private class PlayerEventListener implements Player.EventListener { @Override - public void onPlayerStateChanged(boolean playWhenReady, int playbackState) { + public void onPlayerStateChanged(boolean playWhenReady, @Player.State int playbackState) { if (playbackState == Player.STATE_ENDED) { showControls(); } - updateButtonVisibilities(); - } - - @Override - public void onPositionDiscontinuity(@Player.DiscontinuityReason int reason) { - if (player.getPlaybackError() != null) { - // The user has performed a seek whilst in the error state. Update the resume position so - // that if the user then retries, playback resumes from the position to which they seeked. - updateStartPosition(); - } + updateButtonVisibility(); } @Override @@ -708,8 +685,7 @@ public class PlayerActivity extends Activity clearStartPosition(); initializePlayer(); } else { - updateStartPosition(); - updateButtonVisibilities(); + updateButtonVisibility(); showControls(); } } @@ -717,7 +693,7 @@ public class PlayerActivity extends Activity @Override @SuppressWarnings("ReferenceEquality") public void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray trackSelections) { - updateButtonVisibilities(); + updateButtonVisibility(); if (trackGroups != lastSeenTrackGroupArray) { MappedTrackInfo mappedTrackInfo = trackSelector.getCurrentMappedTrackInfo(); if (mappedTrackInfo != null) { @@ -746,7 +722,7 @@ public class PlayerActivity extends Activity // Special case for decoder initialization failures. DecoderInitializationException decoderInitializationException = (DecoderInitializationException) cause; - if (decoderInitializationException.decoderName == null) { + if (decoderInitializationException.codecInfo == null) { if (decoderInitializationException.getCause() instanceof DecoderQueryException) { errorString = getString(R.string.error_querying_decoders); } else if (decoderInitializationException.secureDecoderRequired) { @@ -761,12 +737,11 @@ public class PlayerActivity extends Activity errorString = getString( R.string.error_instantiating_decoder, - decoderInitializationException.decoderName); + 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..4497b9a984 --- /dev/null +++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/Sample.java @@ -0,0 +1,187 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2.demo; + +import static com.google.android.exoplayer2.demo.PlayerActivity.ACTION_VIEW_LIST; +import static com.google.android.exoplayer2.demo.PlayerActivity.AD_TAG_URI_EXTRA; +import static com.google.android.exoplayer2.demo.PlayerActivity.DRM_KEY_REQUEST_PROPERTIES_EXTRA; +import static com.google.android.exoplayer2.demo.PlayerActivity.DRM_LICENSE_URL_EXTRA; +import static com.google.android.exoplayer2.demo.PlayerActivity.DRM_MULTI_SESSION_EXTRA; +import static com.google.android.exoplayer2.demo.PlayerActivity.DRM_SCHEME_EXTRA; +import static com.google.android.exoplayer2.demo.PlayerActivity.DRM_SCHEME_UUID_EXTRA; +import static com.google.android.exoplayer2.demo.PlayerActivity.EXTENSION_EXTRA; +import static com.google.android.exoplayer2.demo.PlayerActivity.URI_EXTRA; + +import android.content.Intent; +import android.net.Uri; +import androidx.annotation.Nullable; +import com.google.android.exoplayer2.util.Assertions; +import com.google.android.exoplayer2.util.Util; +import java.util.ArrayList; +import java.util.UUID; + +/* package */ abstract class Sample { + + public static final class UriSample extends Sample { + + public static UriSample createFromIntent(Uri uri, Intent intent, String extrasKeySuffix) { + String extension = intent.getStringExtra(EXTENSION_EXTRA + extrasKeySuffix); + String adsTagUriString = intent.getStringExtra(AD_TAG_URI_EXTRA + extrasKeySuffix); + Uri adTagUri = adsTagUriString != null ? Uri.parse(adsTagUriString) : null; + return new UriSample( + /* name= */ null, + DrmInfo.createFromIntent(intent, extrasKeySuffix), + uri, + extension, + adTagUri, + /* sphericalStereoMode= */ null); + } + + public final Uri uri; + public final String extension; + public final DrmInfo drmInfo; + public final Uri adTagUri; + public final String sphericalStereoMode; + + public UriSample( + String name, + DrmInfo drmInfo, + Uri uri, + String extension, + Uri adTagUri, + String sphericalStereoMode) { + super(name); + this.uri = uri; + this.extension = extension; + this.drmInfo = drmInfo; + this.adTagUri = adTagUri; + this.sphericalStereoMode = sphericalStereoMode; + } + + @Override + public void addToIntent(Intent intent) { + intent.setAction(PlayerActivity.ACTION_VIEW).setData(uri); + intent.putExtra(PlayerActivity.SPHERICAL_STEREO_MODE_EXTRA, sphericalStereoMode); + addPlayerConfigToIntent(intent, /* extrasKeySuffix= */ ""); + } + + public void addToPlaylistIntent(Intent intent, String extrasKeySuffix) { + intent.putExtra(PlayerActivity.URI_EXTRA + extrasKeySuffix, uri.toString()); + addPlayerConfigToIntent(intent, extrasKeySuffix); + } + + private void addPlayerConfigToIntent(Intent intent, String extrasKeySuffix) { + intent + .putExtra(EXTENSION_EXTRA + extrasKeySuffix, extension) + .putExtra( + AD_TAG_URI_EXTRA + extrasKeySuffix, adTagUri != null ? adTagUri.toString() : null); + if (drmInfo != null) { + drmInfo.addToIntent(intent, extrasKeySuffix); + } + } + } + + public static final class PlaylistSample extends Sample { + + public final UriSample[] children; + + public PlaylistSample(String name, UriSample... children) { + super(name); + this.children = children; + } + + @Override + public void addToIntent(Intent intent) { + intent.setAction(PlayerActivity.ACTION_VIEW_LIST); + for (int i = 0; i < children.length; i++) { + children[i].addToPlaylistIntent(intent, /* extrasKeySuffix= */ "_" + i); + } + } + } + + public static final class DrmInfo { + + public static DrmInfo createFromIntent(Intent intent, String extrasKeySuffix) { + String schemeKey = DRM_SCHEME_EXTRA + extrasKeySuffix; + String schemeUuidKey = DRM_SCHEME_UUID_EXTRA + extrasKeySuffix; + if (!intent.hasExtra(schemeKey) && !intent.hasExtra(schemeUuidKey)) { + return null; + } + String drmSchemeExtra = + intent.hasExtra(schemeKey) + ? intent.getStringExtra(schemeKey) + : intent.getStringExtra(schemeUuidKey); + UUID drmScheme = Util.getDrmUuid(drmSchemeExtra); + String drmLicenseUrl = intent.getStringExtra(DRM_LICENSE_URL_EXTRA + extrasKeySuffix); + String[] keyRequestPropertiesArray = + intent.getStringArrayExtra(DRM_KEY_REQUEST_PROPERTIES_EXTRA + extrasKeySuffix); + boolean drmMultiSession = + intent.getBooleanExtra(DRM_MULTI_SESSION_EXTRA + extrasKeySuffix, false); + return new DrmInfo(drmScheme, drmLicenseUrl, keyRequestPropertiesArray, drmMultiSession); + } + + public final UUID drmScheme; + public final String drmLicenseUrl; + public final String[] drmKeyRequestProperties; + public final boolean drmMultiSession; + + public DrmInfo( + UUID drmScheme, + String drmLicenseUrl, + String[] drmKeyRequestProperties, + boolean drmMultiSession) { + this.drmScheme = drmScheme; + this.drmLicenseUrl = drmLicenseUrl; + this.drmKeyRequestProperties = drmKeyRequestProperties; + this.drmMultiSession = drmMultiSession; + } + + public void addToIntent(Intent intent, String extrasKeySuffix) { + Assertions.checkNotNull(intent); + intent.putExtra(DRM_SCHEME_EXTRA + extrasKeySuffix, drmScheme.toString()); + intent.putExtra(DRM_LICENSE_URL_EXTRA + extrasKeySuffix, drmLicenseUrl); + intent.putExtra(DRM_KEY_REQUEST_PROPERTIES_EXTRA + extrasKeySuffix, drmKeyRequestProperties); + intent.putExtra(DRM_MULTI_SESSION_EXTRA + extrasKeySuffix, drmMultiSession); + } + } + + public static Sample createFromIntent(Intent intent) { + if (ACTION_VIEW_LIST.equals(intent.getAction())) { + ArrayList 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 index 6817fab780..09fa62e51a 100644 --- 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 @@ -15,16 +15,15 @@ */ 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.support.annotation.Nullable; +import androidx.annotation.Nullable; +import androidx.appcompat.app.AppCompatActivity; import android.util.JsonReader; -import android.util.Log; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; @@ -38,12 +37,17 @@ import android.widget.ImageButton; import android.widget.TextView; import android.widget.Toast; 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; @@ -54,7 +58,7 @@ import java.util.Collections; import java.util.List; /** An activity for selecting from a list of media samples. */ -public class SampleChooserActivity extends Activity +public class SampleChooserActivity extends AppCompatActivity implements DownloadTracker.Listener, OnChildClickListener { private static final String TAG = "SampleChooserActivity"; @@ -160,13 +164,17 @@ public class SampleChooserActivity extends Activity public boolean onChildClick( ExpandableListView parent, View view, int groupPosition, int childPosition, long id) { Sample sample = (Sample) view.getTag(); - startActivity( - sample.buildIntent( - /* context= */ this, - isNonNullAndChecked(preferExtensionDecodersMenuItem), - isNonNullAndChecked(randomAbrMenuItem) - ? PlayerActivity.ABR_ALGORITHM_RANDOM - : PlayerActivity.ABR_ALGORITHM_DEFAULT)); + 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); + sample.addToIntent(intent); + startActivity(intent); return true; } @@ -177,7 +185,15 @@ public class SampleChooserActivity extends Activity .show(); } else { UriSample uriSample = (UriSample) sample; - downloadTracker.toggleDownload(this, sample.name, uriSample.uri, uriSample.extension); + RenderersFactory renderersFactory = + ((DemoApplication) getApplication()) + .buildRenderersFactory(isNonNullAndChecked(preferExtensionDecodersMenuItem)); + downloadTracker.toggleDownload( + getSupportFragmentManager(), + sample.name, + uriSample.uri, + uriSample.extension, + renderersFactory); } } @@ -300,17 +316,12 @@ public class SampleChooserActivity extends Activity extension = reader.nextString(); break; case "drm_scheme": - Assertions.checkState(!insidePlaylist, "Invalid attribute on nested item: drm_scheme"); drmScheme = 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()) { @@ -348,18 +359,21 @@ public class SampleChooserActivity extends Activity DrmInfo drmInfo = drmScheme == null ? null - : new DrmInfo(drmScheme, drmLicenseUrl, drmKeyRequestProperties, drmMultiSession); + : new DrmInfo( + Util.getDrmUuid(drmScheme), + drmLicenseUrl, + drmKeyRequestProperties, + drmMultiSession); if (playlistSamples != null) { - UriSample[] playlistSamplesArray = playlistSamples.toArray( - new UriSample[playlistSamples.size()]); - return new PlaylistSample(sampleName, drmInfo, playlistSamplesArray); + UriSample[] playlistSamplesArray = playlistSamples.toArray(new UriSample[0]); + return new PlaylistSample(sampleName, playlistSamplesArray); } else { return new UriSample( sampleName, drmInfo, uri, extension, - adTagUri, + adTagUri != null ? Uri.parse(adTagUri) : null, sphericalStereoMode); } } @@ -489,116 +503,4 @@ public class SampleChooserActivity extends Activity } } - - private static final class DrmInfo { - public final String drmScheme; - public final String drmLicenseUrl; - public final String[] drmKeyRequestProperties; - public final boolean drmMultiSession; - - public DrmInfo( - String drmScheme, - String drmLicenseUrl, - String[] drmKeyRequestProperties, - boolean drmMultiSession) { - this.drmScheme = drmScheme; - this.drmLicenseUrl = drmLicenseUrl; - this.drmKeyRequestProperties = drmKeyRequestProperties; - this.drmMultiSession = drmMultiSession; - } - - public void updateIntent(Intent intent) { - Assertions.checkNotNull(intent); - intent.putExtra(PlayerActivity.DRM_SCHEME_EXTRA, drmScheme); - intent.putExtra(PlayerActivity.DRM_LICENSE_URL_EXTRA, drmLicenseUrl); - intent.putExtra(PlayerActivity.DRM_KEY_REQUEST_PROPERTIES_EXTRA, drmKeyRequestProperties); - intent.putExtra(PlayerActivity.DRM_MULTI_SESSION_EXTRA, drmMultiSession); - } - } - - private abstract static class Sample { - public final String name; - public final DrmInfo drmInfo; - - public Sample(String name, DrmInfo drmInfo) { - this.name = name; - this.drmInfo = drmInfo; - } - - public Intent buildIntent( - Context context, boolean preferExtensionDecoders, String abrAlgorithm) { - Intent intent = new Intent(context, PlayerActivity.class); - intent.putExtra(PlayerActivity.PREFER_EXTENSION_DECODERS_EXTRA, preferExtensionDecoders); - intent.putExtra(PlayerActivity.ABR_ALGORITHM_EXTRA, abrAlgorithm); - if (drmInfo != null) { - drmInfo.updateIntent(intent); - } - return intent; - } - - } - - private static final class UriSample extends Sample { - - public final Uri uri; - public final String extension; - public final String adTagUri; - public final String sphericalStereoMode; - - public UriSample( - String name, - DrmInfo drmInfo, - Uri uri, - String extension, - String adTagUri, - String sphericalStereoMode) { - super(name, drmInfo); - this.uri = uri; - this.extension = extension; - this.adTagUri = adTagUri; - this.sphericalStereoMode = sphericalStereoMode; - } - - @Override - public Intent buildIntent( - Context context, boolean preferExtensionDecoders, String abrAlgorithm) { - return super.buildIntent(context, preferExtensionDecoders, abrAlgorithm) - .setData(uri) - .putExtra(PlayerActivity.EXTENSION_EXTRA, extension) - .putExtra(PlayerActivity.AD_TAG_URI_EXTRA, adTagUri) - .putExtra(PlayerActivity.SPHERICAL_STEREO_MODE_EXTRA, sphericalStereoMode) - .setAction(PlayerActivity.ACTION_VIEW); - } - - } - - private static final class PlaylistSample extends Sample { - - public final UriSample[] children; - - public PlaylistSample( - String name, - DrmInfo drmInfo, - UriSample... children) { - super(name, drmInfo); - this.children = children; - } - - @Override - public Intent buildIntent( - Context context, boolean preferExtensionDecoders, String abrAlgorithm) { - 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.toString(); - extensions[i] = children[i].extension; - } - return super.buildIntent(context, preferExtensionDecoders, abrAlgorithm) - .putExtra(PlayerActivity.URI_LIST_EXTRA, uris) - .putExtra(PlayerActivity.EXTENSION_LIST_EXTRA, extensions) - .setAction(PlayerActivity.ACTION_VIEW_LIST); - } - - } - } 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..d6fe6e2dc1 --- /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 androidx.annotation.Nullable; +import com.google.android.material.tabs.TabLayout; +import androidx.fragment.app.DialogFragment; +import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentManager; +import androidx.fragment.app.FragmentPagerAdapter; +import androidx.appcompat.app.AppCompatDialog; +import android.util.SparseArray; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Button; +import androidx.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 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-xxhdpi/ic_download.png b/demos/main/src/main/res/drawable-xxhdpi/ic_download.png index f02715177a..4e04a30198 100644 Binary files a/demos/main/src/main/res/drawable-xxhdpi/ic_download.png and b/demos/main/src/main/res/drawable-xxhdpi/ic_download.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 index 6602791545..f9bfb5edba 100644 Binary files a/demos/main/src/main/res/drawable-xxxhdpi/ic_download.png and b/demos/main/src/main/res/drawable-xxxhdpi/ic_download.png differ diff --git a/demos/main/src/main/res/layout/player_activity.xml b/demos/main/src/main/res/layout/player_activity.xml index 6b84033273..ea3de257e2 100644 --- a/demos/main/src/main/res/layout/player_activity.xml +++ b/demos/main/src/main/res/layout/player_activity.xml @@ -42,7 +42,15 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal" - android:visibility="gone"/> + android:visibility="gone"> + +