diff --git a/.github/ISSUE_TEMPLATE/bug.md b/.github/ISSUE_TEMPLATE/bug.md new file mode 100644 index 0000000000..8824c9e8d8 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug.md @@ -0,0 +1,57 @@ +--- +name: Bug report +about: Issue template for a bug report. +title: '' +labels: bug, needs triage +assignees: '' +--- + +Before filing a bug: +----------------------- +- Search existing issues, including issues that are closed: + https://github.com/google/ExoPlayer/issues?q=is%3Aissue +- Consult our developer website, which can be found at https://exoplayer.dev/. + It provides detailed information about supported formats and devices. +- Learn how to create useful log output by using the EventLogger: + https://exoplayer.dev/listening-to-player-events.html#using-eventlogger +- Rule out issues in your own code. A good way to do this is to try and + reproduce the issue in the ExoPlayer demo app. Information about the ExoPlayer + demo app can be found here: + http://exoplayer.dev/demo-application.html. + +When reporting a bug: +----------------------- +Fill out the sections below, leaving the headers but replacing the content. If +you're unable to provide certain information, please explain why in the relevant +section. We may close issues if they do not include sufficient information. + +### [REQUIRED] Issue description +Describe the issue in detail, including observed and expected behavior. + +### [REQUIRED] Reproduction steps +Describe how the issue can be reproduced, ideally using the ExoPlayer demo app +or a small sample app that you’re able to share as source code on GitHub. + +### [REQUIRED] Link to test content +Provide a JSON snippet for the demo app’s media.exolist.json file, or a link to +media that reproduces the issue. If you don't wish to post it publicly, please +submit the issue, then email the link to dev.exoplayer@gmail.com using a subject +in the format "Issue #1234", where "#1234" should be replaced with your issue +number. Provide all the metadata we'd need to play the content like drm license +urls or similar. If the content is accessible only in certain countries or +regions, please say so. + +### [REQUIRED] A full bug report captured from the device +Capture a full bug report using "adb bugreport". Output from "adb logcat" or a +log snippet is NOT sufficient. Please attach the captured bug report as a file. +If you don't wish to post it publicly, please submit the issue, then email the +bug report to dev.exoplayer@gmail.com using a subject in the format +"Issue #1234", where "#1234" should be replaced with your issue number. + +### [REQUIRED] Version of ExoPlayer being used +Specify the absolute version number. Avoid using terms such as "latest". + +### [REQUIRED] Device(s) and version(s) of Android being used +Specify the devices and versions of Android on which the issue can be +reproduced, and how easily it reproduces. If possible, please test on multiple +devices and Android versions. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000000..d660d0342a --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,30 @@ +--- +name: Feature request +about: Issue template for a feature request. +title: '' +labels: enhancement, needs triage +assignees: '' +--- + +Before filing a feature request: +----------------------- +- Search existing open issues, specifically with the label ‘enhancement’: + https://github.com/google/ExoPlayer/labels/enhancement +- Search existing pull requests: https://github.com/google/ExoPlayer/pulls + +When filing a feature request: +----------------------- +Fill out the sections below, leaving the headers but replacing the content. If +you're unable to provide certain information, please explain why in the relevant +section. We may close issues if they do not include sufficient information. + +### [REQUIRED] Use case description +Describe the use case or problem you are trying to solve in detail. If there are +any standards or specifications involved, please provide the relevant details. + +### Proposed solution +A clear and concise description of your proposed solution, if you have one. + +### Alternatives considered +A clear and concise description of any alternative solutions you considered, +if applicable. diff --git a/.github/ISSUE_TEMPLATE/question.md b/.github/ISSUE_TEMPLATE/question.md new file mode 100644 index 0000000000..f3ad83b67d --- /dev/null +++ b/.github/ISSUE_TEMPLATE/question.md @@ -0,0 +1,50 @@ +--- +name: Question +about: Issue template for a question. +title: '' +labels: question, needs triage +assignees: '' +--- + +Before filing a question: +----------------------- +- This issue tracker is intended ExoPlayer specific questions. If you're asking + a general Android development question, please do so on Stack Overflow. +- Search existing issues, including issues that are closed. It’s often the + quickest way to get an answer! + https://github.com/google/ExoPlayer/issues?q=is%3Aissue +- Consult our developer website, which can be found at https://exoplayer.dev/. + It provides detailed information about supported formats, devices as well as + information about how to use the ExoPlayer library. +- The ExoPlayer library Javadoc can be found at + https://exoplayer.dev/doc/reference/ + +When filing a question: +----------------------- +Fill out the sections below, leaving the headers but replacing the content. If +you're unable to provide certain information, please explain why in the relevant +section. We may close issues if they do not include sufficient information. + +### [REQUIRED] Searched documentation and issues +Tell us where you’ve already looked for an answer to your question. It’s +important for us to know this so that we can improve our documentation. + +### [REQUIRED] Question +Describe your question in detail. + +### A full bug report captured from the device +In case your question refers to a problem you are seeing in your app, capture a +full bug report using "adb bugreport". Please attach the captured bug report as +a file. If you don't wish to post it publicly, please submit the issue, then +email the bug report to dev.exoplayer@gmail.com using a subject in the format +"Issue #1234", where "#1234" should be replaced with your issue number. + +### Link to test content +In case your question is related to a piece of media, which you are trying to +play, please provide a JSON snippet for the demo app’s media.exolist.json file, +or a link to media that reproduces the issue. If you don't wish to post it +publicly, please submit the issue, then email the link to +dev.exoplayer@gmail.com using a subject in the format "Issue #1234", where +"#1234" should be replaced with your issue number. Provide all the metadata we'd +need to play the content like drm license urls or similar. If the content is +accessible only in certain countries or regions, please say so. diff --git a/.gitignore b/.gitignore index 4731d5ba99..790a44c22f 100644 --- a/.gitignore +++ b/.gitignore @@ -57,6 +57,9 @@ extensions/vp9/src/main/jni/libvpx extensions/vp9/src/main/jni/libvpx_android_configs extensions/vp9/src/main/jni/libyuv +# AV1 extension +extensions/av1/src/main/jni/libgav1 + # Opus extension extensions/opus/src/main/jni/libopus @@ -71,7 +74,3 @@ extensions/cronet/jniLibs/* !extensions/cronet/jniLibs/README.md extensions/cronet/libs/* !extensions/cronet/libs/README.md - -# Cast receiver -cast_receiver_app/external-js -cast_receiver_app/bazel-cast_receiver_app diff --git a/.hgignore b/.hgignore index 36d3268005..7819a90ac5 100644 --- a/.hgignore +++ b/.hgignore @@ -12,13 +12,14 @@ libs obj lint.xml -# IntelliJ IDEA +# IntelliJ IDEA & Android Studio .idea *.iml *.ipr *.iws classes gen-external-apklibs +*.li # Eclipse .project @@ -61,6 +62,9 @@ extensions/vp9/src/main/jni/libvpx extensions/vp9/src/main/jni/libvpx_android_configs extensions/vp9/src/main/jni/libyuv +# AV1 extension +extensions/av1/src/main/jni/libgav1 + # Opus extension extensions/opus/src/main/jni/libopus @@ -75,7 +79,3 @@ extensions/cronet/jniLibs/* !extensions/cronet/jniLibs/README.md extensions/cronet/libs/* !extensions/cronet/libs/README.md - -# Cast receiver -cast_receiver_app/external-js -cast_receiver_app/bazel-cast_receiver_app 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 03f16bd655..d488f4113e 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 @@ -95,20 +95,6 @@ compileOptions { } ``` -Note that if you want to use Java 8 features in your own code, the following -additional options need to be set: - -```gradle -// For Java compilers: -compileOptions { - sourceCompatibility JavaVersion.VERSION_1_8 -} -// For Kotlin compilers: -kotlinOptions { - jvmTarget = JavaVersion.VERSION_1_8 -} -``` - ### Locally ### Cloning the repository and depending on the modules locally is required when @@ -121,6 +107,7 @@ branch: ```sh git clone https://github.com/google/ExoPlayer.git +cd ExoPlayer git checkout release-v2 ``` diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 053198de57..d3f7cf8067 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -2,20 +2,465 @@ ### dev-v2 (not yet released) ### -* Support for playing spherical videos on Daydream. -* Improve decoder re-use between playbacks. TODO: Write and link a blog post - here ([#2826](https://github.com/google/ExoPlayer/issues/2826)). +* Add Java FLAC extractor + ([#6406](https://github.com/google/ExoPlayer/issues/6406)). + If `DefaultExtractorsFactory` is used, this extractor is only used if the FLAC + extension is not loaded. +* Make `MediaSourceEventListener.LoadEventInfo` and + `MediaSourceEventListener.MediaLoadData` top-level classes. +* Rename `MediaCodecRenderer.onOutputFormatChanged` to + `MediaCodecRenderer.onOutputMediaFormatChanged`, further + clarifying the distinction between `Format` and `MediaFormat`. +* Downloads: Merge downloads in `SegmentDownloader` to improve overall download + speed ([#5978](https://github.com/google/ExoPlayer/issues/5978)). +* In MP4 streams, store the Android capture frame rate only in + `Format.metadata`. `Format.frameRate` now stores the calculated frame rate. +* Add `play` and `pause` methods to `Player`. +* Upgrade Truth dependency from 0.44 to 1.0. +* Upgrade to JUnit 4.13-rc-2. +* Add support for attaching DRM sessions to clear content in the demo app. +* Add `SpannedSubject` to testutils, for assertions on + [Span-styled text]( https://developer.android.com/guide/topics/text/spans) + (e.g. subtitles). +* Add `Player.getCurrentLiveOffset` to conveniently return the live offset. +* Update `IcyDecoder` to try ISO-8859-1 decoding if UTF-8 decoding fails. + Also change `IcyInfo.rawMetadata` from `String` to `byte[]` to allow + developers to handle data that's neither UTF-8 nor ISO-8859-1 + ([#6753](https://github.com/google/ExoPlayer/issues/6753)). +* Add playlist API ([#6161](https://github.com/google/ExoPlayer/issues/6161)). +* Fix handling of network transitions in `RequirementsWatcher` + ([#6733](https://github.com/google/ExoPlayer/issues/6733)). Incorrect handling + could previously cause downloads to be paused when they should have been able + to proceed. + +### 2.11.1 (2019-12-20) ### + +* UI: Exclude `DefaultTimeBar` region from system gesture detection + ([#6685](https://github.com/google/ExoPlayer/issues/6685)). +* ProGuard fixes: + * Ensure `Libgav1VideoRenderer` constructor is kept for use by + `DefaultRenderersFactory` + ([#6773](https://github.com/google/ExoPlayer/issues/6773)). + * Ensure `VideoDecoderOutputBuffer` and its members are kept for use by video + decoder extensions. + * Ensure raw resources used with `RawResourceDataSource` are kept. + * Suppress spurious warnings about the `javax.annotation` package, and + restructure use of `IntDef` annotations to remove spurious warnings about + `SsaStyle$SsaAlignment` + ([#6771](https://github.com/google/ExoPlayer/issues/6771)). +* Fix `CacheDataSource` to correctly propagate `DataSpec.httpRequestHeaders`. +* Fix issue with `DefaultDownloadIndex` that could result in an + `IllegalStateException` being thrown from + `DefaultDownloadIndex.getDownloadForCurrentRow` + ([#6785](https://github.com/google/ExoPlayer/issues/6785)). +* Fix `IndexOutOfBoundsException` in `SinglePeriodTimeline.getWindow` + ([#6776](https://github.com/google/ExoPlayer/issues/6776)). +* Add missing `@Nullable` to `MediaCodecAudioRenderer.getMediaClock` and + `SimpleDecoderAudioRenderer.getMediaClock` + ([#6792](https://github.com/google/ExoPlayer/issues/6792)). + +### 2.11.0 (2019-12-11) ### + +* Core library: + * Replace `ExoPlayerFactory` by `SimpleExoPlayer.Builder` and + `ExoPlayer.Builder`. + * Add automatic `WakeLock` handling to `SimpleExoPlayer`, which can be enabled + by calling `SimpleExoPlayer.setHandleWakeLock` + ([#5846](https://github.com/google/ExoPlayer/issues/5846)). To use this + feature, you must add the + [WAKE_LOCK](https://developer.android.com/reference/android/Manifest.permission.html#WAKE_LOCK) + permission to your application's manifest file. + * Add automatic "audio becoming noisy" handling to `SimpleExoPlayer`, which + can be enabled by calling `SimpleExoPlayer.setHandleAudioBecomingNoisy`. + * Wrap decoder exceptions in a new `DecoderException` class and report them as + renderer errors. + * Add `Timeline.Window.isLive` to indicate that a window is a live stream + ([#2668](https://github.com/google/ExoPlayer/issues/2668) and + [#5973](https://github.com/google/ExoPlayer/issues/5973)). + * Add `Timeline.Window.uid` to uniquely identify window instances. + * Deprecate `setTag` parameter of `Timeline.getWindow`. Tags will always be + set. + * Deprecate passing the manifest directly to + `Player.EventListener.onTimelineChanged`. It can be accessed through + `Timeline.Window.manifest` or `Player.getCurrentManifest()` + * Add `MediaSource.enable` and `MediaSource.disable` to improve resource + management in playlists. + * Add `MediaPeriod.isLoading` to improve `Player.isLoading` state. + * Fix issue where player errors are thrown too early at playlist transitions + ([#5407](https://github.com/google/ExoPlayer/issues/5407)). + * Add `Format` and renderer support flags to renderer `ExoPlaybackException`s. + * Where there are multiple platform decoders for a given MIME type, prefer to + use one that advertises support for the profile and level of the media being + played over one that does not, even if it does not come first in the + `MediaCodecList`. +* DRM: + * Inject `DrmSessionManager` into the `MediaSources` instead of `Renderers`. + This allows each `MediaSource` in a `ConcatenatingMediaSource` to use a + different `DrmSessionManager` + ([#5619](https://github.com/google/ExoPlayer/issues/5619)). + * Add `DefaultDrmSessionManager.Builder`, and remove + `DefaultDrmSessionManager` static factory methods that leaked + `ExoMediaDrm` instances + ([#4721](https://github.com/google/ExoPlayer/issues/4721)). + * Add support for the use of secure decoders when playing clear content + ([#4867](https://github.com/google/ExoPlayer/issues/4867)). This can + be enabled using `DefaultDrmSessionManager.Builder`'s + `setUseDrmSessionsForClearContent` method. + * Add support for custom `LoadErrorHandlingPolicies` in key and provisioning + requests ([#6334](https://github.com/google/ExoPlayer/issues/6334)). Custom + policies can be passed via `DefaultDrmSessionManager.Builder`'s + `setLoadErrorHandlingPolicy` method. + * Use `ExoMediaDrm.Provider` in `OfflineLicenseHelper` to avoid leaking + `ExoMediaDrm` instances + ([#4721](https://github.com/google/ExoPlayer/issues/4721)). * Track selection: - * 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. -* Do not retry failed loads whose error is `FileNotFoundException`. + * Update `DefaultTrackSelector` to set a viewport constraint for the default + display by default. + * Update `DefaultTrackSelector` to set text language and role flag + constraints for the device's accessibility settings by default + ([#5749](https://github.com/google/ExoPlayer/issues/5749)). + * Add option to set preferred text role flags using + `DefaultTrackSelector.ParametersBuilder.setPreferredTextRoleFlags`. +* Android 10: + * Set `compileSdkVersion` to 29 to enable use of Android 10 APIs. + * Expose new `isHardwareAccelerated`, `isSoftwareOnly` and `isVendor` flags + in `MediaCodecInfo` + ([#5839](https://github.com/google/ExoPlayer/issues/5839)). + * Add `allowedCapturePolicy` field to `AudioAttributes` to allow to + configuration of the audio capture policy. +* Video: + * Pass the codec output `MediaFormat` to `VideoFrameMetadataListener`. + * Fix byte order of HDR10+ static metadata to match CTA-861.3. + * Support out-of-band HDR10+ dynamic metadata for VP9 in WebM/Matroska. + * Assume that protected content requires a secure decoder when evaluating + whether `MediaCodecVideoRenderer` supports a given video format + ([#5568](https://github.com/google/ExoPlayer/issues/5568)). + * Fix Dolby Vision fallback to AVC and HEVC. + * Fix early end-of-stream detection when using video tunneling, on API level + 23 and above. + * Fix an issue where a keyframe was rendered rather than skipped when + performing an exact seek to a non-zero position close to the start of the + stream. +* Audio: + * Fix the start of audio getting truncated when transitioning to a new + item in a playlist of Opus streams. + * Workaround broken raw audio decoding on Oppo R9 + ([#5782](https://github.com/google/ExoPlayer/issues/5782)). + * Reconfigure audio sink when PCM encoding changes + ([#6601](https://github.com/google/ExoPlayer/issues/6601)). + * Allow `AdtsExtractor` to encounter EOF when calculating average frame size + ([#6700](https://github.com/google/ExoPlayer/issues/6700)). +* Text: + * Add support for position and overlapping start/end times in SSA/ASS + subtitles ([#6320](https://github.com/google/ExoPlayer/issues/6320)). + * Require an end time or duration for SubRip (SRT) and SubStation Alpha + (SSA/ASS) subtitles. This applies to both sidecar files & subtitles + [embedded in Matroska streams](https://matroska.org/technical/specs/subtitles/index.html). +* UI: + * Make showing and hiding player controls accessible to TalkBack in + `PlayerView`. + * Rename `spherical_view` surface type to `spherical_gl_surface_view`. + * Make it easier to override the shuffle, repeat, fullscreen, VR and small + notification icon assets + ([#6709](https://github.com/google/ExoPlayer/issues/6709)). +* Analytics: + * Remove `AnalyticsCollector.Factory`. Instances should be created directly, + and the `Player` should be set by calling `AnalyticsCollector.setPlayer`. + * Add `PlaybackStatsListener` to collect `PlaybackStats` for analysis and + analytics reporting. +* DataSource + * Add `DataSpec.httpRequestHeaders` to support setting per-request headers for + HTTP and HTTPS. + * Remove the `DataSpec.FLAG_ALLOW_ICY_METADATA` flag. Use is replaced by + setting the `IcyHeaders.REQUEST_HEADER_ENABLE_METADATA_NAME` header in + `DataSpec.httpRequestHeaders`. + * Fail more explicitly when local file URIs contain invalid parts (e.g. a + fragment) ([#6470](https://github.com/google/ExoPlayer/issues/6470)). +* DASH: Support negative @r values in segment timelines + ([#1787](https://github.com/google/ExoPlayer/issues/1787)). +* HLS: + * Use peak bitrate rather than average bitrate for adaptive track selection. + * Fix issue where streams could get stuck in an infinite buffering state + after a postroll ad + ([#6314](https://github.com/google/ExoPlayer/issues/6314)). +* Matroska: Support lacing in Blocks + ([#3026](https://github.com/google/ExoPlayer/issues/3026)). +* AV1 extension: + * New in this release. The AV1 extension allows use of the + [libgav1 software decoder](https://chromium.googlesource.com/codecs/libgav1/) + in ExoPlayer. You can read more about playing AV1 videos with ExoPlayer + [here](https://medium.com/google-exoplayer/playing-av1-videos-with-exoplayer-a7cb19bedef9). +* VP9 extension: + * Update to use NDK r20. + * Rename `VpxVideoSurfaceView` to `VideoDecoderSurfaceView` and move it to the + core library. + * Move `LibvpxVideoRenderer.MSG_SET_OUTPUT_BUFFER_RENDERER` to + `C.MSG_SET_OUTPUT_BUFFER_RENDERER`. + * Use `VideoDecoderRenderer` as an implementation of + `VideoDecoderOutputBufferRenderer`, instead of `VideoDecoderSurfaceView`. +* Flac extension: Update to use NDK r20. +* Opus extension: Update to use NDK r20. +* FFmpeg extension: + * Update to use NDK r20. + * Update to use FFmpeg version 4.2. It is necessary to rebuild the native part + of the extension after this change, following the instructions in the + extension's readme. +* MediaSession extension: Add `MediaSessionConnector.setCaptionCallback` to + support `ACTION_SET_CAPTIONING_ENABLED` events. +* GVR extension: This extension is now deprecated. +* Demo apps: + * Add [SurfaceControl demo app](https://github.com/google/ExoPlayer/tree/r2.11.0/demos/surface) + to show how to use the Android 10 `SurfaceControl` API with ExoPlayer + ([#677](https://github.com/google/ExoPlayer/issues/677)). + * Add support for subtitle files to the + [Main demo app](https://github.com/google/ExoPlayer/tree/r2.11.0/demos/main) + ([#5523](https://github.com/google/ExoPlayer/issues/5523)). + * Remove the IMA demo app. IMA functionality is demonstrated by the + [main demo app](https://github.com/google/ExoPlayer/tree/r2.11.0/demos/main). + * Add basic DRM support to the + [Cast demo app](https://github.com/google/ExoPlayer/tree/r2.11.0/demos/cast). +* TestUtils: Publish the `testutils` module to simplify unit testing with + ExoPlayer ([#6267](https://github.com/google/ExoPlayer/issues/6267)). +* IMA extension: Remove `AdsManager` listeners on release to avoid leaking an + `AdEventListener` provided by the app + ([#6687](https://github.com/google/ExoPlayer/issues/6687)). + +### 2.10.8 (2019-11-19) ### + +* E-AC3 JOC + * Handle new signaling in DASH manifests + ([#6636](https://github.com/google/ExoPlayer/issues/6636)). + * Fix E-AC3 JOC passthrough playback failing to initialize due to incorrect + channel count check. +* FLAC + * Fix sniffing for some FLAC streams. + * Fix FLAC `Format.bitrate` values. +* Parse ALAC channel count and sample rate information from a more robust source + when contained in MP4 + ([#6648](https://github.com/google/ExoPlayer/issues/6648)). +* Fix seeking into multi-period content in the edge case that the period + containing the seek position has just been removed + ([#6641](https://github.com/google/ExoPlayer/issues/6641)). + +### 2.10.7 (2019-11-06) ### + +* HLS: Fix detection of Dolby Atmos to match the HLS authoring specification. +* MediaSession extension: Update shuffle and repeat modes when playback state + is invalidated ([#6582](https://github.com/google/ExoPlayer/issues/6582)). +* Fix the start of audio getting truncated when transitioning to a new + item in a playlist of Opus streams. + +### 2.10.6 (2019-10-17) ### + +* Add `Player.onPlaybackSuppressionReasonChanged` to allow listeners to + detect playbacks suppressions (e.g. transient audio focus loss) directly + ([#6203](https://github.com/google/ExoPlayer/issues/6203)). +* DASH: + * Support `Label` elements + ([#6297](https://github.com/google/ExoPlayer/issues/6297)). + * Support legacy audio channel configuration + ([#6523](https://github.com/google/ExoPlayer/issues/6523)). +* HLS: Add support for ID3 in EMSG when using FMP4 streams + ([spec](https://aomediacodec.github.io/av1-id3/)). +* MP3: Add workaround to avoid prematurely ending playback of some SHOUTcast + live streams ([#6537](https://github.com/google/ExoPlayer/issues/6537), + [#6315](https://github.com/google/ExoPlayer/issues/6315) and + [#5658](https://github.com/google/ExoPlayer/issues/5658)). +* Metadata: Expose the raw ICY metadata through `IcyInfo` + ([#6476](https://github.com/google/ExoPlayer/issues/6476)). +* UI: + * Setting `app:played_color` on `PlayerView` and `PlayerControlView` no longer + adjusts the colors of the scrubber handle , buffered and unplayed parts of + the time bar. These can be set separately using `app:scrubber_color`, + `app:buffered_color` and `app_unplayed_color` respectively. + * Setting `app:ad_marker_color` on `PlayerView` and `PlayerControlView` no + longer adjusts the color of played ad markers. The color of played ad + markers can be set separately using `app:played_ad_marker_color`. + +### 2.10.5 (2019-09-20) ### + +* Add `Player.isPlaying` and `EventListener.onIsPlayingChanged` to check whether + the playback position is advancing. This helps to determine if playback is + suppressed due to audio focus loss. Also add + `Player.getPlaybackSuppressedReason` to determine the reason of the + suppression ([#6203](https://github.com/google/ExoPlayer/issues/6203)). +* Track selection + * Add `allowAudioMixedChannelCountAdaptiveness` parameter to + `DefaultTrackSelector` to allow adaptive selections of audio tracks with + different channel counts. + * Improve text selection logic to always prefer the better language matches + over other selection parameters. + * Fix audio selection issue where languages are compared by bitrate + ([#6335](https://github.com/google/ExoPlayer/issues/6335)). +* Performance + * Increase maximum video buffer size from 13MB to 32MB. The previous default + was too small for high quality streams. + * Reset `DefaultBandwidthMeter` to initial values on network change. + * Bypass sniffing in `ProgressiveMediaPeriod` in case a single extractor is + provided ([#6325](https://github.com/google/ExoPlayer/issues/6325)). +* Metadata + * Support EMSG V1 boxes in FMP4. + * Support unwrapping of nested metadata (e.g. ID3 and SCTE-35 in EMSG). +* Add `HttpDataSource.getResponseCode` to provide the status code associated + with the most recent HTTP response. +* Fix transitions between packed audio and non-packed audio segments in HLS + ([#6444](https://github.com/google/ExoPlayer/issues/6444)). +* Fix issue where a request would be retried after encountering an error, even + though the `LoadErrorHandlingPolicy` classified the error as fatal. +* Fix initialization data handling for FLAC in MP4 + ([#6396](https://github.com/google/ExoPlayer/issues/6396), + [#6397](https://github.com/google/ExoPlayer/issues/6397)). +* Fix decoder selection for E-AC3 JOC streams + ([#6398](https://github.com/google/ExoPlayer/issues/6398)). +* Fix `PlayerNotificationManager` to show play icon rather than pause icon when + playback is ended ([#6324](https://github.com/google/ExoPlayer/issues/6324)). +* RTMP extension: Upgrade LibRtmp-Client-for-Android to fix RTMP playback issues + ([#4200](https://github.com/google/ExoPlayer/issues/4200), + [#4249](https://github.com/google/ExoPlayer/issues/4249), + [#4319](https://github.com/google/ExoPlayer/issues/4319), + [#4337](https://github.com/google/ExoPlayer/issues/4337)). +* IMA extension: Fix crash in `ImaAdsLoader.onTimelineChanged` + ([#5831](https://github.com/google/ExoPlayer/issues/5831)). + +### 2.10.4 (2019-07-26) ### + +* Offline: Add `Scheduler` implementation that uses `WorkManager`. +* Add ability to specify a description when creating notification channels via + ExoPlayer library classes. +* Switch normalized BCP-47 language codes to use 2-letter ISO 639-1 language + tags instead of 3-letter ISO 639-2 language tags. +* Ensure the `SilenceMediaSource` position is in range + ([#6229](https://github.com/google/ExoPlayer/issues/6229)). +* WAV: Calculate correct duration for clipped streams + ([#6241](https://github.com/google/ExoPlayer/issues/6241)). +* MP3: Use CBR header bitrate, not calculated bitrate. This reverts a change + from 2.9.3 ([#6238](https://github.com/google/ExoPlayer/issues/6238)). +* Flac extension: Parse `VORBIS_COMMENT` and `PICTURE` metadata + ([#5527](https://github.com/google/ExoPlayer/issues/5527)). +* Fix issue where initial seek positions get ignored when playing a preroll ad + ([#6201](https://github.com/google/ExoPlayer/issues/6201)). +* Fix issue where invalid language tags were normalized to "und" instead of + keeping the original + ([#6153](https://github.com/google/ExoPlayer/issues/6153)). +* Fix `DataSchemeDataSource` re-opening and range requests + ([#6192](https://github.com/google/ExoPlayer/issues/6192)). +* Fix Flac and ALAC playback on some LG devices + ([#5938](https://github.com/google/ExoPlayer/issues/5938)). +* Fix issue when calling `performClick` on `PlayerView` without + `PlayerControlView` + ([#6260](https://github.com/google/ExoPlayer/issues/6260)). +* Fix issue where playback speeds are not used in adaptive track selections + after manual selection changes for other renderers + ([#6256](https://github.com/google/ExoPlayer/issues/6256)). + +### 2.10.3 (2019-07-09) ### + +* Display last frame when seeking to end of stream + ([#2568](https://github.com/google/ExoPlayer/issues/2568)). +* Audio: + * Fix an issue where not all audio was played out when the configuration + for the underlying track was changing (e.g., at some period transitions). + * Fix an issue where playback speed was applied inaccurately in playlists + ([#6117](https://github.com/google/ExoPlayer/issues/6117)). +* UI: Fix `PlayerView` incorrectly consuming touch events if no controller is + attached ([#6109](https://github.com/google/ExoPlayer/issues/6109)). +* CEA608: Fix repetition of special North American characters + ([#6133](https://github.com/google/ExoPlayer/issues/6133)). +* FLV: Fix bug that caused playback of some live streams to not start + ([#6111](https://github.com/google/ExoPlayer/issues/6111)). +* SmoothStreaming: Parse text stream `Subtype` into `Format.roleFlags`. +* MediaSession extension: Fix `MediaSessionConnector.play()` not resuming + playback ([#6093](https://github.com/google/ExoPlayer/issues/6093)). + +### 2.10.2 (2019-06-03) ### + +* Add `ResolvingDataSource` for just-in-time resolution of `DataSpec`s + ([#5779](https://github.com/google/ExoPlayer/issues/5779)). +* Add `SilenceMediaSource` that can be used to play silence of a given + duration ([#5735](https://github.com/google/ExoPlayer/issues/5735)). * Offline: - * Speed up removal of segmented downloads - ([#5136](https://github.com/google/ExoPlayer/issues/5136)). - * Add `setStreamKeys` method to factories of DASH, SmoothStreaming and HLS - media sources to simplify filtering by downloaded streams. + * Prevent unexpected `DownloadHelper.Callback.onPrepared` callbacks after + preparation of a `DownloadHelper` fails + ([#5915](https://github.com/google/ExoPlayer/issues/5915)). + * Fix `CacheUtil.cache()` downloading too much data + ([#5927](https://github.com/google/ExoPlayer/issues/5927)). + * Fix misreporting cached bytes when caching is paused + ([#5573](https://github.com/google/ExoPlayer/issues/5573)). +* UI: + * Allow setting `DefaultTimeBar` attributes on `PlayerView` and + `PlayerControlView`. + * Change playback controls toggle from touch down to touch up events + ([#5784](https://github.com/google/ExoPlayer/issues/5784)). + * Fix issue where playback controls were not kept visible on key presses + ([#5963](https://github.com/google/ExoPlayer/issues/5963)). +* Subtitles: + * CEA-608: Handle XDS and TEXT modes + ([#5807](https://github.com/google/ExoPlayer/pull/5807)). + * TTML: Fix bitmap rendering + ([#5633](https://github.com/google/ExoPlayer/pull/5633)). +* IMA: Fix ad pod index offset calculation without preroll + ([#5928](https://github.com/google/ExoPlayer/issues/5928)). +* Add a `playWhenReady` flag to MediaSessionConnector.PlaybackPreparer methods + to indicate whether a controller sent a play or only a prepare command. This + allows to take advantage of decoder reuse with the MediaSessionConnector + ([#5891](https://github.com/google/ExoPlayer/issues/5891)). +* Add `ProgressUpdateListener` to `PlayerControlView` + ([#5834](https://github.com/google/ExoPlayer/issues/5834)). +* Add support for auto-detecting UDP streams in `DefaultDataSource` + ([#6036](https://github.com/google/ExoPlayer/pull/6036)). +* Allow enabling decoder fallback with `DefaultRenderersFactory` + ([#5942](https://github.com/google/ExoPlayer/issues/5942)). +* Gracefully handle revoked `ACCESS_NETWORK_STATE` permission + ([#6019](https://github.com/google/ExoPlayer/issues/6019)). +* Fix decoding problems when seeking back after seeking beyond a mid-roll ad + ([#6009](https://github.com/google/ExoPlayer/issues/6009)). +* Fix application of `maxAudioBitrate` for adaptive audio track groups + ([#6006](https://github.com/google/ExoPlayer/issues/6006)). +* Fix bug caused by parallel adaptive track selection using `Format`s without + bitrate information + ([#5971](https://github.com/google/ExoPlayer/issues/5971)). +* Fix bug in `CastPlayer.getCurrentWindowIndex()` + ([#5955](https://github.com/google/ExoPlayer/issues/5955)). + +### 2.10.1 (2019-05-16) ### + +* Offline: Add option to remove all downloads. +* HLS: Fix `NullPointerException` when using HLS chunkless preparation + ([#5868](https://github.com/google/ExoPlayer/issues/5868)). +* Fix handling of empty values and line terminators in SHOUTcast ICY metadata + ([#5876](https://github.com/google/ExoPlayer/issues/5876)). +* Fix DVB subtitles for SDK 28 + ([#5862](https://github.com/google/ExoPlayer/issues/5862)). +* Add a workaround for a decoder failure on ZTE Axon7 mini devices when playing + 48kHz audio ([#5821](https://github.com/google/ExoPlayer/issues/5821)). + +### 2.10.0 (2019-04-15) ### + +* Core library: + * Improve decoder re-use between playbacks + ([#2826](https://github.com/google/ExoPlayer/issues/2826)). Read + [this blog post](https://medium.com/google-exoplayer/improved-decoder-reuse-in-exoplayer-ef4c6d99591d) + for more details. + * Rename `ExtractorMediaSource` to `ProgressiveMediaSource`. + * Fix issue where using `ProgressiveMediaSource.Factory` would mean that + `DefaultExtractorsFactory` would be kept by proguard. Custom + `ExtractorsFactory` instances must now be passed via the + `ProgressiveMediaSource.Factory` constructor, and `setExtractorsFactory` is + deprecated. + * Make the default minimum buffer size equal the maximum buffer size for video + playbacks ([#2083](https://github.com/google/ExoPlayer/issues/2083)). + * Move `PriorityTaskManager` from `DefaultLoadControl` to `SimpleExoPlayer`. + * Add new `ExoPlaybackException` types for remote exceptions and out-of-memory + errors. + * Use full BCP 47 language tags in `Format`. + * Do not retry failed loads whose error is `FileNotFoundException`. + * Fix issue where not resetting the position for a new `MediaSource` in calls + to `ExoPlayer.prepare` causes an `IndexOutOfBoundsException` + ([#5520](https://github.com/google/ExoPlayer/issues/5520)). +* Offline: + * Improve offline support. `DownloadManager` now tracks all offline content, + not just tasks in progress. Read + [this page](https://exoplayer.dev/downloading-media.html) for more details. * Caching: * Improve performance of `SimpleCache` ([#4253](https://github.com/google/ExoPlayer/issues/4253)). @@ -23,31 +468,138 @@ this behavior (`DataSpec.FLAG_ALLOW_CACHING_UNKNOWN_LENGTH`) has been replaced with an opt out flag (`DataSpec.FLAG_DONT_CACHE_IF_LENGTH_UNKNOWN`). -* DownloadManager: - * Create only one task for all DownloadActions for the same content. - * Rename TaskState to DownloadState. - * Add new states to DownloadState. - * Replace DownloadState.action with DownloadAction fields. -* DRM: Fix black flicker when keys rotate in DRM protected content - ([#3561](https://github.com/google/ExoPlayer/issues/3561)). -* Add support for SHOUTcast ICY metadata - ([#3735](https://github.com/google/ExoPlayer/issues/3735)). +* Extractors: + * MP4/FMP4: Add support for Dolby Vision. + * MP4: Fix issue handling meta atoms in some streams + ([#5698](https://github.com/google/ExoPlayer/issues/5698), + [#5694](https://github.com/google/ExoPlayer/issues/5694)). + * MP3: Add support for SHOUTcast ICY metadata + ([#3735](https://github.com/google/ExoPlayer/issues/3735)). + * MP3: Fix ID3 frame unsychronization + ([#5673](https://github.com/google/ExoPlayer/issues/5673)). + * MP3: Fix playback of badly clipped files + ([#5772](https://github.com/google/ExoPlayer/issues/5772)). + * MPEG-TS: Enable HDMV DTS stream detection only if a flag is set. By default + (i.e. if the flag is not set), the 0x82 elementary stream type is now + treated as an SCTE subtitle track + ([#5330](https://github.com/google/ExoPlayer/issues/5330)). +* Track selection: + * Add options for controlling audio track selections to `DefaultTrackSelector` + ([#3314](https://github.com/google/ExoPlayer/issues/3314)). + * Update `TrackSelection.Factory` interface to support creating all track + selections together. + * Allow to specify a selection reason for a `SelectionOverride`. + * Select audio track based on system language if no preference is provided. + * When no text language preference matches, only select forced text tracks + whose language matches the selected audio language. +* UI: + * Update `DefaultTimeBar` based on duration of media and add parameter to set + the minimum update interval to control the smoothness of the updates + ([#5040](https://github.com/google/ExoPlayer/issues/5040)). + * Move creation of dialogs for `TrackSelectionView`s to + `TrackSelectionDialogBuilder` and add option to select multiple overrides. + * Change signature of `PlayerNotificationManager.NotificationListener` to + better fit service requirements. + * Add option to include navigation actions in the compact mode of + notifications created using `PlayerNotificationManager`. + * Fix issues with flickering notifications on KitKat when using + `PlayerNotificationManager` and `DownloadNotificationUtil`. For the latter, + applications should switch to using `DownloadNotificationHelper`. + * Fix accuracy of D-pad seeking in `DefaultTimeBar` + ([#5767](https://github.com/google/ExoPlayer/issues/5767)). +* Audio: + * Allow `AudioProcessor`s to be drained of pending output after they are + reconfigured. + * Fix an issue that caused audio to be truncated at the end of a period + when switching to a new period where gapless playback information was newly + present or newly absent. + * Add support for reading AC-4 streams + ([#5303](https://github.com/google/ExoPlayer/pull/5303)). +* Video: + * Remove `MediaCodecSelector.DEFAULT_WITH_FALLBACK`. Apps should instead + signal that fallback should be used by passing `true` as the + `enableDecoderFallback` parameter when instantiating the video renderer. + * Support video tunneling when the decoder is not listed first for the MIME + type ([#3100](https://github.com/google/ExoPlayer/issues/3100)). + * Query `MediaCodecList.ALL_CODECS` when selecting a tunneling decoder + ([#5547](https://github.com/google/ExoPlayer/issues/5547)). +* DRM: + * Fix black flicker when keys rotate in DRM protected content + ([#3561](https://github.com/google/ExoPlayer/issues/3561)). + * Work around lack of LA_URL attribute in PlayReady key request init data. * CEA-608: Improved conformance to the specification ([#3860](https://github.com/google/ExoPlayer/issues/3860)). -* IMA extension: Require setting the `Player` on `AdsLoader` instances before - playback. -* Add `Handler` parameter to `ConcatenatingMediaSource` methods which take a - callback `Runnable`. -* Remove `player` and `isTopLevelSource` parameters from `MediaSource.prepare`. -* Change signature of `PlayerNotificationManager.NotificationListener` to better - fit service requirements. Remove ability to set a custom stop action. -* Add workaround for video quality problems with Amlogic decoders - ([#5003](https://github.com/google/ExoPlayer/issues/5003)). -* Associate fatal player errors of type SOURCE with the loading source in - `AnalyticsListener.EventTime` - ([#5407](https://github.com/google/ExoPlayer/issues/5407)). +* DASH: + * Parse role and accessibility descriptors into `Format.roleFlags`. + * Support multiple CEA-608 channels muxed into FMP4 representations + ([#5656](https://github.com/google/ExoPlayer/issues/5656)). +* HLS: + * Prevent unnecessary reloads of initialization segments. + * Form an adaptive track group out of audio renditions with matching name. + * Support encrypted initialization segments + ([#5441](https://github.com/google/ExoPlayer/issues/5441)). + * Parse `EXT-X-MEDIA` `CHARACTERISTICS` attribute into `Format.roleFlags`. + * Add metadata entry for HLS tracks to expose master playlist information. + * Prevent `IndexOutOfBoundsException` in some live HLS scenarios + ([#5816](https://github.com/google/ExoPlayer/issues/5816)). +* Support for playing spherical videos on Daydream. +* Cast extension: Work around Cast framework returning a limited-size queue + items list ([#4964](https://github.com/google/ExoPlayer/issues/4964)). +* VP9 extension: Remove RGB output mode and libyuv dependency, and switch to + surface YUV output as the default. Remove constructor parameters `scaleToFit` + and `useSurfaceYuvOutput`. +* MediaSession extension: + * Let apps intercept media button events + ([#5179](https://github.com/google/ExoPlayer/issues/5179)). + * Fix issue with `TimelineQueueNavigator` not publishing the queue in shuffled + order when in shuffle mode. + * Allow handling of custom commands via `registerCustomCommandReceiver`. + * Add ability to include an extras `Bundle` when reporting a custom error. +* Log warnings when extension native libraries can't be used, to help with + diagnosing playback failures + ([#5788](https://github.com/google/ExoPlayer/issues/5788)). -### 2.9.4 ### +### 2.9.6 (2019-02-19) ### + +* Remove `player` and `isTopLevelSource` parameters from `MediaSource.prepare`. +* IMA extension: + * Require setting the `Player` on `AdsLoader` instances before + playback. + * Remove deprecated `ImaAdsMediaSource`. Create `AdsMediaSource` with an + `ImaAdsLoader` instead. + * Remove deprecated `AdsMediaSource` constructors. Listen for media source + events using `AdsMediaSource.addEventListener`, and ad interaction events by + adding a listener when building `ImaAdsLoader`. + * Allow apps to register playback-related obstructing views that are on top of + their ad display containers via `AdsLoader.AdViewProvider`. `PlayerView` + implements this interface and will register its control view. This makes it + possible for ad loading SDKs to calculate ad viewability accurately. +* DASH: Fix issue handling large `EventStream` presentation timestamps + ([#5490](https://github.com/google/ExoPlayer/issues/5490)). +* HLS: Fix transition to STATE_ENDED when playing fragmented mp4 in chunkless + preparation ([#5524](https://github.com/google/ExoPlayer/issues/5524)). +* Revert workaround for video quality problems with Amlogic decoders, as this + may cause problems for some devices and/or non-interlaced content + ([#5003](https://github.com/google/ExoPlayer/issues/5003)). + +### 2.9.5 (2019-01-31) ### + +* HLS: Parse `CHANNELS` attribute from `EXT-X-MEDIA` tag. +* ConcatenatingMediaSource: + * Add `Handler` parameter to methods that take a callback `Runnable`. + * Fix issue with dropped messages when releasing the source + ([#5464](https://github.com/google/ExoPlayer/issues/5464)). +* ExtractorMediaSource: Fix issue that could cause the player to get stuck + buffering at the end of the media. +* PlayerView: Fix issue preventing `OnClickListener` from receiving events + ([#5433](https://github.com/google/ExoPlayer/issues/5433)). +* IMA extension: Upgrade IMA dependency to 3.10.6. +* Cronet extension: Upgrade Cronet dependency to 71.3578.98. +* OkHttp extension: Upgrade OkHttp dependency to 3.12.1. +* MP3: Wider fix for issue where streams would play twice on some Samsung + devices ([#4519](https://github.com/google/ExoPlayer/issues/4519)). + +### 2.9.4 (2019-01-15) ### * IMA extension: Clear ads loader listeners on release ([#4114](https://github.com/google/ExoPlayer/issues/4114)). @@ -56,6 +608,17 @@ * 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)). @@ -64,14 +627,8 @@ ([#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)). -* 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`. -### 2.9.3 ### +### 2.9.3 (2018-12-20) ### * Captions: Support PNG subtitles in SMPTE-TT ([#1583](https://github.com/google/ExoPlayer/issues/1583)). @@ -90,7 +647,7 @@ * Workaround for MiTV (dangal) issue when swapping output surface ([#5169](https://github.com/google/ExoPlayer/issues/5169)). -### 2.9.2 ### +### 2.9.2 (2018-11-28) ### * HLS: * Fix issue causing unnecessary media playlist requests when playing live @@ -117,7 +674,7 @@ ([#5162](https://github.com/google/ExoPlayer/issues/5162)). * Fix UUID passed to `MediaCrypto` when using `C.CLEARKEY_UUID` before API 27. -### 2.9.1 ### +### 2.9.1 (2018-11-01) ### * Add convenience methods `Player.next`, `Player.previous`, `Player.hasNext` and `Player.hasPrevious` @@ -169,7 +726,7 @@ * Swap recommended order for google() and jcenter() in gradle config ([#4997](https://github.com/google/ExoPlayer/issues/4997)). -### 2.9.0 ### +### 2.9.0 (2018-09-06) ### * Turn on Java 8 compiler support for the ExoPlayer library. Apps may need to add `compileOptions { targetCompatibility JavaVersion.VERSION_1_8 }` to their @@ -261,7 +818,7 @@ and `AnalyticsListener` callbacks ([#4361](https://github.com/google/ExoPlayer/issues/4361) and [#4615](https://github.com/google/ExoPlayer/issues/4615)). -* UI components: +* UI: * Add option to `PlayerView` to show buffering view when playWhenReady is false ([#4304](https://github.com/google/ExoPlayer/issues/4304)). * Allow any `Drawable` to be used as `PlayerView` default artwork. @@ -331,13 +888,13 @@ * Cronet extension: Now distributed via jCenter. * FFmpeg extension: Support mu-law and A-law PCM. -### 2.8.4 ### +### 2.8.4 (2018-08-17) ### * IMA extension: Improve handling of consecutive empty ad groups ([#4030](https://github.com/google/ExoPlayer/issues/4030)), ([#4280](https://github.com/google/ExoPlayer/issues/4280)). -### 2.8.3 ### +### 2.8.3 (2018-07-23) ### * IMA extension: * Fix behavior when creating/releasing the player then releasing @@ -380,7 +937,7 @@ ([#4611](https://github.com/google/ExoPlayer/issues/4611)). * Improved compatibility with FireOS devices. -### 2.8.2 ### +### 2.8.2 (2018-06-06) ### * IMA extension: Don't advertise support for video/mpeg ad media, as we don't have an extractor for this @@ -407,7 +964,7 @@ * Allow apps to register custom MIME types ([#4264](https://github.com/google/ExoPlayer/issues/4264)). -### 2.8.1 ### +### 2.8.1 (2018-05-22) ### * HLS: * Fix playback of livestreams with EXT-X-PROGRAM-DATE-TIME tags @@ -417,7 +974,7 @@ * OkHttp extension: Fix to correctly include response headers in thrown `InvalidResponseCodeException`s. * Add possibility to cancel `PlayerMessage`s. -* UI components: +* UI: * Add `PlayerView.setKeepContentOnPlayerReset` to keep the currently displayed video frame or media artwork visible when the player is reset ([#2843](https://github.com/google/ExoPlayer/issues/2843)). @@ -440,7 +997,7 @@ * Support TTML font size using % correctly (as percentage of document cell resolution). -### 2.8.0 ### +### 2.8.0 (2018-05-03) ### * Downloading: * Add `DownloadService`, `DownloadManager` and related classes @@ -467,7 +1024,7 @@ * Support live stream clipping with `ClippingMediaSource`. * Allow setting tags for all media sources in their factories. The tag of the current window can be retrieved with `Player.getCurrentTag`. -* UI components: +* UI: * Add support for displaying error messages and a buffering spinner in `PlayerView`. * Add support for listening to `AspectRatioFrameLayout`'s aspect ratio update @@ -554,12 +1111,12 @@ `BaseRenderer.onStreamChanged`. * Added dependencies on checkerframework annotations for static code analysis. -### 2.7.3 ### +### 2.7.3 (2018-04-04) ### * Fix ProGuard configuration for Cast, IMA and OkHttp extensions. * Update OkHttp extension to depend on OkHttp 3.10.0. -### 2.7.2 ### +### 2.7.2 (2018-03-29) ### * Gradle: Upgrade Gradle version from 4.1 to 4.4 so it can work with Android Studio 3.1 ([#3708](https://github.com/google/ExoPlayer/issues/3708)). @@ -573,7 +1130,7 @@ * MediaSession extension: Omit fast forward and rewind actions when media is not seekable ([#4001](https://github.com/google/ExoPlayer/issues/4001)). -### 2.7.1 ### +### 2.7.1 (2018-03-09) ### * Gradle: Replaced 'compile' (deprecated) with 'implementation' and 'api'. This may lead to build breakage for applications upgrading from @@ -593,7 +1150,7 @@ * Video: Force rendering a frame periodically in `MediaCodecVideoRenderer` and `LibvpxVideoRenderer`, even if it is late. -### 2.7.0 ### +### 2.7.0 (2018-02-19) ### * Player interface: * Add optional parameter to `stop` to reset the player when stopping. @@ -631,7 +1188,7 @@ `SsMediaSource.Factory`, and `MergingMediaSource`. * Play out existing buffer before retrying for progressive live streams ([#1606](https://github.com/google/ExoPlayer/issues/1606)). -* UI components: +* UI: * Generalized player and control views to allow them to bind with any `Player`, and renamed them to `PlayerView` and `PlayerControlView` respectively. @@ -719,7 +1276,7 @@ ([#3340](https://github.com/google/ExoPlayer/issues/3340)). * Demo app: Add ability to download not DRM protected content. -### 2.6.1 ### +### 2.6.1 (2017-12-15) ### * Add factories to `ExtractorMediaSource`, `HlsMediaSource`, `SsMediaSource`, `DashMediaSource` and `SingleSampleMediaSource`. @@ -763,7 +1320,7 @@ * Prevent period transitions when seeking to the end of a period when paused ([#2439](https://github.com/google/ExoPlayer/issues/2439)). -### 2.6.0 ### +### 2.6.0 (2017-11-03) ### * Removed "r" prefix from versions. This release is "2.6.0", not "r2.6.0". * New `Player.DefaultEventListener` abstract class can be extended to avoid @@ -833,7 +1390,7 @@ * Unit tests moved to Robolectric. * Misc bugfixes. -### r2.5.4 ### +### r2.5.4 (2017-10-19) ### * Remove unnecessary media playlist fetches during playback of live HLS streams. * Add the ability to inject a HLS playlist parser through `HlsMediaSource`. @@ -851,7 +1408,7 @@ `FrameworkMediaCrypto` and by making `DefaultDashChunkSource.getNextChunk` non-final. -### r2.5.3 ### +### r2.5.3 (2017-09-20) ### * IMA extension: Support skipping of skippable ads on AndroidTV and other non-touch devices ([#3258](https://github.com/google/ExoPlayer/issues/3258)). @@ -866,7 +1423,7 @@ * Caching: Force use of BouncyCastle on JellyBean to fix decryption issue ([#2755](https://github.com/google/ExoPlayer/issues/2755)). -### r2.5.2 ### +### r2.5.2 (2017-09-11) ### * IMA extension: Fix issue where ad playback could end prematurely for some content types ([#3180](https://github.com/google/ExoPlayer/issues/3180)). @@ -892,14 +1449,14 @@ * Workaround for SimpleExoPlayerView's surface not being hidden properly ([#3160](https://github.com/google/ExoPlayer/issues/3160)). -### r2.5.1 ### +### r2.5.1 (2017-08-08) ### * Fix an issue that could cause the reported playback position to stop advancing in some cases. * Fix an issue where a Surface could be released whilst still in use by the player. -### r2.5.0 ### +### r2.5.0 (2017-08-07) ### * IMA extension: Wraps the Google Interactive Media Ads (IMA) SDK to provide an easy and seamless way of incorporating display ads into ExoPlayer playbacks. @@ -953,7 +1510,7 @@ media playbacks ([#2900](https://github.com/google/ExoPlayer/issues/2900)). * Misc bugfixes. -### r2.4.4 ### +### r2.4.4 (2017-07-19) ### * HLS/MPEG-TS: Some initial optimizations of MPEG-TS extractor performance ([#3040](https://github.com/google/ExoPlayer/issues/3040)). @@ -964,7 +1521,7 @@ * Video: Fix video dimension reporting on some devices ([#3007](https://github.com/google/ExoPlayer/issues/3007)). -### r2.4.3 ### +### r2.4.3 (2017-06-30) ### * Audio: Workaround custom audio decoders misreporting their maximum supported channel counts ([#2940](https://github.com/google/ExoPlayer/issues/2940)). @@ -982,7 +1539,7 @@ ([#2977](https://github.com/google/ExoPlayer/pull/2977)). * Misc bugfixes. -### r2.4.2 ### +### r2.4.2 (2017-06-06) ### * Stability: Work around Nexus 10 reboot when playing certain content ([#2806](https://github.com/google/ExoPlayer/issues/2806)). @@ -996,7 +1553,7 @@ ([#2871](https://github.com/google/ExoPlayer/issues/2871)). * Misc bugfixes. -### r2.4.1 ### +### r2.4.1 (2017-05-23) ### * Stability: Avoid OutOfMemoryError in extractors when parsing malformed media ([#2780](https://github.com/google/ExoPlayer/issues/2780)). @@ -1019,7 +1576,7 @@ ([#2824](https://github.com/google/ExoPlayer/issues/2824)). * Misc bugfixes. -### r2.4.0 ### +### r2.4.0 (2017-04-25) ### * New modular library structure. You can read more about depending on individual library modules @@ -1052,7 +1609,7 @@ * FLAC extension: Enabled 64 bit targets. * Misc bugfixes. -### r2.3.1 ### +### r2.3.1 (2017-03-23) ### * Fix NPE enabling WebVTT subtitles in DASH streams ([#2596](https://github.com/google/ExoPlayer/issues/2596)). @@ -1061,7 +1618,7 @@ * Minor fix for CEA-708 decoder ([#2595](https://github.com/google/ExoPlayer/issues/2595)). -### r2.3.0 ### +### r2.3.0 (2017-03-16) ### * GVR extension: Wraps the Google VR Audio SDK to provide spatial audio rendering. You can read more about the GVR extension @@ -1108,7 +1665,7 @@ ([#2427](https://github.com/google/ExoPlayer/issues/2427)). * Misc bugfixes. -### r2.2.0 ### +### r2.2.0 (2017-01-30) ### * Demo app: Automatic recovery from BehindLiveWindowException, plus improved handling of pausing and resuming live streams @@ -1171,7 +1728,7 @@ [#2264](https://github.com/google/ExoPlayer/issues/2264) and [#2290](https://github.com/google/ExoPlayer/issues/2290). -### r2.1.1 ### +### r2.1.1 (2016-12-20) ### * Fix some subtitle types (e.g. WebVTT) being displayed out of sync ([#2208](https://github.com/google/ExoPlayer/issues/2208)). @@ -1181,7 +1738,7 @@ * Fix issue where playbacks could get stuck in the initial buffering state if over 1MB of data needs to be read to initialize the playback. -### r2.1.0 ### +### r2.1.0 (2016-12-14) ### * HLS: Support for seeking in live streams ([#87](https://github.com/google/ExoPlayer/issues/87)). @@ -1235,12 +1792,12 @@ ([#2145](https://github.com/google/ExoPlayer/issues/2145)). * Misc bugfixes. -### r2.0.4 ### +### r2.0.4 (2016-10-20) ### * Fix crash on Jellybean devices when using playback controls ([#1965](https://github.com/google/ExoPlayer/issues/1965)). -### r2.0.3 ### +### r2.0.3 (2016-10-17) ### * Fixed NullPointerException in ExtractorMediaSource ([#1914](https://github.com/google/ExoPlayer/issues/1914)). @@ -1257,7 +1814,7 @@ * Improvements to Cronet network stack extension. * Misc bug fixes. -### r2.0.2 ### +### r2.0.2 (2016-10-06) ### * Fixes for MergingMediaSource and sideloaded subtitles. ([#1882](https://github.com/google/ExoPlayer/issues/1882), @@ -1268,7 +1825,7 @@ * Initial support for fragmented MP4 in HLS. * Misc bug fixes and minor features. -### r2.0.1 ### +### r2.0.1 (2016-09-30) ### * Fix playback of short duration content ([#1837](https://github.com/google/ExoPlayer/issues/1837)). @@ -1277,7 +1834,7 @@ * Fix live stream buffering (out of memory) issue ([#1825](https://github.com/google/ExoPlayer/issues/1825)). -### r2.0.0 ### +### r2.0.0 (2016-09-14) ### ExoPlayer 2.x is a major iteration of the library. It includes significant API and architectural changes, new features and many bug fixes. You can read about diff --git a/build.gradle b/build.gradle index 96eade1aa3..a4823b94ee 100644 --- a/build.gradle +++ b/build.gradle @@ -17,17 +17,9 @@ buildscript { 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.5.1' + classpath 'com.novoda:bintray-release:0.9.1' + classpath 'com.google.android.gms:strict-version-matcher-plugin:1.2.0' } } allprojects { @@ -36,7 +28,7 @@ allprojects { 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 716ddbadba..88bfe41d5a 100644 --- a/constants.gradle +++ b/constants.gradle @@ -13,26 +13,29 @@ // limitations under the License. project.ext { // ExoPlayer version and version code. - releaseVersion = '2.9.4' - releaseVersionCode = 2009004 - // 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 - 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' - autoValueVersion = '1.6' + releaseVersion = '2.11.1' + releaseVersionCode = 2011001 + minSdkVersion = 16 + appTargetSdkVersion = 29 + targetSdkVersion = 28 // TODO: Bump once b/143232359 is resolved + compileSdkVersion = 29 + dexmakerVersion = '2.21.0' + junitVersion = '4.13-rc-2' + guavaVersion = '23.5-android' + mockitoVersion = '2.25.0' + robolectricVersion = '4.3.1' checkerframeworkVersion = '2.5.0' - testRunnerVersion = '1.1.0-alpha3' + jsr305Version = '3.0.2' + kotlinAnnotationsVersion = '1.3.31' + androidxAnnotationVersion = '1.1.0' + androidxAppCompatVersion = '1.1.0' + androidxCollectionVersion = '1.1.0' + androidxMediaVersion = '1.0.1' + androidxTestCoreVersion = '1.2.0' + androidxTestJUnitVersion = '1.1.1' + androidxTestRunnerVersion = '1.2.0' + androidxTestRulesVersion = '1.2.0' + truthVersion = '1.0' modulePrefix = ':' if (gradle.ext.has('exoplayerModulePrefix')) { modulePrefix += gradle.ext.exoplayerModulePrefix diff --git a/core_settings.gradle b/core_settings.gradle index 4d90fa962a..0f9746af96 100644 --- a/core_settings.gradle +++ b/core_settings.gradle @@ -24,7 +24,7 @@ include modulePrefix + 'library-hls' include modulePrefix + 'library-smoothstreaming' include modulePrefix + 'library-ui' include modulePrefix + 'testutils' -include modulePrefix + 'testutils-robolectric' +include modulePrefix + 'extension-av1' include modulePrefix + 'extension-ffmpeg' include modulePrefix + 'extension-flac' include modulePrefix + 'extension-gvr' @@ -38,6 +38,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 +47,7 @@ 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-av1').projectDir = new File(rootDir, 'extensions/av1') project(modulePrefix + 'extension-ffmpeg').projectDir = new File(rootDir, 'extensions/ffmpeg') project(modulePrefix + 'extension-flac').projectDir = new File(rootDir, 'extensions/flac') project(modulePrefix + 'extension-gvr').projectDir = new File(rootDir, 'extensions/gvr') @@ -60,3 +61,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 8af52a787e..f9228e4b79 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,8 +25,8 @@ android { defaultConfig { versionName project.ext.releaseVersion versionCode project.ext.releaseVersionCode - minSdkVersion 16 - targetSdkVersion project.ext.targetSdkVersion + minSdkVersion project.ext.minSdkVersion + targetSdkVersion project.ext.appTargetSdkVersion } buildTypes { @@ -45,20 +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' } - - flavorDimensions "receiver" - - productFlavors { - defaultCast { - dimension "receiver" - manifestPlaceholders = - [castOptionsProvider: "com.google.android.exoplayer2.ext.cast.DefaultCastOptionsProvider"] - } - } - } dependencies { @@ -68,9 +56,9 @@ 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 'androidx.appcompat:appcompat:' + androidxAppCompatVersion + implementation 'androidx.recyclerview:recyclerview:1.0.0' + implementation 'com.google.android.material:material:1.0.0' } apply plugin: 'com.google.android.gms.strict-version-matcher-plugin' diff --git a/demos/cast/proguard-rules.txt b/demos/cast/proguard-rules.txt 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 c556721863..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"/> mediaQueue; - private final QueuePositionListener queuePositionListener; - private final ConcatenatingMediaSource concatenatingMediaSource; - - private boolean castMediaQueueCreationPending; - private int currentItemIndex; - private Player currentPlayer; - - /** - * @param queuePositionListener A {@link QueuePositionListener} 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 DefaultReceiverPlayerManager createPlayerManager( - QueuePositionListener queuePositionListener, - PlayerView localPlayerView, - PlayerControlView castControlView, - Context context, - CastContext castContext) { - DefaultReceiverPlayerManager defaultReceiverPlayerManager = - new DefaultReceiverPlayerManager( - queuePositionListener, localPlayerView, castControlView, context, castContext); - defaultReceiverPlayerManager.init(); - return defaultReceiverPlayerManager; - } - - private DefaultReceiverPlayerManager( - QueuePositionListener queuePositionListener, - PlayerView localPlayerView, - PlayerControlView castControlView, - Context context, - CastContext castContext) { - this.queuePositionListener = queuePositionListener; - this.localPlayerView = localPlayerView; - this.castControlView = castControlView; - mediaQueue = new ArrayList<>(); - currentItemIndex = C.INDEX_UNSET; - concatenatingMediaSource = new ConcatenatingMediaSource(); - - DefaultTrackSelector trackSelector = new DefaultTrackSelector(); - RenderersFactory renderersFactory = new DefaultRenderersFactory(context); - exoPlayer = ExoPlayerFactory.newSimpleInstance(context, renderersFactory, trackSelector); - exoPlayer.addListener(this); - localPlayerView.setPlayer(exoPlayer); - - castPlayer = new CastPlayer(castContext); - castPlayer.addListener(this); - castPlayer.setSessionAvailabilityListener(this); - castControlView.setPlayer(castPlayer); - } - - // Queue manipulation methods. - - /** - * Plays a specified queue item in the current player. - * - * @param itemIndex The index of the item to play. - */ - public void selectQueueItem(int itemIndex) { - setCurrentItem(itemIndex, C.TIME_UNSET, true); - } - - /** - * Returns the index of the currently played item. - */ - public int getCurrentItemIndex() { - return currentItemIndex; - } - - /** - * Appends {@code item} to the media queue. - * - * @param item The {@link MediaItem} to append. - */ - public void addItem(MediaItem item) { - mediaQueue.add(item); - concatenatingMediaSource.addMediaSource(buildMediaSource(item)); - if (currentPlayer == castPlayer) { - castPlayer.addItems(buildMediaQueueItem(item)); - } - } - - /** - * Returns the size of the media queue. - */ - public int getMediaQueueSize() { - return mediaQueue.size(); - } - - /** - * Returns the item at the given index in the media queue. - * - * @param position The index of the item. - * @return The item at the given index in the media queue. - */ - public MediaItem getItem(int position) { - return mediaQueue.get(position); - } - - /** - * Removes the item at the given index from the media queue. - * - * @param itemIndex The index of the item to remove. - * @return Whether the removal was successful. - */ - public boolean removeItem(int itemIndex) { - concatenatingMediaSource.removeMediaSource(itemIndex); - if (currentPlayer == castPlayer) { - if (castPlayer.getPlaybackState() != Player.STATE_IDLE) { - Timeline castTimeline = castPlayer.getCurrentTimeline(); - if (castTimeline.getPeriodCount() <= itemIndex) { - return false; - } - castPlayer.removeItem((int) castTimeline.getPeriod(itemIndex, new Period()).id); - } - } - mediaQueue.remove(itemIndex); - if (itemIndex == currentItemIndex && itemIndex == mediaQueue.size()) { - maybeSetCurrentItemAndNotify(C.INDEX_UNSET); - } else if (itemIndex < currentItemIndex) { - maybeSetCurrentItemAndNotify(currentItemIndex - 1); - } - return true; - } - - /** - * Moves an item within the queue. - * - * @param fromIndex The index of 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) { - // Player update. - concatenatingMediaSource.moveMediaSource(fromIndex, toIndex); - if (currentPlayer == castPlayer && castPlayer.getPlaybackState() != Player.STATE_IDLE) { - Timeline castTimeline = castPlayer.getCurrentTimeline(); - int periodCount = castTimeline.getPeriodCount(); - if (periodCount <= fromIndex || periodCount <= toIndex) { - return false; - } - int elementId = (int) castTimeline.getPeriod(fromIndex, new Period()).id; - castPlayer.moveItem(elementId, toIndex); - } - - mediaQueue.add(toIndex, mediaQueue.remove(fromIndex)); - - // Index update. - if (fromIndex == currentItemIndex) { - maybeSetCurrentItemAndNotify(toIndex); - } else if (fromIndex < currentItemIndex && toIndex >= currentItemIndex) { - maybeSetCurrentItemAndNotify(currentItemIndex - 1); - } else if (fromIndex > currentItemIndex && toIndex <= currentItemIndex) { - maybeSetCurrentItemAndNotify(currentItemIndex + 1); - } - - return true; - } - - // Miscellaneous methods. - - /** - * Dispatches a given {@link KeyEvent} to the corresponding view of the current player. - * - * @param event The {@link KeyEvent}. - * @return Whether the event was handled by the target view. - */ - public boolean dispatchKeyEvent(KeyEvent event) { - if (currentPlayer == exoPlayer) { - return localPlayerView.dispatchKeyEvent(event); - } else /* currentPlayer == castPlayer */ { - return castControlView.dispatchKeyEvent(event); - } - } - - /** - * Releases the manager and the players that it holds. - */ - public void release() { - currentItemIndex = C.INDEX_UNSET; - mediaQueue.clear(); - concatenatingMediaSource.clear(); - castPlayer.setSessionAvailabilityListener(null); - castPlayer.release(); - localPlayerView.setPlayer(null); - exoPlayer.release(); - } - - // Player.EventListener implementation. - - @Override - public void onPlayerStateChanged(boolean playWhenReady, int playbackState) { - updateCurrentItemIndex(); - } - - @Override - public void onPositionDiscontinuity(@DiscontinuityReason int reason) { - updateCurrentItemIndex(); - } - - @Override - public void onTimelineChanged( - Timeline timeline, @Nullable Object manifest, @TimelineChangeReason int reason) { - updateCurrentItemIndex(); - if (currentPlayer == castPlayer && timeline.isEmpty()) { - castMediaQueueCreationPending = true; - } - } - - // CastPlayer.SessionAvailabilityListener implementation. - - @Override - public void onCastSessionAvailable() { - setCurrentPlayer(castPlayer); - } - - @Override - public void onCastSessionUnavailable() { - setCurrentPlayer(exoPlayer); - } - - // 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); - } - - private void setCurrentPlayer(Player currentPlayer) { - if (this.currentPlayer == currentPlayer) { - return; - } - - // View management. - if (currentPlayer == exoPlayer) { - localPlayerView.setVisibility(View.VISIBLE); - castControlView.hide(); - } else /* currentPlayer == castPlayer */ { - localPlayerView.setVisibility(View.GONE); - castControlView.show(); - } - - // Player state management. - long playbackPositionMs = C.TIME_UNSET; - int windowIndex = C.INDEX_UNSET; - boolean playWhenReady = false; - if (this.currentPlayer != null) { - int playbackState = this.currentPlayer.getPlaybackState(); - if (playbackState != Player.STATE_ENDED) { - playbackPositionMs = this.currentPlayer.getCurrentPosition(); - playWhenReady = this.currentPlayer.getPlayWhenReady(); - windowIndex = this.currentPlayer.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. - } - - this.currentPlayer = currentPlayer; - - // Media queue management. - castMediaQueueCreationPending = currentPlayer == castPlayer; - if (currentPlayer == exoPlayer) { - exoPlayer.prepare(concatenatingMediaSource); - } - - // Playback transition. - if (windowIndex != C.INDEX_UNSET) { - setCurrentItem(windowIndex, playbackPositionMs, playWhenReady); - } - } - - /** - * Starts playback of the item at the given position. - * - * @param itemIndex The index of the item to play. - * @param positionMs The position at which playback should start. - * @param playWhenReady Whether the player should proceed when ready to do so. - */ - private void setCurrentItem(int itemIndex, long positionMs, boolean playWhenReady) { - maybeSetCurrentItemAndNotify(itemIndex); - if (castMediaQueueCreationPending) { - MediaQueueItem[] items = new MediaQueueItem[mediaQueue.size()]; - for (int i = 0; i < items.length; i++) { - items[i] = buildMediaQueueItem(mediaQueue.get(i)); - } - castMediaQueueCreationPending = false; - castPlayer.loadItems(items, itemIndex, positionMs, Player.REPEAT_MODE_OFF); - } else { - currentPlayer.seekTo(itemIndex, positionMs); - currentPlayer.setPlayWhenReady(playWhenReady); - } - } - - private void maybeSetCurrentItemAndNotify(int currentItemIndex) { - if (this.currentItemIndex != currentItemIndex) { - int oldIndex = this.currentItemIndex; - this.currentItemIndex = currentItemIndex; - queuePositionListener.onQueuePositionChanged(oldIndex, currentItemIndex); - } - } - - private static MediaSource buildMediaSource(MediaItem item) { - Uri uri = item.media.uri; - switch (item.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: " + item.mimeType); - } - } - } - - private static MediaQueueItem buildMediaQueueItem(MediaItem item) { - MediaMetadata movieMetadata = new MediaMetadata(MediaMetadata.MEDIA_TYPE_MOVIE); - movieMetadata.putString(MediaMetadata.KEY_TITLE, item.title); - MediaInfo mediaInfo = - new MediaInfo.Builder(item.media.uri.toString()) - .setStreamType(MediaInfo.STREAM_TYPE_BUFFERED) - .setContentType(item.mimeType) - .setMetadata(movieMetadata) - .build(); - return new MediaQueueItem.Builder(mediaInfo).build(); - } - -} diff --git a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/DemoUtil.java b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/DemoUtil.java index e45ceb7c83..dacdbfe616 100644 --- a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/DemoUtil.java +++ b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/DemoUtil.java @@ -16,98 +16,87 @@ package com.google.android.exoplayer2.castdemo; import android.net.Uri; -import android.support.annotation.Nullable; import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.ext.cast.MediaItem; +import com.google.android.exoplayer2.ext.cast.MediaItem.DrmConfiguration; import com.google.android.exoplayer2.util.MimeTypes; import java.util.ArrayList; import java.util.Collections; import java.util.List; -import java.util.UUID; /** Utility methods and constants for the Cast demo application. */ /* package */ final class DemoUtil { - /** Represents a media sample. */ - public static final class Sample { - - /** The uri of the media content. */ - public final String uri; - /** The name of the sample. */ - public final String name; - /** The mime type of the sample media content. */ - public final String mimeType; - /** - * The {@link UUID} of the DRM scheme that protects the content, or null if the content is not - * DRM-protected. - */ - @Nullable public final UUID drmSchemeUuid; - /** - * The url from which players should obtain DRM licenses, or null if the content is not - * DRM-protected. - */ - @Nullable public final Uri licenseServerUri; - - /** - * @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, name, mimeType, /* drmSchemeUuid= */ null, /* licenseServerUriString= */ null); - } - - public Sample( - String uri, - String name, - String mimeType, - @Nullable UUID drmSchemeUuid, - @Nullable String licenseServerUriString) { - this.uri = uri; - this.name = name; - this.mimeType = mimeType; - this.drmSchemeUuid = drmSchemeUuid; - this.licenseServerUri = - licenseServerUriString != null ? Uri.parse(licenseServerUriString) : null; - } - - @Override - public String toString() { - return name; - } - } - public static final String MIME_TYPE_DASH = MimeTypes.APPLICATION_MPD; public static final String MIME_TYPE_HLS = MimeTypes.APPLICATION_M3U8; public static final String MIME_TYPE_SS = MimeTypes.APPLICATION_SS; public static final String MIME_TYPE_VIDEO_MP4 = MimeTypes.VIDEO_MP4; /** The list of samples available in the cast demo app. */ - public static final List SAMPLES; + public static final List SAMPLES; static { - // App samples. - ArrayList samples = new ArrayList<>(); + ArrayList samples = new ArrayList<>(); + // Clear content. samples.add( - new Sample( - "https://storage.googleapis.com/wvmedia/clear/h264/tears/tears.mpd", - "Clear DASH: Tears", - MIME_TYPE_DASH)); + 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 Sample( - "https://commondatastorage.googleapis.com/gtv-videos-bucket/CastVideos/" - + "hls/TearsOfSteel.m3u8", - "Clear HLS: Tears of Steel", - MIME_TYPE_HLS)); + 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 Sample( - "https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_4x3" - + "/bipbop_4x3_variant.m3u8", - "Clear HLS: Basic 4x3", - MIME_TYPE_HLS)); + 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 Sample( - "https://html5demos.com/assets/dizzy.mp4", "Clear MP4: Dizzy", MIME_TYPE_VIDEO_MP4)); + 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); } 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 058adf7c9c..0c5b5037f5 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,6 @@ 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 android.view.KeyEvent; import android.view.LayoutInflater; import android.view.Menu; @@ -33,25 +26,31 @@ import android.view.ViewGroup; import android.widget.ArrayAdapter; import android.widget.ListView; import android.widget.TextView; +import android.widget.Toast; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.app.AlertDialog; +import androidx.appcompat.app.AppCompatActivity; +import androidx.core.graphics.ColorUtils; +import androidx.recyclerview.widget.ItemTouchHelper; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; +import androidx.recyclerview.widget.RecyclerView.ViewHolder; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.SimpleExoPlayer; import com.google.android.exoplayer2.ext.cast.MediaItem; import com.google.android.exoplayer2.ui.PlayerControlView; import com.google.android.exoplayer2.ui.PlayerView; -import com.google.android.gms.cast.CastMediaControlIntent; import com.google.android.gms.cast.framework.CastButtonFactory; import com.google.android.gms.cast.framework.CastContext; import com.google.android.gms.dynamite.DynamiteModule; -import java.util.Collections; /** * 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 { - - private final MediaItem.Builder mediaItemBuilder; + implements OnClickListener, PlayerManager.Listener { private PlayerView localPlayerView; private PlayerControlView castControlView; @@ -60,10 +59,6 @@ public class MainActivity extends AppCompatActivity private MediaQueueListAdapter mediaQueueListAdapter; private CastContext castContext; - public MainActivity() { - mediaItemBuilder = new MediaItem.Builder(); - } - // Activity lifecycle methods. @Override @@ -76,7 +71,7 @@ public class MainActivity extends AppCompatActivity Throwable cause = e.getCause(); while (cause != null) { if (cause instanceof DynamiteModule.LoadingException) { - setContentView(R.layout.cast_context_error_message_layout); + setContentView(R.layout.cast_context_error); return; } cause = cause.getCause(); @@ -117,20 +112,13 @@ public class MainActivity extends AppCompatActivity // There is no Cast context to work with. Do nothing. return; } - String applicationId = castContext.getCastOptions().getReceiverApplicationId(); - switch (applicationId) { - case CastMediaControlIntent.DEFAULT_MEDIA_RECEIVER_APPLICATION_ID: - playerManager = - DefaultReceiverPlayerManager.createPlayerManager( - /* queuePositionListener= */ this, - localPlayerView, - castControlView, - /* context= */ this, - castContext); - break; - default: - throw new IllegalStateException("Illegal receiver app id: " + applicationId); - } + playerManager = + new PlayerManager( + /* listener= */ this, + localPlayerView, + castControlView, + /* context= */ this, + castContext); mediaQueueList.setAdapter(mediaQueueListAdapter); } @@ -157,12 +145,15 @@ public class MainActivity extends AppCompatActivity @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) { @@ -174,27 +165,30 @@ public class MainActivity extends AppCompatActivity } } + @Override + public void onUnsupportedTrack(int trackType) { + if (trackType == C.TRACK_TYPE_AUDIO) { + showToast(R.string.error_unsupported_audio); + } else if (trackType == C.TRACK_TYPE_VIDEO) { + showToast(R.string.error_unsupported_video); + } else { + // Do nothing. + } + } + // Internal methods. + private void showToast(int messageId) { + Toast.makeText(getApplicationContext(), messageId, Toast.LENGTH_LONG).show(); + } + private View buildSampleListView() { View dialogList = getLayoutInflater().inflate(R.layout.sample_list, null); ListView sampleList = dialogList.findViewById(R.id.sample_list); sampleList.setAdapter(new SampleListAdapter(this)); sampleList.setOnItemClickListener( (parent, view, position, id) -> { - DemoUtil.Sample sample = DemoUtil.SAMPLES.get(position); - mediaItemBuilder - .clear() - .setMedia(sample.uri) - .setTitle(sample.name) - .setMimeType(sample.mimeType); - if (sample.drmSchemeUuid != null) { - mediaItemBuilder.setDrmSchemes( - Collections.singletonList( - new MediaItem.DrmScheme( - sample.drmSchemeUuid, new MediaItem.UriBundle(sample.licenseServerUri)))); - } - playerManager.addItem(mediaItemBuilder.build()); + playerManager.addItem(DemoUtil.SAMPLES.get(position)); mediaQueueListAdapter.notifyItemInserted(playerManager.getMediaQueueSize() - 1); }); return dialogList; @@ -202,23 +196,6 @@ public class MainActivity extends AppCompatActivity // 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 @@ -230,11 +207,14 @@ public class MainActivity extends AppCompatActivity @Override public void onBindViewHolder(QueueItemViewHolder holder, int position) { + holder.item = playerManager.getItem(position); TextView view = holder.textView; - view.setText(playerManager.getItem(position).title); + 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 @@ -272,8 +252,11 @@ public class MainActivity extends AppCompatActivity @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); } } @@ -281,8 +264,9 @@ public class MainActivity extends AppCompatActivity 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(); @@ -291,14 +275,37 @@ public class MainActivity extends AppCompatActivity 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 c56f0eb855..85104e0d18 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) 2018 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. @@ -15,50 +15,445 @@ */ package com.google.android.exoplayer2.castdemo; +import android.content.Context; +import android.net.Uri; import android.view.KeyEvent; +import android.view.View; import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.Player; +import com.google.android.exoplayer2.Player.DiscontinuityReason; +import com.google.android.exoplayer2.Player.EventListener; +import com.google.android.exoplayer2.Player.TimelineChangeReason; +import com.google.android.exoplayer2.SimpleExoPlayer; +import com.google.android.exoplayer2.Timeline; +import com.google.android.exoplayer2.Timeline.Period; +import com.google.android.exoplayer2.drm.DefaultDrmSessionManager; +import com.google.android.exoplayer2.drm.DrmSessionManager; +import com.google.android.exoplayer2.drm.ExoMediaCrypto; +import com.google.android.exoplayer2.drm.FrameworkMediaDrm; +import com.google.android.exoplayer2.drm.HttpMediaDrmCallback; +import com.google.android.exoplayer2.ext.cast.CastPlayer; +import com.google.android.exoplayer2.ext.cast.DefaultMediaItemConverter; import com.google.android.exoplayer2.ext.cast.MediaItem; +import com.google.android.exoplayer2.ext.cast.MediaItemConverter; +import com.google.android.exoplayer2.ext.cast.SessionAvailabilityListener; +import com.google.android.exoplayer2.source.ConcatenatingMediaSource; +import com.google.android.exoplayer2.source.MediaSource; +import com.google.android.exoplayer2.source.ProgressiveMediaSource; +import com.google.android.exoplayer2.source.TrackGroupArray; +import com.google.android.exoplayer2.source.dash.DashMediaSource; +import com.google.android.exoplayer2.source.hls.HlsMediaSource; +import com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource; +import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; +import com.google.android.exoplayer2.trackselection.MappingTrackSelector; +import com.google.android.exoplayer2.trackselection.TrackSelectionArray; +import com.google.android.exoplayer2.ui.PlayerControlView; +import com.google.android.exoplayer2.ui.PlayerView; +import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory; +import com.google.android.exoplayer2.util.Util; +import com.google.android.gms.cast.MediaQueueItem; +import com.google.android.gms.cast.framework.CastContext; +import java.util.ArrayList; +import java.util.Map; -/** Manages the players in the Cast demo app. */ -interface PlayerManager { +/** 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. */ - 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); } - /** Redirects the given {@code keyEvent} to the active player. */ - boolean dispatchKeyEvent(KeyEvent keyEvent); + private static final String USER_AGENT = "ExoCastDemoPlayer"; + private static final DefaultHttpDataSourceFactory DATA_SOURCE_FACTORY = + new DefaultHttpDataSourceFactory(USER_AGENT); - /** Appends the given {@link MediaItem} to the media queue. */ - void addItem(MediaItem mediaItem); + private final PlayerView localPlayerView; + private final PlayerControlView castControlView; + private final DefaultTrackSelector trackSelector; + private final SimpleExoPlayer exoPlayer; + private final CastPlayer castPlayer; + private final ArrayList mediaQueue; + private final Listener listener; + private final ConcatenatingMediaSource concatenatingMediaSource; + private final MediaItemConverter mediaItemConverter; - /** Returns the number of items in the media queue. */ - int getMediaQueueSize(); - - /** Selects the item at the given position for playback. */ - void selectQueueItem(int position); + private TrackGroupArray lastSeenTrackGroupArray; + private int currentItemIndex; + private Player currentPlayer; /** - * Returns the position of the item currently being played, or {@link C#INDEX_UNSET} if no item is - * being played. + * 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}. */ - int getCurrentItemIndex(); + public PlayerManager( + Listener listener, + PlayerView localPlayerView, + PlayerControlView castControlView, + Context context, + CastContext castContext) { + this.listener = listener; + this.localPlayerView = localPlayerView; + this.castControlView = castControlView; + mediaQueue = new ArrayList<>(); + currentItemIndex = C.INDEX_UNSET; + concatenatingMediaSource = new ConcatenatingMediaSource(); + mediaItemConverter = new DefaultMediaItemConverter(); - /** Returns the {@link MediaItem} at the given {@code position}. */ - MediaItem getItem(int position); + trackSelector = new DefaultTrackSelector(context); + exoPlayer = new SimpleExoPlayer.Builder(context).setTrackSelector(trackSelector).build(); + exoPlayer.addListener(this); + localPlayerView.setPlayer(exoPlayer); - /** Moves the item at position {@code from} to position {@code to}. */ - boolean moveItem(int from, int to); + castPlayer = new CastPlayer(castContext); + castPlayer.addListener(this); + castPlayer.setSessionAvailabilityListener(this); + castControlView.setPlayer(castPlayer); - /** Removes the item at position {@code index}. */ - boolean removeItem(int index); + setCurrentPlayer(castPlayer.isCastSessionAvailable() ? castPlayer : exoPlayer); + } - /** Releases any acquired resources. */ - void release(); + // Queue manipulation methods. + + /** + * Plays a specified queue item in the current player. + * + * @param itemIndex The index of the item to play. + */ + public void selectQueueItem(int itemIndex) { + setCurrentItem(itemIndex, C.TIME_UNSET, true); + } + + /** Returns the index of the currently played item. */ + public int getCurrentItemIndex() { + return currentItemIndex; + } + + /** + * Appends {@code item} to the media queue. + * + * @param item The {@link MediaItem} to append. + */ + public void addItem(MediaItem item) { + mediaQueue.add(item); + concatenatingMediaSource.addMediaSource(buildMediaSource(item)); + if (currentPlayer == castPlayer) { + castPlayer.addItems(mediaItemConverter.toMediaQueueItem(item)); + } + } + + /** Returns the size of the media queue. */ + public int getMediaQueueSize() { + return mediaQueue.size(); + } + + /** + * Returns the item at the given index in the media queue. + * + * @param position The index of the item. + * @return The item at the given index in the media queue. + */ + public MediaItem getItem(int position) { + return mediaQueue.get(position); + } + + /** + * Removes the item at the given index from the media queue. + * + * @param item The item to remove. + * @return Whether the removal was successful. + */ + public boolean removeItem(MediaItem item) { + int itemIndex = mediaQueue.indexOf(item); + if (itemIndex == -1) { + return false; + } + concatenatingMediaSource.removeMediaSource(itemIndex); + if (currentPlayer == castPlayer) { + if (castPlayer.getPlaybackState() != Player.STATE_IDLE) { + Timeline castTimeline = castPlayer.getCurrentTimeline(); + if (castTimeline.getPeriodCount() <= itemIndex) { + return false; + } + castPlayer.removeItem((int) castTimeline.getPeriod(itemIndex, new Period()).id); + } + } + mediaQueue.remove(itemIndex); + if (itemIndex == currentItemIndex && itemIndex == mediaQueue.size()) { + maybeSetCurrentItemAndNotify(C.INDEX_UNSET); + } else if (itemIndex < currentItemIndex) { + maybeSetCurrentItemAndNotify(currentItemIndex - 1); + } + return true; + } + + /** + * Moves an item within the queue. + * + * @param item The item to move. + * @param toIndex The target index of the item in the queue. + * @return Whether the item move was successful. + */ + public boolean moveItem(MediaItem item, int toIndex) { + int fromIndex = mediaQueue.indexOf(item); + if (fromIndex == -1) { + return false; + } + // Player update. + concatenatingMediaSource.moveMediaSource(fromIndex, toIndex); + if (currentPlayer == castPlayer && castPlayer.getPlaybackState() != Player.STATE_IDLE) { + Timeline castTimeline = castPlayer.getCurrentTimeline(); + int periodCount = castTimeline.getPeriodCount(); + if (periodCount <= fromIndex || periodCount <= toIndex) { + return false; + } + int elementId = (int) castTimeline.getPeriod(fromIndex, new Period()).id; + castPlayer.moveItem(elementId, toIndex); + } + + mediaQueue.add(toIndex, mediaQueue.remove(fromIndex)); + + // Index update. + if (fromIndex == currentItemIndex) { + maybeSetCurrentItemAndNotify(toIndex); + } else if (fromIndex < currentItemIndex && toIndex >= currentItemIndex) { + maybeSetCurrentItemAndNotify(currentItemIndex - 1); + } else if (fromIndex > currentItemIndex && toIndex <= currentItemIndex) { + maybeSetCurrentItemAndNotify(currentItemIndex + 1); + } + + return true; + } + + /** + * Dispatches a given {@link KeyEvent} to the corresponding view of the current player. + * + * @param event The {@link KeyEvent}. + * @return Whether the event was handled by the target view. + */ + public boolean dispatchKeyEvent(KeyEvent event) { + if (currentPlayer == exoPlayer) { + return localPlayerView.dispatchKeyEvent(event); + } else /* currentPlayer == castPlayer */ { + return castControlView.dispatchKeyEvent(event); + } + } + + /** Releases the manager and the players that it holds. */ + public void release() { + currentItemIndex = C.INDEX_UNSET; + mediaQueue.clear(); + concatenatingMediaSource.clear(); + castPlayer.setSessionAvailabilityListener(null); + castPlayer.release(); + localPlayerView.setPlayer(null); + exoPlayer.release(); + } + + // Player.EventListener implementation. + + @Override + public void onPlayerStateChanged(boolean playWhenReady, @Player.State int playbackState) { + updateCurrentItemIndex(); + } + + @Override + public void onPositionDiscontinuity(@DiscontinuityReason int reason) { + updateCurrentItemIndex(); + } + + @Override + public void onTimelineChanged(Timeline timeline, @TimelineChangeReason int reason) { + updateCurrentItemIndex(); + } + + @Override + public void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray trackSelections) { + if (currentPlayer == exoPlayer && trackGroups != lastSeenTrackGroupArray) { + MappingTrackSelector.MappedTrackInfo mappedTrackInfo = + trackSelector.getCurrentMappedTrackInfo(); + if (mappedTrackInfo != null) { + if (mappedTrackInfo.getTypeSupport(C.TRACK_TYPE_VIDEO) + == MappingTrackSelector.MappedTrackInfo.RENDERER_SUPPORT_UNSUPPORTED_TRACKS) { + listener.onUnsupportedTrack(C.TRACK_TYPE_VIDEO); + } + if (mappedTrackInfo.getTypeSupport(C.TRACK_TYPE_AUDIO) + == MappingTrackSelector.MappedTrackInfo.RENDERER_SUPPORT_UNSUPPORTED_TRACKS) { + listener.onUnsupportedTrack(C.TRACK_TYPE_AUDIO); + } + } + lastSeenTrackGroupArray = trackGroups; + } + } + + // CastPlayer.SessionAvailabilityListener implementation. + + @Override + public void onCastSessionAvailable() { + setCurrentPlayer(castPlayer); + } + + @Override + public void onCastSessionUnavailable() { + setCurrentPlayer(exoPlayer); + } + + // Internal methods. + + private void updateCurrentItemIndex() { + int playbackState = currentPlayer.getPlaybackState(); + maybeSetCurrentItemAndNotify( + playbackState != Player.STATE_IDLE && playbackState != Player.STATE_ENDED + ? currentPlayer.getCurrentWindowIndex() + : C.INDEX_UNSET); + } + + private void setCurrentPlayer(Player currentPlayer) { + if (this.currentPlayer == currentPlayer) { + return; + } + + // View management. + if (currentPlayer == exoPlayer) { + localPlayerView.setVisibility(View.VISIBLE); + castControlView.hide(); + } else /* currentPlayer == castPlayer */ { + localPlayerView.setVisibility(View.GONE); + castControlView.show(); + } + + // Player state management. + long playbackPositionMs = C.TIME_UNSET; + int windowIndex = C.INDEX_UNSET; + boolean playWhenReady = false; + + Player previousPlayer = this.currentPlayer; + if (previousPlayer != null) { + // Save state from the previous player. + int playbackState = previousPlayer.getPlaybackState(); + if (playbackState != Player.STATE_ENDED) { + playbackPositionMs = previousPlayer.getCurrentPosition(); + playWhenReady = previousPlayer.getPlayWhenReady(); + windowIndex = previousPlayer.getCurrentWindowIndex(); + if (windowIndex != currentItemIndex) { + playbackPositionMs = C.TIME_UNSET; + windowIndex = currentItemIndex; + } + } + previousPlayer.stop(true); + } + + this.currentPlayer = currentPlayer; + + // Media queue management. + if (currentPlayer == exoPlayer) { + exoPlayer.prepare(concatenatingMediaSource); + } + + // Playback transition. + if (windowIndex != C.INDEX_UNSET) { + setCurrentItem(windowIndex, playbackPositionMs, playWhenReady); + } + } + + /** + * Starts playback of the item at the given position. + * + * @param itemIndex The index of the item to play. + * @param positionMs The position at which playback should start. + * @param playWhenReady Whether the player should proceed when ready to do so. + */ + private void setCurrentItem(int itemIndex, long positionMs, boolean playWhenReady) { + maybeSetCurrentItemAndNotify(itemIndex); + if (currentPlayer == castPlayer && castPlayer.getCurrentTimeline().isEmpty()) { + MediaQueueItem[] items = new MediaQueueItem[mediaQueue.size()]; + for (int i = 0; i < items.length; i++) { + items[i] = mediaItemConverter.toMediaQueueItem(mediaQueue.get(i)); + } + castPlayer.loadItems(items, itemIndex, positionMs, Player.REPEAT_MODE_OFF); + } else { + currentPlayer.seekTo(itemIndex, positionMs); + currentPlayer.setPlayWhenReady(playWhenReady); + } + } + + private void maybeSetCurrentItemAndNotify(int currentItemIndex) { + if (this.currentItemIndex != currentItemIndex) { + int oldIndex = this.currentItemIndex; + this.currentItemIndex = currentItemIndex; + listener.onQueuePositionChanged(oldIndex, currentItemIndex); + } + } + + private MediaSource buildMediaSource(MediaItem item) { + Uri uri = item.uri; + String mimeType = item.mimeType; + if (mimeType == null) { + throw new IllegalArgumentException("mimeType is required"); + } + + DrmSessionManager drmSessionManager = + DrmSessionManager.getDummyDrmSessionManager(); + MediaItem.DrmConfiguration drmConfiguration = item.drmConfiguration; + if (drmConfiguration != null && Util.SDK_INT >= 18) { + String licenseServerUrl = + drmConfiguration.licenseUri != null ? drmConfiguration.licenseUri.toString() : ""; + HttpMediaDrmCallback drmCallback = + new HttpMediaDrmCallback(licenseServerUrl, DATA_SOURCE_FACTORY); + for (Map.Entry requestHeader : drmConfiguration.requestHeaders.entrySet()) { + drmCallback.setKeyRequestProperty(requestHeader.getKey(), requestHeader.getValue()); + } + drmSessionManager = + new DefaultDrmSessionManager.Builder() + .setMultiSession(/* multiSession= */ true) + .setUuidAndExoMediaDrmProvider( + drmConfiguration.uuid, FrameworkMediaDrm.DEFAULT_PROVIDER) + .build(drmCallback); + } + + MediaSource createdMediaSource; + switch (mimeType) { + case DemoUtil.MIME_TYPE_SS: + createdMediaSource = + new SsMediaSource.Factory(DATA_SOURCE_FACTORY) + .setDrmSessionManager(drmSessionManager) + .createMediaSource(uri); + break; + case DemoUtil.MIME_TYPE_DASH: + createdMediaSource = + new DashMediaSource.Factory(DATA_SOURCE_FACTORY) + .setDrmSessionManager(drmSessionManager) + .createMediaSource(uri); + break; + case DemoUtil.MIME_TYPE_HLS: + createdMediaSource = + new HlsMediaSource.Factory(DATA_SOURCE_FACTORY) + .setDrmSessionManager(drmSessionManager) + .createMediaSource(uri); + break; + case DemoUtil.MIME_TYPE_VIDEO_MP4: + createdMediaSource = + new ProgressiveMediaSource.Factory(DATA_SOURCE_FACTORY) + .setDrmSessionManager(drmSessionManager) + .createMediaSource(uri); + break; + default: + throw new IllegalArgumentException("mimeType is unsupported: " + mimeType); + } + return createdMediaSource; + } } diff --git a/demos/cast/src/main/res/drawable/ic_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_message_layout.xml b/demos/cast/src/main/res/layout/cast_context_error.xml similarity index 77% rename from demos/cast/src/main/res/layout/cast_context_error_message_layout.xml rename to demos/cast/src/main/res/layout/cast_context_error.xml index 6d3260de38..0b3fdb63d2 100644 --- a/demos/cast/src/main/res/layout/cast_context_error_message_layout.xml +++ b/demos/cast/src/main/res/layout/cast_context_error.xml @@ -13,17 +13,10 @@ See the License for the specific language governing permissions and limitations under the License. --> - - - - - 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 58f5233412..69f0691630 100644 --- a/demos/cast/src/main/res/values/strings.xml +++ b/demos/cast/src/main/res/values/strings.xml @@ -20,8 +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/ima/README.md b/demos/ima/README.md deleted file mode 100644 index 8002b56667..0000000000 --- a/demos/ima/README.md +++ /dev/null @@ -1,4 +0,0 @@ -# IMA demo application # - -This folder contains a demo application that showcases ExoPlayer integration -with the IMA SDK. diff --git a/demos/ima/src/main/java/com/google/android/exoplayer2/imademo/MainActivity.java b/demos/ima/src/main/java/com/google/android/exoplayer2/imademo/MainActivity.java deleted file mode 100644 index 9988108f32..0000000000 --- a/demos/ima/src/main/java/com/google/android/exoplayer2/imademo/MainActivity.java +++ /dev/null @@ -1,58 +0,0 @@ -/* - * Copyright (C) 2017 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.google.android.exoplayer2.imademo; - -import android.app.Activity; -import android.os.Bundle; -import com.google.android.exoplayer2.ExoPlayer; -import com.google.android.exoplayer2.ui.PlayerView; - -/** - * Main Activity for the IMA plugin demo. {@link ExoPlayer} objects are created by - * {@link PlayerManager}, which this class instantiates. - */ -public final class MainActivity extends Activity { - - private PlayerView playerView; - private PlayerManager player; - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.main_activity); - playerView = findViewById(R.id.player_view); - player = new PlayerManager(this); - } - - @Override - public void onResume() { - super.onResume(); - player.init(this, playerView); - } - - @Override - public void onPause() { - super.onPause(); - player.reset(); - } - - @Override - public void onDestroy() { - player.release(); - super.onDestroy(); - } - -} diff --git a/demos/ima/src/main/java/com/google/android/exoplayer2/imademo/PlayerManager.java b/demos/ima/src/main/java/com/google/android/exoplayer2/imademo/PlayerManager.java deleted file mode 100644 index d67c4549d8..0000000000 --- a/demos/ima/src/main/java/com/google/android/exoplayer2/imademo/PlayerManager.java +++ /dev/null @@ -1,134 +0,0 @@ -/* - * Copyright (C) 2017 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.google.android.exoplayer2.imademo; - -import android.content.Context; -import android.net.Uri; -import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.C.ContentType; -import com.google.android.exoplayer2.ExoPlayer; -import com.google.android.exoplayer2.ExoPlayerFactory; -import com.google.android.exoplayer2.SimpleExoPlayer; -import com.google.android.exoplayer2.ext.ima.ImaAdsLoader; -import com.google.android.exoplayer2.source.ExtractorMediaSource; -import com.google.android.exoplayer2.source.MediaSource; -import com.google.android.exoplayer2.source.ads.AdsMediaSource; -import com.google.android.exoplayer2.source.dash.DashMediaSource; -import com.google.android.exoplayer2.source.hls.HlsMediaSource; -import com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource; -import com.google.android.exoplayer2.trackselection.AdaptiveTrackSelection; -import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; -import com.google.android.exoplayer2.trackselection.TrackSelection; -import com.google.android.exoplayer2.trackselection.TrackSelector; -import com.google.android.exoplayer2.ui.PlayerView; -import com.google.android.exoplayer2.upstream.DataSource; -import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory; -import com.google.android.exoplayer2.util.Util; - -/** Manages the {@link ExoPlayer}, the IMA plugin and all video playback. */ -/* package */ final class PlayerManager implements AdsMediaSource.MediaSourceFactory { - - private final ImaAdsLoader adsLoader; - private final DataSource.Factory dataSourceFactory; - - private SimpleExoPlayer player; - private long contentPosition; - - public PlayerManager(Context context) { - String adTag = context.getString(R.string.ad_tag_url); - adsLoader = new ImaAdsLoader(context, Uri.parse(adTag)); - dataSourceFactory = - new DefaultDataSourceFactory( - context, Util.getUserAgent(context, context.getString(R.string.application_name))); - } - - public void init(Context context, PlayerView playerView) { - // Create a default track selector. - TrackSelection.Factory videoTrackSelectionFactory = new AdaptiveTrackSelection.Factory(); - TrackSelector trackSelector = new DefaultTrackSelector(videoTrackSelectionFactory); - - // Create a player instance. - player = ExoPlayerFactory.newSimpleInstance(context, trackSelector); - - // Bind the player to the view. - playerView.setPlayer(player); - - // This is the MediaSource representing the content media (i.e. not the ad). - String contentUrl = context.getString(R.string.content_url); - MediaSource contentMediaSource = buildMediaSource(Uri.parse(contentUrl)); - - // Compose the content media source into a new AdsMediaSource with both ads and content. - MediaSource mediaSourceWithAds = - new AdsMediaSource( - contentMediaSource, - /* adMediaSourceFactory= */ this, - adsLoader, - playerView.getOverlayFrameLayout()); - - // Prepare the player with the source. - player.seekTo(contentPosition); - player.prepare(mediaSourceWithAds); - player.setPlayWhenReady(true); - } - - public void reset() { - if (player != null) { - contentPosition = player.getContentPosition(); - player.release(); - player = null; - } - } - - public void release() { - if (player != null) { - player.release(); - player = null; - } - adsLoader.release(); - } - - // AdsMediaSource.MediaSourceFactory implementation. - - @Override - public MediaSource createMediaSource(Uri uri) { - return buildMediaSource(uri); - } - - @Override - public int[] getSupportedTypes() { - // IMA does not support Smooth Streaming ads. - return new int[] {C.TYPE_DASH, C.TYPE_HLS, C.TYPE_OTHER}; - } - - // Internal methods. - - private MediaSource buildMediaSource(Uri uri) { - @ContentType int type = Util.inferContentType(uri); - switch (type) { - case C.TYPE_DASH: - return new DashMediaSource.Factory(dataSourceFactory).createMediaSource(uri); - case C.TYPE_SS: - return new SsMediaSource.Factory(dataSourceFactory).createMediaSource(uri); - case C.TYPE_HLS: - return new HlsMediaSource.Factory(dataSourceFactory).createMediaSource(uri); - case C.TYPE_OTHER: - return new ExtractorMediaSource.Factory(dataSourceFactory).createMediaSource(uri); - default: - throw new IllegalStateException("Unsupported type: " + type); - } - } - -} diff --git a/demos/ima/src/main/res/values/strings.xml b/demos/ima/src/main/res/values/strings.xml deleted file mode 100644 index 2eb5700bf0..0000000000 --- a/demos/ima/src/main/res/values/strings.xml +++ /dev/null @@ -1,24 +0,0 @@ - - - - - Exo IMA Demo - - - - - - diff --git a/demos/main/build.gradle b/demos/main/build.gradle index c516ba297f..ab47b6de81 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,8 +25,8 @@ android { defaultConfig { versionName project.ext.releaseVersion versionCode project.ext.releaseVersionCode - minSdkVersion 16 - targetSdkVersion project.ext.targetSdkVersion + minSdkVersion project.ext.minSdkVersion + targetSdkVersion project.ext.appTargetSdkVersion } buildTypes { @@ -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,12 +62,15 @@ android { } dependencies { - implementation 'com.android.support:support-annotations:' + supportLibraryVersion + implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion + implementation 'androidx.appcompat:appcompat:' + androidxAppCompatVersion + implementation 'com.google.android.material:material:1.0.0' implementation project(modulePrefix + 'library-core') implementation project(modulePrefix + 'library-dash') implementation project(modulePrefix + 'library-hls') implementation project(modulePrefix + 'library-smoothstreaming') implementation project(modulePrefix + 'library-ui') + withExtensionsImplementation project(path: modulePrefix + 'extension-av1') withExtensionsImplementation project(path: modulePrefix + 'extension-ffmpeg') withExtensionsImplementation project(path: modulePrefix + 'extension-flac') withExtensionsImplementation project(path: modulePrefix + 'extension-ima') diff --git a/demos/main/src/main/AndroidManifest.xml b/demos/main/src/main/AndroidManifest.xml index e80e37688d..0240a377ac 100644 --- a/demos/main/src/main/AndroidManifest.xml +++ b/demos/main/src/main/AndroidManifest.xml @@ -15,6 +15,7 @@ --> @@ -33,11 +34,14 @@ android:banner="@drawable/ic_banner" android:largeHeap="true" android:allowBackup="false" - android:name="com.google.android.exoplayer2.demo.DemoApplication"> + android:requestLegacyExternalStorage="true" + 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 c2acf3990b..06f063b1c1 100644 --- a/demos/main/src/main/assets/media.exolist.json +++ b/demos/main/src/main/assets/media.exolist.json @@ -208,6 +208,13 @@ "uri": "https://storage.googleapis.com/wvmedia/cbcs/h264/tears/tears_aes_cbcs_uhd.mpd", "drm_scheme": "widevine", "drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test" + }, + { + "name": "WV: Secure and Clear SD & HD (cenc,MP4,H264)", + "uri": "https://storage.googleapis.com/exoplayer-test-media-1/widevine/tears_enc_clear_enc.mpd", + "drm_scheme": "widevine", + "drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test", + "drm_session_for_clear_types": ["audio", "video"] } ] }, @@ -330,11 +337,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" } ] @@ -376,44 +383,52 @@ "uri": "https://html5demos.com/assets/dizzy.mp4" }, { - "name": "Apple AAC 10s", + "name": "Apple 10s (AAC)", "uri": "https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_4x3/gear0/fileSequence0.aac" }, { - "name": "Apple TS 10s", + "name": "Apple 10s (TS)", "uri": "https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_4x3/gear1/fileSequence0.ts" }, { - "name": "Android screens (Matroska)", + "name": "Android screens (MKV)", "uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv" }, { - "name": "Screens 360P (WebM,VP9,No Audio)", + "name": "Screens 360p video (WebM,VP9)", "uri": "https://storage.googleapis.com/exoplayer-test-media-1/gen-3/screens/dash-vod-single-segment/video-vp9-360.webm" }, { - "name": "Screens 480p (FMP4,H264,No Audio)", + "name": "Screens 480p video (FMP4,H264)", "uri": "https://storage.googleapis.com/exoplayer-test-media-1/gen-3/screens/dash-vod-single-segment/video-avc-baseline-480.mp4" }, { - "name": "Screens 1080p (FMP4,H264, No Audio)", + "name": "Screens 1080p video (FMP4,H264)", "uri": "https://storage.googleapis.com/exoplayer-test-media-1/gen-3/screens/dash-vod-single-segment/video-137.mp4" }, { - "name": "Screens (FMP4,AAC Audio)", + "name": "Screens audio (FMP4)", "uri": "https://storage.googleapis.com/exoplayer-test-media-1/gen-3/screens/dash-vod-single-segment/audio-141.mp4" }, { - "name": "Google Play (MP3 Audio)", + "name": "Google Play (MP3)", "uri": "https://storage.googleapis.com/exoplayer-test-media-0/play.mp3" }, { - "name": "Google Play (Ogg/Vorbis Audio)", + "name": "Google Play (Ogg/Vorbis)", "uri": "https://storage.googleapis.com/exoplayer-test-media-1/ogg/play.ogg" }, { - "name": "Big Buck Bunny (FLV Video)", + "name": "Google Play (FLAC)", + "uri": "https://storage.googleapis.com/exoplayer-test-media-1/flac/play.flac" + }, + { + "name": "Big Buck Bunny video (FLV)", "uri": "https://vod.leasewebcdn.com/bbb.flv?ri=1024&rs=150&start=0" + }, + { + "name": "Big Buck Bunny 480p video (MP4,AV1)", + "uri": "https://storage.googleapis.com/downloads.webmproject.org/av1/exoplayer/bbb-av1-480p.mp4" } ] }, @@ -447,23 +462,27 @@ }, { "name": "Clear -> Enc -> Clear -> Enc -> Enc", - "drm_scheme": "widevine", - "drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test", "playlist": [ { "uri": "https://html5demos.com/assets/dizzy.mp4" }, { - "uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears_sd.mpd" + "uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears_sd.mpd", + "drm_scheme": "widevine", + "drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test" }, { "uri": "https://html5demos.com/assets/dizzy.mp4" }, { - "uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears_sd.mpd" + "uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears_sd.mpd", + "drm_scheme": "widevine", + "drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test" }, { - "uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears_sd.mpd" + "uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears_sd.mpd", + "drm_scheme": "widevine", + "drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test" } ] } @@ -578,5 +597,24 @@ "spherical_stereo_mode": "top_bottom" } ] + }, + { + "name": "Subtitles", + "samples": [ + { + "name": "TTML", + "uri": "https://html5demos.com/assets/dizzy.mp4", + "subtitle_uri": "https://storage.googleapis.com/exoplayer-test-media-1/ttml/netflix_ttml_sample.xml", + "subtitle_mime_type": "application/ttml+xml", + "subtitle_language": "en" + }, + { + "name": "SSA/ASS position & alignment", + "uri": "https://storage.googleapis.com/exoplayer-test-media-1/gen-3/screens/dash-vod-single-segment/video-avc-baseline-480.mp4", + "subtitle_uri": "https://storage.googleapis.com/exoplayer-test-media-1/ssa/test-subs-position.ass", + "subtitle_mime_type": "text/x-ssa", + "subtitle_language": "en" + } + ] } ] diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/DemoApplication.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/DemoApplication.java index 560a9be58a..d83d7076c5 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 @@ -18,34 +18,41 @@ package com.google.android.exoplayer2.demo; import android.app.Application; import com.google.android.exoplayer2.DefaultRenderersFactory; import com.google.android.exoplayer2.RenderersFactory; +import com.google.android.exoplayer2.database.DatabaseProvider; +import com.google.android.exoplayer2.database.ExoDatabaseProvider; +import com.google.android.exoplayer2.offline.ActionFileUpgradeUtil; +import com.google.android.exoplayer2.offline.DefaultDownloadIndex; import com.google.android.exoplayer2.offline.DefaultDownloaderFactory; import com.google.android.exoplayer2.offline.DownloadManager; import com.google.android.exoplayer2.offline.DownloaderConstructorHelper; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory; import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory; -import com.google.android.exoplayer2.upstream.FileDataSourceFactory; +import com.google.android.exoplayer2.upstream.FileDataSource; import com.google.android.exoplayer2.upstream.HttpDataSource; import com.google.android.exoplayer2.upstream.cache.Cache; import com.google.android.exoplayer2.upstream.cache.CacheDataSource; import com.google.android.exoplayer2.upstream.cache.CacheDataSourceFactory; import com.google.android.exoplayer2.upstream.cache.NoOpCacheEvictor; import com.google.android.exoplayer2.upstream.cache.SimpleCache; +import com.google.android.exoplayer2.util.Log; import com.google.android.exoplayer2.util.Util; import java.io.File; +import java.io.IOException; /** * Placeholder application to facilitate overriding Application methods for debugging and testing. */ public class DemoApplication extends Application { + private static final String TAG = "DemoApplication"; private static final String DOWNLOAD_ACTION_FILE = "actions"; private static final String DOWNLOAD_TRACKER_ACTION_FILE = "tracked_actions"; private static final String DOWNLOAD_CONTENT_DIRECTORY = "downloads"; - private static final int MAX_SIMULTANEOUS_DOWNLOADS = 2; protected String userAgent; + private DatabaseProvider databaseProvider; private File downloadDirectory; private Cache downloadCache; private DownloadManager downloadManager; @@ -82,7 +89,8 @@ public class DemoApplication extends Application { ? DefaultRenderersFactory.EXTENSION_RENDERER_MODE_PREFER : DefaultRenderersFactory.EXTENSION_RENDERER_MODE_ON) : DefaultRenderersFactory.EXTENSION_RENDERER_MODE_OFF; - return new DefaultRenderersFactory(this, extensionRendererMode); + return new DefaultRenderersFactory(/* context= */ this) + .setExtensionRendererMode(extensionRendererMode); } public DownloadManager getDownloadManager() { @@ -95,33 +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( - this, - new File(getDownloadDirectory(), DOWNLOAD_ACTION_FILE), - new DefaultDownloaderFactory(downloaderConstructorHelper), - MAX_SIMULTANEOUS_DOWNLOADS, - DownloadManager.DEFAULT_MIN_RETRY_COUNT, - DownloadManager.DEFAULT_REQUIREMENTS); + 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() { @@ -134,12 +160,12 @@ 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, - new FileDataSourceFactory(), + new FileDataSource.Factory(), /* cacheWriteDataSinkFactory= */ null, CacheDataSource.FLAG_IGNORE_CACHE_ON_ERROR, /* eventListener= */ null); diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/DemoDownloadService.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/DemoDownloadService.java index 70cbe43dd8..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.DownloadService; -import com.google.android.exoplayer2.offline.DownloadState; 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 { @@ -33,15 +34,24 @@ public class DemoDownloadService extends DownloadService { 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 protected DownloadManager getDownloadManager() { return ((DemoApplication) getApplication()).getDownloadManager(); @@ -53,35 +63,26 @@ public class DemoDownloadService extends DownloadService { } @Override - protected Notification getForegroundNotification(DownloadState[] downloadStates) { - return DownloadNotificationUtil.buildProgressNotification( - /* context= */ this, - R.drawable.ic_download, - CHANNEL_ID, - /* contentIntent= */ null, - /* message= */ null, - downloadStates); + protected Notification getForegroundNotification(List downloads) { + return notificationHelper.buildProgressNotification( + R.drawable.ic_download, /* contentIntent= */ null, /* message= */ null, downloads); } @Override - protected void onDownloadStateChanged(DownloadState downloadState) { - Notification notification = null; - if (downloadState.state == DownloadState.STATE_COMPLETED) { + protected void onDownloadChanged(Download download) { + Notification notification; + if (download.state == Download.STATE_COMPLETED) { notification = - DownloadNotificationUtil.buildDownloadCompletedNotification( - /* context= */ this, + notificationHelper.buildDownloadCompletedNotification( R.drawable.ic_download_done, - CHANNEL_ID, /* contentIntent= */ null, - Util.fromUtf8Bytes(downloadState.customMetadata)); - } else if (downloadState.state == DownloadState.STATE_FAILED) { + Util.fromUtf8Bytes(download.request.data)); + } else if (download.state == Download.STATE_FAILED) { notification = - DownloadNotificationUtil.buildDownloadFailedNotification( - /* context= */ this, + notificationHelper.buildDownloadFailedNotification( R.drawable.ic_download_done, - CHANNEL_ID, /* contentIntent= */ null, - Util.fromUtf8Bytes(downloadState.customMetadata)); + Util.fromUtf8Bytes(download.request.data)); } else { return; } 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 559bbcef0f..143eda93df 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,61 +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.content.res.Resources; import android.net.Uri; -import android.os.Handler; -import android.os.HandlerThread; -import android.support.annotation.Nullable; -import android.util.Pair; -import android.view.LayoutInflater; -import android.view.View; -import android.widget.ImageButton; -import android.widget.LinearLayout; -import android.widget.TextView; import android.widget.Toast; +import androidx.annotation.Nullable; +import androidx.fragment.app.FragmentManager; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.RenderersFactory; -import com.google.android.exoplayer2.offline.ActionFile; -import com.google.android.exoplayer2.offline.DownloadAction; +import com.google.android.exoplayer2.offline.Download; +import com.google.android.exoplayer2.offline.DownloadCursor; import com.google.android.exoplayer2.offline.DownloadHelper; +import com.google.android.exoplayer2.offline.DownloadIndex; import com.google.android.exoplayer2.offline.DownloadManager; +import com.google.android.exoplayer2.offline.DownloadRequest; import com.google.android.exoplayer2.offline.DownloadService; -import com.google.android.exoplayer2.offline.DownloadState; -import com.google.android.exoplayer2.offline.ProgressiveDownloadHelper; -import com.google.android.exoplayer2.offline.StreamKey; -import com.google.android.exoplayer2.scheduler.Requirements; -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.trackselection.DefaultTrackSelector; import com.google.android.exoplayer2.trackselection.MappingTrackSelector.MappedTrackInfo; -import com.google.android.exoplayer2.trackselection.TrackSelection; -import com.google.android.exoplayer2.ui.DefaultTrackNameProvider; -import com.google.android.exoplayer2.ui.TrackNameProvider; -import com.google.android.exoplayer2.ui.TrackSelectionView; 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.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 { @@ -82,23 +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; - public DownloadTracker(Context context, DataSource.Factory dataSourceFactory, File actionFile) { + @Nullable private StartDownloadDialogHelper startDownloadDialogHelper; + + public DownloadTracker( + 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(); + downloads = new HashMap<>(); + downloadIndex = downloadManager.getDownloadIndex(); + trackSelectorParameters = DownloadHelper.getDefaultTrackSelectorParameters(context); + downloadManager.addListener(new DownloadManagerListener()); + loadDownloads(); } public void addListener(Listener listener) { @@ -110,283 +81,194 @@ 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, + FragmentManager fragmentManager, String name, Uri uri, String extension, RenderersFactory renderersFactory) { - if (isDownloaded(uri)) { - DownloadAction removeAction = - getDownloadHelper(uri, extension, renderersFactory).getRemoveAction(); - startServiceWithAction(removeAction); + Download download = downloads.get(uri); + if (download != null) { + DownloadService.sendRemoveDownload( + context, DemoDownloadService.class, download.request.id, /* foreground= */ false); } else { - new StartDownloadDialogHelper( - activity, getDownloadHelper(uri, extension, renderersFactory), name); - } - } - - // DownloadManager.Listener - - @Override - public void onInitialized(DownloadManager downloadManager) { - // Do nothing. - } - - @Override - public void onDownloadStateChanged(DownloadManager downloadManager, DownloadState downloadState) { - if (downloadState.state == DownloadState.STATE_REMOVED - || downloadState.state == DownloadState.STATE_FAILED) { - // A download has been removed, or has failed. Stop tracking it. - if (trackedDownloadStates.remove(downloadState.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. - } - - @Override - public void onRequirementsStateChanged( - DownloadManager downloadManager, - Requirements requirements, - @Requirements.RequirementFlags int notMetRequirements) { - // Do nothing. - } - - // Internal methods - - private void loadTrackedActions() { - try { - DownloadAction[] allActions = actionFile.load(); - 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( + 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, renderersFactory); + return DownloadHelper.forDash(context, uri, dataSourceFactory, renderersFactory); case C.TYPE_SS: - return new SsDownloadHelper(uri, dataSourceFactory, renderersFactory); + return DownloadHelper.forSmoothStreaming(context, uri, dataSourceFactory, renderersFactory); case C.TYPE_HLS: - return new HlsDownloadHelper(uri, dataSourceFactory, renderersFactory); + 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); } } - @SuppressWarnings("UngroupedOverloads") + private class DownloadManagerListener implements DownloadManager.Listener { + + @Override + public void onDownloadChanged(DownloadManager downloadManager, Download download) { + downloads.put(download.request.uri, download); + for (Listener listener : listeners) { + listener.onDownloadsChanged(); + } + } + + @Override + public void onDownloadRemoved(DownloadManager downloadManager, Download download) { + downloads.remove(download.request.uri); + for (Listener listener : listeners) { + listener.onDownloadsChanged(); + } + } + } + private final class StartDownloadDialogHelper implements DownloadHelper.Callback, DialogInterface.OnClickListener, - View.OnClickListener, - TrackSelectionView.DialogCallback { + DialogInterface.OnDismissListener { - private final DownloadHelper downloadHelper; + private final FragmentManager fragmentManager; + private final DownloadHelper downloadHelper; private final String name; - private final LayoutInflater dialogInflater; - private final AlertDialog dialog; - private final LinearLayout selectionList; + private TrackSelectionDialog trackSelectionDialog; private MappedTrackInfo mappedTrackInfo; - private DefaultTrackSelector.Parameters parameters; - private StartDownloadDialogHelper( - Activity activity, DownloadHelper downloadHelper, String name) { + public StartDownloadDialogHelper( + FragmentManager fragmentManager, DownloadHelper downloadHelper, String name) { + this.fragmentManager = fragmentManager; this.downloadHelper = downloadHelper; this.name = name; - AlertDialog.Builder builder = - new AlertDialog.Builder(activity) - .setTitle(R.string.download_preparing) - .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. - dialogInflater = LayoutInflater.from(builder.getContext()); - selectionList = (LinearLayout) dialogInflater.inflate(R.layout.start_download_dialog, null); - builder.setView(selectionList); - dialog = builder.create(); - dialog.show(); - dialog.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(false); - - parameters = DownloadHelper.DEFAULT_TRACK_SELECTOR_PARAMETERS; downloadHelper.prepare(this); } + public void release() { + downloadHelper.release(); + if (trackSelectionDialog != null) { + trackSelectionDialog.dismiss(); + } + } + // DownloadHelper.Callback implementation. @Override - public void onPrepared(DownloadHelper helper) { - if (helper.getPeriodCount() < 1) { - onPrepareError(downloadHelper, new IOException("Content is empty.")); + public void onPrepared(DownloadHelper helper) { + if (helper.getPeriodCount() == 0) { + Log.d(TAG, "No periods found. Downloading entire stream."); + startDownload(); + downloadHelper.release(); return; } mappedTrackInfo = downloadHelper.getMappedTrackInfo(/* periodIndex= */ 0); - updateSelectionList(); - dialog.setTitle(R.string.exo_download_description); - dialog.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(true); - } - - @Override - public void onPrepareError(DownloadHelper helper, IOException e) { - Toast.makeText( - context.getApplicationContext(), R.string.download_start_error, Toast.LENGTH_LONG) - .show(); - Log.e(TAG, "Failed to start download", e); - dialog.cancel(); - } - - // View.OnClickListener implementation. - - @Override - public void onClick(View v) { - Integer rendererIndex = (Integer) v.getTag(); - String dialogTitle = getTrackTypeString(mappedTrackInfo.getRendererType(rendererIndex)); - Pair dialogPair = - TrackSelectionView.getDialog( - dialog.getContext(), - dialogTitle, - mappedTrackInfo, - rendererIndex, - parameters, - /* callback= */ this); - dialogPair.second.setShowDisableOption(true); - dialogPair.second.setAllowAdaptiveSelections(false); - dialogPair.first.show(); - } - - // TrackSelectionView.DialogCallback implementation. - - @Override - public void onTracksSelected(DefaultTrackSelector.Parameters parameters) { - for (int i = 0; i < downloadHelper.getPeriodCount(); i++) { - downloadHelper.replaceTrackSelections(/* periodIndex= */ i, parameters); + if (!TrackSelectionDialog.willHaveContent(mappedTrackInfo)) { + Log.d(TAG, "No dialog content. Downloading entire stream."); + startDownload(); + downloadHelper.release(); + return; } - this.parameters = parameters; - updateSelectionList(); + trackSelectionDialog = + TrackSelectionDialog.createForMappedTrackInfoAndParameters( + /* titleId= */ R.string.exo_download_description, + mappedTrackInfo, + trackSelectorParameters, + /* allowAdaptiveSelections =*/ false, + /* allowMultipleOverrides= */ true, + /* onClickListener= */ this, + /* onDismissListener= */ this); + trackSelectionDialog.show(fragmentManager, /* tag= */ null); + } + + @Override + public void onPrepareError(DownloadHelper helper, IOException e) { + Toast.makeText(context, R.string.download_start_error, Toast.LENGTH_LONG).show(); + Log.e( + TAG, + e instanceof DownloadHelper.LiveContentUnsupportedException + ? "Downloading live content unsupported" + : "Failed to start download", + e); } // DialogInterface.OnClickListener implementation. @Override public void onClick(DialogInterface dialog, int which) { - DownloadAction downloadAction = downloadHelper.getDownloadAction(Util.getUtf8Bytes(name)); - startDownload(downloadAction); + for (int periodIndex = 0; periodIndex < downloadHelper.getPeriodCount(); periodIndex++) { + downloadHelper.clearTrackSelections(periodIndex); + for (int i = 0; i < mappedTrackInfo.getRendererCount(); i++) { + if (!trackSelectionDialog.getIsDisabled(/* rendererIndex= */ i)) { + downloadHelper.addTrackSelectionForSingleRenderer( + periodIndex, + /* rendererIndex= */ i, + trackSelectorParameters, + trackSelectionDialog.getOverrides(/* rendererIndex= */ i)); + } + } + } + DownloadRequest downloadRequest = buildDownloadRequest(); + if (downloadRequest.streamKeys.isEmpty()) { + // All tracks were deselected in the dialog. Don't start the download. + return; + } + startDownload(downloadRequest); + } + + // DialogInterface.OnDismissListener implementation. + + @Override + public void onDismiss(DialogInterface dialogInterface) { + trackSelectionDialog = null; + downloadHelper.release(); } // Internal methods. - private void updateSelectionList() { - selectionList.removeAllViews(); - for (int i = 0; i < mappedTrackInfo.getRendererCount(); i++) { - TrackGroupArray trackGroupArray = mappedTrackInfo.getTrackGroups(i); - if (trackGroupArray.length == 0) { - continue; - } - String trackTypeString = - getTrackTypeString(mappedTrackInfo.getRendererType(/* rendererIndex= */ i)); - if (trackTypeString == null) { - return; - } - String trackSelectionsString = getTrackSelectionString(/* rendererIndex= */ i); - View view = dialogInflater.inflate(R.layout.download_track_item, selectionList, false); - TextView trackTitleView = view.findViewById(R.id.track_title); - TextView trackDescView = view.findViewById(R.id.track_desc); - ImageButton editButton = view.findViewById(R.id.edit_button); - trackTitleView.setText(trackTypeString); - trackDescView.setText(trackSelectionsString); - editButton.setTag(i); - editButton.setOnClickListener(this); - selectionList.addView(view); - } + private void startDownload() { + startDownload(buildDownloadRequest()); } - private String getTrackSelectionString(int rendererIndex) { - List trackSelections = - downloadHelper.getTrackSelections(/* periodIndex= */ 0, rendererIndex); - String selectedTracks = ""; - Resources resources = selectionList.getResources(); - for (int i = 0; i < trackSelections.size(); i++) { - TrackSelection selection = trackSelections.get(i); - for (int j = 0; j < selection.length(); j++) { - String trackName = trackNameProvider.getTrackName(selection.getFormat(j)); - if (i == 0 && j == 0) { - selectedTracks = trackName; - } else { - selectedTracks = resources.getString(R.string.exo_item_list, selectedTracks, trackName); - } - } - } - return selectedTracks.isEmpty() - ? resources.getString(R.string.exo_track_selection_none) - : selectedTracks; + private void startDownload(DownloadRequest downloadRequest) { + DownloadService.sendAddDownload( + context, DemoDownloadService.class, downloadRequest, /* foreground= */ false); } - @Nullable - private String getTrackTypeString(int trackType) { - Resources resources = selectionList.getResources(); - 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: - return null; - } + 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 582638b460..b759c97da5 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,44 +15,47 @@ */ 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.media.MediaDrm; import android.net.Uri; import android.os.Bundle; -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; 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 androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.app.AppCompatActivity; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C.ContentType; import com.google.android.exoplayer2.ExoPlaybackException; -import com.google.android.exoplayer2.ExoPlayerFactory; +import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.PlaybackPreparer; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.RenderersFactory; import com.google.android.exoplayer2.SimpleExoPlayer; +import com.google.android.exoplayer2.demo.Sample.UriSample; import com.google.android.exoplayer2.drm.DefaultDrmSessionManager; -import com.google.android.exoplayer2.drm.FrameworkMediaCrypto; +import com.google.android.exoplayer2.drm.DrmSessionManager; +import com.google.android.exoplayer2.drm.ExoMediaCrypto; import com.google.android.exoplayer2.drm.FrameworkMediaDrm; import com.google.android.exoplayer2.drm.HttpMediaDrmCallback; -import com.google.android.exoplayer2.drm.UnsupportedDrmException; +import com.google.android.exoplayer2.drm.MediaDrmCallback; import com.google.android.exoplayer2.mediacodec.MediaCodecRenderer.DecoderInitializationException; import com.google.android.exoplayer2.mediacodec.MediaCodecUtil.DecoderQueryException; -import com.google.android.exoplayer2.offline.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.MergingMediaSource; +import com.google.android.exoplayer2.source.ProgressiveMediaSource; +import com.google.android.exoplayer2.source.SingleSampleMediaSource; import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.source.ads.AdsLoader; import com.google.android.exoplayer2.source.ads.AdsMediaSource; @@ -68,8 +71,7 @@ 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.ui.spherical.SphericalGLSurfaceView; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.HttpDataSource; import com.google.android.exoplayer2.util.ErrorMessageProvider; @@ -79,42 +81,54 @@ import java.lang.reflect.Constructor; import java.net.CookieHandler; import java.net.CookieManager; import java.net.CookiePolicy; +import java.util.ArrayList; import java.util.List; -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 IS_LIVE_EXTRA = "is_live"; + + public static final String DRM_SCHEME_EXTRA = "drm_scheme"; + public static final String DRM_LICENSE_URL_EXTRA = "drm_license_url"; + public static final String DRM_KEY_REQUEST_PROPERTIES_EXTRA = "drm_key_request_properties"; + public static final String DRM_SESSION_FOR_CLEAR_TYPES_EXTRA = "drm_session_for_clear_types"; + public static final String DRM_MULTI_SESSION_EXTRA = "drm_multi_session"; + public static final String PREFER_EXTENSION_DECODERS_EXTRA = "prefer_extension_decoders"; + public static final String TUNNELING_EXTRA = "tunneling"; + public static final String AD_TAG_URI_EXTRA = "ad_tag_uri"; + public static final String SUBTITLE_URI_EXTRA = "subtitle_uri"; + public static final String SUBTITLE_MIME_TYPE_EXTRA = "subtitle_mime_type"; + public static final String SUBTITLE_LANGUAGE_EXTRA = "subtitle_language"; // For backwards compatibility only. - 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"; @@ -128,12 +142,13 @@ public class PlayerActivity extends Activity 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 List mediaSources; private DefaultTrackSelector trackSelector; private DefaultTrackSelector.Parameters trackSelectorParameters; private DebugTextViewHelper debugViewHelper; @@ -147,13 +162,13 @@ public class PlayerActivity extends Activity private AdsLoader adsLoader; private Uri loadedAdTagUri; - private ViewGroup adUiViewGroup; // 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); } @@ -164,10 +179,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); @@ -186,7 +201,7 @@ public class PlayerActivity extends Activity finish(); return; } - ((SphericalSurfaceView) playerView.getVideoSurfaceView()).setDefaultStereoMode(stereoMode); + ((SphericalGLSurfaceView) playerView.getVideoSurfaceView()).setDefaultStereoMode(stereoMode); } if (savedInstanceState != null) { @@ -195,13 +210,20 @@ public class PlayerActivity extends Activity startWindow = savedInstanceState.getInt(KEY_WINDOW); startPosition = savedInstanceState.getLong(KEY_POSITION); } else { - trackSelectorParameters = new DefaultTrackSelector.ParametersBuilder().build(); + DefaultTrackSelector.ParametersBuilder builder = + new DefaultTrackSelector.ParametersBuilder(/* context= */ this); + boolean tunneling = intent.getBooleanExtra(TUNNELING_EXTRA, false); + if (Util.SDK_INT >= 21 && tunneling) { + builder.setTunnelingAudioSessionId(C.generateAudioSessionIdV21(/* context= */ this)); + } + trackSelectorParameters = builder.build(); clearStartPosition(); } } @Override public void onNewIntent(Intent intent) { + super.onNewIntent(intent); releasePlayer(); releaseAdsLoader(); clearStartPosition(); @@ -276,6 +298,7 @@ public class PlayerActivity extends Activity @Override public void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); updateTrackSelectorParameters(); updateStartPosition(); outState.putParcelable(KEY_TRACK_SELECTOR_PARAMETERS, trackSelectorParameters); @@ -296,23 +319,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); } } @@ -320,7 +335,7 @@ public class PlayerActivity extends Activity @Override public void preparePlayback() { - initializePlayer(); + player.retry(); } // PlaybackControlView.VisibilityListener implementation @@ -335,69 +350,10 @@ 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(); + mediaSources = createTopLevelMediaSources(intent); + if (mediaSources.isEmpty()) { 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; - } - } - TrackSelection.Factory trackSelectionFactory; String abrAlgorithm = intent.getStringExtra(ABR_ALGORITHM_EXTRA); if (abrAlgorithm == null || ABR_ALGORITHM_DEFAULT.equals(abrAlgorithm)) { @@ -415,13 +371,14 @@ public class PlayerActivity extends Activity 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); + new SimpleExoPlayer.Builder(/* context= */ this, renderersFactory) + .setTrackSelector(trackSelector) + .build(); player.addListener(new PlayerEventListener()); player.setPlayWhenReady(startAutoPlay); player.addAnalyticsListener(new EventLogger(trackSelector)); @@ -429,74 +386,151 @@ 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; if (haveStartPosition) { player.seekTo(startWindow, startPosition); } - player.prepare(mediaSource, !haveStartPosition, false); - updateButtonVisibilities(); + player.setMediaSources(mediaSources, /* resetPosition= */ !haveStartPosition); + player.prepare(); + updateButtonVisibility(); } - private MediaSource buildMediaSource(Uri uri) { - return buildMediaSource(uri, null); + private List createTopLevelMediaSources(Intent intent) { + String action = intent.getAction(); + boolean actionIsListView = ACTION_VIEW_LIST.equals(action); + if (!actionIsListView && !ACTION_VIEW.equals(action)) { + showToast(getString(R.string.unexpected_intent_action, action)); + finish(); + return null; + } + + Sample intentAsSample = Sample.createFromIntent(intent); + UriSample[] samples = + intentAsSample instanceof Sample.PlaylistSample + ? ((Sample.PlaylistSample) intentAsSample).children + : new UriSample[] {(UriSample) intentAsSample}; + + boolean seenAdsTagUri = false; + for (UriSample sample : samples) { + seenAdsTagUri |= sample.adTagUri != null; + if (!Util.checkCleartextTrafficPermitted(sample.uri)) { + showToast(R.string.error_cleartext_not_permitted); + return null; + } + if (Util.maybeRequestReadExternalStoragePermission(/* activity= */ this, sample.uri)) { + // The player will be reinitialized if the permission is granted. + return null; + } + } + + List mediaSources = new ArrayList<>(); + for (UriSample sample : samples) { + MediaSource mediaSource = createLeafMediaSource(sample); + Sample.SubtitleInfo subtitleInfo = sample.subtitleInfo; + if (subtitleInfo != null) { + Format subtitleFormat = + Format.createTextSampleFormat( + /* id= */ null, + subtitleInfo.mimeType, + C.SELECTION_FLAG_DEFAULT, + subtitleInfo.language); + MediaSource subtitleMediaSource = + new SingleSampleMediaSource.Factory(dataSourceFactory) + .createMediaSource(subtitleInfo.uri, subtitleFormat, C.TIME_UNSET); + mediaSource = new MergingMediaSource(mediaSource, subtitleMediaSource); + } + mediaSources.add(mediaSource); + } + if (seenAdsTagUri && mediaSources.size() == 1) { + Uri adTagUri = samples[0].adTagUri; + if (!adTagUri.equals(loadedAdTagUri)) { + releaseAdsLoader(); + loadedAdTagUri = adTagUri; + } + MediaSource adsMediaSource = createAdsMediaSource(mediaSources.get(0), adTagUri); + if (adsMediaSource != null) { + mediaSources.set(0, adsMediaSource); + } else { + showToast(R.string.ima_not_loaded); + } + } else if (seenAdsTagUri && mediaSources.size() > 1) { + showToast(R.string.unsupported_ads_in_concatenation); + releaseAdsLoader(); + } else { + releaseAdsLoader(); + } + + return mediaSources; } - @SuppressWarnings("unchecked") - private MediaSource buildMediaSource(Uri uri, @Nullable String overrideExtension) { - @ContentType int type = Util.inferContentType(uri, overrideExtension); - List offlineStreamKeys = getOfflineStreamKeys(uri); + private MediaSource createLeafMediaSource(UriSample parameters) { + Sample.DrmInfo drmInfo = parameters.drmInfo; + int errorStringId = R.string.error_drm_unknown; + DrmSessionManager drmSessionManager = null; + if (drmInfo == null) { + drmSessionManager = DrmSessionManager.getDummyDrmSessionManager(); + } else if (Util.SDK_INT < 18) { + errorStringId = R.string.error_drm_unsupported_before_api_18; + } else if (!MediaDrm.isCryptoSchemeSupported(drmInfo.drmScheme)) { + errorStringId = R.string.error_drm_unsupported_scheme; + } else { + MediaDrmCallback mediaDrmCallback = + createMediaDrmCallback(drmInfo.drmLicenseUrl, drmInfo.drmKeyRequestProperties); + drmSessionManager = + new DefaultDrmSessionManager.Builder() + .setUuidAndExoMediaDrmProvider(drmInfo.drmScheme, FrameworkMediaDrm.DEFAULT_PROVIDER) + .setMultiSession(drmInfo.drmMultiSession) + .setUseDrmSessionsForClearContent(drmInfo.drmSessionForClearTypes) + .build(mediaDrmCallback); + } + + if (drmSessionManager == null) { + showToast(errorStringId); + finish(); + return null; + } + + DownloadRequest downloadRequest = + ((DemoApplication) getApplication()) + .getDownloadTracker() + .getDownloadRequest(parameters.uri); + if (downloadRequest != null) { + return DownloadHelper.createMediaSource(downloadRequest, dataSourceFactory); + } + return createLeafMediaSource(parameters.uri, parameters.extension, drmSessionManager); + } + + private MediaSource createLeafMediaSource( + Uri uri, String extension, DrmSessionManager drmSessionManager) { + @ContentType int type = Util.inferContentType(uri, extension); switch (type) { case C.TYPE_DASH: return new DashMediaSource.Factory(dataSourceFactory) - .setStreamKeys(offlineStreamKeys) + .setDrmSessionManager(drmSessionManager) .createMediaSource(uri); case C.TYPE_SS: return new SsMediaSource.Factory(dataSourceFactory) - .setStreamKeys(offlineStreamKeys) + .setDrmSessionManager(drmSessionManager) .createMediaSource(uri); case C.TYPE_HLS: return new HlsMediaSource.Factory(dataSourceFactory) - .setStreamKeys(offlineStreamKeys) + .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 { + private HttpMediaDrmCallback createMediaDrmCallback( + String licenseUrl, String[] keyRequestPropertiesArray) { HttpDataSource.Factory licenseDataSourceFactory = ((DemoApplication) getApplication()).buildHttpDataSourceFactory(); HttpMediaDrmCallback drmCallback = @@ -507,9 +541,7 @@ public class PlayerActivity extends Activity keyRequestPropertiesArray[i + 1]); } } - releaseMediaDrm(); - mediaDrm = FrameworkMediaDrm.newInstance(uuid); - return new DefaultDrmSessionManager<>(uuid, mediaDrm, drmCallback, null, multiSession); + return drmCallback; } private void releasePlayer() { @@ -520,20 +552,12 @@ public class PlayerActivity extends Activity debugViewHelper = null; player.release(); player = null; - mediaSource = null; + mediaSources = null; trackSelector = null; } if (adsLoader != null) { adsLoader.setPlayer(null); } - releaseMediaDrm(); - } - - private void releaseMediaDrm() { - if (mediaDrm != null) { - mediaDrm.release(); - mediaDrm = null; - } } private void releaseAdsLoader() { @@ -571,7 +595,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 { @@ -585,16 +610,23 @@ 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); } - adsLoader.setPlayer(player); - AdsMediaSource.MediaSourceFactory adMediaSourceFactory = - new AdsMediaSource.MediaSourceFactory() { + MediaSourceFactory adMediaSourceFactory = + new MediaSourceFactory() { + + private DrmSessionManager drmSessionManager = + DrmSessionManager.getDummyDrmSessionManager(); + + @Override + public MediaSourceFactory setDrmSessionManager(DrmSessionManager drmSessionManager) { + this.drmSessionManager = drmSessionManager; + return this; + } + @Override public MediaSource createMediaSource(Uri uri) { - return PlayerActivity.this.buildMediaSource(uri); + return PlayerActivity.this.createLeafMediaSource( + uri, /* extension=*/ null, drmSessionManager); } @Override @@ -602,7 +634,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; @@ -613,41 +645,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() { @@ -679,20 +679,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 @@ -701,8 +692,7 @@ public class PlayerActivity extends Activity clearStartPosition(); initializePlayer(); } else { - updateStartPosition(); - updateButtonVisibilities(); + updateButtonVisibility(); showControls(); } } @@ -710,7 +700,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) { @@ -739,7 +729,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) { @@ -754,12 +744,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..0bf0d2a80c --- /dev/null +++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/Sample.java @@ -0,0 +1,277 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2.demo; + +import static com.google.android.exoplayer2.demo.PlayerActivity.ACTION_VIEW_LIST; +import static com.google.android.exoplayer2.demo.PlayerActivity.AD_TAG_URI_EXTRA; +import static com.google.android.exoplayer2.demo.PlayerActivity.DRM_KEY_REQUEST_PROPERTIES_EXTRA; +import static com.google.android.exoplayer2.demo.PlayerActivity.DRM_LICENSE_URL_EXTRA; +import static com.google.android.exoplayer2.demo.PlayerActivity.DRM_MULTI_SESSION_EXTRA; +import static com.google.android.exoplayer2.demo.PlayerActivity.DRM_SCHEME_EXTRA; +import static com.google.android.exoplayer2.demo.PlayerActivity.DRM_SCHEME_UUID_EXTRA; +import static com.google.android.exoplayer2.demo.PlayerActivity.DRM_SESSION_FOR_CLEAR_TYPES_EXTRA; +import static com.google.android.exoplayer2.demo.PlayerActivity.EXTENSION_EXTRA; +import static com.google.android.exoplayer2.demo.PlayerActivity.IS_LIVE_EXTRA; +import static com.google.android.exoplayer2.demo.PlayerActivity.SUBTITLE_LANGUAGE_EXTRA; +import static com.google.android.exoplayer2.demo.PlayerActivity.SUBTITLE_MIME_TYPE_EXTRA; +import static com.google.android.exoplayer2.demo.PlayerActivity.SUBTITLE_URI_EXTRA; +import static com.google.android.exoplayer2.demo.PlayerActivity.URI_EXTRA; + +import android.content.Intent; +import android.net.Uri; +import androidx.annotation.Nullable; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.util.Assertions; +import com.google.android.exoplayer2.util.Util; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.UUID; + +/* package */ abstract class Sample { + + public static final class UriSample extends Sample { + + public static UriSample createFromIntent(Uri uri, Intent intent, String extrasKeySuffix) { + String extension = intent.getStringExtra(EXTENSION_EXTRA + extrasKeySuffix); + String adsTagUriString = intent.getStringExtra(AD_TAG_URI_EXTRA + extrasKeySuffix); + boolean isLive = + intent.getBooleanExtra(IS_LIVE_EXTRA + extrasKeySuffix, /* defaultValue= */ false); + Uri adTagUri = adsTagUriString != null ? Uri.parse(adsTagUriString) : null; + return new UriSample( + /* name= */ null, + uri, + extension, + isLive, + DrmInfo.createFromIntent(intent, extrasKeySuffix), + adTagUri, + /* sphericalStereoMode= */ null, + SubtitleInfo.createFromIntent(intent, extrasKeySuffix)); + } + + public final Uri uri; + public final String extension; + public final boolean isLive; + public final DrmInfo drmInfo; + public final Uri adTagUri; + @Nullable public final String sphericalStereoMode; + @Nullable SubtitleInfo subtitleInfo; + + public UriSample( + String name, + Uri uri, + String extension, + boolean isLive, + DrmInfo drmInfo, + Uri adTagUri, + @Nullable String sphericalStereoMode, + @Nullable SubtitleInfo subtitleInfo) { + super(name); + this.uri = uri; + this.extension = extension; + this.isLive = isLive; + this.drmInfo = drmInfo; + this.adTagUri = adTagUri; + this.sphericalStereoMode = sphericalStereoMode; + this.subtitleInfo = subtitleInfo; + } + + @Override + public void addToIntent(Intent intent) { + intent.setAction(PlayerActivity.ACTION_VIEW).setData(uri); + intent.putExtra(PlayerActivity.IS_LIVE_EXTRA, isLive); + intent.putExtra(PlayerActivity.SPHERICAL_STEREO_MODE_EXTRA, sphericalStereoMode); + addPlayerConfigToIntent(intent, /* extrasKeySuffix= */ ""); + } + + public void addToPlaylistIntent(Intent intent, String extrasKeySuffix) { + intent.putExtra(PlayerActivity.URI_EXTRA + extrasKeySuffix, uri.toString()); + intent.putExtra(PlayerActivity.IS_LIVE_EXTRA + extrasKeySuffix, isLive); + addPlayerConfigToIntent(intent, extrasKeySuffix); + } + + private void addPlayerConfigToIntent(Intent intent, String extrasKeySuffix) { + intent + .putExtra(EXTENSION_EXTRA + extrasKeySuffix, extension) + .putExtra( + AD_TAG_URI_EXTRA + extrasKeySuffix, adTagUri != null ? adTagUri.toString() : null); + if (drmInfo != null) { + drmInfo.addToIntent(intent, extrasKeySuffix); + } + if (subtitleInfo != null) { + subtitleInfo.addToIntent(intent, extrasKeySuffix); + } + } + } + + public static final class PlaylistSample extends Sample { + + public final UriSample[] children; + + public PlaylistSample(String name, UriSample... children) { + super(name); + this.children = children; + } + + @Override + public void addToIntent(Intent intent) { + intent.setAction(PlayerActivity.ACTION_VIEW_LIST); + for (int i = 0; i < children.length; i++) { + children[i].addToPlaylistIntent(intent, /* extrasKeySuffix= */ "_" + i); + } + } + } + + public static final class DrmInfo { + + public static DrmInfo createFromIntent(Intent intent, String extrasKeySuffix) { + String schemeKey = DRM_SCHEME_EXTRA + extrasKeySuffix; + String schemeUuidKey = DRM_SCHEME_UUID_EXTRA + extrasKeySuffix; + if (!intent.hasExtra(schemeKey) && !intent.hasExtra(schemeUuidKey)) { + return null; + } + String drmSchemeExtra = + intent.hasExtra(schemeKey) + ? intent.getStringExtra(schemeKey) + : intent.getStringExtra(schemeUuidKey); + UUID drmScheme = Util.getDrmUuid(drmSchemeExtra); + String drmLicenseUrl = intent.getStringExtra(DRM_LICENSE_URL_EXTRA + extrasKeySuffix); + String[] keyRequestPropertiesArray = + intent.getStringArrayExtra(DRM_KEY_REQUEST_PROPERTIES_EXTRA + extrasKeySuffix); + String[] drmSessionForClearTypesExtra = + intent.getStringArrayExtra(DRM_SESSION_FOR_CLEAR_TYPES_EXTRA + extrasKeySuffix); + int[] drmSessionForClearTypes = toTrackTypeArray(drmSessionForClearTypesExtra); + boolean drmMultiSession = + intent.getBooleanExtra(DRM_MULTI_SESSION_EXTRA + extrasKeySuffix, false); + return new DrmInfo( + drmScheme, + drmLicenseUrl, + keyRequestPropertiesArray, + drmSessionForClearTypes, + drmMultiSession); + } + + public final UUID drmScheme; + public final String drmLicenseUrl; + public final String[] drmKeyRequestProperties; + public final int[] drmSessionForClearTypes; + public final boolean drmMultiSession; + + public DrmInfo( + UUID drmScheme, + String drmLicenseUrl, + String[] drmKeyRequestProperties, + int[] drmSessionForClearTypes, + boolean drmMultiSession) { + this.drmScheme = drmScheme; + this.drmLicenseUrl = drmLicenseUrl; + this.drmKeyRequestProperties = drmKeyRequestProperties; + this.drmSessionForClearTypes = drmSessionForClearTypes; + this.drmMultiSession = drmMultiSession; + } + + public void addToIntent(Intent intent, String extrasKeySuffix) { + Assertions.checkNotNull(intent); + intent.putExtra(DRM_SCHEME_EXTRA + extrasKeySuffix, drmScheme.toString()); + intent.putExtra(DRM_LICENSE_URL_EXTRA + extrasKeySuffix, drmLicenseUrl); + intent.putExtra(DRM_KEY_REQUEST_PROPERTIES_EXTRA + extrasKeySuffix, drmKeyRequestProperties); + ArrayList typeStrings = new ArrayList<>(); + for (int type : drmSessionForClearTypes) { + // Only audio and video are supported. + typeStrings.add(type == C.TRACK_TYPE_AUDIO ? "audio" : "video"); + } + intent.putExtra( + DRM_SESSION_FOR_CLEAR_TYPES_EXTRA + extrasKeySuffix, typeStrings.toArray(new String[0])); + intent.putExtra(DRM_MULTI_SESSION_EXTRA + extrasKeySuffix, drmMultiSession); + } + } + + public static final class SubtitleInfo { + + @Nullable + public static SubtitleInfo createFromIntent(Intent intent, String extrasKeySuffix) { + if (!intent.hasExtra(SUBTITLE_URI_EXTRA + extrasKeySuffix)) { + return null; + } + return new SubtitleInfo( + Uri.parse(intent.getStringExtra(SUBTITLE_URI_EXTRA + extrasKeySuffix)), + intent.getStringExtra(SUBTITLE_MIME_TYPE_EXTRA + extrasKeySuffix), + intent.getStringExtra(SUBTITLE_LANGUAGE_EXTRA + extrasKeySuffix)); + } + + public final Uri uri; + public final String mimeType; + @Nullable public final String language; + + public SubtitleInfo(Uri uri, String mimeType, @Nullable String language) { + this.uri = Assertions.checkNotNull(uri); + this.mimeType = Assertions.checkNotNull(mimeType); + this.language = language; + } + + public void addToIntent(Intent intent, String extrasKeySuffix) { + intent.putExtra(SUBTITLE_URI_EXTRA + extrasKeySuffix, uri.toString()); + intent.putExtra(SUBTITLE_MIME_TYPE_EXTRA + extrasKeySuffix, mimeType); + intent.putExtra(SUBTITLE_LANGUAGE_EXTRA + extrasKeySuffix, language); + } + } + + public static int[] toTrackTypeArray(@Nullable String[] trackTypeStringsArray) { + if (trackTypeStringsArray == null) { + return new int[0]; + } + HashSet trackTypes = new HashSet<>(); + for (String trackTypeString : trackTypeStringsArray) { + switch (Util.toLowerInvariant(trackTypeString)) { + case "audio": + trackTypes.add(C.TRACK_TYPE_AUDIO); + break; + case "video": + trackTypes.add(C.TRACK_TYPE_VIDEO); + break; + default: + throw new IllegalArgumentException("Invalid track type: " + trackTypeString); + } + } + return Util.toArray(new ArrayList<>(trackTypes)); + } + + public static Sample createFromIntent(Intent intent) { + if (ACTION_VIEW_LIST.equals(intent.getAction())) { + ArrayList intentUris = new ArrayList<>(); + int index = 0; + while (intent.hasExtra(URI_EXTRA + "_" + index)) { + intentUris.add(intent.getStringExtra(URI_EXTRA + "_" + index)); + index++; + } + UriSample[] children = new UriSample[intentUris.size()]; + for (int i = 0; i < children.length; i++) { + Uri uri = Uri.parse(intentUris.get(i)); + children[i] = UriSample.createFromIntent(uri, intent, /* extrasKeySuffix= */ "_" + i); + } + return new PlaylistSample(/* name= */ null, children); + } else { + return UriSample.createFromIntent(intent.getData(), intent, /* extrasKeySuffix= */ ""); + } + } + + @Nullable public final String name; + + public Sample(String name) { + this.name = name; + } + + public abstract void addToIntent(Intent intent); +} diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/SampleChooserActivity.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/SampleChooserActivity.java index 5db52fd575..66bf4bad5a 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,14 +15,12 @@ */ 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 android.util.JsonReader; import android.view.Menu; import android.view.MenuInflater; @@ -36,8 +34,13 @@ import android.widget.ExpandableListView.OnChildClickListener; import android.widget.ImageButton; import android.widget.TextView; import android.widget.Toast; +import androidx.annotation.Nullable; +import androidx.appcompat.app.AppCompatActivity; import com.google.android.exoplayer2.ParserException; import com.google.android.exoplayer2.RenderersFactory; +import com.google.android.exoplayer2.demo.Sample.DrmInfo; +import com.google.android.exoplayer2.demo.Sample.PlaylistSample; +import com.google.android.exoplayer2.demo.Sample.UriSample; import com.google.android.exoplayer2.offline.DownloadService; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DataSourceInputStream; @@ -55,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"; @@ -65,6 +68,7 @@ public class SampleChooserActivity extends Activity private SampleAdapter sampleAdapter; private MenuItem preferExtensionDecodersMenuItem; private MenuItem randomAbrMenuItem; + private MenuItem tunnelingMenuItem; @Override public void onCreate(Bundle savedInstanceState) { @@ -122,6 +126,10 @@ public class SampleChooserActivity extends Activity preferExtensionDecodersMenuItem = menu.findItem(R.id.prefer_extension_decoders); preferExtensionDecodersMenuItem.setVisible(useExtensionRenderers); randomAbrMenuItem = menu.findItem(R.id.random_abr); + tunnelingMenuItem = menu.findItem(R.id.tunneling); + if (Util.SDK_INT < 21) { + tunnelingMenuItem.setEnabled(false); + } return true; } @@ -161,13 +169,18 @@ 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); + intent.putExtra(PlayerActivity.TUNNELING_EXTRA, isNonNullAndChecked(tunnelingMenuItem)); + sample.addToIntent(intent); + startActivity(intent); return true; } @@ -182,7 +195,11 @@ public class SampleChooserActivity extends Activity ((DemoApplication) getApplication()) .buildRenderersFactory(isNonNullAndChecked(preferExtensionDecodersMenuItem)); downloadTracker.toggleDownload( - this, sample.name, uriSample.uri, uriSample.extension, renderersFactory); + getSupportFragmentManager(), + sample.name, + uriSample.uri, + uriSample.extension, + renderersFactory); } } @@ -194,6 +211,9 @@ public class SampleChooserActivity extends Activity if (uriSample.drmInfo != null) { return R.string.download_drm_unsupported; } + if (uriSample.isLive) { + return R.string.download_live_unsupported; + } if (uriSample.adTagUri != null) { return R.string.download_ads_unsupported; } @@ -283,13 +303,19 @@ public class SampleChooserActivity extends Activity String sampleName = null; Uri uri = null; String extension = null; + boolean isLive = false; String drmScheme = null; String drmLicenseUrl = null; String[] drmKeyRequestProperties = null; + String[] drmSessionForClearTypes = null; boolean drmMultiSession = false; ArrayList playlistSamples = null; String adTagUri = null; String sphericalStereoMode = null; + List subtitleInfos = new ArrayList<>(); + Uri subtitleUri = null; + String subtitleMimeType = null; + String subtitleLanguage = null; reader.beginObject(); while (reader.hasNext()) { @@ -305,17 +331,15 @@ 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 "is_live": + isLive = reader.nextBoolean(); + 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()) { @@ -325,6 +349,15 @@ public class SampleChooserActivity extends Activity reader.endObject(); drmKeyRequestProperties = drmKeyRequestPropertiesList.toArray(new String[0]); break; + case "drm_session_for_clear_types": + ArrayList drmSessionForClearTypesList = new ArrayList<>(); + reader.beginArray(); + while (reader.hasNext()) { + drmSessionForClearTypesList.add(reader.nextString()); + } + reader.endArray(); + drmSessionForClearTypes = drmSessionForClearTypesList.toArray(new String[0]); + break; case "drm_multi_session": drmMultiSession = reader.nextBoolean(); break; @@ -333,7 +366,7 @@ public class SampleChooserActivity extends Activity playlistSamples = new ArrayList<>(); reader.beginArray(); while (reader.hasNext()) { - playlistSamples.add((UriSample) readEntry(reader, true)); + playlistSamples.add((UriSample) readEntry(reader, /* insidePlaylist= */ true)); } reader.endArray(); break; @@ -345,6 +378,15 @@ public class SampleChooserActivity extends Activity !insidePlaylist, "Invalid attribute on nested item: spherical_stereo_mode"); sphericalStereoMode = reader.nextString(); break; + case "subtitle_uri": + subtitleUri = Uri.parse(reader.nextString()); + break; + case "subtitle_mime_type": + subtitleMimeType = reader.nextString(); + break; + case "subtitle_language": + subtitleLanguage = reader.nextString(); + break; default: throw new ParserException("Unsupported attribute name: " + name); } @@ -353,18 +395,33 @@ public class SampleChooserActivity extends Activity DrmInfo drmInfo = drmScheme == null ? null - : new DrmInfo(drmScheme, drmLicenseUrl, drmKeyRequestProperties, drmMultiSession); + : new DrmInfo( + Util.getDrmUuid(drmScheme), + drmLicenseUrl, + drmKeyRequestProperties, + Sample.toTrackTypeArray(drmSessionForClearTypes), + drmMultiSession); + Sample.SubtitleInfo subtitleInfo = + subtitleUri == null + ? null + : new Sample.SubtitleInfo( + subtitleUri, + Assertions.checkNotNull( + subtitleMimeType, "subtitle_mime_type is required if subtitle_uri is set."), + subtitleLanguage); if (playlistSamples != null) { UriSample[] playlistSamplesArray = playlistSamples.toArray(new UriSample[0]); - return new PlaylistSample(sampleName, drmInfo, playlistSamplesArray); + return new PlaylistSample(sampleName, playlistSamplesArray); } else { return new UriSample( sampleName, - drmInfo, uri, extension, - adTagUri, - sphericalStereoMode); + isLive, + drmInfo, + adTagUri != null ? Uri.parse(adTagUri) : null, + sphericalStereoMode, + subtitleInfo); } } @@ -476,7 +533,7 @@ public class SampleChooserActivity extends Activity ImageButton downloadButton = view.findViewById(R.id.download_button); downloadButton.setTag(sample); downloadButton.setColorFilter( - canDownload ? (isDownloaded ? 0xFF42A5F5 : 0xFFBDBDBD) : 0xFFEEEEEE); + canDownload ? (isDownloaded ? 0xFF42A5F5 : 0xFFBDBDBD) : 0xFF666666); downloadButton.setImageResource( isDownloaded ? R.drawable.ic_download_done : R.drawable.ic_download); } @@ -493,116 +550,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..9e8009388e --- /dev/null +++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/TrackSelectionDialog.java @@ -0,0 +1,368 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2.demo; + +import android.app.Dialog; +import android.content.DialogInterface; +import android.content.res.Resources; +import android.os.Bundle; +import android.util.SparseArray; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Button; +import androidx.annotation.Nullable; +import androidx.appcompat.app.AppCompatDialog; +import androidx.fragment.app.DialogFragment; +import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentManager; +import androidx.fragment.app.FragmentPagerAdapter; +import androidx.viewpager.widget.ViewPager; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.source.TrackGroupArray; +import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; +import com.google.android.exoplayer2.trackselection.DefaultTrackSelector.SelectionOverride; +import com.google.android.exoplayer2.trackselection.MappingTrackSelector.MappedTrackInfo; +import com.google.android.exoplayer2.ui.TrackSelectionView; +import com.google.android.exoplayer2.util.Assertions; +import com.google.android.material.tabs.TabLayout; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** Dialog to select tracks. */ +public final class TrackSelectionDialog extends DialogFragment { + + private final SparseArray tabFragments; + private final ArrayList tabTrackTypes; + + private int titleId; + private DialogInterface.OnClickListener onClickListener; + private DialogInterface.OnDismissListener onDismissListener; + + /** + * Returns whether a track selection dialog will have content to display if initialized with the + * specified {@link DefaultTrackSelector} in its current state. + */ + public static boolean willHaveContent(DefaultTrackSelector trackSelector) { + MappedTrackInfo mappedTrackInfo = trackSelector.getCurrentMappedTrackInfo(); + return mappedTrackInfo != null && willHaveContent(mappedTrackInfo); + } + + /** + * Returns whether a track selection dialog will have content to display if initialized with the + * specified {@link MappedTrackInfo}. + */ + public static boolean willHaveContent(MappedTrackInfo mappedTrackInfo) { + for (int i = 0; i < mappedTrackInfo.getRendererCount(); i++) { + if (showTabForRenderer(mappedTrackInfo, i)) { + return true; + } + } + return false; + } + + /** + * Creates a dialog for a given {@link DefaultTrackSelector}, whose parameters will be + * automatically updated when tracks are selected. + * + * @param trackSelector The {@link DefaultTrackSelector}. + * @param onDismissListener A {@link DialogInterface.OnDismissListener} to call when the dialog is + * dismissed. + */ + public static TrackSelectionDialog createForTrackSelector( + DefaultTrackSelector trackSelector, DialogInterface.OnDismissListener onDismissListener) { + MappedTrackInfo mappedTrackInfo = + Assertions.checkNotNull(trackSelector.getCurrentMappedTrackInfo()); + TrackSelectionDialog trackSelectionDialog = new TrackSelectionDialog(); + DefaultTrackSelector.Parameters parameters = trackSelector.getParameters(); + trackSelectionDialog.init( + /* titleId= */ R.string.track_selection_title, + mappedTrackInfo, + /* initialParameters = */ parameters, + /* allowAdaptiveSelections =*/ true, + /* allowMultipleOverrides= */ false, + /* onClickListener= */ (dialog, which) -> { + DefaultTrackSelector.ParametersBuilder builder = parameters.buildUpon(); + for (int i = 0; i < mappedTrackInfo.getRendererCount(); i++) { + builder + .clearSelectionOverrides(/* rendererIndex= */ i) + .setRendererDisabled( + /* rendererIndex= */ i, + trackSelectionDialog.getIsDisabled(/* rendererIndex= */ i)); + List overrides = + trackSelectionDialog.getOverrides(/* rendererIndex= */ i); + if (!overrides.isEmpty()) { + builder.setSelectionOverride( + /* rendererIndex= */ i, + mappedTrackInfo.getTrackGroups(/* rendererIndex= */ i), + overrides.get(0)); + } + } + trackSelector.setParameters(builder); + }, + onDismissListener); + return trackSelectionDialog; + } + + /** + * Creates a dialog for given {@link MappedTrackInfo} and {@link DefaultTrackSelector.Parameters}. + * + * @param titleId The resource id of the dialog title. + * @param mappedTrackInfo The {@link MappedTrackInfo} to display. + * @param initialParameters The {@link DefaultTrackSelector.Parameters} describing the initial + * track selection. + * @param allowAdaptiveSelections Whether adaptive selections (consisting of more than one track) + * can be made. + * @param allowMultipleOverrides Whether tracks from multiple track groups can be selected. + * @param onClickListener {@link DialogInterface.OnClickListener} called when tracks are selected. + * @param onDismissListener {@link DialogInterface.OnDismissListener} called when the dialog is + * dismissed. + */ + public static TrackSelectionDialog createForMappedTrackInfoAndParameters( + int titleId, + MappedTrackInfo mappedTrackInfo, + DefaultTrackSelector.Parameters initialParameters, + boolean allowAdaptiveSelections, + boolean allowMultipleOverrides, + DialogInterface.OnClickListener onClickListener, + DialogInterface.OnDismissListener onDismissListener) { + TrackSelectionDialog trackSelectionDialog = new TrackSelectionDialog(); + trackSelectionDialog.init( + titleId, + mappedTrackInfo, + initialParameters, + allowAdaptiveSelections, + allowMultipleOverrides, + onClickListener, + onDismissListener); + return trackSelectionDialog; + } + + public TrackSelectionDialog() { + tabFragments = new SparseArray<>(); + tabTrackTypes = new ArrayList<>(); + // Retain instance across activity re-creation to prevent losing access to init data. + setRetainInstance(true); + } + + private void init( + int titleId, + MappedTrackInfo mappedTrackInfo, + DefaultTrackSelector.Parameters initialParameters, + boolean allowAdaptiveSelections, + boolean allowMultipleOverrides, + DialogInterface.OnClickListener onClickListener, + DialogInterface.OnDismissListener onDismissListener) { + this.titleId = titleId; + this.onClickListener = onClickListener; + this.onDismissListener = onDismissListener; + for (int i = 0; i < mappedTrackInfo.getRendererCount(); i++) { + if (showTabForRenderer(mappedTrackInfo, i)) { + int trackType = mappedTrackInfo.getRendererType(/* rendererIndex= */ i); + TrackGroupArray trackGroupArray = mappedTrackInfo.getTrackGroups(i); + TrackSelectionViewFragment tabFragment = new TrackSelectionViewFragment(); + tabFragment.init( + mappedTrackInfo, + /* rendererIndex= */ i, + initialParameters.getRendererDisabled(/* rendererIndex= */ i), + initialParameters.getSelectionOverride(/* rendererIndex= */ i, trackGroupArray), + allowAdaptiveSelections, + allowMultipleOverrides); + tabFragments.put(i, tabFragment); + tabTrackTypes.add(trackType); + } + } + } + + /** + * Returns whether a renderer is disabled. + * + * @param rendererIndex Renderer index. + * @return Whether the renderer is disabled. + */ + public boolean getIsDisabled(int rendererIndex) { + TrackSelectionViewFragment rendererView = tabFragments.get(rendererIndex); + return rendererView != null && rendererView.isDisabled; + } + + /** + * Returns the list of selected track selection overrides for the specified renderer. There will + * be at most one override for each track group. + * + * @param rendererIndex Renderer index. + * @return The list of track selection overrides for this renderer. + */ + public List getOverrides(int rendererIndex) { + TrackSelectionViewFragment rendererView = tabFragments.get(rendererIndex); + return rendererView == null ? Collections.emptyList() : rendererView.overrides; + } + + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + // We need to own the view to let tab layout work correctly on all API levels. We can't use + // AlertDialog because it owns the view itself, so we use AppCompatDialog instead, themed using + // the AlertDialog theme overlay with force-enabled title. + AppCompatDialog dialog = + new AppCompatDialog(getActivity(), R.style.TrackSelectionDialogThemeOverlay); + dialog.setTitle(titleId); + return dialog; + } + + @Override + public void onDismiss(DialogInterface dialog) { + super.onDismiss(dialog); + onDismissListener.onDismiss(dialog); + } + + @Nullable + @Override + public View onCreateView( + LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + + View dialogView = inflater.inflate(R.layout.track_selection_dialog, container, false); + TabLayout tabLayout = dialogView.findViewById(R.id.track_selection_dialog_tab_layout); + ViewPager viewPager = dialogView.findViewById(R.id.track_selection_dialog_view_pager); + Button cancelButton = dialogView.findViewById(R.id.track_selection_dialog_cancel_button); + Button okButton = dialogView.findViewById(R.id.track_selection_dialog_ok_button); + viewPager.setAdapter(new FragmentAdapter(getChildFragmentManager())); + tabLayout.setupWithViewPager(viewPager); + tabLayout.setVisibility(tabFragments.size() > 1 ? View.VISIBLE : View.GONE); + cancelButton.setOnClickListener(view -> dismiss()); + okButton.setOnClickListener( + view -> { + onClickListener.onClick(getDialog(), DialogInterface.BUTTON_POSITIVE); + dismiss(); + }); + return dialogView; + } + + private static boolean showTabForRenderer(MappedTrackInfo mappedTrackInfo, int rendererIndex) { + TrackGroupArray trackGroupArray = mappedTrackInfo.getTrackGroups(rendererIndex); + if (trackGroupArray.length == 0) { + return false; + } + int trackType = mappedTrackInfo.getRendererType(rendererIndex); + return isSupportedTrackType(trackType); + } + + private static boolean isSupportedTrackType(int trackType) { + switch (trackType) { + case C.TRACK_TYPE_VIDEO: + case C.TRACK_TYPE_AUDIO: + case C.TRACK_TYPE_TEXT: + return true; + default: + return false; + } + } + + private static String getTrackTypeString(Resources resources, int trackType) { + switch (trackType) { + case C.TRACK_TYPE_VIDEO: + return resources.getString(R.string.exo_track_selection_title_video); + case C.TRACK_TYPE_AUDIO: + return resources.getString(R.string.exo_track_selection_title_audio); + case C.TRACK_TYPE_TEXT: + return resources.getString(R.string.exo_track_selection_title_text); + default: + throw new IllegalArgumentException(); + } + } + + private final class FragmentAdapter extends FragmentPagerAdapter { + + public FragmentAdapter(FragmentManager fragmentManager) { + super(fragmentManager); + } + + @Override + public Fragment getItem(int position) { + return tabFragments.valueAt(position); + } + + @Override + public int getCount() { + return tabFragments.size(); + } + + @Nullable + @Override + public CharSequence getPageTitle(int position) { + return getTrackTypeString(getResources(), tabTrackTypes.get(position)); + } + } + + /** Fragment to show a track selection in tab of the track selection dialog. */ + public static final class TrackSelectionViewFragment extends Fragment + implements TrackSelectionView.TrackSelectionListener { + + private MappedTrackInfo mappedTrackInfo; + private int rendererIndex; + private boolean allowAdaptiveSelections; + private boolean allowMultipleOverrides; + + /* package */ boolean isDisabled; + /* package */ List overrides; + + public TrackSelectionViewFragment() { + // Retain instance across activity re-creation to prevent losing access to init data. + setRetainInstance(true); + } + + public void init( + MappedTrackInfo mappedTrackInfo, + int rendererIndex, + boolean initialIsDisabled, + @Nullable SelectionOverride initialOverride, + boolean allowAdaptiveSelections, + boolean allowMultipleOverrides) { + this.mappedTrackInfo = mappedTrackInfo; + this.rendererIndex = rendererIndex; + this.isDisabled = initialIsDisabled; + this.overrides = + initialOverride == null + ? Collections.emptyList() + : Collections.singletonList(initialOverride); + this.allowAdaptiveSelections = allowAdaptiveSelections; + this.allowMultipleOverrides = allowMultipleOverrides; + } + + @Nullable + @Override + public View onCreateView( + LayoutInflater inflater, + @Nullable ViewGroup container, + @Nullable Bundle savedInstanceState) { + View rootView = + inflater.inflate( + R.layout.exo_track_selection_dialog, container, /* attachToRoot= */ false); + TrackSelectionView trackSelectionView = rootView.findViewById(R.id.exo_track_selection_view); + trackSelectionView.setShowDisableOption(true); + trackSelectionView.setAllowMultipleOverrides(allowMultipleOverrides); + trackSelectionView.setAllowAdaptiveSelections(allowAdaptiveSelections); + trackSelectionView.init( + mappedTrackInfo, rendererIndex, isDisabled, overrides, /* listener= */ this); + return rootView; + } + + @Override + public void onTrackSelectionChanged(boolean isDisabled, List overrides) { + this.isDisabled = isDisabled; + this.overrides = overrides; + } + } +} diff --git a/demos/main/src/main/res/drawable-hdpi/ic_edit.png b/demos/main/src/main/res/drawable-hdpi/ic_edit.png deleted file mode 100755 index 25678d6de9..0000000000 Binary files a/demos/main/src/main/res/drawable-hdpi/ic_edit.png and /dev/null differ diff --git a/demos/main/src/main/res/drawable-mdpi/ic_edit.png b/demos/main/src/main/res/drawable-mdpi/ic_edit.png deleted file mode 100755 index dffcd9f61a..0000000000 Binary files a/demos/main/src/main/res/drawable-mdpi/ic_edit.png and /dev/null differ diff --git a/demos/main/src/main/res/drawable-xhdpi/ic_edit.png b/demos/main/src/main/res/drawable-xhdpi/ic_edit.png deleted file mode 100755 index 82f8563d1e..0000000000 Binary files a/demos/main/src/main/res/drawable-xhdpi/ic_edit.png and /dev/null differ diff --git a/demos/main/src/main/res/drawable-xxhdpi/ic_edit.png b/demos/main/src/main/res/drawable-xxhdpi/ic_edit.png deleted file mode 100755 index f00b4b68c5..0000000000 Binary files a/demos/main/src/main/res/drawable-xxhdpi/ic_edit.png and /dev/null differ diff --git a/demos/main/src/main/res/drawable-xxxhdpi/ic_edit.png b/demos/main/src/main/res/drawable-xxxhdpi/ic_edit.png deleted file mode 100755 index a9f99417fb..0000000000 Binary files a/demos/main/src/main/res/drawable-xxxhdpi/ic_edit.png and /dev/null differ diff --git a/demos/main/src/main/res/layout/download_track_item.xml b/demos/main/src/main/res/layout/download_track_item.xml deleted file mode 100644 index fe1c62b391..0000000000 --- a/demos/main/src/main/res/layout/download_track_item.xml +++ /dev/null @@ -1,53 +0,0 @@ - - - - - - - - - - - - - - - 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"> + +