diff --git a/.gitignore b/.gitignore index 1146c06456..db5a8c4305 100644 --- a/.gitignore +++ b/.gitignore @@ -39,9 +39,13 @@ proguard-project.txt # Other .DS_Store +cmake-build-debug dist tmp +# External native builds +.externalNativeBuild + # VP9 extension extensions/vp9/src/main/jni/libvpx extensions/vp9/src/main/jni/libvpx_android_configs @@ -61,3 +65,4 @@ extensions/cronet/jniLibs/* !extensions/cronet/jniLibs/README.md extensions/cronet/libs/* !extensions/cronet/libs/README.md + diff --git a/.hgignore b/.hgignore new file mode 100644 index 0000000000..f7c3656f65 --- /dev/null +++ b/.hgignore @@ -0,0 +1,71 @@ +# Mercurial's .hgignore files can only be used in the root directory. +# You can still apply these rules by adding +# include:path/to/this/directory/.hgignore to the top-level .hgignore file. + +# Ensure same syntax as in .gitignore can be used +syntax:glob + +# Android generated +bin +gen +libs +obj +lint.xml + +# IntelliJ IDEA +.idea +*.iml +*.ipr +*.iws +classes +gen-external-apklibs + +# Eclipse +.project +.classpath +.settings +.checkstyle +.cproject + +# Gradle +.gradle +build +buildout +out + +# Maven +target +release.properties +pom.xml.* + +# Ant +ant.properties +local.properties +proguard.cfg +proguard-project.txt + +# Other +.DS_Store +cmake-build-debug +dist +tmp + +# VP9 extension +extensions/vp9/src/main/jni/libvpx +extensions/vp9/src/main/jni/libvpx_android_configs +extensions/vp9/src/main/jni/libyuv + +# Opus extension +extensions/opus/src/main/jni/libopus + +# FLAC extension +extensions/flac/src/main/jni/flac + +# FFmpeg extension +extensions/ffmpeg/src/main/jni/ffmpeg + +# Cronet extension +extensions/cronet/jniLibs/* +!extensions/cronet/jniLibs/README.md +extensions/cronet/libs/* +!extensions/cronet/libs/README.md diff --git a/ISSUE_TEMPLATE b/ISSUE_TEMPLATE index 1b912312d1..8d2f66093d 100644 --- a/ISSUE_TEMPLATE +++ b/ISSUE_TEMPLATE @@ -1,5 +1,3 @@ -*** ISSUES THAT IGNORE THIS TEMPLATE WILL BE CLOSED WITHOUT INVESTIGATION *** - Before filing an issue: ----------------------- - Search existing issues, including issues that are closed. @@ -26,7 +24,7 @@ Describe how the issue can be reproduced, ideally using the ExoPlayer demo app. ### Link to test content Provide a link to media that reproduces the issue. If you don't wish to post it publicly, please submit the issue, then email the link to -dev.exoplayer@gmail.com including the issue number in the subject line. +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". @@ -40,5 +38,6 @@ devices and Android versions. Capture a full bug report using "adb bugreport". Output from "adb logcat" or a log snippet is NOT sufficient. Please attach the captured bug report as a file. If you don't wish to post it publicly, please submit the issue, then email the -bug report to dev.exoplayer@gmail.com including the issue number in the subject -line. +bug report to dev.exoplayer@gmail.com using a subject in the format +"Issue #1234". + diff --git a/README.md b/README.md index c67fb09d73..13dfaddab3 100644 --- a/README.md +++ b/README.md @@ -28,34 +28,32 @@ repository and depend on the modules locally. ### From JCenter ### The easiest way to get started using ExoPlayer is to add it as a gradle -dependency. You need to make sure you have the JCenter and Google Maven -repositories included in the `build.gradle` file in the root of your project: +dependency. You need to make sure you have the JCenter and Google repositories +included in the `build.gradle` file in the root of your project: ```gradle repositories { jcenter() - maven { - url "https://maven.google.com" - } + google() } ``` -Next add a gradle compile dependency to the `build.gradle` file of your app -module. The following will add a dependency to the full library: +Next add a dependency in the `build.gradle` file of your app module. The +following will add a dependency to the full library: ```gradle -compile 'com.google.android.exoplayer:exoplayer:r2.X.X' +implementation 'com.google.android.exoplayer:exoplayer:2.X.X' ``` -where `r2.X.X` is your preferred version. Alternatively, you can depend on only +where `2.X.X` is your preferred version. Alternatively, you can depend on only the library modules that you actually need. For example the following will add dependencies on the Core, DASH and UI library modules, as might be required for an app that plays DASH content: ```gradle -compile 'com.google.android.exoplayer:exoplayer-core:r2.X.X' -compile 'com.google.android.exoplayer:exoplayer-dash:r2.X.X' -compile 'com.google.android.exoplayer:exoplayer-ui:r2.X.X' +implementation 'com.google.android.exoplayer:exoplayer-core:2.X.X' +implementation 'com.google.android.exoplayer:exoplayer-dash:2.X.X' +implementation 'com.google.android.exoplayer:exoplayer-ui:2.X.X' ``` The available library modules are listed below. Adding a dependency to the full @@ -70,13 +68,13 @@ individually. In addition to library modules, ExoPlayer has multiple extension modules that depend on external libraries to provide additional functionality. Some -extensions are available from JCenter, whereas others must be built manaully. -Browse the [extensions directory] and their individual READMEs for details. +extensions are available from JCenter, whereas others must be built manually. +Browse the [extensions directory][] and their individual READMEs for details. More information on the library and extension modules that are available from JCenter can be found on [Bintray][]. -[extensions directory][]: https://github.com/google/ExoPlayer/tree/release-v2/extensions/ +[extensions directory]: https://github.com/google/ExoPlayer/tree/release-v2/extensions/ [Bintray]: https://bintray.com/google/exoplayer ### Locally ### @@ -107,9 +105,9 @@ You should now see the ExoPlayer modules appear as part of your project. You can depend on them as you would on any other local module, for example: ```gradle -compile project(':exoplayer-library-core') -compile project(':exoplayer-library-dash') -compile project(':exoplayer-library-ui) +implementation project(':exoplayer-library-core') +implementation project(':exoplayer-library-dash') +implementation project(':exoplayer-library-ui') ``` ## Developing ExoPlayer ## diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 8ad866395e..242fe2c119 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -1,5 +1,462 @@ # Release notes # +### dev-v2 (not yet released) ### + +* OkHttp extension: Fix to correctly include response headers in thrown + `InvalidResponseCodeException`s. + +### 2.8.0 ### + +* Downloading: + * Add `DownloadService`, `DownloadManager` and related classes + ([#2643](https://github.com/google/ExoPlayer/issues/2643)). Information on + using these components to download progressive formats can be found + [here](https://medium.com/google-exoplayer/downloading-streams-6d259eec7f95). + To see how to download DASH, HLS and SmoothStreaming media, take a look at + the app. + * Updated main demo app to support downloading DASH, HLS, SmoothStreaming and + progressive media. +* MediaSources: + * Allow reusing media sources after they have been released and + also in parallel to allow adding them multiple times to a concatenation. + ([#3498](https://github.com/google/ExoPlayer/issues/3498)). + * Merged `DynamicConcatenatingMediaSource` into `ConcatenatingMediaSource` and + deprecated `DynamicConcatenatingMediaSource`. + * Allow clipping of child media sources where the period and window have a + non-zero offset with `ClippingMediaSource`. + * Allow adding and removing `MediaSourceEventListener`s to MediaSources after + they have been created. Listening to events is now supported for all + media sources including composite sources. + * Added callbacks to `MediaSourceEventListener` to get notified when media + periods are created, released and being read from. + * Support live stream clipping with `ClippingMediaSource`. + * Allow setting tags for all media sources in their factories. The tag of the + current window can be retrieved with `ExoPlayer.getCurrentTag`. +* UI components: + * Add support for displaying error messages and a buffering spinner in + `PlayerView`. + * Add support for listening to `AspectRatioFrameLayout`'s aspect ratio update + ([#3736](https://github.com/google/ExoPlayer/issues/3736)). + * Add `PlayerNotificationManager` for displaying notifications reflecting the + player state. + * Add `TrackSelectionView` for selecting tracks with `DefaultTrackSelector`. + * Add `TrackNameProvider` for converting track `Format`s to textual + descriptions, and `DefaultTrackNameProvider` as a default implementation. +* Track selection: + * Reworked `MappingTrackSelector` and `DefaultTrackSelector`. + * `DefaultTrackSelector.Parameters` now implements `Parcelable`. + * Added UI components for track selection (see above). +* Audio: + * Support extracting data from AMR container formats, including both narrow + and wide band ([#2527](https://github.com/google/ExoPlayer/issues/2527)). + * FLAC: + * Sniff FLAC files correctly if they have ID3 headers + ([#4055](https://github.com/google/ExoPlayer/issues/4055)). + * Supports FLAC files with high sample rate (176400 and 192000) + ([#3769](https://github.com/google/ExoPlayer/issues/3769)). + * Factor out `AudioTrack` position tracking from `DefaultAudioSink`. + * Fix an issue where the playback position would pause just after playback + begins, and poll the audio timestamp less frequently once it starts + advancing ([#3841](https://github.com/google/ExoPlayer/issues/3841)). + * Add an option to skip silent audio in `PlaybackParameters` + ((#2635)[https://github.com/google/ExoPlayer/issues/2635]). + * Fix an issue where playback of TrueHD streams would get stuck after seeking + due to not finding a syncframe + ((#3845)[https://github.com/google/ExoPlayer/issues/3845]). + * Fix an issue with eac3-joc playback where a codec would fail to configure + ((#4165)[https://github.com/google/ExoPlayer/issues/4165]). + * Handle non-empty end-of-stream buffers, to fix gapless playback of streams + with encoder padding when the decoder returns a non-empty final buffer. + * Allow trimming more than one sample when applying an elst audio edit via + gapless playback info. + * Allow overriding skipping/scaling with custom `AudioProcessor`s + ((#3142)[https://github.com/google/ExoPlayer/issues/3142]). +* Caching: + * Add release method to the `Cache` interface, and prevent multiple instances + of `SimpleCache` using the same folder at the same time. + * Cache redirect URLs + ([#2360](https://github.com/google/ExoPlayer/issues/2360)). +* DRM: + * Allow multiple listeners for `DefaultDrmSessionManager`. + * Pass `DrmSessionManager` to `ExoPlayerFactory` instead of `RendererFactory`. + * Change minimum API requirement for CBC and pattern encryption from 24 to 25 + ([#4022][https://github.com/google/ExoPlayer/issues/4022]). + * Fix handling of 307/308 redirects when making license requests + ([#4108](https://github.com/google/ExoPlayer/issues/4108)). +* HLS: + * Fix playlist loading error propagation when the current selection does + not include all of the playlist's variants. + * Fix SAMPLE-AES-CENC and SAMPLE-AES-CTR EXT-X-KEY methods + ([#4145](https://github.com/google/ExoPlayer/issues/4145)). + * Preeptively declare an ID3 track in chunkless preparation + ([#4016](https://github.com/google/ExoPlayer/issues/4016)). + * Add support for multiple #EXT-X-MAP tags in a media playlist + ([#4164](https://github.com/google/ExoPlayer/issues/4182)). + * Fix seeking in live streams + ([#4187](https://github.com/google/ExoPlayer/issues/4187)). +* IMA: + * Allow setting the ad media load timeout + ([#3691](https://github.com/google/ExoPlayer/issues/3691)). + * Expose ad load errors via `MediaSourceEventListener` on `AdsMediaSource`, + and allow setting an ad event listener on `ImaAdsLoader`. Deprecate the + `AdsMediaSource.EventListener`. +* Add `AnalyticsListener` interface which can be registered in + `SimpleExoPlayer` to receive detailed metadata for each ExoPlayer event. +* Optimize seeking in FMP4 by enabling seeking to the nearest sync sample within + a fragment. This benefits standalone FMP4 playbacks, DASH and SmoothStreaming. +* Updated default max buffer length in `DefaultLoadControl`. +* Fix ClearKey decryption error if the key contains a forward slash + ([#4075](https://github.com/google/ExoPlayer/issues/4075)). +* Fix crash when switching surface on Huawei P9 Lite + ([#4084](https://github.com/google/ExoPlayer/issues/4084)), and Philips QM163E + ([#4104](https://github.com/google/ExoPlayer/issues/4104)). +* Support ZLIB compressed PGS subtitles. +* Added `getPlaybackError` to `Player` interface. +* Moved initial bitrate estimate from `AdaptiveTrackSelection` to + `DefaultBandwidthMeter`. +* Removed default renderer time offset of 60000000 from internal player. The + actual renderer timestamp offset can be obtained by listening to + `BaseRenderer.onStreamChanged`. +* Added dependencies on checkerframework annotations for static code analysis. + +### 2.7.3 ### + +* Fix ProGuard configuration for Cast, IMA and OkHttp extensions. +* Update OkHttp extension to depend on OkHttp 3.10.0. + +### 2.7.2 ### + +* Gradle: Upgrade Gradle version from 4.1 to 4.4 so it can work with Android + Studio 3.1 ([#3708](https://github.com/google/ExoPlayer/issues/3708)). +* Match codecs starting with "mp4a" to different Audio MimeTypes + ([#3779](https://github.com/google/ExoPlayer/issues/3779)). +* Fix ANR issue on Redmi 4X and Redmi Note 4 + ([#4006](https://github.com/google/ExoPlayer/issues/4006)). +* Fix handling of zero padded strings when parsing Matroska streams + ([#4010](https://github.com/google/ExoPlayer/issues/4010)). +* Fix "Decoder input buffer too small" error when playing some FLAC streams. +* MediaSession extension: Omit fast forward and rewind actions when media is not + seekable ([#4001](https://github.com/google/ExoPlayer/issues/4001)). + +### 2.7.1 ### + +* Gradle: Replaced 'compile' (deprecated) with 'implementation' and + 'api'. This may lead to build breakage for applications upgrading from + previous version that rely on indirect dependencies of certain modules. In + such cases, application developers need to add the missing dependency to + their gradle file. You can read more about the new dependency configurations + [here](https://developer.android.com/studio/build/gradle-plugin-3-0-0-migration.html#new_configurations). +* HlsMediaSource: Make HLS periods start at zero instead of the epoch. + Applications that rely on HLS timelines having a period starting at + the epoch will need to update their handling of HLS timelines. The program + date time is still available via the informational + `Timeline.Window.windowStartTimeMs` field + ([#3865](https://github.com/google/ExoPlayer/issues/3865), + [#3888](https://github.com/google/ExoPlayer/issues/3888)). +* Enable seeking in MP4 streams where duration is set incorrectly in the track + header ([#3926](https://github.com/google/ExoPlayer/issues/3926)). +* Video: Force rendering a frame periodically in `MediaCodecVideoRenderer` and + `LibvpxVideoRenderer`, even if it is late. + +### 2.7.0 ### + +* Player interface: + * Add optional parameter to `stop` to reset the player when stopping. + * Add a reason to `EventListener.onTimelineChanged` to distinguish between + initial preparation, reset and dynamic updates. + * Add `Player.DISCONTINUITY_REASON_AD_INSERTION` to the possible reasons + reported in `Eventlistener.onPositionDiscontinuity` to distinguish + transitions to and from ads within one period from transitions between + periods. + * Replaced `ExoPlayer.sendMessages` with `ExoPlayer.createMessage` to allow + more customization of the message. Now supports setting a message delivery + playback position and/or a delivery handler + ([#2189](https://github.com/google/ExoPlayer/issues/2189)). + * Add `Player.VideoComponent`, `Player.TextComponent` and + `Player.MetadataComponent` interfaces that define optional video, text and + metadata output functionality. New `getVideoComponent`, `getTextComponent` + and `getMetadataComponent` methods provide access to this functionality. +* Add `ExoPlayer.setSeekParameters` for controlling how seek operations are + performed. The `SeekParameters` class contains defaults for exact seeking and + seeking to the closest sync points before, either side or after specified seek + positions. `SeekParameters` are not currently supported when playing HLS + streams. +* DefaultTrackSelector: + * Replace `DefaultTrackSelector.Parameters` copy methods with a builder. + * Support disabling of individual text track selection flags. +* Buffering: + * Allow a back-buffer of media to be retained behind the current playback + position, for fast backward seeking. The back-buffer can be configured by + custom `LoadControl` implementations. + * Add ability for `SequenceableLoader` to re-evaluate its buffer and discard + buffered media so that it can be re-buffered in a different quality. + * Allow more flexible loading strategy when playing media containing multiple + sub-streams, by allowing injection of custom `CompositeSequenceableLoader` + factories through `DashMediaSource.Factory`, `HlsMediaSource.Factory`, + `SsMediaSource.Factory`, and `MergingMediaSource`. + * Play out existing buffer before retrying for progressive live streams + ([#1606](https://github.com/google/ExoPlayer/issues/1606)). +* UI components: + * Generalized player and control views to allow them to bind with any + `Player`, and renamed them to `PlayerView` and `PlayerControlView` + respectively. + * Made `PlayerView` automatically apply video rotation when configured to use + `TextureView` ([#91](https://github.com/google/ExoPlayer/issues/91)). + * Made `PlayerView` play button behave correctly when the player is ended + ([#3689](https://github.com/google/ExoPlayer/issues/3689)), and call a + `PlaybackPreparer` when the player is idle. +* DRM: Optimistically attempt playback of DRM protected content that does not + declare scheme specific init data in the manifest. If playback of clear + samples without keys is allowed, delay DRM session error propagation until + keys are actually needed + ([#3630](https://github.com/google/ExoPlayer/issues/3630)). +* DASH: + * Support in-band Emsg events targeting the player with scheme id + `urn:mpeg:dash:event:2012` and scheme values "1", "2" and "3". + * Support EventStream elements in DASH manifests. +* HLS: + * Add opt-in support for chunkless preparation in HLS. This allows an + HLS source to finish preparation without downloading any chunks, which can + significantly reduce initial buffering time + ([#3149](https://github.com/google/ExoPlayer/issues/3149)). More details + can be found + [here](https://medium.com/google-exoplayer/faster-hls-preparation-f6611aa15ea6). + * Fail if unable to sync with the Transport Stream, rather than entering + stuck in an indefinite buffering state. + * Fix mime type propagation + ([#3653](https://github.com/google/ExoPlayer/issues/3653)). + * Fix ID3 context reuse across segment format changes + ([#3622](https://github.com/google/ExoPlayer/issues/3622)). + * Use long for media sequence numbers + ([#3747](https://github.com/google/ExoPlayer/issues/3747)) + * Add initial support for the EXT-X-GAP tag. +* Audio: + * Support TrueHD passthrough for rechunked samples in Matroska files + ([#2147](https://github.com/google/ExoPlayer/issues/2147)). + * Support resampling 24-bit and 32-bit integer to 32-bit float for high + resolution output in `DefaultAudioSink` + ([#3635](https://github.com/google/ExoPlayer/pull/3635)). +* Captions: + * Basic support for PGS subtitles + ([#3008](https://github.com/google/ExoPlayer/issues/3008)). + * Fix handling of CEA-608 captions where multiple buffers have the same + presentation timestamp + ([#3782](https://github.com/google/ExoPlayer/issues/3782)). +* Caching: + * Fix cache corruption issue + ([#3762](https://github.com/google/ExoPlayer/issues/3762)). + * Implement periodic check in `CacheDataSource` to see whether it's possible + to switch to reading/writing the cache having initially bypassed it. +* IMA extension: + * Fix the player getting stuck when an ad group fails to load + ([#3584](https://github.com/google/ExoPlayer/issues/3584)). + * Work around loadAd not being called beore the LOADED AdEvent arrives + ([#3552](https://github.com/google/ExoPlayer/issues/3552)). + * Handle asset mismatch errors + ([#3801](https://github.com/google/ExoPlayer/issues/3801)). + * Add support for playing non-Extractor content MediaSources in + the IMA demo app + ([#3676](https://github.com/google/ExoPlayer/issues/3676)). + * Fix handling of ad tags where ad groups are out of order + ([#3716](https://github.com/google/ExoPlayer/issues/3716)). + * Fix handling of ad tags with only preroll/postroll ad groups + ([#3715](https://github.com/google/ExoPlayer/issues/3715)). + * Propagate ad media preparation errors to IMA so that the ads can be + skipped. + * Handle exceptions in IMA callbacks so that can be logged less verbosely. +* New Cast extension. Simplifies toggling between local and Cast playbacks. +* `EventLogger` moved from the demo app into the core library. +* Fix ANR issue on the Huawei P8 Lite, Huawei Y6II, Moto C+, Meizu M5C, + Lenovo K4 Note and Sony Xperia E5. + ([#3724](https://github.com/google/ExoPlayer/issues/3724), + [#3835](https://github.com/google/ExoPlayer/issues/3835)). +* Fix potential NPE when removing media sources from a + DynamicConcatenatingMediaSource + ([#3796](https://github.com/google/ExoPlayer/issues/3796)). +* Check `sys.display-size` on Philips ATVs + ([#3807](https://github.com/google/ExoPlayer/issues/3807)). +* Release `Extractor`s on the loading thread to avoid potentially leaking + resources when the playback thread has quit by the time the loading task has + completed. +* ID3: Better handle malformed ID3 data + ([#3792](https://github.com/google/ExoPlayer/issues/3792). +* Support 14-bit mode and little endianness in DTS PES packets + ([#3340](https://github.com/google/ExoPlayer/issues/3340)). +* Demo app: Add ability to download not DRM protected content. + +### 2.6.1 ### + +* Add factories to `ExtractorMediaSource`, `HlsMediaSource`, `SsMediaSource`, + `DashMediaSource` and `SingleSampleMediaSource`. +* Use the same listener `MediaSourceEventListener` for all MediaSource + implementations. +* IMA extension: + * Support non-ExtractorMediaSource ads + ([#3302](https://github.com/google/ExoPlayer/issues/3302)). + * Skip ads before the ad preceding the player's initial seek position + ([#3527](https://github.com/google/ExoPlayer/issues/3527)). + * Fix ad loading when there is no preroll. + * Add an option to turn off hiding controls during ad playback + ([#3532](https://github.com/google/ExoPlayer/issues/3532)). + * Support specifying an ads response instead of an ad tag + ([#3548](https://github.com/google/ExoPlayer/issues/3548)). + * Support overriding the ad load timeout + ([#3556](https://github.com/google/ExoPlayer/issues/3556)). +* DASH: Support time zone designators in ISO8601 UTCTiming elements + ([#3524](https://github.com/google/ExoPlayer/issues/3524)). +* Audio: + * Support 32-bit PCM float output from `DefaultAudioSink`, and add an option + to use this with `FfmpegAudioRenderer`. + * Add support for extracting 32-bit WAVE files + ([#3379](https://github.com/google/ExoPlayer/issues/3379)). + * Support extraction and decoding of Dolby Atmos + ([#2465](https://github.com/google/ExoPlayer/issues/2465)). + * Fix handling of playback parameter changes while paused when followed by a + seek. +* SimpleExoPlayer: Allow multiple audio and video debug listeners. +* DefaultTrackSelector: Support undefined language text track selection when the + preferred language is not available + ([#2980](https://github.com/google/ExoPlayer/issues/2980)). +* Add options to `DefaultLoadControl` to set maximum buffer size in bytes and + to choose whether size or time constraints are prioritized. +* Use surfaceless context for secure `DummySurface`, if available + ([#3558](https://github.com/google/ExoPlayer/issues/3558)). +* FLV: Fix playback of live streams that do not contain an audio track + ([#3188](https://github.com/google/ExoPlayer/issues/3188)). +* CEA-608: Fix handling of row count changes in roll-up mode + ([#3513](https://github.com/google/ExoPlayer/issues/3513)). +* Prevent period transitions when seeking to the end of a period when paused + ([#2439](https://github.com/google/ExoPlayer/issues/2439)). + +### 2.6.0 ### + +* Removed "r" prefix from versions. This release is "2.6.0", not "r2.6.0". +* New `Player.DefaultEventListener` abstract class can be extended to avoid + having to implement all methods defined by `Player.EventListener`. +* Added a reason to `EventListener.onPositionDiscontinuity` + ([#3252](https://github.com/google/ExoPlayer/issues/3252)). +* New `setShuffleModeEnabled` method for enabling shuffled playback. +* SimpleExoPlayer: Support for multiple video, text and metadata outputs. +* Support for `Renderer`s that don't consume any media + ([#3212](https://github.com/google/ExoPlayer/issues/3212)). +* Fix reporting of internal position discontinuities via + `Player.onPositionDiscontinuity`. `DISCONTINUITY_REASON_SEEK_ADJUSTMENT` is + added to disambiguate position adjustments during seeks from other types of + internal position discontinuity. +* Fix potential `IndexOutOfBoundsException` when calling `ExoPlayer.getDuration` + ([#3362](https://github.com/google/ExoPlayer/issues/3362)). +* Fix playbacks involving looping, concatenation and ads getting stuck when + media contains tracks with uneven durations + ([#1874](https://github.com/google/ExoPlayer/issues/1874)). +* Fix issue with `ContentDataSource` when reading from certain `ContentProvider` + implementations ([#3426](https://github.com/google/ExoPlayer/issues/3426)). +* Better playback experience when the video decoder cannot keep up, by skipping + to key-frames. This is particularly relevant for variable speed playbacks. +* Allow `SingleSampleMediaSource` to suppress load errors + ([#3140](https://github.com/google/ExoPlayer/issues/3140)). +* `DynamicConcatenatingMediaSource`: Allow specifying a callback to be invoked + after a dynamic playlist modification has been applied + ([#3407](https://github.com/google/ExoPlayer/issues/3407)). +* Audio: New `AudioSink` interface allows customization of audio output path. +* Offline: Added `Downloader` implementations for DASH, HLS, SmoothStreaming + and progressive streams. +* Track selection: + * Fixed adaptive track selection logic for live playbacks + ([#3017](https://github.com/google/ExoPlayer/issues/3017)). + * Added ability to select the lowest bitrate tracks. +* DASH: + * Don't crash when a malformed or unexpected manifest update occurs + ([#2795](https://github.com/google/ExoPlayer/issues/2795)). +* HLS: + * Support for Widevine protected FMP4 variants. + * Support CEA-608 in FMP4 variants. + * Support extractor injection + ([#2748](https://github.com/google/ExoPlayer/issues/2748)). +* DRM: + * Improved compatibility with ClearKey content + ([#3138](https://github.com/google/ExoPlayer/issues/3138)). + * Support multiple PSSH boxes of the same type. + * Retry initial provisioning and key requests if they fail + * Fix incorrect parsing of non-CENC sinf boxes. +* IMA extension: + * Expose `AdsLoader` via getter + ([#3322](https://github.com/google/ExoPlayer/issues/3322)). + * Handle `setPlayWhenReady` calls during ad playbacks + ([#3303](https://github.com/google/ExoPlayer/issues/3303)). + * Ignore seeks if an ad is playing + ([#3309](https://github.com/google/ExoPlayer/issues/3309)). + * Improve robustness of `ImaAdsLoader` in case content is not paused between + content to ad transitions + ([#3430](https://github.com/google/ExoPlayer/issues/3430)). +* UI: + * Allow specifying a `Drawable` for the `TimeBar` scrubber + ([#3337](https://github.com/google/ExoPlayer/issues/3337)). + * Allow multiple listeners on `TimeBar` + ([#3406](https://github.com/google/ExoPlayer/issues/3406)). +* New Leanback extension: Simplifies binding Exoplayer to Leanback UI + components. +* Unit tests moved to Robolectric. +* Misc bugfixes. + +### r2.5.4 ### + +* Remove unnecessary media playlist fetches during playback of live HLS streams. +* Add the ability to inject a HLS playlist parser through `HlsMediaSource`. +* Fix potential `IndexOutOfBoundsException` when using `ImaMediaSource` + ([#3334](https://github.com/google/ExoPlayer/issues/3334)). +* Fix an issue parsing MP4 content containing non-CENC sinf boxes. +* Fix memory leak when seeking with repeated periods. +* Fix playback position when `ExoPlayer.prepare` is called with `resetPosition` + set to false. +* Ignore MP4 edit lists that seem invalid + ([#3351](https://github.com/google/ExoPlayer/issues/3351)). +* Add extractor flag for ignoring all MP4 edit lists + ([#3358](https://github.com/google/ExoPlayer/issues/3358)). +* Improve extensibility by exposing public constructors for + `FrameworkMediaCrypto` and by making `DefaultDashChunkSource.getNextChunk` + non-final. + +### r2.5.3 ### + +* IMA extension: Support skipping of skippable ads on AndroidTV and other + non-touch devices ([#3258](https://github.com/google/ExoPlayer/issues/3258)). +* HLS: Fix broken WebVTT captions when PTS wraps around + ([#2928](https://github.com/google/ExoPlayer/issues/2928)). +* Captions: Fix issues rendering CEA-608 captions + ([#3250](https://github.com/google/ExoPlayer/issues/3250)). +* Workaround broken AAC decoders on Galaxy S6 + ([#3249](https://github.com/google/ExoPlayer/issues/3249)). +* Caching: Fix infinite loop when cache eviction fails + ([#3260](https://github.com/google/ExoPlayer/issues/3260)). +* Caching: Force use of BouncyCastle on JellyBean to fix decryption issue + ([#2755](https://github.com/google/ExoPlayer/issues/2755)). + +### r2.5.2 ### + +* IMA extension: Fix issue where ad playback could end prematurely for some + content types ([#3180](https://github.com/google/ExoPlayer/issues/3180)). +* RTMP extension: Fix SIGABRT on fast RTMP stream restart + ([#3156](https://github.com/google/ExoPlayer/issues/3156)). +* UI: Allow app to manually specify ad markers + ([#3184](https://github.com/google/ExoPlayer/issues/3184)). +* DASH: Expose segment indices to subclasses of DefaultDashChunkSource + ([#3037](https://github.com/google/ExoPlayer/issues/3037)). +* Captions: Added robustness against malformed WebVTT captions + ([#3228](https://github.com/google/ExoPlayer/issues/3228)). +* DRM: Support forcing a specific license URL. +* Fix playback error when seeking in media loaded through content:// URIs + ([#3216](https://github.com/google/ExoPlayer/issues/3216)). +* Fix issue playing MP4s in which the last atom specifies a size of zero + ([#3191](https://github.com/google/ExoPlayer/issues/3191)). +* Workaround playback failures on some Xiaomi devices + ([#3171](https://github.com/google/ExoPlayer/issues/3171)). +* Workaround SIGSEGV issue on some devices when setting and swapping surface for + secure playbacks ([#3215](https://github.com/google/ExoPlayer/issues/3215)). +* Workaround for Nexus 7 issue when swapping output surface + ([#3236](https://github.com/google/ExoPlayer/issues/3236)). +* Workaround for SimpleExoPlayerView's surface not being hidden properly + ([#3160](https://github.com/google/ExoPlayer/issues/3160)). + ### r2.5.1 ### * Fix an issue that could cause the reported playback position to stop advancing @@ -13,7 +470,7 @@ easy and seamless way of incorporating display ads into ExoPlayer playbacks. You can read more about the IMA extension [here](https://medium.com/google-exoplayer/playing-ads-with-exoplayer-and-ima-868dfd767ea). -* MediaSession extension: Provides an easy to to connect ExoPlayer with +* MediaSession extension: Provides an easy way to connect ExoPlayer with MediaSessionCompat in the Android Support Library. * RTMP extension: An extension for playing streams over RTMP. * Build: Made it easier for application developers to depend on a local checkout diff --git a/build.gradle b/build.gradle index d5cc64baa1..3813a241e0 100644 --- a/build.gradle +++ b/build.gradle @@ -14,13 +14,11 @@ buildscript { repositories { jcenter() - maven { - url "https://maven.google.com" - } + google() } dependencies { - classpath 'com.android.tools.build:gradle:2.3.3' - classpath 'com.novoda:bintray-release:0.5.0' + classpath 'com.android.tools.build:gradle:3.1.0' + classpath 'com.novoda:bintray-release:0.8.1' } // Workaround for the following test coverage issue. Remove when fixed: // https://code.google.com/p/android/issues/detail?id=226070 @@ -34,9 +32,7 @@ buildscript { allprojects { repositories { jcenter() - maven { - url "https://maven.google.com" - } + google() } project.ext { exoplayerPublishEnabled = true diff --git a/extensions/mediasession/src/main/res/values-ms-rMY/strings.xml b/checker-framework-lint.xml similarity index 62% rename from extensions/mediasession/src/main/res/values-ms-rMY/strings.xml rename to checker-framework-lint.xml index 829542b668..1d45f9de05 100644 --- a/extensions/mediasession/src/main/res/values-ms-rMY/strings.xml +++ b/checker-framework-lint.xml @@ -1,6 +1,4 @@ - - - - "Ulang semua" - "Tiada ulangan" - "Ulangan" - + + + + + diff --git a/constants.gradle b/constants.gradle index 4107faab4c..dcadcceb4f 100644 --- a/constants.gradle +++ b/constants.gradle @@ -12,20 +12,27 @@ // See the License for the specific language governing permissions and // limitations under the License. project.ext { + // ExoPlayer version and version code. + releaseVersion = '2.8.0' + releaseVersionCode = 2800 // 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 - compileSdkVersion = 26 - targetSdkVersion = 26 - buildToolsVersion = '26' + targetSdkVersion = 27 + compileSdkVersion = 27 + buildToolsVersion = '27.0.3' testSupportLibraryVersion = '0.5' - supportLibraryVersion = '26.0.1' - playServicesLibraryVersion = '11.0.2' + supportLibraryVersion = '27.0.0' + playServicesLibraryVersion = '12.0.0' dexmakerVersion = '1.2' mockitoVersion = '1.9.5' - releaseVersion = 'r2.5.1' + junitVersion = '4.12' + truthVersion = '0.39' + robolectricVersion = '3.7.1' + autoValueVersion = '1.6' + checkerframeworkVersion = '2.5.0' modulePrefix = ':' if (gradle.ext.has('exoplayerModulePrefix')) { modulePrefix += gradle.ext.exoplayerModulePrefix diff --git a/core_settings.gradle b/core_settings.gradle index 20a7c87bde..fc738c8476 100644 --- a/core_settings.gradle +++ b/core_settings.gradle @@ -24,6 +24,7 @@ include modulePrefix + 'library-hls' include modulePrefix + 'library-smoothstreaming' include modulePrefix + 'library-ui' include modulePrefix + 'testutils' +include modulePrefix + 'testutils-robolectric' include modulePrefix + 'extension-ffmpeg' include modulePrefix + 'extension-flac' include modulePrefix + 'extension-gvr' @@ -35,6 +36,7 @@ include modulePrefix + 'extension-opus' include modulePrefix + 'extension-vp9' include modulePrefix + 'extension-rtmp' include modulePrefix + 'extension-leanback' +include modulePrefix + 'extension-jobdispatcher' project(modulePrefix + 'library').projectDir = new File(rootDir, 'library/all') project(modulePrefix + 'library-core').projectDir = new File(rootDir, 'library/core') @@ -43,6 +45,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-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') @@ -54,6 +57,7 @@ project(modulePrefix + 'extension-opus').projectDir = new File(rootDir, 'extensi project(modulePrefix + 'extension-vp9').projectDir = new File(rootDir, 'extensions/vp9') project(modulePrefix + 'extension-rtmp').projectDir = new File(rootDir, 'extensions/rtmp') project(modulePrefix + 'extension-leanback').projectDir = new File(rootDir, 'extensions/leanback') +project(modulePrefix + 'extension-jobdispatcher').projectDir = new File(rootDir, 'extensions/jobdispatcher') if (gradle.ext.has('exoplayerIncludeCronetExtension') && gradle.ext.exoplayerIncludeCronetExtension) { diff --git a/demos/cast/build.gradle b/demos/cast/build.gradle index a9fa27ad58..ae6bdd1d94 100644 --- a/demos/cast/build.gradle +++ b/demos/cast/build.gradle @@ -19,6 +19,8 @@ android { buildToolsVersion project.ext.buildToolsVersion defaultConfig { + versionName project.ext.releaseVersion + versionCode project.ext.releaseVersionCode minSdkVersion 16 targetSdkVersion project.ext.targetSdkVersion } @@ -27,7 +29,10 @@ android { release { shrinkResources true minifyEnabled true - proguardFiles getDefaultProguardFile('proguard-android.txt') + proguardFiles = [ + "proguard-rules.txt", + getDefaultProguardFile('proguard-android.txt') + ] } debug { jniDebuggable = true @@ -42,10 +47,13 @@ android { } dependencies { - compile project(modulePrefix + 'library-core') - compile project(modulePrefix + 'library-dash') - compile project(modulePrefix + 'library-hls') - compile project(modulePrefix + 'library-smoothstreaming') - compile project(modulePrefix + 'library-ui') - compile project(modulePrefix + 'extension-cast') + implementation project(modulePrefix + 'library-core') + implementation project(modulePrefix + 'library-dash') + implementation project(modulePrefix + 'library-hls') + implementation project(modulePrefix + 'library-smoothstreaming') + implementation project(modulePrefix + 'library-ui') + implementation project(modulePrefix + 'extension-cast') + implementation 'com.android.support:support-v4:' + supportLibraryVersion + implementation 'com.android.support:appcompat-v7:' + supportLibraryVersion + implementation 'com.android.support:recyclerview-v7:' + supportLibraryVersion } diff --git a/demos/cast/proguard-rules.txt b/demos/cast/proguard-rules.txt new file mode 100644 index 0000000000..3221818080 --- /dev/null +++ b/demos/cast/proguard-rules.txt @@ -0,0 +1,6 @@ +# Proguard rules specific to the Cast demo app. + +# Accessed via menu.xml +-keep class android.support.v7.app.MediaRouteActionProvider { + *; +} diff --git a/demos/cast/src/main/AndroidManifest.xml b/demos/cast/src/main/AndroidManifest.xml index eeb28438bd..ae16776333 100644 --- a/demos/cast/src/main/AndroidManifest.xml +++ b/demos/cast/src/main/AndroidManifest.xml @@ -14,12 +14,10 @@ limitations under the License. --> + package="com.google.android.exoplayer2.castdemo"> - + @@ -34,7 +32,6 @@ - diff --git a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/CastDemoUtil.java b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/DemoUtil.java similarity index 83% rename from demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/CastDemoUtil.java rename to demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/DemoUtil.java index f819e54e50..26ab5eb0dd 100644 --- a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/CastDemoUtil.java +++ b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/DemoUtil.java @@ -24,11 +24,11 @@ import java.util.List; /** * Utility methods and constants for the Cast demo application. */ -/* package */ final class CastDemoUtil { +/* package */ final class DemoUtil { - public static final String MIME_TYPE_DASH = "application/dash+xml"; - public static final String MIME_TYPE_HLS = "application/vnd.apple.mpegurl"; - public static final String MIME_TYPE_SS = "application/vnd.ms-sstr+xml"; + 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; /** @@ -52,17 +52,17 @@ import java.util.List; /** * The mime type of the media sample, as required by {@link MediaInfo#setContentType}. */ - public final String type; + public final String mimeType; /** * @param uri See {@link #uri}. * @param name See {@link #name}. - * @param type See {@link #type}. + * @param mimeType See {@link #mimeType}. */ - public Sample(String uri, String name, String type) { + public Sample(String uri, String name, String mimeType) { this.uri = uri; this.name = name; - this.type = type; + this.mimeType = mimeType; } @Override @@ -87,6 +87,6 @@ import java.util.List; } - private CastDemoUtil() {} + private DemoUtil() {} } diff --git a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/MainActivity.java b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/MainActivity.java index e1367858aa..3e48ab2ab4 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 @@ -15,89 +15,100 @@ */ package com.google.android.exoplayer2.castdemo; -import android.graphics.Color; +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; import android.view.View; +import android.view.View.OnClickListener; import android.view.ViewGroup; import android.widget.AdapterView; +import android.widget.AdapterView.OnItemClickListener; import android.widget.ArrayAdapter; import android.widget.ListView; +import android.widget.TextView; +import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.SimpleExoPlayer; +import com.google.android.exoplayer2.castdemo.DemoUtil.Sample; import com.google.android.exoplayer2.ext.cast.CastPlayer; -import com.google.android.exoplayer2.ui.PlaybackControlView; -import com.google.android.exoplayer2.ui.SimpleExoPlayerView; -import com.google.android.exoplayer2.util.Util; +import com.google.android.exoplayer2.ui.PlayerControlView; +import com.google.android.exoplayer2.ui.PlayerView; import com.google.android.gms.cast.framework.CastButtonFactory; +import com.google.android.gms.cast.framework.CastContext; /** * An activity that plays video using {@link SimpleExoPlayer} and {@link CastPlayer}. */ -public class MainActivity extends AppCompatActivity { +public class MainActivity extends AppCompatActivity implements OnClickListener, + PlayerManager.QueuePositionListener { - private SimpleExoPlayerView simpleExoPlayerView; - private PlaybackControlView castControlView; + private PlayerView localPlayerView; + private PlayerControlView castControlView; private PlayerManager playerManager; + private RecyclerView mediaQueueList; + private MediaQueueListAdapter mediaQueueListAdapter; + private CastContext castContext; // Activity lifecycle methods. @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); + // Getting the cast context later than onStart can cause device discovery not to take place. + castContext = CastContext.getSharedInstance(this); setContentView(R.layout.main_activity); - simpleExoPlayerView = (SimpleExoPlayerView) findViewById(R.id.player_view); - simpleExoPlayerView.requestFocus(); + localPlayerView = findViewById(R.id.local_player_view); + localPlayerView.requestFocus(); - castControlView = (PlaybackControlView) findViewById(R.id.cast_control_view); + castControlView = findViewById(R.id.cast_control_view); - ListView sampleList = (ListView) findViewById(R.id.sample_list); - sampleList.setAdapter(new SampleListAdapter()); - sampleList.setOnItemClickListener(new SampleClickListener()); + mediaQueueList = findViewById(R.id.sample_list); + ItemTouchHelper helper = new ItemTouchHelper(new RecyclerViewCallback()); + helper.attachToRecyclerView(mediaQueueList); + mediaQueueList.setLayoutManager(new LinearLayoutManager(this)); + mediaQueueList.setHasFixedSize(true); + mediaQueueListAdapter = new MediaQueueListAdapter(); + + findViewById(R.id.add_sample_button).setOnClickListener(this); } @Override public boolean onCreateOptionsMenu(Menu menu) { super.onCreateOptionsMenu(menu); getMenuInflater().inflate(R.menu.menu, menu); - CastButtonFactory.setUpMediaRouteButton(getApplicationContext(), menu, - R.id.media_route_menu_item); + CastButtonFactory.setUpMediaRouteButton(this, menu, R.id.media_route_menu_item); return true; } - @Override - public void onStart() { - super.onStart(); - if (Util.SDK_INT > 23) { - setupPlayerManager(); - } - } - @Override public void onResume() { super.onResume(); - if ((Util.SDK_INT <= 23)) { - setupPlayerManager(); - } + playerManager = + PlayerManager.createPlayerManager( + /* queuePositionListener= */ this, + localPlayerView, + castControlView, + /* context= */ this, + castContext); + mediaQueueList.setAdapter(mediaQueueListAdapter); } @Override public void onPause() { super.onPause(); - if (Util.SDK_INT <= 23) { - releasePlayerManager(); - } - } - - @Override - public void onStop() { - super.onStop(); - if (Util.SDK_INT > 23) { - releasePlayerManager(); - } + mediaQueueListAdapter.notifyItemRangeRemoved(0, mediaQueueListAdapter.getItemCount()); + mediaQueueList.setAdapter(null); + playerManager.release(); } // Activity input. @@ -108,43 +119,141 @@ public class MainActivity extends AppCompatActivity { return super.dispatchKeyEvent(event) || playerManager.dispatchKeyEvent(event); } + @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() + .show(); + } + + // PlayerManager.QueuePositionListener implementation. + + @Override + public void onQueuePositionChanged(int previousIndex, int newIndex) { + if (previousIndex != C.INDEX_UNSET) { + mediaQueueListAdapter.notifyItemChanged(previousIndex); + } + if (newIndex != C.INDEX_UNSET) { + mediaQueueListAdapter.notifyItemChanged(newIndex); + } + } + // Internal methods. - private void setupPlayerManager() { - playerManager = new PlayerManager(simpleExoPlayerView, castControlView, - getApplicationContext()); + 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( + new OnItemClickListener() { + + @Override + public void onItemClick(AdapterView parent, View view, int position, long id) { + playerManager.addItem(DemoUtil.SAMPLES.get(position)); + mediaQueueListAdapter.notifyItemInserted(playerManager.getMediaQueueSize() - 1); + } + }); + return dialogList; } - private void releasePlayerManager() { - playerManager.release(); - playerManager = null; - } + // Internal classes. - // User controls. + private class QueueItemViewHolder extends RecyclerView.ViewHolder implements OnClickListener { - private final class SampleListAdapter extends ArrayAdapter { + public final TextView textView; - public SampleListAdapter() { - super(getApplicationContext(), android.R.layout.simple_list_item_1, CastDemoUtil.SAMPLES); + public QueueItemViewHolder(TextView textView) { + super(textView); + this.textView = textView; + textView.setOnClickListener(this); } @Override - public View getView(int position, View convertView, ViewGroup parent) { - View view = super.getView(position, convertView, parent); - view.setBackgroundColor(Color.WHITE); - return view; + public void onClick(View v) { + playerManager.selectQueueItem(getAdapterPosition()); } } - private class SampleClickListener implements AdapterView.OnItemClickListener { + private class MediaQueueListAdapter extends RecyclerView.Adapter { @Override - public void onItemClick(AdapterView parent, View view, int position, long id) { - if (parent.getSelectedItemPosition() != position) { - CastDemoUtil.Sample currentSample = CastDemoUtil.SAMPLES.get(position); - playerManager.setCurrentSample(currentSample, 0, true); + public QueueItemViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { + TextView v = (TextView) LayoutInflater.from(parent.getContext()) + .inflate(android.R.layout.simple_list_item_1, parent, false); + return new QueueItemViewHolder(v); + } + + @Override + public void onBindViewHolder(QueueItemViewHolder holder, int position) { + TextView view = holder.textView; + view.setText(playerManager.getItem(position).name); + // TODO: Solve coloring using the theme's ColorStateList. + view.setTextColor(ColorUtils.setAlphaComponent(view.getCurrentTextColor(), + position == playerManager.getCurrentItemIndex() ? 255 : 100)); + } + + @Override + public int getItemCount() { + return playerManager.getMediaQueueSize(); + } + + } + + private class RecyclerViewCallback extends ItemTouchHelper.SimpleCallback { + + private int draggingFromPosition; + private int draggingToPosition; + + public RecyclerViewCallback() { + super(ItemTouchHelper.UP | ItemTouchHelper.DOWN, ItemTouchHelper.START | ItemTouchHelper.END); + draggingFromPosition = C.INDEX_UNSET; + draggingToPosition = C.INDEX_UNSET; + } + + @Override + public boolean onMove(RecyclerView list, RecyclerView.ViewHolder origin, + RecyclerView.ViewHolder target) { + int fromPosition = origin.getAdapterPosition(); + int toPosition = target.getAdapterPosition(); + if (draggingFromPosition == C.INDEX_UNSET) { + // A drag has started, but changes to the media queue will be reflected in clearView(). + draggingFromPosition = fromPosition; } + draggingToPosition = toPosition; + mediaQueueListAdapter.notifyItemMoved(fromPosition, toPosition); + return true; + } + + @Override + public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) { + int position = viewHolder.getAdapterPosition(); + if (playerManager.removeItem(position)) { + mediaQueueListAdapter.notifyItemRemoved(position); + } + } + + @Override + public void clearView(RecyclerView recyclerView, ViewHolder viewHolder) { + super.clearView(recyclerView, viewHolder); + if (draggingFromPosition != C.INDEX_UNSET) { + // A drag has ended. We reflect the media queue change in the player. + if (!playerManager.moveItem(draggingFromPosition, draggingToPosition)) { + // The move failed. The entire sequence of onMove calls since the drag started needs to be + // invalidated. + mediaQueueListAdapter.notifyDataSetChanged(); + } + } + draggingFromPosition = C.INDEX_UNSET; + draggingToPosition = C.INDEX_UNSET; + } + + } + + private static final class SampleListAdapter extends ArrayAdapter { + + public SampleListAdapter(Context context) { + super(context, android.R.layout.simple_list_item_1, DemoUtil.SAMPLES); } } 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 741df7eff1..63b18b0aa7 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 @@ -19,12 +19,20 @@ 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.DefaultRenderersFactory; import com.google.android.exoplayer2.ExoPlayerFactory; +import com.google.android.exoplayer2.Player; +import com.google.android.exoplayer2.Player.DefaultEventListener; +import com.google.android.exoplayer2.Player.DiscontinuityReason; +import com.google.android.exoplayer2.Player.TimelineChangeReason; import com.google.android.exoplayer2.RenderersFactory; import com.google.android.exoplayer2.SimpleExoPlayer; +import com.google.android.exoplayer2.Timeline; +import com.google.android.exoplayer2.Timeline.Period; +import com.google.android.exoplayer2.castdemo.DemoUtil.Sample; import com.google.android.exoplayer2.ext.cast.CastPlayer; -import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory; +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.dash.DashMediaSource; @@ -33,89 +41,217 @@ import com.google.android.exoplayer2.source.hls.HlsMediaSource; import com.google.android.exoplayer2.source.smoothstreaming.DefaultSsChunkSource; import com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource; import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; -import com.google.android.exoplayer2.ui.PlaybackControlView; -import com.google.android.exoplayer2.ui.SimpleExoPlayerView; +import com.google.android.exoplayer2.ui.PlayerControlView; +import com.google.android.exoplayer2.ui.PlayerView; import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter; import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory; +import com.google.android.gms.cast.MediaInfo; +import com.google.android.gms.cast.MediaMetadata; +import com.google.android.gms.cast.MediaQueueItem; import com.google.android.gms.cast.framework.CastContext; +import java.util.ArrayList; /** - * Manages players for the ExoPlayer/Cast integration app. + * Manages players and an internal media queue for the ExoPlayer/Cast demo app. */ -/* package */ final class PlayerManager implements CastPlayer.SessionAvailabilityListener { +/* package */ final class PlayerManager extends DefaultEventListener + implements CastPlayer.SessionAvailabilityListener { - private static final int PLAYBACK_REMOTE = 1; - private static final int PLAYBACK_LOCAL = 2; + /** + * Listener for changes in the media queue playback position. + */ + public interface QueuePositionListener { + + /** + * Called when the currently played item of the media queue changes. + */ + void onQueuePositionChanged(int previousIndex, int newIndex); + + } private static final String USER_AGENT = "ExoCastDemoPlayer"; private static final DefaultBandwidthMeter BANDWIDTH_METER = new DefaultBandwidthMeter(); private static final DefaultHttpDataSourceFactory DATA_SOURCE_FACTORY = new DefaultHttpDataSourceFactory(USER_AGENT, BANDWIDTH_METER); - private final SimpleExoPlayerView exoPlayerView; - private final PlaybackControlView castControlView; - private final CastContext castContext; + private final PlayerView localPlayerView; + private final PlayerControlView castControlView; private final SimpleExoPlayer exoPlayer; private final CastPlayer castPlayer; + private final ArrayList mediaQueue; + private final QueuePositionListener queuePositionListener; + private final ConcatenatingMediaSource concatenatingMediaSource; - private int playbackLocation; - private CastDemoUtil.Sample currentSample; + private boolean castMediaQueueCreationPending; + private int currentItemIndex; + private Player currentPlayer; /** - * @param exoPlayerView The {@link SimpleExoPlayerView} for local playback. - * @param castControlView The {@link PlaybackControlView} to control remote playback. + * @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 PlayerManager(SimpleExoPlayerView exoPlayerView, PlaybackControlView castControlView, - Context context) { - this.exoPlayerView = exoPlayerView; + public static PlayerManager createPlayerManager( + QueuePositionListener queuePositionListener, + PlayerView localPlayerView, + PlayerControlView castControlView, + Context context, + CastContext castContext) { + PlayerManager playerManager = + new PlayerManager( + queuePositionListener, localPlayerView, castControlView, context, castContext); + playerManager.init(); + return playerManager; + } + + private PlayerManager( + QueuePositionListener queuePositionListener, + PlayerView localPlayerView, + PlayerControlView castControlView, + Context context, + CastContext castContext) { + this.queuePositionListener = queuePositionListener; + this.localPlayerView = localPlayerView; this.castControlView = castControlView; - castContext = CastContext.getSharedInstance(context); + mediaQueue = new ArrayList<>(); + currentItemIndex = C.INDEX_UNSET; + concatenatingMediaSource = new ConcatenatingMediaSource(); DefaultTrackSelector trackSelector = new DefaultTrackSelector(BANDWIDTH_METER); - RenderersFactory renderersFactory = new DefaultRenderersFactory(context, null); + RenderersFactory renderersFactory = new DefaultRenderersFactory(context); exoPlayer = ExoPlayerFactory.newSimpleInstance(renderersFactory, trackSelector); - exoPlayerView.setPlayer(exoPlayer); + exoPlayer.addListener(this); + localPlayerView.setPlayer(exoPlayer); castPlayer = new CastPlayer(castContext); + castPlayer.addListener(this); castPlayer.setSessionAvailabilityListener(this); castControlView.setPlayer(castPlayer); + } - setPlaybackLocation(castPlayer.isCastSessionAvailable() ? PLAYBACK_REMOTE : PLAYBACK_LOCAL); + // 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); } /** - * Starts playback of the given sample at the given position. - * - * @param currentSample The {@link CastDemoUtil} to play. - * @param positionMs The position at which playback should start. - * @param playWhenReady Whether the player should proceed when ready to do so. + * Returns the index of the currently played item. */ - public void setCurrentSample(CastDemoUtil.Sample currentSample, long positionMs, - boolean playWhenReady) { - this.currentSample = currentSample; - if (playbackLocation == PLAYBACK_REMOTE) { - castPlayer.load(currentSample.name, currentSample.uri, currentSample.type, positionMs, - playWhenReady); - } else /* playbackLocation == PLAYBACK_LOCAL */ { - exoPlayer.setPlayWhenReady(playWhenReady); - exoPlayer.seekTo(positionMs); - exoPlayer.prepare(buildMediaSource(currentSample), true, true); + public int getCurrentItemIndex() { + return currentItemIndex; + } + + /** + * Appends {@code sample} to the media queue. + * + * @param sample The {@link Sample} to append. + */ + public void addItem(Sample sample) { + mediaQueue.add(sample); + concatenatingMediaSource.addMediaSource(buildMediaSource(sample)); + if (currentPlayer == castPlayer) { + castPlayer.addItems(buildMediaQueueItem(sample)); } } /** - * Dispatches a given {@link KeyEvent} to whichever view corresponds according to the current - * playback location. + * 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 Sample 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 (playbackLocation == PLAYBACK_REMOTE) { + if (currentPlayer == exoPlayer) { + return localPlayerView.dispatchKeyEvent(event); + } else /* currentPlayer == castPlayer */ { return castControlView.dispatchKeyEvent(event); - } else /* playbackLocation == PLAYBACK_REMOTE */ { - return exoPlayerView.dispatchKeyEvent(event); } } @@ -123,74 +259,167 @@ import com.google.android.gms.cast.framework.CastContext; * 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(); - exoPlayerView.setPlayer(null); + 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, Object manifest, @TimelineChangeReason int reason) { + updateCurrentItemIndex(); + if (timeline.isEmpty()) { + castMediaQueueCreationPending = true; + } + } + // CastPlayer.SessionAvailabilityListener implementation. @Override public void onCastSessionAvailable() { - setPlaybackLocation(PLAYBACK_REMOTE); + setCurrentPlayer(castPlayer); } @Override public void onCastSessionUnavailable() { - setPlaybackLocation(PLAYBACK_LOCAL); + setCurrentPlayer(exoPlayer); } // Internal methods. - private static MediaSource buildMediaSource(CastDemoUtil.Sample sample) { - Uri uri = Uri.parse(sample.uri); - switch (sample.type) { - case CastDemoUtil.MIME_TYPE_SS: - return new SsMediaSource(uri, DATA_SOURCE_FACTORY, - new DefaultSsChunkSource.Factory(DATA_SOURCE_FACTORY), null, null); - case CastDemoUtil.MIME_TYPE_DASH: - return new DashMediaSource(uri, DATA_SOURCE_FACTORY, - new DefaultDashChunkSource.Factory(DATA_SOURCE_FACTORY), null, null); - case CastDemoUtil.MIME_TYPE_HLS: - return new HlsMediaSource(uri, DATA_SOURCE_FACTORY, null, null); - case CastDemoUtil.MIME_TYPE_VIDEO_MP4: - return new ExtractorMediaSource(uri, DATA_SOURCE_FACTORY, new DefaultExtractorsFactory(), - null, null); - default: { - throw new IllegalStateException("Unsupported type: " + sample.type); - } - } + private void init() { + setCurrentPlayer(castPlayer.isCastSessionAvailable() ? castPlayer : exoPlayer); } - private void setPlaybackLocation(int playbackLocation) { - if (this.playbackLocation == playbackLocation) { + 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 (playbackLocation == PLAYBACK_LOCAL) { - exoPlayerView.setVisibility(View.VISIBLE); + if (currentPlayer == exoPlayer) { + localPlayerView.setVisibility(View.VISIBLE); castControlView.hide(); - } else { - exoPlayerView.setVisibility(View.GONE); + } else /* currentPlayer == castPlayer */ { + localPlayerView.setVisibility(View.GONE); castControlView.show(); } - long playbackPositionMs = 0; - boolean playWhenReady = true; - if (exoPlayer != null) { - playbackPositionMs = exoPlayer.getCurrentPosition(); - playWhenReady = exoPlayer.getPlayWhenReady(); - } else if (this.playbackLocation == PLAYBACK_REMOTE) { - playbackPositionMs = castPlayer.getCurrentPosition(); - playWhenReady = castPlayer.getPlayWhenReady(); + // 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.playbackLocation = playbackLocation; - if (currentSample != null) { - setCurrentSample(currentSample, playbackPositionMs, playWhenReady); + 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(DemoUtil.Sample sample) { + Uri uri = Uri.parse(sample.uri); + switch (sample.mimeType) { + case DemoUtil.MIME_TYPE_SS: + return new SsMediaSource.Factory( + new DefaultSsChunkSource.Factory(DATA_SOURCE_FACTORY), DATA_SOURCE_FACTORY) + .createMediaSource(uri); + case DemoUtil.MIME_TYPE_DASH: + return new DashMediaSource.Factory( + new DefaultDashChunkSource.Factory(DATA_SOURCE_FACTORY), DATA_SOURCE_FACTORY) + .createMediaSource(uri); + case DemoUtil.MIME_TYPE_HLS: + return new HlsMediaSource.Factory(DATA_SOURCE_FACTORY).createMediaSource(uri); + case DemoUtil.MIME_TYPE_VIDEO_MP4: + return new ExtractorMediaSource.Factory(DATA_SOURCE_FACTORY).createMediaSource(uri); + default: { + throw new IllegalStateException("Unsupported type: " + sample.mimeType); + } + } + } + + private static MediaQueueItem buildMediaQueueItem(DemoUtil.Sample sample) { + MediaMetadata movieMetadata = new MediaMetadata(MediaMetadata.MEDIA_TYPE_MOVIE); + movieMetadata.putString(MediaMetadata.KEY_TITLE, sample.name); + MediaInfo mediaInfo = new MediaInfo.Builder(sample.uri) + .setStreamType(MediaInfo.STREAM_TYPE_BUFFERED).setContentType(sample.mimeType) + .setMetadata(movieMetadata).build(); + return new MediaQueueItem.Builder(mediaInfo).build(); + } + } diff --git a/extensions/mediasession/src/main/res/values-be-rBY/strings.xml b/demos/cast/src/main/res/drawable/ic_add_circle_white_24dp.xml similarity index 55% rename from extensions/mediasession/src/main/res/values-be-rBY/strings.xml rename to demos/cast/src/main/res/drawable/ic_add_circle_white_24dp.xml index 2f05607235..5f3c8961ef 100644 --- a/extensions/mediasession/src/main/res/values-be-rBY/strings.xml +++ b/demos/cast/src/main/res/drawable/ic_add_circle_white_24dp.xml @@ -1,6 +1,5 @@ - - - - "Паўтарыць усё" - "Паўтараць ні" - "Паўтарыць адзін" - + + + diff --git a/demos/cast/src/main/res/layout/main_activity.xml b/demos/cast/src/main/res/layout/main_activity.xml index 7e39320e3b..01e48cdea7 100644 --- a/demos/cast/src/main/res/layout/main_activity.xml +++ b/demos/cast/src/main/res/layout/main_activity.xml @@ -13,29 +13,40 @@ See the License for the specific language governing permissions and limitations under the License. --> - - - - + + + + + + android:visibility="gone" + app:repeat_toggle_modes="all|one" + app:show_timeout="-1"/> diff --git a/extensions/mediasession/src/main/res/values-az-rAZ/strings.xml b/demos/cast/src/main/res/layout/sample_list.xml similarity index 57% rename from extensions/mediasession/src/main/res/values-az-rAZ/strings.xml rename to demos/cast/src/main/res/layout/sample_list.xml index 34408143fa..910db9e058 100644 --- a/extensions/mediasession/src/main/res/values-az-rAZ/strings.xml +++ b/demos/cast/src/main/res/layout/sample_list.xml @@ -1,6 +1,5 @@ - - - - "Bütün təkrarlayın" - "Təkrar bir" - "Heç bir təkrar" - + + + + + diff --git a/demos/cast/src/main/res/values/strings.xml b/demos/cast/src/main/res/values/strings.xml index 503892da27..3505c40400 100644 --- a/demos/cast/src/main/res/values/strings.xml +++ b/demos/cast/src/main/res/values/strings.xml @@ -16,10 +16,10 @@ - ExoCast Demo + Exo Cast Demo - ExoCast + Cast - DRM scheme not supported by this device. + Add samples diff --git a/demos/ima/README.md b/demos/ima/README.md new file mode 100644 index 0000000000..8002b56667 --- /dev/null +++ b/demos/ima/README.md @@ -0,0 +1,4 @@ +# IMA demo application # + +This folder contains a demo application that showcases ExoPlayer integration +with the IMA SDK. diff --git a/demos/ima/build.gradle b/demos/ima/build.gradle new file mode 100644 index 0000000000..35c2daf88e --- /dev/null +++ b/demos/ima/build.gradle @@ -0,0 +1,53 @@ +// Copyright (C) 2017 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +apply from: '../../constants.gradle' +apply plugin: 'com.android.application' + +android { + compileSdkVersion project.ext.compileSdkVersion + buildToolsVersion project.ext.buildToolsVersion + + defaultConfig { + versionName project.ext.releaseVersion + versionCode project.ext.releaseVersionCode + minSdkVersion 16 + targetSdkVersion project.ext.targetSdkVersion + } + + buildTypes { + release { + shrinkResources true + minifyEnabled true + proguardFiles getDefaultProguardFile('proguard-android.txt') + } + debug { + jniDebuggable = true + } + } + + lintOptions { + // The demo app does not have translations. + disable 'MissingTranslation' + } +} + +dependencies { + implementation project(modulePrefix + 'library-core') + implementation project(modulePrefix + 'library-ui') + implementation project(modulePrefix + 'library-dash') + implementation project(modulePrefix + 'library-hls') + implementation project(modulePrefix + 'library-smoothstreaming') + implementation project(modulePrefix + 'extension-ima') + implementation 'com.android.support:support-annotations:' + supportLibraryVersion +} diff --git a/library/dash/src/androidTest/AndroidManifest.xml b/demos/ima/src/main/AndroidManifest.xml similarity index 50% rename from library/dash/src/androidTest/AndroidManifest.xml rename to demos/ima/src/main/AndroidManifest.xml index 3a5b0c1fa2..50ad0c1b54 100644 --- a/library/dash/src/androidTest/AndroidManifest.xml +++ b/demos/ima/src/main/AndroidManifest.xml @@ -13,21 +13,25 @@ See the License for the specific language governing permissions and limitations under the License. --> - + package="com.google.android.exoplayer2.imademo"> - + + + + + + + + + + + - - - - 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 new file mode 100644 index 0000000000..9988108f32 --- /dev/null +++ b/demos/ima/src/main/java/com/google/android/exoplayer2/imademo/MainActivity.java @@ -0,0 +1,58 @@ +/* + * 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 new file mode 100644 index 0000000000..4fab1966fe --- /dev/null +++ b/demos/ima/src/main/java/com/google/android/exoplayer2/imademo/PlayerManager.java @@ -0,0 +1,153 @@ +/* + * 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.dash.DefaultDashChunkSource; +import com.google.android.exoplayer2.source.hls.HlsMediaSource; +import com.google.android.exoplayer2.source.smoothstreaming.DefaultSsChunkSource; +import com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource; +import com.google.android.exoplayer2.trackselection.AdaptiveTrackSelection; +import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; +import com.google.android.exoplayer2.trackselection.TrackSelection; +import com.google.android.exoplayer2.trackselection.TrackSelector; +import com.google.android.exoplayer2.ui.PlayerView; +import com.google.android.exoplayer2.upstream.BandwidthMeter; +import com.google.android.exoplayer2.upstream.DataSource; +import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter; +import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory; +import com.google.android.exoplayer2.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 manifestDataSourceFactory; + private final DataSource.Factory mediaDataSourceFactory; + + 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)); + manifestDataSourceFactory = + new DefaultDataSourceFactory( + context, Util.getUserAgent(context, context.getString(R.string.application_name))); + mediaDataSourceFactory = + new DefaultDataSourceFactory( + context, + Util.getUserAgent(context, context.getString(R.string.application_name)), + new DefaultBandwidthMeter()); + } + + public void init(Context context, PlayerView playerView) { + // Create a default track selector. + BandwidthMeter bandwidthMeter = new DefaultBandwidthMeter(); + TrackSelection.Factory videoTrackSelectionFactory = + new AdaptiveTrackSelection.Factory(bandwidthMeter); + 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(), + /* eventHandler= */ null, + /* eventListener= */ null); + + // 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( + new DefaultDashChunkSource.Factory(mediaDataSourceFactory), + manifestDataSourceFactory) + .createMediaSource(uri); + case C.TYPE_SS: + return new SsMediaSource.Factory( + new DefaultSsChunkSource.Factory(mediaDataSourceFactory), manifestDataSourceFactory) + .createMediaSource(uri); + case C.TYPE_HLS: + return new HlsMediaSource.Factory(mediaDataSourceFactory).createMediaSource(uri); + case C.TYPE_OTHER: + return new ExtractorMediaSource.Factory(mediaDataSourceFactory).createMediaSource(uri); + default: + throw new IllegalStateException("Unsupported type: " + type); + } + } + +} diff --git a/demos/ima/src/main/res/layout/main_activity.xml b/demos/ima/src/main/res/layout/main_activity.xml new file mode 100644 index 0000000000..f7ea5c9b88 --- /dev/null +++ b/demos/ima/src/main/res/layout/main_activity.xml @@ -0,0 +1,21 @@ + + + diff --git a/demos/ima/src/main/res/mipmap-hdpi/ic_launcher.png b/demos/ima/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000000..adaa93220e Binary files /dev/null and b/demos/ima/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/demos/ima/src/main/res/mipmap-mdpi/ic_launcher.png b/demos/ima/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000000..9b6f7d5e80 Binary files /dev/null and b/demos/ima/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/demos/ima/src/main/res/mipmap-xhdpi/ic_launcher.png b/demos/ima/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000000..2101026c9f Binary files /dev/null and b/demos/ima/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/demos/ima/src/main/res/mipmap-xxhdpi/ic_launcher.png b/demos/ima/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000000..223ec8bd11 Binary files /dev/null and b/demos/ima/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/demos/ima/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/demos/ima/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000000..698ed68c42 Binary files /dev/null and b/demos/ima/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/library/hls/src/androidTest/AndroidManifest.xml b/demos/ima/src/main/res/values/strings.xml similarity index 51% rename from library/hls/src/androidTest/AndroidManifest.xml rename to demos/ima/src/main/res/values/strings.xml index 1abbcad810..67a7f06f8b 100644 --- a/library/hls/src/androidTest/AndroidManifest.xml +++ b/demos/ima/src/main/res/values/strings.xml @@ -13,21 +13,12 @@ See the License for the specific language governing permissions and limitations under the License. --> + - + Exo IMA Demo - + - - - + - - - + diff --git a/demos/cast/src/main/res/values/styles.xml b/demos/ima/src/main/res/values/styles.xml similarity index 94% rename from demos/cast/src/main/res/values/styles.xml rename to demos/ima/src/main/res/values/styles.xml index 1484a68a68..1c78ad58df 100644 --- a/demos/cast/src/main/res/values/styles.xml +++ b/demos/ima/src/main/res/values/styles.xml @@ -16,6 +16,7 @@ diff --git a/demos/main/build.gradle b/demos/main/build.gradle index 099741d167..ce0992eb7a 100644 --- a/demos/main/build.gradle +++ b/demos/main/build.gradle @@ -19,6 +19,8 @@ android { buildToolsVersion project.ext.buildToolsVersion defaultConfig { + versionName project.ext.releaseVersion + versionCode project.ext.releaseVersionCode minSdkVersion 16 targetSdkVersion project.ext.targetSdkVersion } @@ -27,7 +29,10 @@ android { release { shrinkResources true minifyEnabled true - proguardFiles getDefaultProguardFile('proguard-android.txt') + proguardFiles = [ + "proguard-rules.txt", + getDefaultProguardFile('proguard-android.txt') + ] } debug { jniDebuggable = true @@ -52,14 +57,16 @@ android { } dependencies { - compile project(modulePrefix + 'library-core') - compile project(modulePrefix + 'library-dash') - compile project(modulePrefix + 'library-hls') - compile project(modulePrefix + 'library-smoothstreaming') - compile project(modulePrefix + 'library-ui') - withExtensionsCompile project(path: modulePrefix + 'extension-ffmpeg') - withExtensionsCompile project(path: modulePrefix + 'extension-flac') - withExtensionsCompile project(path: modulePrefix + 'extension-ima') - withExtensionsCompile project(path: modulePrefix + 'extension-opus') - withExtensionsCompile project(path: modulePrefix + 'extension-vp9') + implementation 'com.android.support:support-annotations:' + supportLibraryVersion + 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-ffmpeg') + withExtensionsImplementation project(path: modulePrefix + 'extension-flac') + withExtensionsImplementation project(path: modulePrefix + 'extension-ima') + withExtensionsImplementation project(path: modulePrefix + 'extension-opus') + withExtensionsImplementation project(path: modulePrefix + 'extension-vp9') + withExtensionsImplementation project(path: modulePrefix + 'extension-rtmp') } diff --git a/demos/main/proguard-rules.txt b/demos/main/proguard-rules.txt new file mode 100644 index 0000000000..cd201892ab --- /dev/null +++ b/demos/main/proguard-rules.txt @@ -0,0 +1,7 @@ +# Proguard rules specific to the main demo app. + +# Constructor accessed via reflection in PlayerActivity +-dontnote com.google.android.exoplayer2.ext.ima.ImaAdsLoader +-keepclassmembers class com.google.android.exoplayer2.ext.ima.ImaAdsLoader { + (android.content.Context, android.net.Uri); +} diff --git a/demos/main/src/main/AndroidManifest.xml b/demos/main/src/main/AndroidManifest.xml index 4f90cef623..3bedefc60e 100644 --- a/demos/main/src/main/AndroidManifest.xml +++ b/demos/main/src/main/AndroidManifest.xml @@ -15,15 +15,15 @@ --> + package="com.google.android.exoplayer2.demo"> + + - + + + + + + + + + + diff --git a/demos/main/src/main/assets/media.exolist.json b/demos/main/src/main/assets/media.exolist.json index 59d8259d37..0d26f196c1 100644 --- a/demos/main/src/main/assets/media.exolist.json +++ b/demos/main/src/main/assets/media.exolist.json @@ -344,11 +344,11 @@ "samples": [ { "name": "Apple 4x3 basic stream", - "uri": "https://devimages.apple.com.edgekey.net/streaming/examples/bipbop_4x3/bipbop_4x3_variant.m3u8" + "uri": "https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_4x3/bipbop_4x3_variant.m3u8" }, { "name": "Apple 16x9 basic stream", - "uri": "https://devimages.apple.com.edgekey.net/streaming/examples/bipbop_16x9/bipbop_16x9_variant.m3u8" + "uri": "https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_16x9/bipbop_16x9_variant.m3u8" }, { "name": "Apple master playlist advanced (TS)", @@ -360,11 +360,11 @@ }, { "name": "Apple TS media playlist", - "uri": "https://devimages.apple.com.edgekey.net/streaming/examples/bipbop_4x3/gear1/prog_index.m3u8" + "uri": "https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_4x3/gear1/prog_index.m3u8" }, { "name": "Apple AAC media playlist", - "uri": "https://devimages.apple.com.edgekey.net/streaming/examples/bipbop_4x3/gear0/prog_index.m3u8" + "uri": "https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_4x3/gear0/prog_index.m3u8" }, { "name": "Apple ID3 metadata", @@ -381,11 +381,11 @@ }, { "name": "Apple AAC 10s", - "uri": "https://devimages.apple.com.edgekey.net/streaming/examples/bipbop_4x3/gear0/fileSequence0.aac" + "uri": "https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_4x3/gear0/fileSequence0.aac" }, { "name": "Apple TS 10s", - "uri": "https://devimages.apple.com.edgekey.net/streaming/examples/bipbop_4x3/gear1/fileSequence0.ts" + "uri": "https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_4x3/gear1/fileSequence0.ts" }, { "name": "Android screens (Matroska)", @@ -540,7 +540,7 @@ { "name": "VMAP pre-, mid- and post-rolls, single ads", "uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv", - "ad_tag_uri": "https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/single_ad_samples&ciu_szs=300x250&impl=s&gdfp_req=1&env=vp&output=vast&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ct%3Dlinear&correlator=" + "ad_tag_uri": "https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/ad_rule_samples&ciu_szs=300x250&ad_rule=1&impl=s&gdfp_req=1&env=vp&output=vmap&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ar%3Dpremidpost&cmsid=496&vid=short_onecue&correlator=" }, { "name": "VMAP pre-roll single ad, mid-roll standard pod with 3 ads, post-roll single ad", @@ -566,6 +566,27 @@ "name": "VMAP pre-roll single ad, mid-roll standard pods with 5 ads every 10 seconds for 1:40, post-roll single ad", "uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv", "ad_tag_uri": "https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/ad_rule_samples&ciu_szs=300x250&ad_rule=1&impl=s&gdfp_req=1&env=vp&output=vmap&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ar%3Dpremidpostlongpod&cmsid=496&vid=short_tencue&correlator=" + }, + { + "name": "VMAP empty midroll", + "uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv", + "ad_tag_uri": "http://vastsynthesizer.appspot.com/empty-midroll" + }, + { + "name": "VMAP full, empty, full midrolls", + "uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv", + "ad_tag_uri": "http://vastsynthesizer.appspot.com/empty-midroll-2" + } + ] + }, + { + "name": "ABR", + "samples": [ + { + "name": "Random ABR - Google Glass (MP4,H264)", + "uri": "http://www.youtube.com/api/manifest/dash/id/bf5bb2419360daf1/source/youtube?as=fmp4_audio_clear,fmp4_sd_hd_clear&sparams=ip,ipbits,expire,source,id,as&ip=0.0.0.0&ipbits=0&expire=19000000000&signature=51AF5F39AB0CEC3E5497CD9C900EBFEAECCCB5C7.8506521BFC350652163895D4C26DEE124209AA9E&key=ik0", + "extension": "mpd", + "abr_algorithm": "random" } ] } 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 b5db4c018d..b5c127d2e3 100644 --- a/demos/main/src/main/java/com/google/android/exoplayer2/demo/DemoApplication.java +++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/DemoApplication.java @@ -16,37 +16,133 @@ package com.google.android.exoplayer2.demo; import android.app.Application; +import com.google.android.exoplayer2.offline.DownloadAction.Deserializer; +import com.google.android.exoplayer2.offline.DownloadManager; +import com.google.android.exoplayer2.offline.DownloaderConstructorHelper; +import com.google.android.exoplayer2.offline.ProgressiveDownloadAction; +import com.google.android.exoplayer2.source.dash.offline.DashDownloadAction; +import com.google.android.exoplayer2.source.hls.offline.HlsDownloadAction; +import com.google.android.exoplayer2.source.smoothstreaming.offline.SsDownloadAction; import com.google.android.exoplayer2.upstream.DataSource; -import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter; import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory; import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory; +import com.google.android.exoplayer2.upstream.FileDataSourceFactory; import com.google.android.exoplayer2.upstream.HttpDataSource; +import com.google.android.exoplayer2.upstream.TransferListener; +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.Util; +import java.io.File; /** * Placeholder application to facilitate overriding Application methods for debugging and testing. */ public class DemoApplication extends Application { + 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; + private static final Deserializer[] DOWNLOAD_DESERIALIZERS = + new Deserializer[] { + DashDownloadAction.DESERIALIZER, + HlsDownloadAction.DESERIALIZER, + SsDownloadAction.DESERIALIZER, + ProgressiveDownloadAction.DESERIALIZER + }; + protected String userAgent; + private File downloadDirectory; + private Cache downloadCache; + private DownloadManager downloadManager; + private DownloadTracker downloadTracker; + @Override public void onCreate() { super.onCreate(); userAgent = Util.getUserAgent(this, "ExoPlayerDemo"); } - public DataSource.Factory buildDataSourceFactory(DefaultBandwidthMeter bandwidthMeter) { - return new DefaultDataSourceFactory(this, bandwidthMeter, - buildHttpDataSourceFactory(bandwidthMeter)); + /** Returns a {@link DataSource.Factory}. */ + public DataSource.Factory buildDataSourceFactory(TransferListener listener) { + DefaultDataSourceFactory upstreamFactory = + new DefaultDataSourceFactory(this, listener, buildHttpDataSourceFactory(listener)); + return buildReadOnlyCacheDataSource(upstreamFactory, getDownloadCache()); } - public HttpDataSource.Factory buildHttpDataSourceFactory(DefaultBandwidthMeter bandwidthMeter) { - return new DefaultHttpDataSourceFactory(userAgent, bandwidthMeter); + /** Returns a {@link HttpDataSource.Factory}. */ + public HttpDataSource.Factory buildHttpDataSourceFactory( + TransferListener listener) { + return new DefaultHttpDataSourceFactory(userAgent, listener); } + /** Returns whether extension renderers should be used. */ public boolean useExtensionRenderers() { - return BuildConfig.FLAVOR.equals("withExtensions"); + return "withExtensions".equals(BuildConfig.FLAVOR); } + public DownloadManager getDownloadManager() { + initDownloadManager(); + return downloadManager; + } + + public DownloadTracker getDownloadTracker() { + initDownloadManager(); + return downloadTracker; + } + + private synchronized void initDownloadManager() { + if (downloadManager == null) { + DownloaderConstructorHelper downloaderConstructorHelper = + new DownloaderConstructorHelper( + getDownloadCache(), buildHttpDataSourceFactory(/* listener= */ null)); + downloadManager = + new DownloadManager( + downloaderConstructorHelper, + MAX_SIMULTANEOUS_DOWNLOADS, + DownloadManager.DEFAULT_MIN_RETRY_COUNT, + new File(getDownloadDirectory(), DOWNLOAD_ACTION_FILE), + DOWNLOAD_DESERIALIZERS); + downloadTracker = + new DownloadTracker( + /* context= */ this, + buildDataSourceFactory(/* listener= */ null), + new File(getDownloadDirectory(), DOWNLOAD_TRACKER_ACTION_FILE), + DOWNLOAD_DESERIALIZERS); + downloadManager.addListener(downloadTracker); + } + } + + private synchronized Cache getDownloadCache() { + if (downloadCache == null) { + File downloadContentDirectory = new File(getDownloadDirectory(), DOWNLOAD_CONTENT_DIRECTORY); + downloadCache = new SimpleCache(downloadContentDirectory, new NoOpCacheEvictor()); + } + return downloadCache; + } + + private File getDownloadDirectory() { + if (downloadDirectory == null) { + downloadDirectory = getExternalFilesDir(null); + if (downloadDirectory == null) { + downloadDirectory = getFilesDir(); + } + } + return downloadDirectory; + } + + private static CacheDataSourceFactory buildReadOnlyCacheDataSource( + DefaultDataSourceFactory upstreamFactory, Cache cache) { + return new CacheDataSourceFactory( + cache, + upstreamFactory, + new FileDataSourceFactory(), + /* cacheWriteDataSinkFactory= */ null, + CacheDataSource.FLAG_IGNORE_CACHE_ON_ERROR, + /* eventListener= */ null); + } } diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/DemoDownloadService.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/DemoDownloadService.java new file mode 100644 index 0000000000..7d1ab16ce4 --- /dev/null +++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/DemoDownloadService.java @@ -0,0 +1,89 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2.demo; + +import android.app.Notification; +import com.google.android.exoplayer2.offline.DownloadManager; +import com.google.android.exoplayer2.offline.DownloadManager.TaskState; +import com.google.android.exoplayer2.offline.DownloadService; +import com.google.android.exoplayer2.scheduler.PlatformScheduler; +import com.google.android.exoplayer2.ui.DownloadNotificationUtil; +import com.google.android.exoplayer2.util.NotificationUtil; +import com.google.android.exoplayer2.util.Util; + +/** A service for downloading media. */ +public class DemoDownloadService extends DownloadService { + + private static final String CHANNEL_ID = "download_channel"; + private static final int JOB_ID = 1; + private static final int FOREGROUND_NOTIFICATION_ID = 1; + + public DemoDownloadService() { + super( + FOREGROUND_NOTIFICATION_ID, + DEFAULT_FOREGROUND_NOTIFICATION_UPDATE_INTERVAL, + CHANNEL_ID, + R.string.exo_download_notification_channel_name); + } + + @Override + protected DownloadManager getDownloadManager() { + return ((DemoApplication) getApplication()).getDownloadManager(); + } + + @Override + protected PlatformScheduler getScheduler() { + return Util.SDK_INT >= 21 ? new PlatformScheduler(this, JOB_ID) : null; + } + + @Override + protected Notification getForegroundNotification(TaskState[] taskStates) { + return DownloadNotificationUtil.buildProgressNotification( + /* context= */ this, + R.drawable.exo_controls_play, + CHANNEL_ID, + /* contentIntent= */ null, + /* message= */ null, + taskStates); + } + + @Override + protected void onTaskStateChanged(TaskState taskState) { + if (taskState.action.isRemoveAction) { + return; + } + Notification notification = null; + if (taskState.state == TaskState.STATE_COMPLETED) { + notification = + DownloadNotificationUtil.buildDownloadCompletedNotification( + /* context= */ this, + R.drawable.exo_controls_play, + CHANNEL_ID, + /* contentIntent= */ null, + Util.fromUtf8Bytes(taskState.action.data)); + } else if (taskState.state == TaskState.STATE_FAILED) { + notification = + DownloadNotificationUtil.buildDownloadFailedNotification( + /* context= */ this, + R.drawable.exo_controls_play, + CHANNEL_ID, + /* contentIntent= */ null, + Util.fromUtf8Bytes(taskState.action.data)); + } + int notificationId = FOREGROUND_NOTIFICATION_ID + 1 + taskState.taskId; + NotificationUtil.setNotification(this, notificationId, notification); + } +} diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/DemoUtil.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/DemoUtil.java deleted file mode 100644 index f9e9c34158..0000000000 --- a/demos/main/src/main/java/com/google/android/exoplayer2/demo/DemoUtil.java +++ /dev/null @@ -1,86 +0,0 @@ -/* - * Copyright (C) 2017 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.google.android.exoplayer2.demo; - -import android.text.TextUtils; -import com.google.android.exoplayer2.Format; -import com.google.android.exoplayer2.util.MimeTypes; -import java.util.Locale; - -/** - * Utility methods for demo application. - */ -/*package*/ final class DemoUtil { - - /** - * Builds a track name for display. - * - * @param format {@link Format} of the track. - * @return a generated name specific to the track. - */ - public static String buildTrackName(Format format) { - String trackName; - if (MimeTypes.isVideo(format.sampleMimeType)) { - trackName = joinWithSeparator(joinWithSeparator(joinWithSeparator( - buildResolutionString(format), buildBitrateString(format)), buildTrackIdString(format)), - buildSampleMimeTypeString(format)); - } else if (MimeTypes.isAudio(format.sampleMimeType)) { - trackName = joinWithSeparator(joinWithSeparator(joinWithSeparator(joinWithSeparator( - buildLanguageString(format), buildAudioPropertyString(format)), - buildBitrateString(format)), buildTrackIdString(format)), - buildSampleMimeTypeString(format)); - } else { - trackName = joinWithSeparator(joinWithSeparator(joinWithSeparator(buildLanguageString(format), - buildBitrateString(format)), buildTrackIdString(format)), - buildSampleMimeTypeString(format)); - } - return trackName.length() == 0 ? "unknown" : trackName; - } - - private static String buildResolutionString(Format format) { - return format.width == Format.NO_VALUE || format.height == Format.NO_VALUE - ? "" : format.width + "x" + format.height; - } - - private static String buildAudioPropertyString(Format format) { - return format.channelCount == Format.NO_VALUE || format.sampleRate == Format.NO_VALUE - ? "" : format.channelCount + "ch, " + format.sampleRate + "Hz"; - } - - private static String buildLanguageString(Format format) { - return TextUtils.isEmpty(format.language) || "und".equals(format.language) ? "" - : format.language; - } - - private static String buildBitrateString(Format format) { - return format.bitrate == Format.NO_VALUE ? "" - : String.format(Locale.US, "%.2fMbit", format.bitrate / 1000000f); - } - - private static String joinWithSeparator(String first, String second) { - return first.length() == 0 ? second : (second.length() == 0 ? first : first + ", " + second); - } - - private static String buildTrackIdString(Format format) { - return format.id == null ? "" : ("id:" + format.id); - } - - private static String buildSampleMimeTypeString(Format format) { - return format.sampleMimeType == null ? "" : format.sampleMimeType; - } - - private DemoUtil() {} -} diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/DownloadTracker.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/DownloadTracker.java new file mode 100644 index 0000000000..b4bce01c7a --- /dev/null +++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/DownloadTracker.java @@ -0,0 +1,303 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2.demo; + +import android.app.Activity; +import android.app.AlertDialog; +import android.content.Context; +import android.content.DialogInterface; +import android.net.Uri; +import android.os.Handler; +import android.os.HandlerThread; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.ArrayAdapter; +import android.widget.ListView; +import android.widget.Toast; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.offline.ActionFile; +import com.google.android.exoplayer2.offline.DownloadAction; +import com.google.android.exoplayer2.offline.DownloadHelper; +import com.google.android.exoplayer2.offline.DownloadManager; +import com.google.android.exoplayer2.offline.DownloadManager.TaskState; +import com.google.android.exoplayer2.offline.DownloadService; +import com.google.android.exoplayer2.offline.ProgressiveDownloadHelper; +import com.google.android.exoplayer2.offline.SegmentDownloadAction; +import com.google.android.exoplayer2.offline.TrackKey; +import com.google.android.exoplayer2.source.TrackGroup; +import com.google.android.exoplayer2.source.TrackGroupArray; +import com.google.android.exoplayer2.source.dash.offline.DashDownloadHelper; +import com.google.android.exoplayer2.source.hls.offline.HlsDownloadHelper; +import com.google.android.exoplayer2.source.smoothstreaming.offline.SsDownloadHelper; +import com.google.android.exoplayer2.ui.DefaultTrackNameProvider; +import com.google.android.exoplayer2.ui.TrackNameProvider; +import com.google.android.exoplayer2.upstream.DataSource; +import com.google.android.exoplayer2.util.Util; +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.concurrent.CopyOnWriteArraySet; + +/** + * Tracks media that has been downloaded. + * + *

Tracked downloads are persisted using an {@link ActionFile}, however in a real application + * it's expected that state will be stored directly in the application's media database, so that it + * can be queried efficiently together with other information about the media. + */ +public class DownloadTracker implements DownloadManager.Listener { + + /** Listens for changes in the tracked downloads. */ + public interface Listener { + + /** Called when the tracked downloads changed. */ + void onDownloadsChanged(); + } + + private static final String TAG = "DownloadTracker"; + + private final Context context; + private final DataSource.Factory dataSourceFactory; + private final TrackNameProvider trackNameProvider; + private final CopyOnWriteArraySet listeners; + private final HashMap trackedDownloadStates; + private final ActionFile actionFile; + private final Handler actionFileWriteHandler; + + public DownloadTracker( + Context context, + DataSource.Factory dataSourceFactory, + File actionFile, + DownloadAction.Deserializer[] deserializers) { + this.context = context.getApplicationContext(); + this.dataSourceFactory = dataSourceFactory; + this.actionFile = new ActionFile(actionFile); + trackNameProvider = new DefaultTrackNameProvider(context.getResources()); + listeners = new CopyOnWriteArraySet<>(); + trackedDownloadStates = new HashMap<>(); + HandlerThread actionFileWriteThread = new HandlerThread("DownloadTracker"); + actionFileWriteThread.start(); + actionFileWriteHandler = new Handler(actionFileWriteThread.getLooper()); + loadTrackedActions(deserializers); + } + + public void addListener(Listener listener) { + listeners.add(listener); + } + + public void removeListener(Listener listener) { + listeners.remove(listener); + } + + public boolean isDownloaded(Uri uri) { + return trackedDownloadStates.containsKey(uri); + } + + @SuppressWarnings("unchecked") + public List getOfflineStreamKeys(Uri uri) { + if (!trackedDownloadStates.containsKey(uri)) { + return Collections.emptyList(); + } + DownloadAction action = trackedDownloadStates.get(uri); + if (action instanceof SegmentDownloadAction) { + return ((SegmentDownloadAction) action).keys; + } + return Collections.emptyList(); + } + + public void toggleDownload(Activity activity, String name, Uri uri, String extension) { + if (isDownloaded(uri)) { + DownloadAction removeAction = + getDownloadHelper(uri, extension).getRemoveAction(Util.getUtf8Bytes(name)); + startServiceWithAction(removeAction); + } else { + StartDownloadDialogHelper helper = + new StartDownloadDialogHelper(activity, getDownloadHelper(uri, extension), name); + helper.prepare(); + } + } + + // DownloadManager.Listener + + @Override + public void onInitialized(DownloadManager downloadManager) { + // Do nothing. + } + + @Override + public void onTaskStateChanged(DownloadManager downloadManager, TaskState taskState) { + DownloadAction action = taskState.action; + Uri uri = action.uri; + if ((action.isRemoveAction && taskState.state == TaskState.STATE_COMPLETED) + || (!action.isRemoveAction && taskState.state == TaskState.STATE_FAILED)) { + // A download has been removed, or has failed. Stop tracking it. + if (trackedDownloadStates.remove(uri) != null) { + handleTrackedDownloadStatesChanged(); + } + } + } + + @Override + public void onIdle(DownloadManager downloadManager) { + // Do nothing. + } + + // Internal methods + + private void loadTrackedActions(DownloadAction.Deserializer[] deserializers) { + try { + DownloadAction[] allActions = actionFile.load(deserializers); + for (DownloadAction action : allActions) { + trackedDownloadStates.put(action.uri, action); + } + } catch (IOException e) { + Log.e(TAG, "Failed to load tracked actions", e); + } + } + + private void handleTrackedDownloadStatesChanged() { + for (Listener listener : listeners) { + listener.onDownloadsChanged(); + } + final DownloadAction[] actions = trackedDownloadStates.values().toArray(new DownloadAction[0]); + actionFileWriteHandler.post( + new Runnable() { + @Override + public void run() { + try { + actionFile.store(actions); + } catch (IOException e) { + Log.e(TAG, "Failed to store tracked actions", e); + } + } + }); + } + + private void startDownload(DownloadAction action) { + if (trackedDownloadStates.containsKey(action.uri)) { + // This content is already being downloaded. Do nothing. + return; + } + trackedDownloadStates.put(action.uri, action); + handleTrackedDownloadStatesChanged(); + startServiceWithAction(action); + } + + private void startServiceWithAction(DownloadAction action) { + DownloadService.startWithAction(context, DemoDownloadService.class, action, false); + } + + private DownloadHelper getDownloadHelper(Uri uri, String extension) { + int type = Util.inferContentType(uri, extension); + switch (type) { + case C.TYPE_DASH: + return new DashDownloadHelper(uri, dataSourceFactory); + case C.TYPE_SS: + return new SsDownloadHelper(uri, dataSourceFactory); + case C.TYPE_HLS: + return new HlsDownloadHelper(uri, dataSourceFactory); + case C.TYPE_OTHER: + return new ProgressiveDownloadHelper(uri); + default: + throw new IllegalStateException("Unsupported type: " + type); + } + } + + private final class StartDownloadDialogHelper + implements DownloadHelper.Callback, DialogInterface.OnClickListener { + + private final DownloadHelper downloadHelper; + private final String name; + + private final AlertDialog.Builder builder; + private final View dialogView; + private final List trackKeys; + private final ArrayAdapter trackTitles; + private final ListView representationList; + + public StartDownloadDialogHelper( + Activity activity, DownloadHelper downloadHelper, String name) { + this.downloadHelper = downloadHelper; + this.name = name; + builder = + new AlertDialog.Builder(activity) + .setTitle(R.string.exo_download_description) + .setPositiveButton(android.R.string.ok, this) + .setNegativeButton(android.R.string.cancel, null); + + // Inflate with the builder's context to ensure the correct style is used. + LayoutInflater dialogInflater = LayoutInflater.from(builder.getContext()); + dialogView = dialogInflater.inflate(R.layout.start_download_dialog, null); + + trackKeys = new ArrayList<>(); + trackTitles = + new ArrayAdapter<>( + builder.getContext(), android.R.layout.simple_list_item_multiple_choice); + representationList = dialogView.findViewById(R.id.representation_list); + representationList.setChoiceMode(ListView.CHOICE_MODE_MULTIPLE); + representationList.setAdapter(trackTitles); + } + + public void prepare() { + downloadHelper.prepare(this); + } + + @Override + public void onPrepared(DownloadHelper helper) { + for (int i = 0; i < downloadHelper.getPeriodCount(); i++) { + TrackGroupArray trackGroups = downloadHelper.getTrackGroups(i); + for (int j = 0; j < trackGroups.length; j++) { + TrackGroup trackGroup = trackGroups.get(j); + for (int k = 0; k < trackGroup.length; k++) { + trackKeys.add(new TrackKey(i, j, k)); + trackTitles.add(trackNameProvider.getTrackName(trackGroup.getFormat(k))); + } + } + if (!trackKeys.isEmpty()) { + builder.setView(dialogView); + } + builder.create().show(); + } + } + + @Override + public void onPrepareError(DownloadHelper helper, IOException e) { + Toast.makeText( + context.getApplicationContext(), R.string.download_start_error, Toast.LENGTH_LONG) + .show(); + } + + @Override + public void onClick(DialogInterface dialog, int which) { + ArrayList selectedTrackKeys = new ArrayList<>(); + for (int i = 0; i < representationList.getChildCount(); i++) { + if (representationList.isItemChecked(i)) { + selectedTrackKeys.add(trackKeys.get(i)); + } + } + if (!selectedTrackKeys.isEmpty() || trackKeys.isEmpty()) { + // We have selected keys, or we're dealing with single stream content. + DownloadAction downloadAction = + downloadHelper.getDownloadAction(Util.getUtf8Bytes(name), selectedTrackKeys); + startDownload(downloadAction); + } + } + } +} diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java deleted file mode 100644 index 533306e0a2..0000000000 --- a/demos/main/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java +++ /dev/null @@ -1,487 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.google.android.exoplayer2.demo; - -import android.os.SystemClock; -import android.util.Log; -import android.view.Surface; -import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.ExoPlaybackException; -import com.google.android.exoplayer2.Format; -import com.google.android.exoplayer2.PlaybackParameters; -import com.google.android.exoplayer2.Player; -import com.google.android.exoplayer2.RendererCapabilities; -import com.google.android.exoplayer2.Timeline; -import com.google.android.exoplayer2.audio.AudioRendererEventListener; -import com.google.android.exoplayer2.decoder.DecoderCounters; -import com.google.android.exoplayer2.drm.DefaultDrmSessionManager; -import com.google.android.exoplayer2.metadata.Metadata; -import com.google.android.exoplayer2.metadata.MetadataOutput; -import com.google.android.exoplayer2.metadata.emsg.EventMessage; -import com.google.android.exoplayer2.metadata.id3.ApicFrame; -import com.google.android.exoplayer2.metadata.id3.CommentFrame; -import com.google.android.exoplayer2.metadata.id3.GeobFrame; -import com.google.android.exoplayer2.metadata.id3.Id3Frame; -import com.google.android.exoplayer2.metadata.id3.PrivFrame; -import com.google.android.exoplayer2.metadata.id3.TextInformationFrame; -import com.google.android.exoplayer2.metadata.id3.UrlLinkFrame; -import com.google.android.exoplayer2.source.AdaptiveMediaSourceEventListener; -import com.google.android.exoplayer2.source.ExtractorMediaSource; -import com.google.android.exoplayer2.source.TrackGroup; -import com.google.android.exoplayer2.source.TrackGroupArray; -import com.google.android.exoplayer2.trackselection.MappingTrackSelector; -import com.google.android.exoplayer2.trackselection.MappingTrackSelector.MappedTrackInfo; -import com.google.android.exoplayer2.trackselection.TrackSelection; -import com.google.android.exoplayer2.trackselection.TrackSelectionArray; -import com.google.android.exoplayer2.upstream.DataSpec; -import com.google.android.exoplayer2.video.VideoRendererEventListener; -import java.io.IOException; -import java.text.NumberFormat; -import java.util.Locale; - -/** - * Logs player events using {@link Log}. - */ -/* package */ final class EventLogger implements Player.EventListener, MetadataOutput, - AudioRendererEventListener, VideoRendererEventListener, AdaptiveMediaSourceEventListener, - ExtractorMediaSource.EventListener, DefaultDrmSessionManager.EventListener { - - private static final String TAG = "EventLogger"; - private static final int MAX_TIMELINE_ITEM_LINES = 3; - private static final NumberFormat TIME_FORMAT; - static { - TIME_FORMAT = NumberFormat.getInstance(Locale.US); - TIME_FORMAT.setMinimumFractionDigits(2); - TIME_FORMAT.setMaximumFractionDigits(2); - TIME_FORMAT.setGroupingUsed(false); - } - - private final MappingTrackSelector trackSelector; - private final Timeline.Window window; - private final Timeline.Period period; - private final long startTimeMs; - - public EventLogger(MappingTrackSelector trackSelector) { - this.trackSelector = trackSelector; - window = new Timeline.Window(); - period = new Timeline.Period(); - startTimeMs = SystemClock.elapsedRealtime(); - } - - // Player.EventListener - - @Override - public void onLoadingChanged(boolean isLoading) { - Log.d(TAG, "loading [" + isLoading + "]"); - } - - @Override - public void onPlayerStateChanged(boolean playWhenReady, int state) { - Log.d(TAG, "state [" + getSessionTimeString() + ", " + playWhenReady + ", " - + getStateString(state) + "]"); - } - - @Override - public void onRepeatModeChanged(@Player.RepeatMode int repeatMode) { - Log.d(TAG, "repeatMode [" + getRepeatModeString(repeatMode) + "]"); - } - - @Override - public void onShuffleModeEnabledChanged(boolean shuffleModeEnabled) { - Log.d(TAG, "shuffleModeEnabled [" + shuffleModeEnabled + "]"); - } - - @Override - public void onPositionDiscontinuity() { - Log.d(TAG, "positionDiscontinuity"); - } - - @Override - public void onPlaybackParametersChanged(PlaybackParameters playbackParameters) { - Log.d(TAG, "playbackParameters " + String.format( - "[speed=%.2f, pitch=%.2f]", playbackParameters.speed, playbackParameters.pitch)); - } - - @Override - public void onTimelineChanged(Timeline timeline, Object manifest) { - int periodCount = timeline.getPeriodCount(); - int windowCount = timeline.getWindowCount(); - Log.d(TAG, "sourceInfo [periodCount=" + periodCount + ", windowCount=" + windowCount); - for (int i = 0; i < Math.min(periodCount, MAX_TIMELINE_ITEM_LINES); i++) { - timeline.getPeriod(i, period); - Log.d(TAG, " " + "period [" + getTimeString(period.getDurationMs()) + "]"); - } - if (periodCount > MAX_TIMELINE_ITEM_LINES) { - Log.d(TAG, " ..."); - } - for (int i = 0; i < Math.min(windowCount, MAX_TIMELINE_ITEM_LINES); i++) { - timeline.getWindow(i, window); - Log.d(TAG, " " + "window [" + getTimeString(window.getDurationMs()) + ", " - + window.isSeekable + ", " + window.isDynamic + "]"); - } - if (windowCount > MAX_TIMELINE_ITEM_LINES) { - Log.d(TAG, " ..."); - } - Log.d(TAG, "]"); - } - - @Override - public void onPlayerError(ExoPlaybackException e) { - Log.e(TAG, "playerFailed [" + getSessionTimeString() + "]", e); - } - - @Override - public void onTracksChanged(TrackGroupArray ignored, TrackSelectionArray trackSelections) { - MappedTrackInfo mappedTrackInfo = trackSelector.getCurrentMappedTrackInfo(); - if (mappedTrackInfo == null) { - Log.d(TAG, "Tracks []"); - return; - } - Log.d(TAG, "Tracks ["); - // Log tracks associated to renderers. - for (int rendererIndex = 0; rendererIndex < mappedTrackInfo.length; rendererIndex++) { - TrackGroupArray rendererTrackGroups = mappedTrackInfo.getTrackGroups(rendererIndex); - TrackSelection trackSelection = trackSelections.get(rendererIndex); - if (rendererTrackGroups.length > 0) { - Log.d(TAG, " Renderer:" + rendererIndex + " ["); - for (int groupIndex = 0; groupIndex < rendererTrackGroups.length; groupIndex++) { - TrackGroup trackGroup = rendererTrackGroups.get(groupIndex); - String adaptiveSupport = getAdaptiveSupportString(trackGroup.length, - mappedTrackInfo.getAdaptiveSupport(rendererIndex, groupIndex, false)); - Log.d(TAG, " Group:" + groupIndex + ", adaptive_supported=" + adaptiveSupport + " ["); - for (int trackIndex = 0; trackIndex < trackGroup.length; trackIndex++) { - String status = getTrackStatusString(trackSelection, trackGroup, trackIndex); - String formatSupport = getFormatSupportString( - mappedTrackInfo.getTrackFormatSupport(rendererIndex, groupIndex, trackIndex)); - Log.d(TAG, " " + status + " Track:" + trackIndex + ", " - + Format.toLogString(trackGroup.getFormat(trackIndex)) - + ", supported=" + formatSupport); - } - Log.d(TAG, " ]"); - } - // Log metadata for at most one of the tracks selected for the renderer. - if (trackSelection != null) { - for (int selectionIndex = 0; selectionIndex < trackSelection.length(); selectionIndex++) { - Metadata metadata = trackSelection.getFormat(selectionIndex).metadata; - if (metadata != null) { - Log.d(TAG, " Metadata ["); - printMetadata(metadata, " "); - Log.d(TAG, " ]"); - break; - } - } - } - Log.d(TAG, " ]"); - } - } - // Log tracks not associated with a renderer. - TrackGroupArray unassociatedTrackGroups = mappedTrackInfo.getUnassociatedTrackGroups(); - if (unassociatedTrackGroups.length > 0) { - Log.d(TAG, " Renderer:None ["); - for (int groupIndex = 0; groupIndex < unassociatedTrackGroups.length; groupIndex++) { - Log.d(TAG, " Group:" + groupIndex + " ["); - TrackGroup trackGroup = unassociatedTrackGroups.get(groupIndex); - for (int trackIndex = 0; trackIndex < trackGroup.length; trackIndex++) { - String status = getTrackStatusString(false); - String formatSupport = getFormatSupportString( - RendererCapabilities.FORMAT_UNSUPPORTED_TYPE); - Log.d(TAG, " " + status + " Track:" + trackIndex + ", " - + Format.toLogString(trackGroup.getFormat(trackIndex)) - + ", supported=" + formatSupport); - } - Log.d(TAG, " ]"); - } - Log.d(TAG, " ]"); - } - Log.d(TAG, "]"); - } - - // MetadataOutput - - @Override - public void onMetadata(Metadata metadata) { - Log.d(TAG, "onMetadata ["); - printMetadata(metadata, " "); - Log.d(TAG, "]"); - } - - // AudioRendererEventListener - - @Override - public void onAudioEnabled(DecoderCounters counters) { - Log.d(TAG, "audioEnabled [" + getSessionTimeString() + "]"); - } - - @Override - public void onAudioSessionId(int audioSessionId) { - Log.d(TAG, "audioSessionId [" + audioSessionId + "]"); - } - - @Override - public void onAudioDecoderInitialized(String decoderName, long elapsedRealtimeMs, - long initializationDurationMs) { - Log.d(TAG, "audioDecoderInitialized [" + getSessionTimeString() + ", " + decoderName + "]"); - } - - @Override - public void onAudioInputFormatChanged(Format format) { - Log.d(TAG, "audioFormatChanged [" + getSessionTimeString() + ", " + Format.toLogString(format) - + "]"); - } - - @Override - public void onAudioDisabled(DecoderCounters counters) { - Log.d(TAG, "audioDisabled [" + getSessionTimeString() + "]"); - } - - @Override - public void onAudioTrackUnderrun(int bufferSize, long bufferSizeMs, long elapsedSinceLastFeedMs) { - printInternalError("audioTrackUnderrun [" + bufferSize + ", " + bufferSizeMs + ", " - + elapsedSinceLastFeedMs + "]", null); - } - - // VideoRendererEventListener - - @Override - public void onVideoEnabled(DecoderCounters counters) { - Log.d(TAG, "videoEnabled [" + getSessionTimeString() + "]"); - } - - @Override - public void onVideoDecoderInitialized(String decoderName, long elapsedRealtimeMs, - long initializationDurationMs) { - Log.d(TAG, "videoDecoderInitialized [" + getSessionTimeString() + ", " + decoderName + "]"); - } - - @Override - public void onVideoInputFormatChanged(Format format) { - Log.d(TAG, "videoFormatChanged [" + getSessionTimeString() + ", " + Format.toLogString(format) - + "]"); - } - - @Override - public void onVideoDisabled(DecoderCounters counters) { - Log.d(TAG, "videoDisabled [" + getSessionTimeString() + "]"); - } - - @Override - public void onDroppedFrames(int count, long elapsed) { - Log.d(TAG, "droppedFrames [" + getSessionTimeString() + ", " + count + "]"); - } - - @Override - public void onVideoSizeChanged(int width, int height, int unappliedRotationDegrees, - float pixelWidthHeightRatio) { - Log.d(TAG, "videoSizeChanged [" + width + ", " + height + "]"); - } - - @Override - public void onRenderedFirstFrame(Surface surface) { - Log.d(TAG, "renderedFirstFrame [" + surface + "]"); - } - - // DefaultDrmSessionManager.EventListener - - @Override - public void onDrmSessionManagerError(Exception e) { - printInternalError("drmSessionManagerError", e); - } - - @Override - public void onDrmKeysRestored() { - Log.d(TAG, "drmKeysRestored [" + getSessionTimeString() + "]"); - } - - @Override - public void onDrmKeysRemoved() { - Log.d(TAG, "drmKeysRemoved [" + getSessionTimeString() + "]"); - } - - @Override - public void onDrmKeysLoaded() { - Log.d(TAG, "drmKeysLoaded [" + getSessionTimeString() + "]"); - } - - // ExtractorMediaSource.EventListener - - @Override - public void onLoadError(IOException error) { - printInternalError("loadError", error); - } - - // AdaptiveMediaSourceEventListener - - @Override - public void onLoadStarted(DataSpec dataSpec, int dataType, int trackType, Format trackFormat, - int trackSelectionReason, Object trackSelectionData, long mediaStartTimeMs, - long mediaEndTimeMs, long elapsedRealtimeMs) { - // Do nothing. - } - - @Override - public void onLoadError(DataSpec dataSpec, int dataType, int trackType, Format trackFormat, - int trackSelectionReason, Object trackSelectionData, long mediaStartTimeMs, - long mediaEndTimeMs, long elapsedRealtimeMs, long loadDurationMs, long bytesLoaded, - IOException error, boolean wasCanceled) { - printInternalError("loadError", error); - } - - @Override - public void onLoadCanceled(DataSpec dataSpec, int dataType, int trackType, Format trackFormat, - int trackSelectionReason, Object trackSelectionData, long mediaStartTimeMs, - long mediaEndTimeMs, long elapsedRealtimeMs, long loadDurationMs, long bytesLoaded) { - // Do nothing. - } - - @Override - public void onLoadCompleted(DataSpec dataSpec, int dataType, int trackType, Format trackFormat, - int trackSelectionReason, Object trackSelectionData, long mediaStartTimeMs, - long mediaEndTimeMs, long elapsedRealtimeMs, long loadDurationMs, long bytesLoaded) { - // Do nothing. - } - - @Override - public void onUpstreamDiscarded(int trackType, long mediaStartTimeMs, long mediaEndTimeMs) { - // Do nothing. - } - - @Override - public void onDownstreamFormatChanged(int trackType, Format trackFormat, int trackSelectionReason, - Object trackSelectionData, long mediaTimeMs) { - // Do nothing. - } - - // Internal methods - - private void printInternalError(String type, Exception e) { - Log.e(TAG, "internalError [" + getSessionTimeString() + ", " + type + "]", e); - } - - private void printMetadata(Metadata metadata, String prefix) { - for (int i = 0; i < metadata.length(); i++) { - Metadata.Entry entry = metadata.get(i); - if (entry instanceof TextInformationFrame) { - TextInformationFrame textInformationFrame = (TextInformationFrame) entry; - Log.d(TAG, prefix + String.format("%s: value=%s", textInformationFrame.id, - textInformationFrame.value)); - } else if (entry instanceof UrlLinkFrame) { - UrlLinkFrame urlLinkFrame = (UrlLinkFrame) entry; - Log.d(TAG, prefix + String.format("%s: url=%s", urlLinkFrame.id, urlLinkFrame.url)); - } else if (entry instanceof PrivFrame) { - PrivFrame privFrame = (PrivFrame) entry; - Log.d(TAG, prefix + String.format("%s: owner=%s", privFrame.id, privFrame.owner)); - } else if (entry instanceof GeobFrame) { - GeobFrame geobFrame = (GeobFrame) entry; - Log.d(TAG, prefix + String.format("%s: mimeType=%s, filename=%s, description=%s", - geobFrame.id, geobFrame.mimeType, geobFrame.filename, geobFrame.description)); - } else if (entry instanceof ApicFrame) { - ApicFrame apicFrame = (ApicFrame) entry; - Log.d(TAG, prefix + String.format("%s: mimeType=%s, description=%s", - apicFrame.id, apicFrame.mimeType, apicFrame.description)); - } else if (entry instanceof CommentFrame) { - CommentFrame commentFrame = (CommentFrame) entry; - Log.d(TAG, prefix + String.format("%s: language=%s, description=%s", commentFrame.id, - commentFrame.language, commentFrame.description)); - } else if (entry instanceof Id3Frame) { - Id3Frame id3Frame = (Id3Frame) entry; - Log.d(TAG, prefix + String.format("%s", id3Frame.id)); - } else if (entry instanceof EventMessage) { - EventMessage eventMessage = (EventMessage) entry; - Log.d(TAG, prefix + String.format("EMSG: scheme=%s, id=%d, value=%s", - eventMessage.schemeIdUri, eventMessage.id, eventMessage.value)); - } - } - } - - private String getSessionTimeString() { - return getTimeString(SystemClock.elapsedRealtime() - startTimeMs); - } - - private static String getTimeString(long timeMs) { - return timeMs == C.TIME_UNSET ? "?" : TIME_FORMAT.format((timeMs) / 1000f); - } - - private static String getStateString(int state) { - switch (state) { - case Player.STATE_BUFFERING: - return "B"; - case Player.STATE_ENDED: - return "E"; - case Player.STATE_IDLE: - return "I"; - case Player.STATE_READY: - return "R"; - default: - return "?"; - } - } - - private static String getFormatSupportString(int formatSupport) { - switch (formatSupport) { - case RendererCapabilities.FORMAT_HANDLED: - return "YES"; - case RendererCapabilities.FORMAT_EXCEEDS_CAPABILITIES: - return "NO_EXCEEDS_CAPABILITIES"; - case RendererCapabilities.FORMAT_UNSUPPORTED_DRM: - return "NO_UNSUPPORTED_DRM"; - case RendererCapabilities.FORMAT_UNSUPPORTED_SUBTYPE: - return "NO_UNSUPPORTED_TYPE"; - case RendererCapabilities.FORMAT_UNSUPPORTED_TYPE: - return "NO"; - default: - return "?"; - } - } - - private static String getAdaptiveSupportString(int trackCount, int adaptiveSupport) { - if (trackCount < 2) { - return "N/A"; - } - switch (adaptiveSupport) { - case RendererCapabilities.ADAPTIVE_SEAMLESS: - return "YES"; - case RendererCapabilities.ADAPTIVE_NOT_SEAMLESS: - return "YES_NOT_SEAMLESS"; - case RendererCapabilities.ADAPTIVE_NOT_SUPPORTED: - return "NO"; - default: - return "?"; - } - } - - private static String getTrackStatusString(TrackSelection selection, TrackGroup group, - int trackIndex) { - return getTrackStatusString(selection != null && selection.getTrackGroup() == group - && selection.indexOf(trackIndex) != C.INDEX_UNSET); - } - - private static String getTrackStatusString(boolean enabled) { - return enabled ? "[X]" : "[ ]"; - } - - private static String getRepeatModeString(@Player.RepeatMode int repeatMode) { - switch (repeatMode) { - case Player.REPEAT_MODE_OFF: - return "OFF"; - case Player.REPEAT_MODE_ONE: - return "ONE"; - case Player.REPEAT_MODE_ALL: - return "ALL"; - default: - return "?"; - } - } -} 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 6d733c9f97..091e483155 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 @@ -16,14 +16,14 @@ package com.google.android.exoplayer2.demo; import android.app.Activity; -import android.content.Context; +import android.app.AlertDialog; import android.content.Intent; import android.content.pm.PackageManager; import android.net.Uri; import android.os.Bundle; -import android.os.Handler; import android.support.annotation.NonNull; -import android.text.TextUtils; +import android.support.annotation.Nullable; +import android.util.Pair; import android.view.KeyEvent; import android.view.View; import android.view.View.OnClickListener; @@ -34,62 +34,71 @@ import android.widget.LinearLayout; import android.widget.TextView; import android.widget.Toast; import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.C.ContentType; import com.google.android.exoplayer2.DefaultRenderersFactory; import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.ExoPlayerFactory; -import com.google.android.exoplayer2.PlaybackParameters; +import com.google.android.exoplayer2.PlaybackPreparer; import com.google.android.exoplayer2.Player; -import com.google.android.exoplayer2.Player.EventListener; import com.google.android.exoplayer2.SimpleExoPlayer; -import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.drm.DefaultDrmSessionManager; -import com.google.android.exoplayer2.drm.DrmSessionManager; import com.google.android.exoplayer2.drm.FrameworkMediaCrypto; import com.google.android.exoplayer2.drm.FrameworkMediaDrm; import com.google.android.exoplayer2.drm.HttpMediaDrmCallback; import com.google.android.exoplayer2.drm.UnsupportedDrmException; -import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory; import com.google.android.exoplayer2.mediacodec.MediaCodecRenderer.DecoderInitializationException; import com.google.android.exoplayer2.mediacodec.MediaCodecUtil.DecoderQueryException; +import com.google.android.exoplayer2.offline.FilteringManifestParser; import com.google.android.exoplayer2.source.BehindLiveWindowException; import com.google.android.exoplayer2.source.ConcatenatingMediaSource; import com.google.android.exoplayer2.source.ExtractorMediaSource; import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.TrackGroupArray; +import com.google.android.exoplayer2.source.ads.AdsLoader; +import com.google.android.exoplayer2.source.ads.AdsMediaSource; import com.google.android.exoplayer2.source.dash.DashMediaSource; import com.google.android.exoplayer2.source.dash.DefaultDashChunkSource; +import com.google.android.exoplayer2.source.dash.manifest.DashManifestParser; +import com.google.android.exoplayer2.source.dash.manifest.RepresentationKey; import com.google.android.exoplayer2.source.hls.HlsMediaSource; +import com.google.android.exoplayer2.source.hls.playlist.HlsPlaylistParser; +import com.google.android.exoplayer2.source.hls.playlist.RenditionKey; import com.google.android.exoplayer2.source.smoothstreaming.DefaultSsChunkSource; import com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource; +import com.google.android.exoplayer2.source.smoothstreaming.manifest.SsManifestParser; +import com.google.android.exoplayer2.source.smoothstreaming.manifest.StreamKey; import com.google.android.exoplayer2.trackselection.AdaptiveTrackSelection; import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; import com.google.android.exoplayer2.trackselection.MappingTrackSelector.MappedTrackInfo; +import com.google.android.exoplayer2.trackselection.RandomTrackSelection; import com.google.android.exoplayer2.trackselection.TrackSelection; import com.google.android.exoplayer2.trackselection.TrackSelectionArray; import com.google.android.exoplayer2.ui.DebugTextViewHelper; -import com.google.android.exoplayer2.ui.PlaybackControlView; -import com.google.android.exoplayer2.ui.SimpleExoPlayerView; +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.upstream.DataSource; import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter; import com.google.android.exoplayer2.upstream.HttpDataSource; +import com.google.android.exoplayer2.util.ErrorMessageProvider; +import com.google.android.exoplayer2.util.EventLogger; import com.google.android.exoplayer2.util.Util; import java.lang.reflect.Constructor; -import java.lang.reflect.Method; import java.net.CookieHandler; import java.net.CookieManager; import java.net.CookiePolicy; +import java.util.List; import java.util.UUID; -/** - * An activity that plays media using {@link SimpleExoPlayer}. - */ -public class PlayerActivity extends Activity implements OnClickListener, EventListener, - PlaybackControlView.VisibilityListener { +/** An activity that plays media using {@link SimpleExoPlayer}. */ +public class PlayerActivity extends Activity + implements OnClickListener, PlaybackPreparer, PlayerControlView.VisibilityListener { - public static final String DRM_SCHEME_UUID_EXTRA = "drm_scheme_uuid"; - public static final String DRM_LICENSE_URL = "drm_license_url"; - public static final String DRM_KEY_REQUEST_PROPERTIES = "drm_key_request_properties"; - public static final String PREFER_EXTENSION_DECODERS = "prefer_extension_decoders"; + public static final String 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"; @@ -98,8 +107,22 @@ public class PlayerActivity extends Activity implements OnClickListener, EventLi "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"; + private static final String ABR_ALGORITHM_DEFAULT = "default"; + private static final String ABR_ALGORITHM_RANDOM = "random"; + + // For backwards compatibility only. + private static final String DRM_SCHEME_UUID_EXTRA = "drm_scheme_uuid"; + + // Saved instance state keys. + private static final String KEY_TRACK_SELECTOR_PARAMETERS = "track_selector_parameters"; + private static final String KEY_WINDOW = "window"; + private static final String KEY_POSITION = "position"; + private static final String KEY_AUTO_PLAY = "auto_play"; + private static final DefaultBandwidthMeter BANDWIDTH_METER = new DefaultBandwidthMeter(); private static final CookieManager DEFAULT_COOKIE_MANAGER; static { @@ -107,40 +130,34 @@ public class PlayerActivity extends Activity implements OnClickListener, EventLi DEFAULT_COOKIE_MANAGER.setCookiePolicy(CookiePolicy.ACCEPT_ORIGINAL_SERVER); } - private Handler mainHandler; - private EventLogger eventLogger; - private SimpleExoPlayerView simpleExoPlayerView; + private PlayerView playerView; private LinearLayout debugRootView; private TextView debugTextView; - private Button retryButton; private DataSource.Factory mediaDataSourceFactory; private SimpleExoPlayer player; + private MediaSource mediaSource; private DefaultTrackSelector trackSelector; - private TrackSelectionHelper trackSelectionHelper; + private DefaultTrackSelector.Parameters trackSelectorParameters; private DebugTextViewHelper debugViewHelper; - private boolean inErrorState; private TrackGroupArray lastSeenTrackGroupArray; - private boolean shouldAutoPlay; - private int resumeWindow; - private long resumePosition; + private boolean startAutoPlay; + private int startWindow; + private long startPosition; // Fields used only for ad playback. The ads loader is loaded via reflection. - private Object imaAdsLoader; // com.google.android.exoplayer2.ext.ima.ImaAdsLoader + private AdsLoader adsLoader; private Uri loadedAdTagUri; - private ViewGroup adOverlayViewGroup; + private ViewGroup adUiViewGroup; // Activity lifecycle @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - shouldAutoPlay = true; - clearResumePosition(); mediaDataSourceFactory = buildDataSourceFactory(true); - mainHandler = new Handler(); if (CookieHandler.getDefault() != DEFAULT_COOKIE_MANAGER) { CookieHandler.setDefault(DEFAULT_COOKIE_MANAGER); } @@ -148,21 +165,29 @@ public class PlayerActivity extends Activity implements OnClickListener, EventLi setContentView(R.layout.player_activity); View rootView = findViewById(R.id.root); rootView.setOnClickListener(this); - debugRootView = (LinearLayout) findViewById(R.id.controls_root); - debugTextView = (TextView) findViewById(R.id.debug_text_view); - retryButton = (Button) findViewById(R.id.retry_button); - retryButton.setOnClickListener(this); + debugRootView = findViewById(R.id.controls_root); + debugTextView = findViewById(R.id.debug_text_view); - simpleExoPlayerView = (SimpleExoPlayerView) findViewById(R.id.player_view); - simpleExoPlayerView.setControllerVisibilityListener(this); - simpleExoPlayerView.requestFocus(); + playerView = findViewById(R.id.player_view); + playerView.setControllerVisibilityListener(this); + playerView.setErrorMessageProvider(new PlayerErrorMessageProvider()); + playerView.requestFocus(); + + if (savedInstanceState != null) { + trackSelectorParameters = savedInstanceState.getParcelable(KEY_TRACK_SELECTOR_PARAMETERS); + startAutoPlay = savedInstanceState.getBoolean(KEY_AUTO_PLAY); + startWindow = savedInstanceState.getInt(KEY_WINDOW); + startPosition = savedInstanceState.getLong(KEY_POSITION); + } else { + trackSelectorParameters = new DefaultTrackSelector.ParametersBuilder().build(); + clearStartPosition(); + } } @Override public void onNewIntent(Intent intent) { releasePlayer(); - shouldAutoPlay = true; - clearResumePosition(); + clearStartPosition(); setIntent(intent); } @@ -177,7 +202,7 @@ public class PlayerActivity extends Activity implements OnClickListener, EventLi @Override public void onResume() { super.onResume(); - if ((Util.SDK_INT <= 23 || player == null)) { + if (Util.SDK_INT <= 23 || player == null) { initializePlayer(); } } @@ -207,7 +232,12 @@ public class PlayerActivity extends Activity implements OnClickListener, EventLi @Override public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { - if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { + if (grantResults.length == 0) { + // Empty results are triggered if a permission is requested while another request was already + // pending and can be safely ignored in this case. + return; + } + if (grantResults[0] == PackageManager.PERMISSION_GRANTED) { initializePlayer(); } else { showToast(R.string.storage_permission_denied); @@ -215,29 +245,55 @@ public class PlayerActivity extends Activity implements OnClickListener, EventLi } } + @Override + public void onSaveInstanceState(Bundle outState) { + updateTrackSelectorParameters(); + updateStartPosition(); + outState.putParcelable(KEY_TRACK_SELECTOR_PARAMETERS, trackSelectorParameters); + outState.putBoolean(KEY_AUTO_PLAY, startAutoPlay); + outState.putInt(KEY_WINDOW, startWindow); + outState.putLong(KEY_POSITION, startPosition); + } + // Activity input @Override public boolean dispatchKeyEvent(KeyEvent event) { - // If the event was not handled then see if the player view can handle it. - return super.dispatchKeyEvent(event) || simpleExoPlayerView.dispatchKeyEvent(event); + // See whether the player view wants to handle media or DPAD keys events. + return playerView.dispatchKeyEvent(event) || super.dispatchKeyEvent(event); } // OnClickListener methods @Override public void onClick(View view) { - if (view == retryButton) { - initializePlayer(); - } else if (view.getParent() == debugRootView) { + if (view.getParent() == debugRootView) { MappedTrackInfo mappedTrackInfo = trackSelector.getCurrentMappedTrackInfo(); if (mappedTrackInfo != null) { - trackSelectionHelper.showSelectionDialog(this, ((Button) view).getText(), - trackSelector.getCurrentMappedTrackInfo(), (int) view.getTag()); + 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(); } } } + // PlaybackControlView.PlaybackPreparer implementation + + @Override + public void preparePlayback() { + initializePlayer(); + } + // PlaybackControlView.VisibilityListener implementation @Override @@ -248,29 +304,55 @@ public class PlayerActivity extends Activity implements OnClickListener, EventLi // Internal methods private void initializePlayer() { - Intent intent = getIntent(); - boolean needNewPlayer = player == null; - if (needNewPlayer) { - TrackSelection.Factory adaptiveTrackSelectionFactory = - new AdaptiveTrackSelection.Factory(BANDWIDTH_METER); - trackSelector = new DefaultTrackSelector(adaptiveTrackSelectionFactory); - trackSelectionHelper = new TrackSelectionHelper(trackSelector, adaptiveTrackSelectionFactory); - lastSeenTrackGroupArray = null; - eventLogger = new EventLogger(trackSelector); + if (player == null) { + Intent intent = getIntent(); + String action = intent.getAction(); + Uri[] uris; + String[] extensions; + if (ACTION_VIEW.equals(action)) { + uris = new Uri[] {intent.getData()}; + extensions = new String[] {intent.getStringExtra(EXTENSION_EXTRA)}; + } else if (ACTION_VIEW_LIST.equals(action)) { + String[] uriStrings = intent.getStringArrayExtra(URI_LIST_EXTRA); + uris = new Uri[uriStrings.length]; + for (int i = 0; i < uriStrings.length; i++) { + uris[i] = Uri.parse(uriStrings[i]); + } + extensions = intent.getStringArrayExtra(EXTENSION_LIST_EXTRA); + if (extensions == null) { + extensions = new String[uriStrings.length]; + } + } else { + showToast(getString(R.string.unexpected_intent_action, action)); + finish(); + return; + } + if (Util.maybeRequestReadExternalStoragePermission(this, uris)) { + // The player will be reinitialized if the permission is granted. + return; + } - UUID drmSchemeUuid = intent.hasExtra(DRM_SCHEME_UUID_EXTRA) - ? UUID.fromString(intent.getStringExtra(DRM_SCHEME_UUID_EXTRA)) : null; - DrmSessionManager drmSessionManager = null; - if (drmSchemeUuid != null) { - String drmLicenseUrl = intent.getStringExtra(DRM_LICENSE_URL); - String[] keyRequestPropertiesArray = intent.getStringArrayExtra(DRM_KEY_REQUEST_PROPERTIES); + 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 { - drmSessionManager = buildDrmSessionManagerV18(drmSchemeUuid, drmLicenseUrl, - keyRequestPropertiesArray); + 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; @@ -278,142 +360,168 @@ public class PlayerActivity extends Activity implements OnClickListener, EventLi } if (drmSessionManager == null) { showToast(errorStringId); + finish(); return; } } - boolean preferExtensionDecoders = intent.getBooleanExtra(PREFER_EXTENSION_DECODERS, false); + TrackSelection.Factory trackSelectionFactory; + String abrAlgorithm = intent.getStringExtra(ABR_ALGORITHM_EXTRA); + if (abrAlgorithm == null || ABR_ALGORITHM_DEFAULT.equals(abrAlgorithm)) { + trackSelectionFactory = new AdaptiveTrackSelection.Factory(BANDWIDTH_METER); + } else if (ABR_ALGORITHM_RANDOM.equals(abrAlgorithm)) { + trackSelectionFactory = new RandomTrackSelection.Factory(); + } else { + showToast(R.string.error_unrecognized_abr_algorithm); + finish(); + return; + } + + boolean preferExtensionDecoders = + intent.getBooleanExtra(PREFER_EXTENSION_DECODERS_EXTRA, false); @DefaultRenderersFactory.ExtensionRendererMode int extensionRendererMode = ((DemoApplication) getApplication()).useExtensionRenderers() ? (preferExtensionDecoders ? DefaultRenderersFactory.EXTENSION_RENDERER_MODE_PREFER : DefaultRenderersFactory.EXTENSION_RENDERER_MODE_ON) : DefaultRenderersFactory.EXTENSION_RENDERER_MODE_OFF; - DefaultRenderersFactory renderersFactory = new DefaultRenderersFactory(this, - drmSessionManager, extensionRendererMode); + DefaultRenderersFactory renderersFactory = + new DefaultRenderersFactory(this, extensionRendererMode); - player = ExoPlayerFactory.newSimpleInstance(renderersFactory, trackSelector); - player.addListener(this); - player.addListener(eventLogger); - player.addMetadataOutput(eventLogger); - player.setAudioDebugListener(eventLogger); - player.setVideoDebugListener(eventLogger); + trackSelector = new DefaultTrackSelector(trackSelectionFactory); + trackSelector.setParameters(trackSelectorParameters); + lastSeenTrackGroupArray = null; - simpleExoPlayerView.setPlayer(player); - player.setPlayWhenReady(shouldAutoPlay); + player = + ExoPlayerFactory.newSimpleInstance(renderersFactory, trackSelector, drmSessionManager); + player.addListener(new PlayerEventListener()); + player.setPlayWhenReady(startAutoPlay); + player.addAnalyticsListener(new EventLogger(trackSelector)); + playerView.setPlayer(player); + playerView.setPlaybackPreparer(this); debugViewHelper = new DebugTextViewHelper(player, debugTextView); debugViewHelper.start(); - } - 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]); + + MediaSource[] mediaSources = new MediaSource[uris.length]; + for (int i = 0; i < uris.length; i++) { + mediaSources[i] = buildMediaSource(uris[i], extensions[i]); } - extensions = intent.getStringArrayExtra(EXTENSION_LIST_EXTRA); - if (extensions == null) { - extensions = new String[uriStrings.length]; - } - } else { - showToast(getString(R.string.unexpected_intent_action, action)); - return; - } - if (Util.maybeRequestReadExternalStoragePermission(this, uris)) { - // The player will be reinitialized if the permission is granted. - return; - } - MediaSource[] mediaSources = new MediaSource[uris.length]; - for (int i = 0; i < uris.length; i++) { - mediaSources[i] = buildMediaSource(uris[i], extensions[i]); - } - MediaSource mediaSource = mediaSources.length == 1 ? mediaSources[0] - : new ConcatenatingMediaSource(mediaSources); - String adTagUriString = intent.getStringExtra(AD_TAG_URI_EXTRA); - if (adTagUriString != null) { - Uri adTagUri = Uri.parse(adTagUriString); - if (!adTagUri.equals(loadedAdTagUri)) { + 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(); - loadedAdTagUri = adTagUri; } - try { - mediaSource = createAdsMediaSource(mediaSource, Uri.parse(adTagUriString)); - } catch (Exception e) { - showToast(R.string.ima_not_loaded); - } - } else { - releaseAdsLoader(); } - boolean haveResumePosition = resumeWindow != C.INDEX_UNSET; - if (haveResumePosition) { - player.seekTo(resumeWindow, resumePosition); + boolean haveStartPosition = startWindow != C.INDEX_UNSET; + if (haveStartPosition) { + player.seekTo(startWindow, startPosition); } - player.prepare(mediaSource, !haveResumePosition, false); - inErrorState = false; + player.prepare(mediaSource, !haveStartPosition, false); updateButtonVisibilities(); } - private MediaSource buildMediaSource(Uri uri, String overrideExtension) { - int type = TextUtils.isEmpty(overrideExtension) ? Util.inferContentType(uri) - : Util.inferContentType("." + overrideExtension); + private MediaSource buildMediaSource(Uri uri) { + return buildMediaSource(uri, null); + } + + @SuppressWarnings("unchecked") + private MediaSource buildMediaSource(Uri uri, @Nullable String overrideExtension) { + @ContentType int type = Util.inferContentType(uri, overrideExtension); switch (type) { - case C.TYPE_SS: - return new SsMediaSource(uri, buildDataSourceFactory(false), - new DefaultSsChunkSource.Factory(mediaDataSourceFactory), mainHandler, eventLogger); case C.TYPE_DASH: - return new DashMediaSource(uri, buildDataSourceFactory(false), - new DefaultDashChunkSource.Factory(mediaDataSourceFactory), mainHandler, eventLogger); + return new DashMediaSource.Factory( + new DefaultDashChunkSource.Factory(mediaDataSourceFactory), + buildDataSourceFactory(false)) + .setManifestParser( + new FilteringManifestParser<>( + new DashManifestParser(), (List) getOfflineStreamKeys(uri))) + .createMediaSource(uri); + case C.TYPE_SS: + return new SsMediaSource.Factory( + new DefaultSsChunkSource.Factory(mediaDataSourceFactory), + buildDataSourceFactory(false)) + .setManifestParser( + new FilteringManifestParser<>( + new SsManifestParser(), (List) getOfflineStreamKeys(uri))) + .createMediaSource(uri); case C.TYPE_HLS: - return new HlsMediaSource(uri, mediaDataSourceFactory, mainHandler, eventLogger); + return new HlsMediaSource.Factory(mediaDataSourceFactory) + .setPlaylistParser( + new FilteringManifestParser<>( + new HlsPlaylistParser(), (List) getOfflineStreamKeys(uri))) + .createMediaSource(uri); case C.TYPE_OTHER: - return new ExtractorMediaSource(uri, mediaDataSourceFactory, new DefaultExtractorsFactory(), - mainHandler, eventLogger); + return new ExtractorMediaSource.Factory(mediaDataSourceFactory).createMediaSource(uri); default: { throw new IllegalStateException("Unsupported type: " + type); } } } - private DrmSessionManager buildDrmSessionManagerV18(UUID uuid, - String licenseUrl, String[] keyRequestPropertiesArray) throws UnsupportedDrmException { - HttpMediaDrmCallback drmCallback = new HttpMediaDrmCallback(licenseUrl, - buildHttpDataSourceFactory(false)); + private List getOfflineStreamKeys(Uri uri) { + return ((DemoApplication) getApplication()).getDownloadTracker().getOfflineStreamKeys(uri); + } + + private DefaultDrmSessionManager buildDrmSessionManagerV18( + UUID uuid, String licenseUrl, String[] keyRequestPropertiesArray, boolean multiSession) + throws UnsupportedDrmException { + HttpDataSource.Factory licenseDataSourceFactory = + ((DemoApplication) getApplication()).buildHttpDataSourceFactory(/* listener= */ null); + HttpMediaDrmCallback drmCallback = + new HttpMediaDrmCallback(licenseUrl, licenseDataSourceFactory); if (keyRequestPropertiesArray != null) { for (int i = 0; i < keyRequestPropertiesArray.length - 1; i += 2) { drmCallback.setKeyRequestProperty(keyRequestPropertiesArray[i], keyRequestPropertiesArray[i + 1]); } } - return new DefaultDrmSessionManager<>(uuid, FrameworkMediaDrm.newInstance(uuid), drmCallback, - null, mainHandler, eventLogger); + return new DefaultDrmSessionManager<>( + uuid, FrameworkMediaDrm.newInstance(uuid), drmCallback, null, multiSession); } private void releasePlayer() { if (player != null) { + updateTrackSelectorParameters(); + updateStartPosition(); debugViewHelper.stop(); debugViewHelper = null; - shouldAutoPlay = player.getPlayWhenReady(); - updateResumePosition(); player.release(); player = null; + mediaSource = null; trackSelector = null; - trackSelectionHelper = null; - eventLogger = null; } } - private void updateResumePosition() { - resumeWindow = player.getCurrentWindowIndex(); - resumePosition = Math.max(0, player.getContentPosition()); + private void updateTrackSelectorParameters() { + if (trackSelector != null) { + trackSelectorParameters = trackSelector.getParameters(); + } } - private void clearResumePosition() { - resumeWindow = C.INDEX_UNSET; - resumePosition = C.TIME_UNSET; + private void updateStartPosition() { + if (player != null) { + startAutoPlay = player.getPlayWhenReady(); + startWindow = player.getCurrentWindowIndex(); + startPosition = Math.max(0, player.getContentPosition()); + } + } + + private void clearStartPosition() { + startAutoPlay = true; + startWindow = C.INDEX_UNSET; + startPosition = C.TIME_UNSET; } /** @@ -428,160 +536,52 @@ public class PlayerActivity extends Activity implements OnClickListener, EventLi .buildDataSourceFactory(useBandwidthMeter ? BANDWIDTH_METER : null); } - /** - * Returns a new HttpDataSource factory. - * - * @param useBandwidthMeter Whether to set {@link #BANDWIDTH_METER} as a listener to the new - * DataSource factory. - * @return A new HttpDataSource factory. - */ - private HttpDataSource.Factory buildHttpDataSourceFactory(boolean useBandwidthMeter) { - return ((DemoApplication) getApplication()) - .buildHttpDataSourceFactory(useBandwidthMeter ? BANDWIDTH_METER : null); - } - - /** - * Returns an ads media source, reusing the ads loader if one exists. - * - * @throws Exception Thrown if it was not possible to create an ads media source, for example, due - * to a missing dependency. - */ - private MediaSource createAdsMediaSource(MediaSource mediaSource, Uri adTagUri) throws Exception { + /** Returns an ads media source, reusing the ads loader if one exists. */ + private @Nullable 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. - Class loaderClass = Class.forName("com.google.android.exoplayer2.ext.ima.ImaAdsLoader"); - if (imaAdsLoader == null) { - imaAdsLoader = loaderClass.getConstructor(Context.class, Uri.class) - .newInstance(this, adTagUri); - adOverlayViewGroup = new FrameLayout(this); - // The demo app has a non-null overlay frame layout. - simpleExoPlayerView.getOverlayFrameLayout().addView(adOverlayViewGroup); + try { + Class loaderClass = Class.forName("com.google.android.exoplayer2.ext.ima.ImaAdsLoader"); + if (adsLoader == null) { + // Full class names used so the LINT.IfChange rule triggers should any of the classes move. + // LINT.IfChange + Constructor loaderConstructor = + loaderClass + .asSubclass(AdsLoader.class) + .getConstructor(android.content.Context.class, android.net.Uri.class); + // LINT.ThenChange(../../../../../../../../proguard-rules.txt) + adsLoader = loaderConstructor.newInstance(this, adTagUri); + adUiViewGroup = new FrameLayout(this); + // The demo app has a non-null overlay frame layout. + playerView.getOverlayFrameLayout().addView(adUiViewGroup); + } + AdsMediaSource.MediaSourceFactory adMediaSourceFactory = + new AdsMediaSource.MediaSourceFactory() { + @Override + public MediaSource createMediaSource(Uri uri) { + return PlayerActivity.this.buildMediaSource(uri); + } + + @Override + public int[] getSupportedTypes() { + return new int[] {C.TYPE_DASH, C.TYPE_SS, C.TYPE_HLS, C.TYPE_OTHER}; + } + }; + return new AdsMediaSource(mediaSource, adMediaSourceFactory, adsLoader, adUiViewGroup); + } catch (ClassNotFoundException e) { + // IMA extension not loaded. + return null; + } catch (Exception e) { + throw new RuntimeException(e); } - Class sourceClass = - Class.forName("com.google.android.exoplayer2.ext.ima.ImaAdsMediaSource"); - Constructor constructor = sourceClass.getConstructor(MediaSource.class, - DataSource.Factory.class, loaderClass, ViewGroup.class); - return (MediaSource) constructor.newInstance(mediaSource, mediaDataSourceFactory, imaAdsLoader, - adOverlayViewGroup); } private void releaseAdsLoader() { - if (imaAdsLoader != null) { - try { - Class loaderClass = Class.forName("com.google.android.exoplayer2.ext.ima.ImaAdsLoader"); - Method releaseMethod = loaderClass.getMethod("release"); - releaseMethod.invoke(imaAdsLoader); - } catch (Exception e) { - // Should never happen. - throw new IllegalStateException(e); - } - imaAdsLoader = null; + if (adsLoader != null) { + adsLoader.release(); + adsLoader = null; loadedAdTagUri = null; - simpleExoPlayerView.getOverlayFrameLayout().removeAllViews(); - } - } - - // Player.EventListener implementation - - @Override - public void onLoadingChanged(boolean isLoading) { - // Do nothing. - } - - @Override - public void onPlayerStateChanged(boolean playWhenReady, int playbackState) { - if (playbackState == Player.STATE_ENDED) { - showControls(); - } - updateButtonVisibilities(); - } - - @Override - public void onRepeatModeChanged(int repeatMode) { - // Do nothing. - } - - @Override - public void onShuffleModeEnabledChanged(boolean shuffleModeEnabled) { - // Do nothing. - } - - @Override - public void onPositionDiscontinuity() { - if (inErrorState) { - // This will only occur if the user has performed a seek whilst in the error state. Update the - // resume position so that if the user then retries, playback will resume from the position to - // which they seeked. - updateResumePosition(); - } - } - - @Override - public void onPlaybackParametersChanged(PlaybackParameters playbackParameters) { - // Do nothing. - } - - @Override - public void onTimelineChanged(Timeline timeline, Object manifest) { - // Do nothing. - } - - @Override - public void onPlayerError(ExoPlaybackException e) { - String errorString = null; - if (e.type == ExoPlaybackException.TYPE_RENDERER) { - Exception cause = e.getRendererException(); - if (cause instanceof DecoderInitializationException) { - // Special case for decoder initialization failures. - DecoderInitializationException decoderInitializationException = - (DecoderInitializationException) cause; - if (decoderInitializationException.decoderName == null) { - if (decoderInitializationException.getCause() instanceof DecoderQueryException) { - errorString = getString(R.string.error_querying_decoders); - } else if (decoderInitializationException.secureDecoderRequired) { - errorString = getString(R.string.error_no_secure_decoder, - decoderInitializationException.mimeType); - } else { - errorString = getString(R.string.error_no_decoder, - decoderInitializationException.mimeType); - } - } else { - errorString = getString(R.string.error_instantiating_decoder, - decoderInitializationException.decoderName); - } - } - } - if (errorString != null) { - showToast(errorString); - } - inErrorState = true; - if (isBehindLiveWindow(e)) { - clearResumePosition(); - initializePlayer(); - } else { - updateResumePosition(); - updateButtonVisibilities(); - showControls(); - } - } - - @Override - @SuppressWarnings("ReferenceEquality") - public void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray trackSelections) { - updateButtonVisibilities(); - if (trackGroups != lastSeenTrackGroupArray) { - MappedTrackInfo mappedTrackInfo = trackSelector.getCurrentMappedTrackInfo(); - if (mappedTrackInfo != null) { - if (mappedTrackInfo.getTrackTypeRendererSupport(C.TRACK_TYPE_VIDEO) - == MappedTrackInfo.RENDERER_SUPPORT_UNSUPPORTED_TRACKS) { - showToast(R.string.error_unsupported_video); - } - if (mappedTrackInfo.getTrackTypeRendererSupport(C.TRACK_TYPE_AUDIO) - == MappedTrackInfo.RENDERER_SUPPORT_UNSUPPORTED_TRACKS) { - showToast(R.string.error_unsupported_audio); - } - } - lastSeenTrackGroupArray = trackGroups; + playerView.getOverlayFrameLayout().removeAllViews(); } } @@ -589,10 +589,6 @@ public class PlayerActivity extends Activity implements OnClickListener, EventLi private void updateButtonVisibilities() { debugRootView.removeAllViews(); - - retryButton.setVisibility(inErrorState ? View.VISIBLE : View.GONE); - debugRootView.addView(retryButton); - if (player == null) { return; } @@ -602,20 +598,20 @@ public class PlayerActivity extends Activity implements OnClickListener, EventLi return; } - for (int i = 0; i < mappedTrackInfo.length; i++) { + 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.audio; + label = R.string.exo_track_selection_title_audio; break; case C.TRACK_TYPE_VIDEO: - label = R.string.video; + label = R.string.exo_track_selection_title_video; break; case C.TRACK_TYPE_TEXT: - label = R.string.text; + label = R.string.exo_track_selection_title_text; break; default: continue; @@ -623,7 +619,7 @@ public class PlayerActivity extends Activity implements OnClickListener, EventLi button.setText(label); button.setTag(i); button.setOnClickListener(this); - debugRootView.addView(button, debugRootView.getChildCount() - 1); + debugRootView.addView(button); } } } @@ -654,4 +650,90 @@ public class PlayerActivity extends Activity implements OnClickListener, EventLi return false; } + private class PlayerEventListener extends Player.DefaultEventListener { + + @Override + public void onPlayerStateChanged(boolean playWhenReady, 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(); + } + } + + @Override + public void onPlayerError(ExoPlaybackException e) { + if (isBehindLiveWindow(e)) { + clearStartPosition(); + initializePlayer(); + } else { + updateStartPosition(); + updateButtonVisibilities(); + showControls(); + } + } + + @Override + @SuppressWarnings("ReferenceEquality") + public void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray trackSelections) { + updateButtonVisibilities(); + if (trackGroups != lastSeenTrackGroupArray) { + MappedTrackInfo mappedTrackInfo = trackSelector.getCurrentMappedTrackInfo(); + if (mappedTrackInfo != null) { + if (mappedTrackInfo.getTypeSupport(C.TRACK_TYPE_VIDEO) + == MappedTrackInfo.RENDERER_SUPPORT_UNSUPPORTED_TRACKS) { + showToast(R.string.error_unsupported_video); + } + if (mappedTrackInfo.getTypeSupport(C.TRACK_TYPE_AUDIO) + == MappedTrackInfo.RENDERER_SUPPORT_UNSUPPORTED_TRACKS) { + showToast(R.string.error_unsupported_audio); + } + } + lastSeenTrackGroupArray = trackGroups; + } + } + } + + private class PlayerErrorMessageProvider implements ErrorMessageProvider { + + @Override + public Pair getErrorMessage(ExoPlaybackException e) { + String errorString = getString(R.string.error_generic); + if (e.type == ExoPlaybackException.TYPE_RENDERER) { + Exception cause = e.getRendererException(); + if (cause instanceof DecoderInitializationException) { + // Special case for decoder initialization failures. + DecoderInitializationException decoderInitializationException = + (DecoderInitializationException) cause; + if (decoderInitializationException.decoderName == null) { + if (decoderInitializationException.getCause() instanceof DecoderQueryException) { + errorString = getString(R.string.error_querying_decoders); + } else if (decoderInitializationException.secureDecoderRequired) { + errorString = + getString( + R.string.error_no_secure_decoder, decoderInitializationException.mimeType); + } else { + errorString = + getString(R.string.error_no_decoder, decoderInitializationException.mimeType); + } + } else { + errorString = + getString( + R.string.error_instantiating_decoder, + decoderInitializationException.decoderName); + } + } + } + return Pair.create(0, errorString); + } + } + } 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 382c783598..5524f98257 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 @@ -24,16 +24,17 @@ import android.os.AsyncTask; import android.os.Bundle; import android.util.JsonReader; import android.util.Log; -import android.view.LayoutInflater; import android.view.View; +import android.view.View.OnClickListener; import android.view.ViewGroup; import android.widget.BaseExpandableListAdapter; import android.widget.ExpandableListView; import android.widget.ExpandableListView.OnChildClickListener; +import android.widget.ImageButton; import android.widget.TextView; import android.widget.Toast; -import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ParserException; +import com.google.android.exoplayer2.offline.DownloadService; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DataSourceInputStream; import com.google.android.exoplayer2.upstream.DataSpec; @@ -45,20 +46,27 @@ import java.io.InputStream; import java.io.InputStreamReader; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.List; -import java.util.UUID; -/** - * An activity for selecting from a list of samples. - */ -public class SampleChooserActivity extends Activity { +/** An activity for selecting from a list of media samples. */ +public class SampleChooserActivity extends Activity + implements DownloadTracker.Listener, OnChildClickListener { private static final String TAG = "SampleChooserActivity"; + private DownloadTracker downloadTracker; + private SampleAdapter sampleAdapter; + @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.sample_chooser_activity); + sampleAdapter = new SampleAdapter(); + ExpandableListView sampleListView = findViewById(R.id.sample_list); + sampleListView.setAdapter(sampleAdapter); + sampleListView.setOnChildClickListener(this); + Intent intent = getIntent(); String dataUri = intent.getDataString(); String[] uris; @@ -81,8 +89,32 @@ public class SampleChooserActivity extends Activity { uriList.toArray(uris); Arrays.sort(uris); } + + downloadTracker = ((DemoApplication) getApplication()).getDownloadTracker(); SampleListLoader loaderTask = new SampleListLoader(); loaderTask.execute(uris); + + // Ping the download service in case it's not running (but should be). + startService( + new Intent(this, DemoDownloadService.class).setAction(DownloadService.ACTION_INIT)); + } + + @Override + public void onStart() { + super.onStart(); + downloadTracker.addListener(this); + sampleAdapter.notifyDataSetChanged(); + } + + @Override + public void onStop() { + downloadTracker.removeListener(this); + super.onStop(); + } + + @Override + public void onDownloadsChanged() { + sampleAdapter.notifyDataSetChanged(); } private void onSampleGroups(final List groups, boolean sawError) { @@ -90,20 +122,44 @@ public class SampleChooserActivity extends Activity { Toast.makeText(getApplicationContext(), R.string.sample_list_load_error, Toast.LENGTH_LONG) .show(); } - ExpandableListView sampleList = (ExpandableListView) findViewById(R.id.sample_list); - sampleList.setAdapter(new SampleAdapter(this, groups)); - sampleList.setOnChildClickListener(new OnChildClickListener() { - @Override - public boolean onChildClick(ExpandableListView parent, View view, int groupPosition, - int childPosition, long id) { - onSampleSelected(groups.get(groupPosition).samples.get(childPosition)); - return true; - } - }); + sampleAdapter.setSampleGroups(groups); } - private void onSampleSelected(Sample sample) { + @Override + public boolean onChildClick( + ExpandableListView parent, View view, int groupPosition, int childPosition, long id) { + Sample sample = (Sample) view.getTag(); startActivity(sample.buildIntent(this)); + return true; + } + + private void onSampleDownloadButtonClicked(Sample sample) { + int downloadUnsupportedStringId = getDownloadUnsupportedStringId(sample); + if (downloadUnsupportedStringId != 0) { + Toast.makeText(getApplicationContext(), downloadUnsupportedStringId, Toast.LENGTH_LONG) + .show(); + } else { + UriSample uriSample = (UriSample) sample; + downloadTracker.toggleDownload(this, sample.name, uriSample.uri, uriSample.extension); + } + } + + private int getDownloadUnsupportedStringId(Sample sample) { + if (sample instanceof PlaylistSample) { + return R.string.download_playlist_unsupported; + } + UriSample uriSample = (UriSample) sample; + if (uriSample.drmInfo != null) { + return R.string.download_drm_unsupported; + } + if (uriSample.adTagUri != null) { + return R.string.download_ads_unsupported; + } + String scheme = uriSample.uri.getScheme(); + if (!("http".equals(scheme) || "https".equals(scheme))) { + return R.string.download_scheme_unsupported; + } + return 0; } private final class SampleListLoader extends AsyncTask> { @@ -177,14 +233,16 @@ public class SampleChooserActivity extends Activity { private Sample readEntry(JsonReader reader, boolean insidePlaylist) throws IOException { String sampleName = null; - String uri = null; + Uri uri = null; String extension = null; - UUID drmUuid = null; + String drmScheme = null; String drmLicenseUrl = null; String[] drmKeyRequestProperties = null; + boolean drmMultiSession = false; boolean preferExtensionDecoders = false; ArrayList playlistSamples = null; String adTagUri = null; + String abrAlgorithm = null; reader.beginObject(); while (reader.hasNext()) { @@ -194,14 +252,14 @@ public class SampleChooserActivity extends Activity { sampleName = reader.nextString(); break; case "uri": - uri = reader.nextString(); + uri = Uri.parse(reader.nextString()); break; case "extension": extension = reader.nextString(); break; case "drm_scheme": Assertions.checkState(!insidePlaylist, "Invalid attribute on nested item: drm_scheme"); - drmUuid = getDrmUuid(reader.nextString()); + drmScheme = reader.nextString(); break; case "drm_license_url": Assertions.checkState(!insidePlaylist, @@ -220,6 +278,9 @@ public class SampleChooserActivity extends Activity { reader.endObject(); drmKeyRequestProperties = drmKeyRequestPropertiesList.toArray(new String[0]); break; + case "drm_multi_session": + drmMultiSession = reader.nextBoolean(); + break; case "prefer_extension_decoders": Assertions.checkState(!insidePlaylist, "Invalid attribute on nested item: prefer_extension_decoders"); @@ -237,20 +298,28 @@ public class SampleChooserActivity extends Activity { case "ad_tag_uri": adTagUri = reader.nextString(); break; + case "abr_algorithm": + Assertions.checkState( + !insidePlaylist, "Invalid attribute on nested item: abr_algorithm"); + abrAlgorithm = reader.nextString(); + break; default: throw new ParserException("Unsupported attribute name: " + name); } } reader.endObject(); - + DrmInfo drmInfo = + drmScheme == null + ? null + : new DrmInfo(drmScheme, drmLicenseUrl, drmKeyRequestProperties, drmMultiSession); if (playlistSamples != null) { UriSample[] playlistSamplesArray = playlistSamples.toArray( new UriSample[playlistSamples.size()]); - return new PlaylistSample(sampleName, drmUuid, drmLicenseUrl, drmKeyRequestProperties, - preferExtensionDecoders, playlistSamplesArray); + return new PlaylistSample( + sampleName, preferExtensionDecoders, abrAlgorithm, drmInfo, playlistSamplesArray); } else { - return new UriSample(sampleName, drmUuid, drmLicenseUrl, drmKeyRequestProperties, - preferExtensionDecoders, uri, extension, adTagUri); + return new UriSample( + sampleName, preferExtensionDecoders, abrAlgorithm, drmInfo, uri, extension, adTagUri); } } @@ -265,33 +334,19 @@ public class SampleChooserActivity extends Activity { return group; } - private UUID getDrmUuid(String typeString) throws ParserException { - switch (Util.toLowerInvariant(typeString)) { - case "widevine": - return C.WIDEVINE_UUID; - case "playready": - return C.PLAYREADY_UUID; - case "clearkey": - return C.CLEARKEY_UUID; - default: - try { - return UUID.fromString(typeString); - } catch (RuntimeException e) { - throw new ParserException("Unsupported drm type: " + typeString); - } - } - } - } - private static final class SampleAdapter extends BaseExpandableListAdapter { + private final class SampleAdapter extends BaseExpandableListAdapter implements OnClickListener { - private final Context context; - private final List sampleGroups; + private List sampleGroups; - public SampleAdapter(Context context, List sampleGroups) { - this.context = context; + public SampleAdapter() { + sampleGroups = Collections.emptyList(); + } + + public void setSampleGroups(List sampleGroups) { this.sampleGroups = sampleGroups; + notifyDataSetChanged(); } @Override @@ -309,10 +364,12 @@ public class SampleChooserActivity extends Activity { View convertView, ViewGroup parent) { View view = convertView; if (view == null) { - view = LayoutInflater.from(context).inflate(android.R.layout.simple_list_item_1, parent, - false); + view = getLayoutInflater().inflate(R.layout.sample_list_item, parent, false); + View downloadButton = view.findViewById(R.id.download_button); + downloadButton.setOnClickListener(this); + downloadButton.setFocusable(false); } - ((TextView) view).setText(getChild(groupPosition, childPosition).name); + initializeChildView(view, getChild(groupPosition, childPosition)); return view; } @@ -336,8 +393,9 @@ public class SampleChooserActivity extends Activity { ViewGroup parent) { View view = convertView; if (view == null) { - view = LayoutInflater.from(context).inflate(android.R.layout.simple_expandable_list_item_1, - parent, false); + view = + getLayoutInflater() + .inflate(android.R.layout.simple_expandable_list_item_1, parent, false); } ((TextView) view).setText(getGroup(groupPosition).title); return view; @@ -358,6 +416,25 @@ public class SampleChooserActivity extends Activity { return true; } + @Override + public void onClick(View view) { + onSampleDownloadButtonClicked((Sample) view.getTag()); + } + + private void initializeChildView(View view, Sample sample) { + view.setTag(sample); + TextView sampleTitle = view.findViewById(R.id.sample_title); + sampleTitle.setText(sample.name); + + boolean canDownload = getDownloadUnsupportedStringId(sample) == 0; + boolean isDownloaded = canDownload && downloadTracker.isDownloaded(((UriSample) sample).uri); + ImageButton downloadButton = view.findViewById(R.id.download_button); + downloadButton.setTag(sample); + downloadButton.setColorFilter( + canDownload ? (isDownloaded ? 0xFF42A5F5 : 0xFFBDBDBD) : 0xFFEEEEEE); + downloadButton.setImageResource( + isDownloaded ? R.drawable.ic_download_done : R.drawable.ic_download); + } } private static final class SampleGroup { @@ -372,30 +449,52 @@ public class SampleChooserActivity extends Activity { } - private abstract static class Sample { - - public final String name; - public final boolean preferExtensionDecoders; - public final UUID drmSchemeUuid; + private static final class DrmInfo { + public final String drmScheme; public final String drmLicenseUrl; public final String[] drmKeyRequestProperties; + public final boolean drmMultiSession; - public Sample(String name, UUID drmSchemeUuid, String drmLicenseUrl, - String[] drmKeyRequestProperties, boolean preferExtensionDecoders) { - this.name = name; - this.drmSchemeUuid = drmSchemeUuid; + 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 boolean preferExtensionDecoders; + public final String abrAlgorithm; + public final DrmInfo drmInfo; + + public Sample( + String name, boolean preferExtensionDecoders, String abrAlgorithm, DrmInfo drmInfo) { + this.name = name; this.preferExtensionDecoders = preferExtensionDecoders; + this.abrAlgorithm = abrAlgorithm; + this.drmInfo = drmInfo; } public Intent buildIntent(Context context) { Intent intent = new Intent(context, PlayerActivity.class); - intent.putExtra(PlayerActivity.PREFER_EXTENSION_DECODERS, preferExtensionDecoders); - if (drmSchemeUuid != null) { - intent.putExtra(PlayerActivity.DRM_SCHEME_UUID_EXTRA, drmSchemeUuid.toString()); - intent.putExtra(PlayerActivity.DRM_LICENSE_URL, drmLicenseUrl); - intent.putExtra(PlayerActivity.DRM_KEY_REQUEST_PROPERTIES, drmKeyRequestProperties); + intent.putExtra(PlayerActivity.PREFER_EXTENSION_DECODERS_EXTRA, preferExtensionDecoders); + intent.putExtra(PlayerActivity.ABR_ALGORITHM_EXTRA, abrAlgorithm); + if (drmInfo != null) { + drmInfo.updateIntent(intent); } return intent; } @@ -404,14 +503,19 @@ public class SampleChooserActivity extends Activity { private static final class UriSample extends Sample { - public final String uri; + public final Uri uri; public final String extension; public final String adTagUri; - public UriSample(String name, UUID drmSchemeUuid, String drmLicenseUrl, - String[] drmKeyRequestProperties, boolean preferExtensionDecoders, String uri, - String extension, String adTagUri) { - super(name, drmSchemeUuid, drmLicenseUrl, drmKeyRequestProperties, preferExtensionDecoders); + public UriSample( + String name, + boolean preferExtensionDecoders, + String abrAlgorithm, + DrmInfo drmInfo, + Uri uri, + String extension, + String adTagUri) { + super(name, preferExtensionDecoders, abrAlgorithm, drmInfo); this.uri = uri; this.extension = extension; this.adTagUri = adTagUri; @@ -420,7 +524,7 @@ public class SampleChooserActivity extends Activity { @Override public Intent buildIntent(Context context) { return super.buildIntent(context) - .setData(Uri.parse(uri)) + .setData(uri) .putExtra(PlayerActivity.EXTENSION_EXTRA, extension) .putExtra(PlayerActivity.AD_TAG_URI_EXTRA, adTagUri) .setAction(PlayerActivity.ACTION_VIEW); @@ -432,10 +536,13 @@ public class SampleChooserActivity extends Activity { public final UriSample[] children; - public PlaylistSample(String name, UUID drmSchemeUuid, String drmLicenseUrl, - String[] drmKeyRequestProperties, boolean preferExtensionDecoders, + public PlaylistSample( + String name, + boolean preferExtensionDecoders, + String abrAlgorithm, + DrmInfo drmInfo, UriSample... children) { - super(name, drmSchemeUuid, drmLicenseUrl, drmKeyRequestProperties, preferExtensionDecoders); + super(name, preferExtensionDecoders, abrAlgorithm, drmInfo); this.children = children; } @@ -444,7 +551,7 @@ public class SampleChooserActivity extends Activity { 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; + uris[i] = children[i].uri.toString(); extensions[i] = children[i].extension; } return super.buildIntent(context) diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/TrackSelectionHelper.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/TrackSelectionHelper.java deleted file mode 100644 index fb7217f8fd..0000000000 --- a/demos/main/src/main/java/com/google/android/exoplayer2/demo/TrackSelectionHelper.java +++ /dev/null @@ -1,290 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.google.android.exoplayer2.demo; - -import android.annotation.SuppressLint; -import android.app.Activity; -import android.app.AlertDialog; -import android.content.Context; -import android.content.DialogInterface; -import android.content.res.TypedArray; -import android.util.Pair; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.CheckedTextView; -import com.google.android.exoplayer2.RendererCapabilities; -import com.google.android.exoplayer2.source.TrackGroup; -import com.google.android.exoplayer2.source.TrackGroupArray; -import com.google.android.exoplayer2.trackselection.FixedTrackSelection; -import com.google.android.exoplayer2.trackselection.MappingTrackSelector; -import com.google.android.exoplayer2.trackselection.MappingTrackSelector.MappedTrackInfo; -import com.google.android.exoplayer2.trackselection.MappingTrackSelector.SelectionOverride; -import com.google.android.exoplayer2.trackselection.RandomTrackSelection; -import com.google.android.exoplayer2.trackselection.TrackSelection; -import java.util.Arrays; - -/** - * Helper class for displaying track selection dialogs. - */ -/* package */ final class TrackSelectionHelper implements View.OnClickListener, - DialogInterface.OnClickListener { - - private static final TrackSelection.Factory FIXED_FACTORY = new FixedTrackSelection.Factory(); - private static final TrackSelection.Factory RANDOM_FACTORY = new RandomTrackSelection.Factory(); - - private final MappingTrackSelector selector; - private final TrackSelection.Factory adaptiveTrackSelectionFactory; - - private MappedTrackInfo trackInfo; - private int rendererIndex; - private TrackGroupArray trackGroups; - private boolean[] trackGroupsAdaptive; - private boolean isDisabled; - private SelectionOverride override; - - private CheckedTextView disableView; - private CheckedTextView defaultView; - private CheckedTextView enableRandomAdaptationView; - private CheckedTextView[][] trackViews; - - /** - * @param selector The track selector. - * @param adaptiveTrackSelectionFactory A factory for adaptive {@link TrackSelection}s, or null - * if the selection helper should not support adaptive tracks. - */ - public TrackSelectionHelper(MappingTrackSelector selector, - TrackSelection.Factory adaptiveTrackSelectionFactory) { - this.selector = selector; - this.adaptiveTrackSelectionFactory = adaptiveTrackSelectionFactory; - } - - /** - * Shows the selection dialog for a given renderer. - * - * @param activity The parent activity. - * @param title The dialog's title. - * @param trackInfo The current track information. - * @param rendererIndex The index of the renderer. - */ - public void showSelectionDialog(Activity activity, CharSequence title, MappedTrackInfo trackInfo, - int rendererIndex) { - this.trackInfo = trackInfo; - this.rendererIndex = rendererIndex; - - trackGroups = trackInfo.getTrackGroups(rendererIndex); - trackGroupsAdaptive = new boolean[trackGroups.length]; - for (int i = 0; i < trackGroups.length; i++) { - trackGroupsAdaptive[i] = adaptiveTrackSelectionFactory != null - && trackInfo.getAdaptiveSupport(rendererIndex, i, false) - != RendererCapabilities.ADAPTIVE_NOT_SUPPORTED - && trackGroups.get(i).length > 1; - } - isDisabled = selector.getRendererDisabled(rendererIndex); - override = selector.getSelectionOverride(rendererIndex, trackGroups); - - AlertDialog.Builder builder = new AlertDialog.Builder(activity); - builder.setTitle(title) - .setView(buildView(builder.getContext())) - .setPositiveButton(android.R.string.ok, this) - .setNegativeButton(android.R.string.cancel, null) - .create() - .show(); - } - - @SuppressLint("InflateParams") - private View buildView(Context context) { - LayoutInflater inflater = LayoutInflater.from(context); - View view = inflater.inflate(R.layout.track_selection_dialog, null); - ViewGroup root = (ViewGroup) view.findViewById(R.id.root); - - TypedArray attributeArray = context.getTheme().obtainStyledAttributes( - new int[] {android.R.attr.selectableItemBackground}); - int selectableItemBackgroundResourceId = attributeArray.getResourceId(0, 0); - attributeArray.recycle(); - - // View for disabling the renderer. - disableView = (CheckedTextView) inflater.inflate( - android.R.layout.simple_list_item_single_choice, root, false); - disableView.setBackgroundResource(selectableItemBackgroundResourceId); - disableView.setText(R.string.selection_disabled); - disableView.setFocusable(true); - disableView.setOnClickListener(this); - root.addView(disableView); - - // View for clearing the override to allow the selector to use its default selection logic. - defaultView = (CheckedTextView) inflater.inflate( - android.R.layout.simple_list_item_single_choice, root, false); - defaultView.setBackgroundResource(selectableItemBackgroundResourceId); - defaultView.setText(R.string.selection_default); - defaultView.setFocusable(true); - defaultView.setOnClickListener(this); - root.addView(inflater.inflate(R.layout.list_divider, root, false)); - root.addView(defaultView); - - // Per-track views. - boolean haveAdaptiveTracks = false; - trackViews = new CheckedTextView[trackGroups.length][]; - for (int groupIndex = 0; groupIndex < trackGroups.length; groupIndex++) { - TrackGroup group = trackGroups.get(groupIndex); - boolean groupIsAdaptive = trackGroupsAdaptive[groupIndex]; - haveAdaptiveTracks |= groupIsAdaptive; - trackViews[groupIndex] = new CheckedTextView[group.length]; - for (int trackIndex = 0; trackIndex < group.length; trackIndex++) { - if (trackIndex == 0) { - root.addView(inflater.inflate(R.layout.list_divider, root, false)); - } - int trackViewLayoutId = groupIsAdaptive ? android.R.layout.simple_list_item_multiple_choice - : android.R.layout.simple_list_item_single_choice; - CheckedTextView trackView = (CheckedTextView) inflater.inflate( - trackViewLayoutId, root, false); - trackView.setBackgroundResource(selectableItemBackgroundResourceId); - trackView.setText(DemoUtil.buildTrackName(group.getFormat(trackIndex))); - if (trackInfo.getTrackFormatSupport(rendererIndex, groupIndex, trackIndex) - == RendererCapabilities.FORMAT_HANDLED) { - trackView.setFocusable(true); - trackView.setTag(Pair.create(groupIndex, trackIndex)); - trackView.setOnClickListener(this); - } else { - trackView.setFocusable(false); - trackView.setEnabled(false); - } - trackViews[groupIndex][trackIndex] = trackView; - root.addView(trackView); - } - } - - if (haveAdaptiveTracks) { - // View for using random adaptation. - enableRandomAdaptationView = (CheckedTextView) inflater.inflate( - android.R.layout.simple_list_item_multiple_choice, root, false); - enableRandomAdaptationView.setBackgroundResource(selectableItemBackgroundResourceId); - enableRandomAdaptationView.setText(R.string.enable_random_adaptation); - enableRandomAdaptationView.setOnClickListener(this); - root.addView(inflater.inflate(R.layout.list_divider, root, false)); - root.addView(enableRandomAdaptationView); - } - - updateViews(); - return view; - } - - private void updateViews() { - disableView.setChecked(isDisabled); - defaultView.setChecked(!isDisabled && override == null); - for (int i = 0; i < trackViews.length; i++) { - for (int j = 0; j < trackViews[i].length; j++) { - trackViews[i][j].setChecked(override != null && override.groupIndex == i - && override.containsTrack(j)); - } - } - if (enableRandomAdaptationView != null) { - boolean enableView = !isDisabled && override != null && override.length > 1; - enableRandomAdaptationView.setEnabled(enableView); - enableRandomAdaptationView.setFocusable(enableView); - if (enableView) { - enableRandomAdaptationView.setChecked(!isDisabled - && override.factory instanceof RandomTrackSelection.Factory); - } - } - } - - // DialogInterface.OnClickListener - - @Override - public void onClick(DialogInterface dialog, int which) { - selector.setRendererDisabled(rendererIndex, isDisabled); - if (override != null) { - selector.setSelectionOverride(rendererIndex, trackGroups, override); - } else { - selector.clearSelectionOverrides(rendererIndex); - } - } - - // View.OnClickListener - - @Override - public void onClick(View view) { - if (view == disableView) { - isDisabled = true; - override = null; - } else if (view == defaultView) { - isDisabled = false; - override = null; - } else if (view == enableRandomAdaptationView) { - setOverride(override.groupIndex, override.tracks, !enableRandomAdaptationView.isChecked()); - } else { - isDisabled = false; - @SuppressWarnings("unchecked") - Pair tag = (Pair) view.getTag(); - int groupIndex = tag.first; - int trackIndex = tag.second; - if (!trackGroupsAdaptive[groupIndex] || override == null - || override.groupIndex != groupIndex) { - override = new SelectionOverride(FIXED_FACTORY, groupIndex, trackIndex); - } else { - // The group being modified is adaptive and we already have a non-null override. - boolean isEnabled = ((CheckedTextView) view).isChecked(); - int overrideLength = override.length; - if (isEnabled) { - // Remove the track from the override. - if (overrideLength == 1) { - // The last track is being removed, so the override becomes empty. - override = null; - isDisabled = true; - } else { - setOverride(groupIndex, getTracksRemoving(override, trackIndex), - enableRandomAdaptationView.isChecked()); - } - } else { - // Add the track to the override. - setOverride(groupIndex, getTracksAdding(override, trackIndex), - enableRandomAdaptationView.isChecked()); - } - } - } - // Update the views with the new state. - updateViews(); - } - - private void setOverride(int group, int[] tracks, boolean enableRandomAdaptation) { - TrackSelection.Factory factory = tracks.length == 1 ? FIXED_FACTORY - : (enableRandomAdaptation ? RANDOM_FACTORY : adaptiveTrackSelectionFactory); - override = new SelectionOverride(factory, group, tracks); - } - - // Track array manipulation. - - private static int[] getTracksAdding(SelectionOverride override, int addedTrack) { - int[] tracks = override.tracks; - tracks = Arrays.copyOf(tracks, tracks.length + 1); - tracks[tracks.length - 1] = addedTrack; - return tracks; - } - - private static int[] getTracksRemoving(SelectionOverride override, int removedTrack) { - int[] tracks = new int[override.length - 1]; - int trackCount = 0; - for (int i = 0; i < tracks.length + 1; i++) { - int track = override.tracks[i]; - if (track != removedTrack) { - tracks[trackCount++] = track; - } - } - return tracks; - } - -} diff --git a/demos/main/src/main/res/drawable-hdpi/ic_download.png b/demos/main/src/main/res/drawable-hdpi/ic_download.png new file mode 100644 index 0000000000..fa3ebbb310 Binary files /dev/null and b/demos/main/src/main/res/drawable-hdpi/ic_download.png differ diff --git a/demos/main/src/main/res/drawable-hdpi/ic_download_done.png b/demos/main/src/main/res/drawable-hdpi/ic_download_done.png new file mode 100644 index 0000000000..fa0ec9dd68 Binary files /dev/null and b/demos/main/src/main/res/drawable-hdpi/ic_download_done.png differ diff --git a/demos/main/src/main/res/drawable-mdpi/ic_download.png b/demos/main/src/main/res/drawable-mdpi/ic_download.png new file mode 100644 index 0000000000..c8a2039c58 Binary files /dev/null and b/demos/main/src/main/res/drawable-mdpi/ic_download.png differ diff --git a/demos/main/src/main/res/drawable-mdpi/ic_download_done.png b/demos/main/src/main/res/drawable-mdpi/ic_download_done.png new file mode 100644 index 0000000000..08073a2a6d Binary files /dev/null and b/demos/main/src/main/res/drawable-mdpi/ic_download_done.png differ diff --git a/demos/main/src/main/res/drawable-xhdpi/ic_download.png b/demos/main/src/main/res/drawable-xhdpi/ic_download.png new file mode 100644 index 0000000000..671e0b3ece Binary files /dev/null and b/demos/main/src/main/res/drawable-xhdpi/ic_download.png differ diff --git a/demos/main/src/main/res/drawable-xhdpi/ic_download_done.png b/demos/main/src/main/res/drawable-xhdpi/ic_download_done.png new file mode 100644 index 0000000000..2339c0bf16 Binary files /dev/null and b/demos/main/src/main/res/drawable-xhdpi/ic_download_done.png differ diff --git a/demos/main/src/main/res/drawable-xxhdpi/ic_download.png b/demos/main/src/main/res/drawable-xxhdpi/ic_download.png new file mode 100644 index 0000000000..f02715177a Binary files /dev/null and b/demos/main/src/main/res/drawable-xxhdpi/ic_download.png differ diff --git a/demos/main/src/main/res/drawable-xxhdpi/ic_download_done.png b/demos/main/src/main/res/drawable-xxhdpi/ic_download_done.png new file mode 100644 index 0000000000..b631a00088 Binary files /dev/null and b/demos/main/src/main/res/drawable-xxhdpi/ic_download_done.png differ diff --git a/demos/main/src/main/res/drawable-xxxhdpi/ic_download.png b/demos/main/src/main/res/drawable-xxxhdpi/ic_download.png new file mode 100644 index 0000000000..6602791545 Binary files /dev/null and b/demos/main/src/main/res/drawable-xxxhdpi/ic_download.png differ diff --git a/demos/main/src/main/res/drawable-xxxhdpi/ic_download_done.png b/demos/main/src/main/res/drawable-xxxhdpi/ic_download_done.png new file mode 100644 index 0000000000..52fe8f6990 Binary files /dev/null and b/demos/main/src/main/res/drawable-xxxhdpi/ic_download_done.png differ diff --git a/demos/main/src/main/res/layout/player_activity.xml b/demos/main/src/main/res/layout/player_activity.xml index 3f8cdaa7d6..6b84033273 100644 --- a/demos/main/src/main/res/layout/player_activity.xml +++ b/demos/main/src/main/res/layout/player_activity.xml @@ -20,7 +20,7 @@ android:layout_height="match_parent" android:keepScreenOn="true"> - @@ -42,15 +42,7 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal" - android:visibility="gone"> - -