Merge branch 'dev-v2' of https://github.com/google/ExoPlayer into dev-v2
5
.gitignore
vendored
|
|
@ -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
|
||||
|
||||
|
|
|
|||
71
.hgignore
Normal file
|
|
@ -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
|
||||
|
|
@ -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".
|
||||
|
||||
|
|
|
|||
34
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 ##
|
||||
|
|
|
|||
459
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
|
||||
|
|
|
|||
12
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
|
||||
|
|
|
|||
|
|
@ -1,6 +1,4 @@
|
|||
<?xml version="1.0"?>
|
||||
<!--
|
||||
Copyright (C) 2016 The Android Open Source Project
|
||||
<!-- Copyright (C) 2018 The Android Open Source Project
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
|
@ -14,8 +12,8 @@
|
|||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
<resources>
|
||||
<string name="exo_media_action_repeat_all_description">"Ulang semua"</string>
|
||||
<string name="exo_media_action_repeat_off_description">"Tiada ulangan"</string>
|
||||
<string name="exo_media_action_repeat_one_description">"Ulangan"</string>
|
||||
</resources>
|
||||
<lint>
|
||||
<issue id="InvalidPackage">
|
||||
<ignore path="**/checker-qual-*.jar"/>
|
||||
</issue>
|
||||
</lint>
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
6
demos/cast/proguard-rules.txt
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
# Proguard rules specific to the Cast demo app.
|
||||
|
||||
# Accessed via menu.xml
|
||||
-keep class android.support.v7.app.MediaRouteActionProvider {
|
||||
*;
|
||||
}
|
||||
|
|
@ -14,12 +14,10 @@
|
|||
limitations under the License.
|
||||
-->
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.google.android.exoplayer2.castdemo"
|
||||
android:versionCode="0001"
|
||||
android:versionName="0.0.1">
|
||||
package="com.google.android.exoplayer2.castdemo">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
<uses-sdk android:minSdkVersion="16" android:targetSdkVersion="26"/>
|
||||
<uses-sdk/>
|
||||
|
||||
<application android:label="@string/application_name" android:icon="@mipmap/ic_launcher"
|
||||
android:largeHeap="true" android:allowBackup="false">
|
||||
|
|
@ -34,7 +32,6 @@
|
|||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN"/>
|
||||
<category android:name="android.intent.category.LAUNCHER"/>
|
||||
<category android:name="android.intent.category.BROWSABLE"/>
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
|
|
|
|||
|
|
@ -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() {}
|
||||
|
||||
}
|
||||
|
|
@ -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<CastDemoUtil.Sample> {
|
||||
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<QueueItemViewHolder> {
|
||||
|
||||
@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<Sample> {
|
||||
|
||||
public SampleListAdapter(Context context) {
|
||||
super(context, android.R.layout.simple_list_item_1, DemoUtil.SAMPLES);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<DemoUtil.Sample> 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();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
<?xml version="1.0"?>
|
||||
<!--
|
||||
Copyright (C) 2016 The Android Open Source Project
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- 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.
|
||||
|
|
@ -14,8 +13,8 @@
|
|||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
<resources>
|
||||
<string name="exo_media_action_repeat_all_description">"Паўтарыць усё"</string>
|
||||
<string name="exo_media_action_repeat_off_description">"Паўтараць ні"</string>
|
||||
<string name="exo_media_action_repeat_one_description">"Паўтарыць адзін"</string>
|
||||
</resources>
|
||||
<vector android:alpha="0.8" android:height="24dp" android:viewportHeight="24.0"
|
||||
android:viewportWidth="24.0" android:width="24dp"
|
||||
xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="#FFFFFF" android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM17,13h-4v4h-2v-4L7,13v-2h4L11,7h2v4h4v2z"/>
|
||||
</vector>
|
||||
|
|
@ -13,29 +13,40 @@
|
|||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
<LinearLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:orientation="vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:keepScreenOn="true">
|
||||
<com.google.android.exoplayer2.ui.SimpleExoPlayerView android:id="@+id/player_view"
|
||||
android:layout_width="match_parent"
|
||||
app:repeat_toggle_modes="all|one"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="12" />
|
||||
<ListView
|
||||
android:id="@+id/sample_list"
|
||||
android:choiceMode="singleChoice"
|
||||
<com.google.android.exoplayer2.ui.PlayerView android:id="@+id/local_player_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="12" />
|
||||
<com.google.android.exoplayer2.ui.PlaybackControlView
|
||||
android:id="@+id/cast_control_view"
|
||||
android:layout_weight="12"
|
||||
app:repeat_toggle_modes="all|one"/>
|
||||
<RelativeLayout android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="12">
|
||||
<android.support.v7.widget.RecyclerView android:id="@+id/sample_list"
|
||||
android:choiceMode="singleChoice"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:scrollbars="vertical"
|
||||
android:fadeScrollbars="false"/>
|
||||
<ImageButton android:id="@+id/add_sample_button"
|
||||
android:background="@drawable/ic_add_circle_white_24dp"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:layout_alignParentRight="true"
|
||||
android:layout_alignParentBottom="true"
|
||||
android:padding="30dp"/>
|
||||
</RelativeLayout>
|
||||
<com.google.android.exoplayer2.ui.PlayerControlView android:id="@+id/cast_control_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
app:show_timeout="-1"
|
||||
android:layout_weight="2"
|
||||
android:visibility="gone"/>
|
||||
android:visibility="gone"
|
||||
app:repeat_toggle_modes="all|one"
|
||||
app:show_timeout="-1"/>
|
||||
</LinearLayout>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
<?xml version="1.0"?>
|
||||
<!--
|
||||
Copyright (C) 2016 The Android Open Source Project
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- 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.
|
||||
|
|
@ -14,8 +13,13 @@
|
|||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
<resources>
|
||||
<string name="exo_media_action_repeat_all_description">"Bütün təkrarlayın"</string>
|
||||
<string name="exo_media_action_repeat_one_description">"Təkrar bir"</string>
|
||||
<string name="exo_media_action_repeat_off_description">"Heç bir təkrar"</string>
|
||||
</resources>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<ListView android:id="@+id/sample_list"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="250dp"
|
||||
android:fadeScrollbars="false"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
|
@ -16,10 +16,10 @@
|
|||
|
||||
<resources>
|
||||
|
||||
<string name="application_name">ExoCast Demo</string>
|
||||
<string name="application_name">Exo Cast Demo</string>
|
||||
|
||||
<string name="media_route_menu_title">ExoCast</string>
|
||||
<string name="media_route_menu_title">Cast</string>
|
||||
|
||||
<string name="error_unsupported_drm">DRM scheme not supported by this device.</string>
|
||||
<string name="sample_list_dialog_title">Add samples</string>
|
||||
|
||||
</resources>
|
||||
|
|
|
|||
4
demos/ima/README.md
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
# IMA demo application #
|
||||
|
||||
This folder contains a demo application that showcases ExoPlayer integration
|
||||
with the IMA SDK.
|
||||
53
demos/ima/build.gradle
Normal file
|
|
@ -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
|
||||
}
|
||||
|
|
@ -13,21 +13,25 @@
|
|||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
package="com.google.android.exoplayer2.source.dash.test">
|
||||
package="com.google.android.exoplayer2.imademo">
|
||||
|
||||
<uses-sdk android:minSdkVersion="14" android:targetSdkVersion="26"/>
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
<uses-sdk/>
|
||||
|
||||
<application android:label="@string/application_name" android:icon="@mipmap/ic_launcher"
|
||||
android:largeHeap="true" android:allowBackup="false">
|
||||
|
||||
<activity android:name="com.google.android.exoplayer2.imademo.MainActivity"
|
||||
android:configChanges="keyboard|keyboardHidden|orientation|screenSize|screenLayout|smallestScreenSize|uiMode"
|
||||
android:label="@string/application_name"
|
||||
android:theme="@style/PlayerTheme">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN"/>
|
||||
<category android:name="android.intent.category.LAUNCHER"/>
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<application android:debuggable="true"
|
||||
android:allowBackup="false"
|
||||
tools:ignore="MissingApplicationIcon,HardcodedDebugMode">
|
||||
<uses-library android:name="android.test.runner"/>
|
||||
</application>
|
||||
|
||||
<instrumentation
|
||||
android:targetPackage="com.google.android.exoplayer2.source.dash.test"
|
||||
android:name="android.test.InstrumentationTestRunner"/>
|
||||
|
||||
</manifest>
|
||||
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
21
demos/ima/src/main/res/layout/main_activity.xml
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- 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.
|
||||
-->
|
||||
<com.google.android.exoplayer2.ui.PlayerView
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@+id/player_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:keepScreenOn="true"/>
|
||||
BIN
demos/ima/src/main/res/mipmap-hdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 3.3 KiB |
BIN
demos/ima/src/main/res/mipmap-mdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 2.1 KiB |
BIN
demos/ima/src/main/res/mipmap-xhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 4.8 KiB |
BIN
demos/ima/src/main/res/mipmap-xxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 7.3 KiB |
BIN
demos/ima/src/main/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
|
|
@ -13,21 +13,12 @@
|
|||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
<resources>
|
||||
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
package="com.google.android.exoplayer2.source.hls.test">
|
||||
<string name="application_name">Exo IMA Demo</string>
|
||||
|
||||
<uses-sdk android:minSdkVersion="14" android:targetSdkVersion="26"/>
|
||||
<string name="content_url"><![CDATA[http://rmcdn.2mdn.net/MotifFiles/html/1248596/android_1330378998288.mp4]]></string>
|
||||
|
||||
<application android:debuggable="true"
|
||||
android:allowBackup="false"
|
||||
tools:ignore="MissingApplicationIcon,HardcodedDebugMode">
|
||||
<uses-library android:name="android.test.runner"/>
|
||||
</application>
|
||||
<string name="ad_tag_url"><![CDATA[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=]]></string>
|
||||
|
||||
<instrumentation
|
||||
android:targetPackage="com.google.android.exoplayer2.source.hls.test"
|
||||
android:name="android.test.InstrumentationTestRunner"/>
|
||||
|
||||
</manifest>
|
||||
</resources>
|
||||
|
|
@ -16,6 +16,7 @@
|
|||
<resources xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<style name="PlayerTheme" parent="android:Theme.Holo">
|
||||
<item name="android:windowNoTitle">true</item>
|
||||
<item name="android:windowBackground">@android:color/black</item>
|
||||
</style>
|
||||
|
||||
|
|
@ -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')
|
||||
}
|
||||
|
|
|
|||
7
demos/main/proguard-rules.txt
Normal file
|
|
@ -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 {
|
||||
<init>(android.content.Context, android.net.Uri);
|
||||
}
|
||||
|
|
@ -15,15 +15,15 @@
|
|||
-->
|
||||
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.google.android.exoplayer2.demo"
|
||||
android:versionCode="2501"
|
||||
android:versionName="2.5.1">
|
||||
package="com.google.android.exoplayer2.demo">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
|
||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
|
||||
|
||||
<uses-feature android:name="android.software.leanback" android:required="false"/>
|
||||
<uses-feature android:name="android.hardware.touchscreen" android:required="false"/>
|
||||
<uses-sdk android:minSdkVersion="16" android:targetSdkVersion="26"/>
|
||||
<uses-sdk/>
|
||||
|
||||
<application
|
||||
android:label="@string/application_name"
|
||||
|
|
@ -75,6 +75,18 @@
|
|||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<service android:name="com.google.android.exoplayer2.demo.DemoDownloadService"
|
||||
android:exported="false">
|
||||
<intent-filter>
|
||||
<action android:name="com.google.android.exoplayer.downloadService.action.INIT"/>
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
</intent-filter>
|
||||
</service>
|
||||
|
||||
<service android:name="com.google.android.exoplayer2.scheduler.PlatformScheduler$PlatformSchedulerService"
|
||||
android:permission="android.permission.BIND_JOB_SERVICE"
|
||||
android:exported="true"/>
|
||||
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<? super DataSource> 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<? super DataSource> 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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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() {}
|
||||
}
|
||||
|
|
@ -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.
|
||||
*
|
||||
* <p>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<Listener> listeners;
|
||||
private final HashMap<Uri, DownloadAction> 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 <K> List<K> 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<TrackKey> trackKeys;
|
||||
private final ArrayAdapter<String> 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<TrackKey> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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 "?";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<AlertDialog, TrackSelectionView> 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<FrameworkMediaCrypto> drmSessionManager = null;
|
||||
if (drmSchemeUuid != null) {
|
||||
String drmLicenseUrl = intent.getStringExtra(DRM_LICENSE_URL);
|
||||
String[] keyRequestPropertiesArray = intent.getStringArrayExtra(DRM_KEY_REQUEST_PROPERTIES);
|
||||
DefaultDrmSessionManager<FrameworkMediaCrypto> 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<RepresentationKey>) getOfflineStreamKeys(uri)))
|
||||
.createMediaSource(uri);
|
||||
case C.TYPE_SS:
|
||||
return new SsMediaSource.Factory(
|
||||
new DefaultSsChunkSource.Factory(mediaDataSourceFactory),
|
||||
buildDataSourceFactory(false))
|
||||
.setManifestParser(
|
||||
new FilteringManifestParser<>(
|
||||
new SsManifestParser(), (List<StreamKey>) 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<RenditionKey>) 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<FrameworkMediaCrypto> 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<FrameworkMediaCrypto> 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<? extends AdsLoader> 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<ExoPlaybackException> {
|
||||
|
||||
@Override
|
||||
public Pair<Integer, String> 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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<SampleGroup> 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<String, Void, List<SampleGroup>> {
|
||||
|
|
@ -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<UriSample> 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<SampleGroup> sampleGroups;
|
||||
private List<SampleGroup> sampleGroups;
|
||||
|
||||
public SampleAdapter(Context context, List<SampleGroup> sampleGroups) {
|
||||
this.context = context;
|
||||
public SampleAdapter() {
|
||||
sampleGroups = Collections.emptyList();
|
||||
}
|
||||
|
||||
public void setSampleGroups(List<SampleGroup> 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)
|
||||
|
|
|
|||
|
|
@ -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<Integer, Integer> tag = (Pair<Integer, Integer>) 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;
|
||||
}
|
||||
|
||||
}
|
||||
BIN
demos/main/src/main/res/drawable-hdpi/ic_download.png
Normal file
|
After Width: | Height: | Size: 199 B |
BIN
demos/main/src/main/res/drawable-hdpi/ic_download_done.png
Normal file
|
After Width: | Height: | Size: 218 B |
BIN
demos/main/src/main/res/drawable-mdpi/ic_download.png
Normal file
|
After Width: | Height: | Size: 163 B |
BIN
demos/main/src/main/res/drawable-mdpi/ic_download_done.png
Normal file
|
After Width: | Height: | Size: 182 B |
BIN
demos/main/src/main/res/drawable-xhdpi/ic_download.png
Normal file
|
After Width: | Height: | Size: 187 B |
BIN
demos/main/src/main/res/drawable-xhdpi/ic_download_done.png
Normal file
|
After Width: | Height: | Size: 304 B |
BIN
demos/main/src/main/res/drawable-xxhdpi/ic_download.png
Normal file
|
After Width: | Height: | Size: 303 B |
BIN
demos/main/src/main/res/drawable-xxhdpi/ic_download_done.png
Normal file
|
After Width: | Height: | Size: 450 B |
BIN
demos/main/src/main/res/drawable-xxxhdpi/ic_download.png
Normal file
|
After Width: | Height: | Size: 304 B |
BIN
demos/main/src/main/res/drawable-xxxhdpi/ic_download_done.png
Normal file
|
After Width: | Height: | Size: 575 B |
|
|
@ -20,7 +20,7 @@
|
|||
android:layout_height="match_parent"
|
||||
android:keepScreenOn="true">
|
||||
|
||||
<com.google.android.exoplayer2.ui.SimpleExoPlayerView android:id="@+id/player_view"
|
||||
<com.google.android.exoplayer2.ui.PlayerView android:id="@+id/player_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"/>
|
||||
|
||||
|
|
@ -42,15 +42,7 @@
|
|||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:visibility="gone">
|
||||
|
||||
<Button android:id="@+id/retry_button"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/retry"
|
||||
android:visibility="gone"/>
|
||||
|
||||
</LinearLayout>
|
||||
android:visibility="gone"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
|
|
|
|||
38
demos/main/src/main/res/layout/sample_list_item.xml
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Copyright (C) 2018 The Android Open Source Project
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingStart="12dp"
|
||||
android:paddingEnd="12dp"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<TextView android:id="@+id/sample_title"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:gravity="center_vertical"
|
||||
android:minHeight="?android:attr/listPreferredItemHeightSmall"
|
||||
android:textAppearance="?android:attr/textAppearanceListItemSmall"/>
|
||||
|
||||
<ImageButton android:id="@+id/download_button"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:contentDescription="@string/exo_download_description"
|
||||
android:background="@android:color/transparent"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
|
@ -1,6 +1,5 @@
|
|||
<?xml version="1.0"?>
|
||||
<!--
|
||||
Copyright (C) 2016 The Android Open Source Project
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Copyright (C) 2018 The Android Open Source Project
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
|
@ -14,8 +13,7 @@
|
|||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
<resources>
|
||||
<string name="exo_media_action_repeat_all_description">"Repetir todo"</string>
|
||||
<string name="exo_media_action_repeat_off_description">"Non repetir"</string>
|
||||
<string name="exo_media_action_repeat_one_description">"Repetir un"</string>
|
||||
</resources>
|
||||
<ListView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@+id/representation_list"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"/>
|
||||
|
|
@ -13,26 +13,15 @@
|
|||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
|
||||
<string name="application_name">ExoPlayer</string>
|
||||
|
||||
<string name="video">Video</string>
|
||||
|
||||
<string name="audio">Audio</string>
|
||||
|
||||
<string name="text">Text</string>
|
||||
|
||||
<string name="retry">Retry</string>
|
||||
|
||||
<string name="selection_disabled">Disabled</string>
|
||||
|
||||
<string name="selection_default">Default</string>
|
||||
|
||||
<string name="unexpected_intent_action">Unexpected intent action: <xliff:g id="action">%1$s</xliff:g></string>
|
||||
|
||||
<string name="enable_random_adaptation">Enable random adaptation</string>
|
||||
<string name="error_generic">Playback failed</string>
|
||||
|
||||
<string name="error_unrecognized_abr_algorithm">Unrecognized ABR algorithm</string>
|
||||
|
||||
<string name="error_drm_not_supported">Protected content not supported on API levels below 18</string>
|
||||
|
||||
|
|
@ -58,4 +47,14 @@
|
|||
|
||||
<string name="ima_not_loaded">Playing sample without ads, as the IMA extension was not loaded</string>
|
||||
|
||||
<string name="download_start_error">Failed to start download</string>
|
||||
|
||||
<string name="download_playlist_unsupported">This demo app does not support downloading playlists</string>
|
||||
|
||||
<string name="download_drm_unsupported">This demo app does not support downloading protected content</string>
|
||||
|
||||
<string name="download_scheme_unsupported">This demo app only supports downloading http streams</string>
|
||||
|
||||
<string name="download_ads_unsupported">IMA does not support offline ads</string>
|
||||
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -13,7 +13,6 @@
|
|||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
|
||||
<resources xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<style name="PlayerTheme" parent="android:Theme.Holo">
|
||||
|
|
|
|||
|
|
@ -12,10 +12,10 @@ Cast receiver app.
|
|||
The easiest way to use the extension is to add it as a gradle dependency:
|
||||
|
||||
```gradle
|
||||
compile 'com.google.android.exoplayer:extension-cast:rX.X.X'
|
||||
implementation 'com.google.android.exoplayer:extension-cast:2.X.X'
|
||||
```
|
||||
|
||||
where `rX.X.X` is the version, which must match the version of the ExoPlayer
|
||||
where `2.X.X` is the version, which must match the version of the ExoPlayer
|
||||
library being used.
|
||||
|
||||
Alternatively, you can clone the ExoPlayer repository and depend on the module
|
||||
|
|
@ -27,7 +27,4 @@ locally. Instructions for doing this can be found in ExoPlayer's
|
|||
## Using the extension ##
|
||||
|
||||
Create a `CastPlayer` and use it to integrate Cast into your app using
|
||||
ExoPlayer's common Player interface. You can try the Cast Extension to see how a
|
||||
[PlaybackControlView][] can be used to control playback in a remote receiver app.
|
||||
|
||||
[PlaybackControlView]: https://google.github.io/ExoPlayer/doc/reference/com/google/android/exoplayer2/ui/PlaybackControlView.html
|
||||
ExoPlayer's common `Player` interface.
|
||||
|
|
|
|||
|
|
@ -21,16 +21,29 @@ android {
|
|||
defaultConfig {
|
||||
minSdkVersion 14
|
||||
targetSdkVersion project.ext.targetSdkVersion
|
||||
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
|
||||
consumerProguardFiles 'proguard-rules.txt'
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
compile 'com.android.support:appcompat-v7:' + supportLibraryVersion
|
||||
compile 'com.android.support:mediarouter-v7:' + supportLibraryVersion
|
||||
compile 'com.google.android.gms:play-services-cast-framework:' + playServicesLibraryVersion
|
||||
compile project(modulePrefix + 'library-core')
|
||||
compile project(modulePrefix + 'library-ui')
|
||||
// These dependencies are necessary to force the supportLibraryVersion of
|
||||
// com.android.support:support-v4, com.android.support:appcompat-v7 and
|
||||
// com.android.support:mediarouter-v7 to be used. Else older versions are
|
||||
// used, for example:
|
||||
// com.google.android.gms:play-services-cast-framework:12.0.0
|
||||
// |-- com.google.android.gms:play-services-basement:12.0.0
|
||||
// |-- com.android.support:support-v4:26.1.0
|
||||
api 'com.android.support:support-v4:' + supportLibraryVersion
|
||||
api 'com.android.support:appcompat-v7:' + supportLibraryVersion
|
||||
api 'com.android.support:mediarouter-v7:' + supportLibraryVersion
|
||||
api 'com.google.android.gms:play-services-cast-framework:' + playServicesLibraryVersion
|
||||
implementation project(modulePrefix + 'library-core')
|
||||
implementation project(modulePrefix + 'library-ui')
|
||||
testImplementation project(modulePrefix + 'testutils')
|
||||
testImplementation 'junit:junit:' + junitVersion
|
||||
testImplementation 'org.mockito:mockito-core:' + mockitoVersion
|
||||
testImplementation 'org.robolectric:robolectric:' + robolectricVersion
|
||||
testImplementation project(modulePrefix + 'testutils-robolectric')
|
||||
}
|
||||
|
||||
ext {
|
||||
|
|
|
|||
4
extensions/cast/proguard-rules.txt
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
# Proguard rules specific to the Cast extension.
|
||||
|
||||
# DefaultCastOptionsProvider is commonly referred to only by the app's manifest.
|
||||
-keep class com.google.android.exoplayer2.ext.cast.DefaultCastOptionsProvider
|
||||
|
|
@ -15,23 +15,25 @@
|
|||
*/
|
||||
package com.google.android.exoplayer2.ext.cast;
|
||||
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.util.Log;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.ExoPlaybackException;
|
||||
import com.google.android.exoplayer2.PlaybackParameters;
|
||||
import com.google.android.exoplayer2.Player;
|
||||
import com.google.android.exoplayer2.Timeline;
|
||||
import com.google.android.exoplayer2.source.SinglePeriodTimeline;
|
||||
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.TrackSelection;
|
||||
import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
|
||||
import com.google.android.exoplayer2.util.Assertions;
|
||||
import com.google.android.exoplayer2.util.MimeTypes;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
import com.google.android.gms.cast.CastStatusCodes;
|
||||
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.MediaStatus;
|
||||
import com.google.android.gms.cast.MediaTrack;
|
||||
import com.google.android.gms.cast.framework.CastContext;
|
||||
|
|
@ -40,7 +42,7 @@ import com.google.android.gms.cast.framework.SessionManager;
|
|||
import com.google.android.gms.cast.framework.SessionManagerListener;
|
||||
import com.google.android.gms.cast.framework.media.RemoteMediaClient;
|
||||
import com.google.android.gms.cast.framework.media.RemoteMediaClient.MediaChannelResult;
|
||||
import com.google.android.gms.common.api.CommonStatusCodes;
|
||||
import com.google.android.gms.common.api.PendingResult;
|
||||
import com.google.android.gms.common.api.ResultCallback;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CopyOnWriteArraySet;
|
||||
|
|
@ -48,19 +50,16 @@ import java.util.concurrent.CopyOnWriteArraySet;
|
|||
/**
|
||||
* {@link Player} implementation that communicates with a Cast receiver app.
|
||||
*
|
||||
* <p>Calls to the methods in this class depend on the availability of an underlying cast session.
|
||||
* If no session is available, method calls have no effect. To keep track of the underyling session,
|
||||
* <p>The behavior of this class depends on the underlying Cast session, which is obtained from the
|
||||
* Cast context passed to {@link #CastPlayer}. To keep track of the session,
|
||||
* {@link #isCastSessionAvailable()} can be queried and {@link SessionAvailabilityListener} can be
|
||||
* implemented and attached to the player.
|
||||
* implemented and attached to the player.</p>
|
||||
*
|
||||
* <p>Methods should be called on the application's main thread.
|
||||
* <p> If no session is available, the player state will remain unchanged and calls to methods that
|
||||
* alter it will be ignored. Querying the player state is possible even when no session is
|
||||
* available, in which case, the last observed receiver app state is reported.</p>
|
||||
*
|
||||
* <p>Known issues:
|
||||
* <ul>
|
||||
* <li>Part of the Cast API is not exposed through this interface. For instance, volume settings
|
||||
* and track selection.</li>
|
||||
* <li> Repeat mode is not working. See [internal: b/64137174].</li>
|
||||
* </ul>
|
||||
* <p>Methods should be called on the application's main thread.</p>
|
||||
*/
|
||||
public final class CastPlayer implements Player {
|
||||
|
||||
|
|
@ -88,17 +87,20 @@ public final class CastPlayer implements Player {
|
|||
private static final int RENDERER_INDEX_AUDIO = 1;
|
||||
private static final int RENDERER_INDEX_TEXT = 2;
|
||||
private static final long PROGRESS_REPORT_PERIOD_MS = 1000;
|
||||
private static final TrackGroupArray EMPTY_TRACK_GROUP_ARRAY = new TrackGroupArray();
|
||||
private static final TrackSelectionArray EMPTY_TRACK_SELECTION_ARRAY =
|
||||
new TrackSelectionArray(null, null, null);
|
||||
private static final long[] EMPTY_TRACK_ID_ARRAY = new long[0];
|
||||
|
||||
private final CastContext castContext;
|
||||
// TODO: Allow custom implementations of CastTimelineTracker.
|
||||
private final CastTimelineTracker timelineTracker;
|
||||
private final Timeline.Window window;
|
||||
private final Timeline.Period period;
|
||||
|
||||
private RemoteMediaClient remoteMediaClient;
|
||||
|
||||
// Result callbacks.
|
||||
private final StatusListener statusListener;
|
||||
private final RepeatModeResultCallback repeatModeResultCallback;
|
||||
private final SeekResultCallback seekResultCallback;
|
||||
|
||||
// Listeners.
|
||||
|
|
@ -106,54 +108,165 @@ public final class CastPlayer implements Player {
|
|||
private SessionAvailabilityListener sessionAvailabilityListener;
|
||||
|
||||
// Internal state.
|
||||
private RemoteMediaClient remoteMediaClient;
|
||||
private Timeline currentTimeline;
|
||||
private CastTimeline currentTimeline;
|
||||
private TrackGroupArray currentTrackGroups;
|
||||
private TrackSelectionArray currentTrackSelection;
|
||||
private int playbackState;
|
||||
private int repeatMode;
|
||||
private int currentWindowIndex;
|
||||
private boolean playWhenReady;
|
||||
private long lastReportedPositionMs;
|
||||
private int pendingSeekCount;
|
||||
private int pendingSeekWindowIndex;
|
||||
private long pendingSeekPositionMs;
|
||||
private boolean waitingForInitialTimeline;
|
||||
|
||||
/**
|
||||
* @param castContext The context from which the cast session is obtained.
|
||||
*/
|
||||
public CastPlayer(CastContext castContext) {
|
||||
this.castContext = castContext;
|
||||
timelineTracker = new CastTimelineTracker();
|
||||
window = new Timeline.Window();
|
||||
period = new Timeline.Period();
|
||||
statusListener = new StatusListener();
|
||||
repeatModeResultCallback = new RepeatModeResultCallback();
|
||||
seekResultCallback = new SeekResultCallback();
|
||||
listeners = new CopyOnWriteArraySet<>();
|
||||
|
||||
SessionManager sessionManager = castContext.getSessionManager();
|
||||
sessionManager.addSessionManagerListener(statusListener, CastSession.class);
|
||||
CastSession session = sessionManager.getCurrentCastSession();
|
||||
remoteMediaClient = session != null ? session.getRemoteMediaClient() : null;
|
||||
|
||||
playbackState = STATE_IDLE;
|
||||
repeatMode = REPEAT_MODE_OFF;
|
||||
currentTimeline = CastTimeline.EMPTY_CAST_TIMELINE;
|
||||
currentTrackGroups = TrackGroupArray.EMPTY;
|
||||
currentTrackSelection = EMPTY_TRACK_SELECTION_ARRAY;
|
||||
pendingSeekWindowIndex = C.INDEX_UNSET;
|
||||
pendingSeekPositionMs = C.TIME_UNSET;
|
||||
updateInternalState();
|
||||
}
|
||||
|
||||
// Media Queue manipulation methods.
|
||||
|
||||
/**
|
||||
* Loads media into the receiver app.
|
||||
* Loads a single item media queue. If no session is available, does nothing.
|
||||
*
|
||||
* @param title The title of the media sample.
|
||||
* @param url The url from which the media is obtained.
|
||||
* @param contentMimeType The mime type of the content to play.
|
||||
* @param positionMs The position at which the playback should start in milliseconds.
|
||||
* @param playWhenReady Whether the player should start playback as soon as it is ready to do so.
|
||||
* @param item The item to load.
|
||||
* @param positionMs The position at which the playback should start in milliseconds relative to
|
||||
* the start of the item at {@code startIndex}. If {@link C#TIME_UNSET} is passed, playback
|
||||
* starts at position 0.
|
||||
* @return The Cast {@code PendingResult}, or null if no session is available.
|
||||
*/
|
||||
public void load(String title, String url, String contentMimeType, long positionMs,
|
||||
boolean playWhenReady) {
|
||||
lastReportedPositionMs = 0;
|
||||
if (remoteMediaClient != null) {
|
||||
MediaMetadata movieMetadata = new MediaMetadata(MediaMetadata.MEDIA_TYPE_MOVIE);
|
||||
movieMetadata.putString(MediaMetadata.KEY_TITLE, title);
|
||||
MediaInfo mediaInfo = new MediaInfo.Builder(url).setStreamType(MediaInfo.STREAM_TYPE_BUFFERED)
|
||||
.setContentType(contentMimeType).setMetadata(movieMetadata).build();
|
||||
remoteMediaClient.load(mediaInfo, playWhenReady, positionMs);
|
||||
}
|
||||
public PendingResult<MediaChannelResult> loadItem(MediaQueueItem item, long positionMs) {
|
||||
return loadItems(new MediaQueueItem[] {item}, 0, positionMs, REPEAT_MODE_OFF);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether a cast session is available for playback.
|
||||
* Loads a media queue. If no session is available, does nothing.
|
||||
*
|
||||
* @param items The items to load.
|
||||
* @param startIndex The index of the item at which playback should start.
|
||||
* @param positionMs The position at which the playback should start in milliseconds relative to
|
||||
* the start of the item at {@code startIndex}. If {@link C#TIME_UNSET} is passed, playback
|
||||
* starts at position 0.
|
||||
* @param repeatMode The repeat mode for the created media queue.
|
||||
* @return The Cast {@code PendingResult}, or null if no session is available.
|
||||
*/
|
||||
public PendingResult<MediaChannelResult> loadItems(MediaQueueItem[] items, int startIndex,
|
||||
long positionMs, @RepeatMode int repeatMode) {
|
||||
if (remoteMediaClient != null) {
|
||||
positionMs = positionMs != C.TIME_UNSET ? positionMs : 0;
|
||||
waitingForInitialTimeline = true;
|
||||
return remoteMediaClient.queueLoad(items, startIndex, getCastRepeatMode(repeatMode),
|
||||
positionMs, null);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Appends a sequence of items to the media queue. If no media queue exists, does nothing.
|
||||
*
|
||||
* @param items The items to append.
|
||||
* @return The Cast {@code PendingResult}, or null if no media queue exists.
|
||||
*/
|
||||
public PendingResult<MediaChannelResult> addItems(MediaQueueItem... items) {
|
||||
return addItems(MediaQueueItem.INVALID_ITEM_ID, items);
|
||||
}
|
||||
|
||||
/**
|
||||
* Inserts a sequence of items into the media queue. If no media queue or period with id {@code
|
||||
* periodId} exist, does nothing.
|
||||
*
|
||||
* @param periodId The id of the period ({@link #getCurrentTimeline}) that corresponds to the item
|
||||
* that will follow immediately after the inserted items.
|
||||
* @param items The items to insert.
|
||||
* @return The Cast {@code PendingResult}, or null if no media queue or no period with id {@code
|
||||
* periodId} exist.
|
||||
*/
|
||||
public PendingResult<MediaChannelResult> addItems(int periodId, MediaQueueItem... items) {
|
||||
if (getMediaStatus() != null && (periodId == MediaQueueItem.INVALID_ITEM_ID
|
||||
|| currentTimeline.getIndexOfPeriod(periodId) != C.INDEX_UNSET)) {
|
||||
return remoteMediaClient.queueInsertItems(items, periodId, null);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes an item from the media queue. If no media queue or period with id {@code periodId}
|
||||
* exist, does nothing.
|
||||
*
|
||||
* @param periodId The id of the period ({@link #getCurrentTimeline}) that corresponds to the item
|
||||
* to remove.
|
||||
* @return The Cast {@code PendingResult}, or null if no media queue or no period with id {@code
|
||||
* periodId} exist.
|
||||
*/
|
||||
public PendingResult<MediaChannelResult> removeItem(int periodId) {
|
||||
if (getMediaStatus() != null && currentTimeline.getIndexOfPeriod(periodId) != C.INDEX_UNSET) {
|
||||
return remoteMediaClient.queueRemoveItem(periodId, null);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Moves an existing item within the media queue. If no media queue or period with id {@code
|
||||
* periodId} exist, does nothing.
|
||||
*
|
||||
* @param periodId The id of the period ({@link #getCurrentTimeline}) that corresponds to the item
|
||||
* to move.
|
||||
* @param newIndex The target index of the item in the media queue. Must be in the range 0 <=
|
||||
* index < {@link Timeline#getPeriodCount()}, as provided by {@link #getCurrentTimeline()}.
|
||||
* @return The Cast {@code PendingResult}, or null if no media queue or no period with id {@code
|
||||
* periodId} exist.
|
||||
*/
|
||||
public PendingResult<MediaChannelResult> moveItem(int periodId, int newIndex) {
|
||||
Assertions.checkArgument(newIndex >= 0 && newIndex < currentTimeline.getPeriodCount());
|
||||
if (getMediaStatus() != null && currentTimeline.getIndexOfPeriod(periodId) != C.INDEX_UNSET) {
|
||||
return remoteMediaClient.queueMoveItemToNewIndex(periodId, newIndex, null);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the item that corresponds to the period with the given id, or null if no media queue or
|
||||
* period with id {@code periodId} exist.
|
||||
*
|
||||
* @param periodId The id of the period ({@link #getCurrentTimeline}) that corresponds to the item
|
||||
* to get.
|
||||
* @return The item that corresponds to the period with the given id, or null if no media queue or
|
||||
* period with id {@code periodId} exist.
|
||||
*/
|
||||
public MediaQueueItem getItem(int periodId) {
|
||||
MediaStatus mediaStatus = getMediaStatus();
|
||||
return mediaStatus != null && currentTimeline.getIndexOfPeriod(periodId) != C.INDEX_UNSET
|
||||
? mediaStatus.getItemById(periodId) : null;
|
||||
}
|
||||
|
||||
// CastSession methods.
|
||||
|
||||
/**
|
||||
* Returns whether a cast session is available.
|
||||
*/
|
||||
public boolean isCastSessionAvailable() {
|
||||
return remoteMediaClient != null;
|
||||
|
|
@ -170,6 +283,16 @@ public final class CastPlayer implements Player {
|
|||
|
||||
// Player implementation.
|
||||
|
||||
@Override
|
||||
public VideoComponent getVideoComponent() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public TextComponent getTextComponent() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addListener(EventListener listener) {
|
||||
listeners.add(listener);
|
||||
|
|
@ -182,21 +305,12 @@ public final class CastPlayer implements Player {
|
|||
|
||||
@Override
|
||||
public int getPlaybackState() {
|
||||
if (remoteMediaClient == null) {
|
||||
return STATE_IDLE;
|
||||
}
|
||||
int receiverAppStatus = remoteMediaClient.getPlayerState();
|
||||
switch (receiverAppStatus) {
|
||||
case MediaStatus.PLAYER_STATE_BUFFERING:
|
||||
return STATE_BUFFERING;
|
||||
case MediaStatus.PLAYER_STATE_PLAYING:
|
||||
case MediaStatus.PLAYER_STATE_PAUSED:
|
||||
return STATE_READY;
|
||||
case MediaStatus.PLAYER_STATE_IDLE:
|
||||
case MediaStatus.PLAYER_STATE_UNKNOWN:
|
||||
default:
|
||||
return STATE_IDLE;
|
||||
}
|
||||
return playbackState;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ExoPlaybackException getPlaybackError() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -213,7 +327,7 @@ public final class CastPlayer implements Player {
|
|||
|
||||
@Override
|
||||
public boolean getPlayWhenReady() {
|
||||
return remoteMediaClient != null && !remoteMediaClient.isPaused();
|
||||
return playWhenReady;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -228,16 +342,31 @@ public final class CastPlayer implements Player {
|
|||
|
||||
@Override
|
||||
public void seekTo(long positionMs) {
|
||||
seekTo(0, positionMs);
|
||||
seekTo(getCurrentWindowIndex(), positionMs);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void seekTo(int windowIndex, long positionMs) {
|
||||
if (remoteMediaClient != null) {
|
||||
remoteMediaClient.seek(positionMs).setResultCallback(seekResultCallback);
|
||||
MediaStatus mediaStatus = getMediaStatus();
|
||||
// We assume the default position is 0. There is no support for seeking to the default position
|
||||
// in RemoteMediaClient.
|
||||
positionMs = positionMs != C.TIME_UNSET ? positionMs : 0;
|
||||
if (mediaStatus != null) {
|
||||
if (getCurrentWindowIndex() != windowIndex) {
|
||||
remoteMediaClient.queueJumpToItem((int) currentTimeline.getPeriod(windowIndex, period).uid,
|
||||
positionMs, null).setResultCallback(seekResultCallback);
|
||||
} else {
|
||||
remoteMediaClient.seek(positionMs).setResultCallback(seekResultCallback);
|
||||
}
|
||||
pendingSeekCount++;
|
||||
pendingSeekWindowIndex = windowIndex;
|
||||
pendingSeekPositionMs = positionMs;
|
||||
for (EventListener listener : listeners) {
|
||||
listener.onPositionDiscontinuity();
|
||||
listener.onPositionDiscontinuity(Player.DISCONTINUITY_REASON_SEEK);
|
||||
}
|
||||
} else if (pendingSeekCount == 0) {
|
||||
for (EventListener listener : listeners) {
|
||||
listener.onSeekProcessed();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -254,14 +383,23 @@ public final class CastPlayer implements Player {
|
|||
|
||||
@Override
|
||||
public void stop() {
|
||||
stop(/* reset= */ false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop(boolean reset) {
|
||||
playbackState = STATE_IDLE;
|
||||
if (remoteMediaClient != null) {
|
||||
// TODO(b/69792021): Support or emulate stop without position reset.
|
||||
remoteMediaClient.stop();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void release() {
|
||||
castContext.getSessionManager().removeSessionManagerListener(statusListener, CastSession.class);
|
||||
SessionManager sessionManager = castContext.getSessionManager();
|
||||
sessionManager.removeSessionManagerListener(statusListener, CastSession.class);
|
||||
sessionManager.endCurrentSession(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -287,47 +425,13 @@ public final class CastPlayer implements Player {
|
|||
@Override
|
||||
public void setRepeatMode(@RepeatMode int repeatMode) {
|
||||
if (remoteMediaClient != null) {
|
||||
int castRepeatMode;
|
||||
switch (repeatMode) {
|
||||
case REPEAT_MODE_ONE:
|
||||
castRepeatMode = MediaStatus.REPEAT_MODE_REPEAT_SINGLE;
|
||||
break;
|
||||
case REPEAT_MODE_ALL:
|
||||
castRepeatMode = MediaStatus.REPEAT_MODE_REPEAT_ALL;
|
||||
break;
|
||||
case REPEAT_MODE_OFF:
|
||||
castRepeatMode = MediaStatus.REPEAT_MODE_REPEAT_OFF;
|
||||
break;
|
||||
default:
|
||||
throw new IllegalArgumentException();
|
||||
}
|
||||
remoteMediaClient.queueSetRepeatMode(castRepeatMode, null)
|
||||
.setResultCallback(repeatModeResultCallback);
|
||||
remoteMediaClient.queueSetRepeatMode(getCastRepeatMode(repeatMode), null);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@RepeatMode public int getRepeatMode() {
|
||||
if (remoteMediaClient == null) {
|
||||
return REPEAT_MODE_OFF;
|
||||
}
|
||||
MediaStatus mediaStatus = getMediaStatus();
|
||||
if (mediaStatus == null) {
|
||||
// No media session active, yet.
|
||||
return REPEAT_MODE_OFF;
|
||||
}
|
||||
int castRepeatMode = mediaStatus.getQueueRepeatMode();
|
||||
switch (castRepeatMode) {
|
||||
case MediaStatus.REPEAT_MODE_REPEAT_SINGLE:
|
||||
return REPEAT_MODE_ONE;
|
||||
case MediaStatus.REPEAT_MODE_REPEAT_ALL:
|
||||
case MediaStatus.REPEAT_MODE_REPEAT_ALL_AND_SHUFFLE:
|
||||
return REPEAT_MODE_ALL;
|
||||
case MediaStatus.REPEAT_MODE_REPEAT_OFF:
|
||||
return REPEAT_MODE_OFF;
|
||||
default:
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
return repeatMode;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -363,25 +467,49 @@ public final class CastPlayer implements Player {
|
|||
|
||||
@Override
|
||||
public int getCurrentPeriodIndex() {
|
||||
return 0;
|
||||
return getCurrentWindowIndex();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getCurrentWindowIndex() {
|
||||
return 0;
|
||||
return pendingSeekWindowIndex != C.INDEX_UNSET ? pendingSeekWindowIndex : currentWindowIndex;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getNextWindowIndex() {
|
||||
return currentTimeline.isEmpty() ? C.INDEX_UNSET
|
||||
: currentTimeline.getNextWindowIndex(getCurrentWindowIndex(), repeatMode, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getPreviousWindowIndex() {
|
||||
return currentTimeline.isEmpty() ? C.INDEX_UNSET
|
||||
: currentTimeline.getPreviousWindowIndex(getCurrentWindowIndex(), repeatMode, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable Object getCurrentTag() {
|
||||
int windowIndex = getCurrentWindowIndex();
|
||||
return windowIndex > currentTimeline.getWindowCount()
|
||||
? null
|
||||
: currentTimeline.getWindow(windowIndex, window, /* setTag= */ true).tag;
|
||||
}
|
||||
|
||||
// TODO: Fill the cast timeline information with ProgressListener's duration updates.
|
||||
// See [Internal: b/65152553].
|
||||
@Override
|
||||
public long getDuration() {
|
||||
return currentTimeline.isEmpty() ? C.TIME_UNSET
|
||||
: currentTimeline.getWindow(0, window).getDurationMs();
|
||||
: currentTimeline.getWindow(getCurrentWindowIndex(), window).getDurationMs();
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getCurrentPosition() {
|
||||
return remoteMediaClient == null ? lastReportedPositionMs
|
||||
: pendingSeekPositionMs != C.TIME_UNSET ? pendingSeekPositionMs
|
||||
: remoteMediaClient.getApproximateStreamPosition();
|
||||
return pendingSeekPositionMs != C.TIME_UNSET
|
||||
? pendingSeekPositionMs
|
||||
: remoteMediaClient != null
|
||||
? remoteMediaClient.getApproximateStreamPosition()
|
||||
: lastReportedPositionMs;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -393,9 +521,9 @@ public final class CastPlayer implements Player {
|
|||
public int getBufferedPercentage() {
|
||||
long position = getBufferedPosition();
|
||||
long duration = getDuration();
|
||||
return position == C.TIME_UNSET || duration == C.TIME_UNSET ? 0
|
||||
: duration == 0 ? 100
|
||||
: Util.constrainValue((int) ((position * 100) / duration), 0, 100);
|
||||
return position == C.TIME_UNSET || duration == C.TIME_UNSET
|
||||
? 0
|
||||
: duration == 0 ? 100 : Util.constrainValue((int) ((position * 100) / duration), 0, 100);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -437,6 +565,115 @@ public final class CastPlayer implements Player {
|
|||
|
||||
// Internal methods.
|
||||
|
||||
public void updateInternalState() {
|
||||
if (remoteMediaClient == null) {
|
||||
// There is no session. We leave the state of the player as it is now.
|
||||
return;
|
||||
}
|
||||
|
||||
int playbackState = fetchPlaybackState(remoteMediaClient);
|
||||
boolean playWhenReady = !remoteMediaClient.isPaused();
|
||||
if (this.playbackState != playbackState
|
||||
|| this.playWhenReady != playWhenReady) {
|
||||
this.playbackState = playbackState;
|
||||
this.playWhenReady = playWhenReady;
|
||||
for (EventListener listener : listeners) {
|
||||
listener.onPlayerStateChanged(this.playWhenReady, this.playbackState);
|
||||
}
|
||||
}
|
||||
@RepeatMode int repeatMode = fetchRepeatMode(remoteMediaClient);
|
||||
if (this.repeatMode != repeatMode) {
|
||||
this.repeatMode = repeatMode;
|
||||
for (EventListener listener : listeners) {
|
||||
listener.onRepeatModeChanged(repeatMode);
|
||||
}
|
||||
}
|
||||
int currentWindowIndex = fetchCurrentWindowIndex(getMediaStatus());
|
||||
if (this.currentWindowIndex != currentWindowIndex && pendingSeekCount == 0) {
|
||||
this.currentWindowIndex = currentWindowIndex;
|
||||
for (EventListener listener : listeners) {
|
||||
listener.onPositionDiscontinuity(DISCONTINUITY_REASON_PERIOD_TRANSITION);
|
||||
}
|
||||
}
|
||||
if (updateTracksAndSelections()) {
|
||||
for (EventListener listener : listeners) {
|
||||
listener.onTracksChanged(currentTrackGroups, currentTrackSelection);
|
||||
}
|
||||
}
|
||||
maybeUpdateTimelineAndNotify();
|
||||
}
|
||||
|
||||
private void maybeUpdateTimelineAndNotify() {
|
||||
if (updateTimeline()) {
|
||||
@Player.TimelineChangeReason int reason = waitingForInitialTimeline
|
||||
? Player.TIMELINE_CHANGE_REASON_PREPARED : Player.TIMELINE_CHANGE_REASON_DYNAMIC;
|
||||
waitingForInitialTimeline = false;
|
||||
for (EventListener listener : listeners) {
|
||||
listener.onTimelineChanged(currentTimeline, null, reason);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the current timeline and returns whether it has changed.
|
||||
*/
|
||||
private boolean updateTimeline() {
|
||||
CastTimeline oldTimeline = currentTimeline;
|
||||
MediaStatus status = getMediaStatus();
|
||||
currentTimeline =
|
||||
status != null ? timelineTracker.getCastTimeline(status) : CastTimeline.EMPTY_CAST_TIMELINE;
|
||||
return !oldTimeline.equals(currentTimeline);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the internal tracks and selection and returns whether they have changed.
|
||||
*/
|
||||
private boolean updateTracksAndSelections() {
|
||||
if (remoteMediaClient == null) {
|
||||
// There is no session. We leave the state of the player as it is now.
|
||||
return false;
|
||||
}
|
||||
|
||||
MediaStatus mediaStatus = getMediaStatus();
|
||||
MediaInfo mediaInfo = mediaStatus != null ? mediaStatus.getMediaInfo() : null;
|
||||
List<MediaTrack> castMediaTracks = mediaInfo != null ? mediaInfo.getMediaTracks() : null;
|
||||
if (castMediaTracks == null || castMediaTracks.isEmpty()) {
|
||||
boolean hasChanged = !currentTrackGroups.isEmpty();
|
||||
currentTrackGroups = TrackGroupArray.EMPTY;
|
||||
currentTrackSelection = EMPTY_TRACK_SELECTION_ARRAY;
|
||||
return hasChanged;
|
||||
}
|
||||
long[] activeTrackIds = mediaStatus.getActiveTrackIds();
|
||||
if (activeTrackIds == null) {
|
||||
activeTrackIds = EMPTY_TRACK_ID_ARRAY;
|
||||
}
|
||||
|
||||
TrackGroup[] trackGroups = new TrackGroup[castMediaTracks.size()];
|
||||
TrackSelection[] trackSelections = new TrackSelection[RENDERER_COUNT];
|
||||
for (int i = 0; i < castMediaTracks.size(); i++) {
|
||||
MediaTrack mediaTrack = castMediaTracks.get(i);
|
||||
trackGroups[i] = new TrackGroup(CastUtils.mediaTrackToFormat(mediaTrack));
|
||||
|
||||
long id = mediaTrack.getId();
|
||||
int trackType = MimeTypes.getTrackType(mediaTrack.getContentType());
|
||||
int rendererIndex = getRendererIndexForTrackType(trackType);
|
||||
if (isTrackActive(id, activeTrackIds) && rendererIndex != C.INDEX_UNSET
|
||||
&& trackSelections[rendererIndex] == null) {
|
||||
trackSelections[rendererIndex] = new FixedTrackSelection(trackGroups[i], 0);
|
||||
}
|
||||
}
|
||||
TrackGroupArray newTrackGroups = new TrackGroupArray(trackGroups);
|
||||
TrackSelectionArray newTrackSelections = new TrackSelectionArray(trackSelections);
|
||||
|
||||
if (!newTrackGroups.equals(currentTrackGroups)
|
||||
|| !newTrackSelections.equals(currentTrackSelection)) {
|
||||
currentTrackSelection = new TrackSelectionArray(trackSelections);
|
||||
currentTrackGroups = new TrackGroupArray(trackGroups);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private void setRemoteMediaClient(@Nullable RemoteMediaClient remoteMediaClient) {
|
||||
if (this.remoteMediaClient == remoteMediaClient) {
|
||||
// Do nothing.
|
||||
|
|
@ -453,6 +690,7 @@ public final class CastPlayer implements Player {
|
|||
}
|
||||
remoteMediaClient.addListener(statusListener);
|
||||
remoteMediaClient.addProgressListener(statusListener, PROGRESS_REPORT_PERIOD_MS);
|
||||
updateInternalState();
|
||||
} else {
|
||||
if (sessionAvailabilityListener != null) {
|
||||
sessionAvailabilityListener.onCastSessionUnavailable();
|
||||
|
|
@ -464,50 +702,58 @@ public final class CastPlayer implements Player {
|
|||
return remoteMediaClient != null ? remoteMediaClient.getMediaStatus() : null;
|
||||
}
|
||||
|
||||
private @Nullable MediaInfo getMediaInfo() {
|
||||
return remoteMediaClient != null ? remoteMediaClient.getMediaInfo() : null;
|
||||
/**
|
||||
* Retrieves the playback state from {@code remoteMediaClient} and maps it into a {@link Player}
|
||||
* state
|
||||
*/
|
||||
private static int fetchPlaybackState(RemoteMediaClient remoteMediaClient) {
|
||||
int receiverAppStatus = remoteMediaClient.getPlayerState();
|
||||
switch (receiverAppStatus) {
|
||||
case MediaStatus.PLAYER_STATE_BUFFERING:
|
||||
return STATE_BUFFERING;
|
||||
case MediaStatus.PLAYER_STATE_PLAYING:
|
||||
case MediaStatus.PLAYER_STATE_PAUSED:
|
||||
return STATE_READY;
|
||||
case MediaStatus.PLAYER_STATE_IDLE:
|
||||
case MediaStatus.PLAYER_STATE_UNKNOWN:
|
||||
default:
|
||||
return STATE_IDLE;
|
||||
}
|
||||
}
|
||||
|
||||
private void updateInternalState() {
|
||||
currentTimeline = Timeline.EMPTY;
|
||||
currentTrackGroups = EMPTY_TRACK_GROUP_ARRAY;
|
||||
currentTrackSelection = EMPTY_TRACK_SELECTION_ARRAY;
|
||||
MediaInfo mediaInfo = getMediaInfo();
|
||||
if (mediaInfo == null) {
|
||||
return;
|
||||
/**
|
||||
* Retrieves the repeat mode from {@code remoteMediaClient} and maps it into a
|
||||
* {@link Player.RepeatMode}.
|
||||
*/
|
||||
@RepeatMode
|
||||
private static int fetchRepeatMode(RemoteMediaClient remoteMediaClient) {
|
||||
MediaStatus mediaStatus = remoteMediaClient.getMediaStatus();
|
||||
if (mediaStatus == null) {
|
||||
// No media session active, yet.
|
||||
return REPEAT_MODE_OFF;
|
||||
}
|
||||
long streamDurationMs = mediaInfo.getStreamDuration();
|
||||
boolean isSeekable = streamDurationMs != MediaInfo.UNKNOWN_DURATION;
|
||||
currentTimeline = new SinglePeriodTimeline(
|
||||
isSeekable ? C.msToUs(streamDurationMs) : C.TIME_UNSET, isSeekable);
|
||||
|
||||
List<MediaTrack> tracks = mediaInfo.getMediaTracks();
|
||||
if (tracks == null) {
|
||||
return;
|
||||
int castRepeatMode = mediaStatus.getQueueRepeatMode();
|
||||
switch (castRepeatMode) {
|
||||
case MediaStatus.REPEAT_MODE_REPEAT_SINGLE:
|
||||
return REPEAT_MODE_ONE;
|
||||
case MediaStatus.REPEAT_MODE_REPEAT_ALL:
|
||||
case MediaStatus.REPEAT_MODE_REPEAT_ALL_AND_SHUFFLE:
|
||||
return REPEAT_MODE_ALL;
|
||||
case MediaStatus.REPEAT_MODE_REPEAT_OFF:
|
||||
return REPEAT_MODE_OFF;
|
||||
default:
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
}
|
||||
|
||||
MediaStatus mediaStatus = getMediaStatus();
|
||||
long[] activeTrackIds = mediaStatus != null ? mediaStatus.getActiveTrackIds() : null;
|
||||
if (activeTrackIds == null) {
|
||||
activeTrackIds = EMPTY_TRACK_ID_ARRAY;
|
||||
}
|
||||
|
||||
TrackGroup[] trackGroups = new TrackGroup[tracks.size()];
|
||||
TrackSelection[] trackSelections = new TrackSelection[RENDERER_COUNT];
|
||||
for (int i = 0; i < tracks.size(); i++) {
|
||||
MediaTrack mediaTrack = tracks.get(i);
|
||||
trackGroups[i] = new TrackGroup(CastUtils.mediaTrackToFormat(mediaTrack));
|
||||
|
||||
long id = mediaTrack.getId();
|
||||
int trackType = MimeTypes.getTrackType(mediaTrack.getContentType());
|
||||
int rendererIndex = getRendererIndexForTrackType(trackType);
|
||||
if (isTrackActive(id, activeTrackIds) && rendererIndex != C.INDEX_UNSET
|
||||
&& trackSelections[rendererIndex] == null) {
|
||||
trackSelections[rendererIndex] = new FixedTrackSelection(trackGroups[i], 0);
|
||||
}
|
||||
}
|
||||
currentTrackSelection = new TrackSelectionArray(trackSelections);
|
||||
currentTrackGroups = new TrackGroupArray(trackGroups);
|
||||
/**
|
||||
* Retrieves the current item index from {@code mediaStatus} and maps it into a window index. If
|
||||
* there is no media session, returns 0.
|
||||
*/
|
||||
private static int fetchCurrentWindowIndex(@Nullable MediaStatus mediaStatus) {
|
||||
Integer currentItemId = mediaStatus != null
|
||||
? mediaStatus.getIndexById(mediaStatus.getCurrentItemId()) : null;
|
||||
return currentItemId != null ? currentItemId : 0;
|
||||
}
|
||||
|
||||
private static boolean isTrackActive(long id, long[] activeTrackIds) {
|
||||
|
|
@ -520,10 +766,24 @@ public final class CastPlayer implements Player {
|
|||
}
|
||||
|
||||
private static int getRendererIndexForTrackType(int trackType) {
|
||||
return trackType == C.TRACK_TYPE_VIDEO ? RENDERER_INDEX_VIDEO
|
||||
: trackType == C.TRACK_TYPE_AUDIO ? RENDERER_INDEX_AUDIO
|
||||
: trackType == C.TRACK_TYPE_TEXT ? RENDERER_INDEX_TEXT
|
||||
: C.INDEX_UNSET;
|
||||
return trackType == C.TRACK_TYPE_VIDEO
|
||||
? RENDERER_INDEX_VIDEO
|
||||
: trackType == C.TRACK_TYPE_AUDIO
|
||||
? RENDERER_INDEX_AUDIO
|
||||
: trackType == C.TRACK_TYPE_TEXT ? RENDERER_INDEX_TEXT : C.INDEX_UNSET;
|
||||
}
|
||||
|
||||
private static int getCastRepeatMode(@RepeatMode int repeatMode) {
|
||||
switch (repeatMode) {
|
||||
case REPEAT_MODE_ONE:
|
||||
return MediaStatus.REPEAT_MODE_REPEAT_SINGLE;
|
||||
case REPEAT_MODE_ALL:
|
||||
return MediaStatus.REPEAT_MODE_REPEAT_ALL;
|
||||
case REPEAT_MODE_OFF:
|
||||
return MediaStatus.REPEAT_MODE_REPEAT_OFF;
|
||||
default:
|
||||
throw new IllegalArgumentException();
|
||||
}
|
||||
}
|
||||
|
||||
private final class StatusListener implements RemoteMediaClient.Listener,
|
||||
|
|
@ -540,24 +800,16 @@ public final class CastPlayer implements Player {
|
|||
|
||||
@Override
|
||||
public void onStatusUpdated() {
|
||||
boolean playWhenReady = getPlayWhenReady();
|
||||
int playbackState = getPlaybackState();
|
||||
for (EventListener listener : listeners) {
|
||||
listener.onPlayerStateChanged(playWhenReady, playbackState);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMetadataUpdated() {
|
||||
updateInternalState();
|
||||
for (EventListener listener : listeners) {
|
||||
listener.onTracksChanged(currentTrackGroups, currentTrackSelection);
|
||||
listener.onTimelineChanged(currentTimeline, null);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onQueueStatusUpdated() {}
|
||||
public void onMetadataUpdated() {}
|
||||
|
||||
@Override
|
||||
public void onQueueStatusUpdated() {
|
||||
maybeUpdateTimelineAndNotify();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPreloadStatusUpdated() {}
|
||||
|
|
@ -622,39 +874,23 @@ public final class CastPlayer implements Player {
|
|||
|
||||
// Result callbacks hooks.
|
||||
|
||||
private final class RepeatModeResultCallback implements ResultCallback<MediaChannelResult> {
|
||||
|
||||
@Override
|
||||
public void onResult(MediaChannelResult result) {
|
||||
int statusCode = result.getStatus().getStatusCode();
|
||||
if (statusCode == CommonStatusCodes.SUCCESS) {
|
||||
int repeatMode = getRepeatMode();
|
||||
for (EventListener listener : listeners) {
|
||||
listener.onRepeatModeChanged(repeatMode);
|
||||
}
|
||||
} else {
|
||||
Log.e(TAG, "Set repeat mode failed. Error code " + statusCode + ": "
|
||||
+ CastUtils.getLogString(statusCode));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private final class SeekResultCallback implements ResultCallback<MediaChannelResult> {
|
||||
|
||||
@Override
|
||||
public void onResult(MediaChannelResult result) {
|
||||
public void onResult(@NonNull MediaChannelResult result) {
|
||||
int statusCode = result.getStatus().getStatusCode();
|
||||
if (statusCode == CommonStatusCodes.SUCCESS) {
|
||||
pendingSeekPositionMs = C.TIME_UNSET;
|
||||
} else if (statusCode == CastStatusCodes.REPLACED) {
|
||||
// A seek was executed before this one completed. Do nothing.
|
||||
} else {
|
||||
if (statusCode != CastStatusCodes.SUCCESS && statusCode != CastStatusCodes.REPLACED) {
|
||||
Log.e(TAG, "Seek failed. Error code " + statusCode + ": "
|
||||
+ CastUtils.getLogString(statusCode));
|
||||
}
|
||||
if (--pendingSeekCount == 0) {
|
||||
pendingSeekWindowIndex = C.INDEX_UNSET;
|
||||
pendingSeekPositionMs = C.TIME_UNSET;
|
||||
for (EventListener listener : listeners) {
|
||||
listener.onSeekProcessed();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,134 @@
|
|||
/*
|
||||
* 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.ext.cast;
|
||||
|
||||
import android.support.annotation.Nullable;
|
||||
import android.util.SparseIntArray;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.Timeline;
|
||||
import com.google.android.gms.cast.MediaInfo;
|
||||
import com.google.android.gms.cast.MediaQueueItem;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* A {@link Timeline} for Cast media queues.
|
||||
*/
|
||||
/* package */ final class CastTimeline extends Timeline {
|
||||
|
||||
public static final CastTimeline EMPTY_CAST_TIMELINE =
|
||||
new CastTimeline(
|
||||
Collections.<MediaQueueItem>emptyList(), Collections.<String, Long>emptyMap());
|
||||
|
||||
private final SparseIntArray idsToIndex;
|
||||
private final int[] ids;
|
||||
private final long[] durationsUs;
|
||||
private final long[] defaultPositionsUs;
|
||||
|
||||
/**
|
||||
* @param items A list of cast media queue items to represent.
|
||||
* @param contentIdToDurationUsMap A map of content id to duration in microseconds.
|
||||
*/
|
||||
public CastTimeline(List<MediaQueueItem> items, Map<String, Long> contentIdToDurationUsMap) {
|
||||
int itemCount = items.size();
|
||||
int index = 0;
|
||||
idsToIndex = new SparseIntArray(itemCount);
|
||||
ids = new int[itemCount];
|
||||
durationsUs = new long[itemCount];
|
||||
defaultPositionsUs = new long[itemCount];
|
||||
for (MediaQueueItem item : items) {
|
||||
int itemId = item.getItemId();
|
||||
ids[index] = itemId;
|
||||
idsToIndex.put(itemId, index);
|
||||
MediaInfo mediaInfo = item.getMedia();
|
||||
String contentId = mediaInfo.getContentId();
|
||||
durationsUs[index] =
|
||||
contentIdToDurationUsMap.containsKey(contentId)
|
||||
? contentIdToDurationUsMap.get(contentId)
|
||||
: CastUtils.getStreamDurationUs(mediaInfo);
|
||||
defaultPositionsUs[index] = (long) (item.getStartTime() * C.MICROS_PER_SECOND);
|
||||
index++;
|
||||
}
|
||||
}
|
||||
|
||||
// Timeline implementation.
|
||||
|
||||
@Override
|
||||
public int getWindowCount() {
|
||||
return ids.length;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Window getWindow(
|
||||
int windowIndex, Window window, boolean setTag, long defaultPositionProjectionUs) {
|
||||
long durationUs = durationsUs[windowIndex];
|
||||
boolean isDynamic = durationUs == C.TIME_UNSET;
|
||||
Object tag = setTag ? ids[windowIndex] : null;
|
||||
return window.set(
|
||||
tag,
|
||||
/* presentationStartTimeMs= */ C.TIME_UNSET,
|
||||
/* windowStartTimeMs= */ C.TIME_UNSET,
|
||||
/* isSeekable= */ !isDynamic,
|
||||
isDynamic,
|
||||
defaultPositionsUs[windowIndex],
|
||||
durationUs,
|
||||
/* firstPeriodIndex= */ windowIndex,
|
||||
/* lastPeriodIndex= */ windowIndex,
|
||||
/* positionInFirstPeriodUs= */ 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getPeriodCount() {
|
||||
return ids.length;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Period getPeriod(int periodIndex, Period period, boolean setIds) {
|
||||
int id = ids[periodIndex];
|
||||
return period.set(id, id, periodIndex, durationsUs[periodIndex], 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getIndexOfPeriod(Object uid) {
|
||||
return uid instanceof Integer ? idsToIndex.get((int) uid, C.INDEX_UNSET) : C.INDEX_UNSET;
|
||||
}
|
||||
|
||||
// equals and hashCode implementations.
|
||||
|
||||
@Override
|
||||
public boolean equals(@Nullable Object other) {
|
||||
if (this == other) {
|
||||
return true;
|
||||
} else if (!(other instanceof CastTimeline)) {
|
||||
return false;
|
||||
}
|
||||
CastTimeline that = (CastTimeline) other;
|
||||
return Arrays.equals(ids, that.ids)
|
||||
&& Arrays.equals(durationsUs, that.durationsUs)
|
||||
&& Arrays.equals(defaultPositionsUs, that.defaultPositionsUs);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int result = Arrays.hashCode(ids);
|
||||
result = 31 * result + Arrays.hashCode(durationsUs);
|
||||
result = 31 * result + Arrays.hashCode(defaultPositionsUs);
|
||||
return result;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,67 @@
|
|||
/*
|
||||
* Copyright (C) 2018 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.google.android.exoplayer2.ext.cast;
|
||||
|
||||
import com.google.android.gms.cast.MediaInfo;
|
||||
import com.google.android.gms.cast.MediaQueueItem;
|
||||
import com.google.android.gms.cast.MediaStatus;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Creates {@link CastTimeline}s from cast receiver app media status.
|
||||
*
|
||||
* <p>This class keeps track of the duration reported by the current item to fill any missing
|
||||
* durations in the media queue items [See internal: b/65152553].
|
||||
*/
|
||||
/* package */ final class CastTimelineTracker {
|
||||
|
||||
private final HashMap<String, Long> contentIdToDurationUsMap;
|
||||
private final HashSet<String> scratchContentIdSet;
|
||||
|
||||
public CastTimelineTracker() {
|
||||
contentIdToDurationUsMap = new HashMap<>();
|
||||
scratchContentIdSet = new HashSet<>();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a {@link CastTimeline} that represent the given {@code status}.
|
||||
*
|
||||
* @param status The Cast media status.
|
||||
* @return A {@link CastTimeline} that represent the given {@code status}.
|
||||
*/
|
||||
public CastTimeline getCastTimeline(MediaStatus status) {
|
||||
MediaInfo mediaInfo = status.getMediaInfo();
|
||||
List<MediaQueueItem> items = status.getQueueItems();
|
||||
removeUnusedDurationEntries(items);
|
||||
|
||||
if (mediaInfo != null) {
|
||||
String contentId = mediaInfo.getContentId();
|
||||
long durationUs = CastUtils.getStreamDurationUs(mediaInfo);
|
||||
contentIdToDurationUsMap.put(contentId, durationUs);
|
||||
}
|
||||
return new CastTimeline(items, contentIdToDurationUsMap);
|
||||
}
|
||||
|
||||
private void removeUnusedDurationEntries(List<MediaQueueItem> items) {
|
||||
scratchContentIdSet.clear();
|
||||
for (MediaQueueItem item : items) {
|
||||
scratchContentIdSet.add(item.getMedia().getContentId());
|
||||
}
|
||||
contentIdToDurationUsMap.keySet().retainAll(scratchContentIdSet);
|
||||
}
|
||||
}
|
||||
|
|
@ -15,19 +15,29 @@
|
|||
*/
|
||||
package com.google.android.exoplayer2.ext.cast;
|
||||
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.Format;
|
||||
import com.google.android.gms.cast.CastStatusCodes;
|
||||
import com.google.android.gms.cast.MediaInfo;
|
||||
import com.google.android.gms.cast.MediaTrack;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Utility methods for ExoPlayer/Cast integration.
|
||||
*/
|
||||
/* package */ final class CastUtils {
|
||||
|
||||
private static final Map<Integer, String> CAST_STATUS_CODE_TO_STRING;
|
||||
/**
|
||||
* Returns the duration in microseconds advertised by a media info, or {@link C#TIME_UNSET} if
|
||||
* unknown or not applicable.
|
||||
*
|
||||
* @param mediaInfo The media info to get the duration from.
|
||||
* @return The duration in microseconds.
|
||||
*/
|
||||
public static long getStreamDurationUs(MediaInfo mediaInfo) {
|
||||
long durationMs =
|
||||
mediaInfo != null ? mediaInfo.getStreamDuration() : MediaInfo.UNKNOWN_DURATION;
|
||||
return durationMs != MediaInfo.UNKNOWN_DURATION ? C.msToUs(durationMs) : C.TIME_UNSET;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a descriptive log string for the given {@code statusCode}, or "Unknown." if not one of
|
||||
|
|
@ -38,8 +48,49 @@ import java.util.Map;
|
|||
* {@link CastStatusCodes}.
|
||||
*/
|
||||
public static String getLogString(int statusCode) {
|
||||
String description = CAST_STATUS_CODE_TO_STRING.get(statusCode);
|
||||
return description != null ? description : "Unknown.";
|
||||
switch (statusCode) {
|
||||
case CastStatusCodes.APPLICATION_NOT_FOUND:
|
||||
return "A requested application could not be found.";
|
||||
case CastStatusCodes.APPLICATION_NOT_RUNNING:
|
||||
return "A requested application is not currently running.";
|
||||
case CastStatusCodes.AUTHENTICATION_FAILED:
|
||||
return "Authentication failure.";
|
||||
case CastStatusCodes.CANCELED:
|
||||
return "An in-progress request has been canceled, most likely because another action has "
|
||||
+ "preempted it.";
|
||||
case CastStatusCodes.ERROR_SERVICE_CREATION_FAILED:
|
||||
return "The Cast Remote Display service could not be created.";
|
||||
case CastStatusCodes.ERROR_SERVICE_DISCONNECTED:
|
||||
return "The Cast Remote Display service was disconnected.";
|
||||
case CastStatusCodes.FAILED:
|
||||
return "The in-progress request failed.";
|
||||
case CastStatusCodes.INTERNAL_ERROR:
|
||||
return "An internal error has occurred.";
|
||||
case CastStatusCodes.INTERRUPTED:
|
||||
return "A blocking call was interrupted while waiting and did not run to completion.";
|
||||
case CastStatusCodes.INVALID_REQUEST:
|
||||
return "An invalid request was made.";
|
||||
case CastStatusCodes.MESSAGE_SEND_BUFFER_TOO_FULL:
|
||||
return "A message could not be sent because there is not enough room in the send buffer at "
|
||||
+ "this time.";
|
||||
case CastStatusCodes.MESSAGE_TOO_LARGE:
|
||||
return "A message could not be sent because it is too large.";
|
||||
case CastStatusCodes.NETWORK_ERROR:
|
||||
return "Network I/O error.";
|
||||
case CastStatusCodes.NOT_ALLOWED:
|
||||
return "The request was disallowed and could not be completed.";
|
||||
case CastStatusCodes.REPLACED:
|
||||
return "The request's progress is no longer being tracked because another request of the "
|
||||
+ "same type has been made before the first request completed.";
|
||||
case CastStatusCodes.SUCCESS:
|
||||
return "Success.";
|
||||
case CastStatusCodes.TIMEOUT:
|
||||
return "An operation has timed out.";
|
||||
case CastStatusCodes.UNKNOWN_ERROR:
|
||||
return "An unknown, unexpected error has occurred.";
|
||||
default:
|
||||
return CastStatusCodes.getStatusCodeString(statusCode);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -54,41 +105,6 @@ import java.util.Map;
|
|||
null, null, Format.NO_VALUE, 0, mediaTrack.getLanguage());
|
||||
}
|
||||
|
||||
static {
|
||||
HashMap<Integer, String> statusCodeToString = new HashMap<>();
|
||||
statusCodeToString.put(CastStatusCodes.APPLICATION_NOT_FOUND,
|
||||
"A requested application could not be found.");
|
||||
statusCodeToString.put(CastStatusCodes.APPLICATION_NOT_RUNNING,
|
||||
"A requested application is not currently running.");
|
||||
statusCodeToString.put(CastStatusCodes.AUTHENTICATION_FAILED, "Authentication failure.");
|
||||
statusCodeToString.put(CastStatusCodes.CANCELED, "An in-progress request has been "
|
||||
+ "canceled, most likely because another action has preempted it.");
|
||||
statusCodeToString.put(CastStatusCodes.ERROR_SERVICE_CREATION_FAILED,
|
||||
"The Cast Remote Display service could not be created.");
|
||||
statusCodeToString.put(CastStatusCodes.ERROR_SERVICE_DISCONNECTED,
|
||||
"The Cast Remote Display service was disconnected.");
|
||||
statusCodeToString.put(CastStatusCodes.FAILED, "The in-progress request failed.");
|
||||
statusCodeToString.put(CastStatusCodes.INTERNAL_ERROR, "An internal error has occurred.");
|
||||
statusCodeToString.put(CastStatusCodes.INTERRUPTED,
|
||||
"A blocking call was interrupted while waiting and did not run to completion.");
|
||||
statusCodeToString.put(CastStatusCodes.INVALID_REQUEST, "An invalid request was made.");
|
||||
statusCodeToString.put(CastStatusCodes.MESSAGE_SEND_BUFFER_TOO_FULL, "A message could "
|
||||
+ "not be sent because there is not enough room in the send buffer at this time.");
|
||||
statusCodeToString.put(CastStatusCodes.MESSAGE_TOO_LARGE,
|
||||
"A message could not be sent because it is too large.");
|
||||
statusCodeToString.put(CastStatusCodes.NETWORK_ERROR, "Network I/O error.");
|
||||
statusCodeToString.put(CastStatusCodes.NOT_ALLOWED,
|
||||
"The request was disallowed and could not be completed.");
|
||||
statusCodeToString.put(CastStatusCodes.REPLACED,
|
||||
"The request's progress is no longer being tracked because another request of the same type"
|
||||
+ " has been made before the first request completed.");
|
||||
statusCodeToString.put(CastStatusCodes.SUCCESS, "Success.");
|
||||
statusCodeToString.put(CastStatusCodes.TIMEOUT, "An operation has timed out.");
|
||||
statusCodeToString.put(CastStatusCodes.UNKNOWN_ERROR,
|
||||
"An unknown, unexpected error has occurred.");
|
||||
CAST_STATUS_CODE_TO_STRING = Collections.unmodifiableMap(statusCodeToString);
|
||||
}
|
||||
|
||||
private CastUtils() {}
|
||||
|
||||
}
|
||||
|
|
|
|||
17
extensions/cast/src/test/AndroidManifest.xml
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Copyright (C) 2018 The Android Open Source Project
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
|
||||
<manifest package="com.google.android.exoplayer2.ext.cast.test"/>
|
||||
|
|
@ -0,0 +1,132 @@
|
|||
/*
|
||||
* Copyright (C) 2018 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.google.android.exoplayer2.ext.cast;
|
||||
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.testutil.TimelineAsserts;
|
||||
import com.google.android.exoplayer2.util.MimeTypes;
|
||||
import com.google.android.gms.cast.MediaInfo;
|
||||
import com.google.android.gms.cast.MediaQueueItem;
|
||||
import com.google.android.gms.cast.MediaStatus;
|
||||
import java.util.ArrayList;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.Mockito;
|
||||
import org.robolectric.RobolectricTestRunner;
|
||||
|
||||
/** Tests for {@link CastTimelineTracker}. */
|
||||
@RunWith(RobolectricTestRunner.class)
|
||||
public class CastTimelineTrackerTest {
|
||||
|
||||
private static final long DURATION_1_MS = 1000;
|
||||
private static final long DURATION_2_MS = 2000;
|
||||
private static final long DURATION_3_MS = 3000;
|
||||
private static final long DURATION_4_MS = 4000;
|
||||
private static final long DURATION_5_MS = 5000;
|
||||
|
||||
/** Tests that duration of the current media info is correctly propagated to the timeline. */
|
||||
@Test
|
||||
public void testGetCastTimeline() {
|
||||
MediaInfo mediaInfo;
|
||||
MediaStatus status =
|
||||
mockMediaStatus(
|
||||
new int[] {1, 2, 3},
|
||||
new String[] {"contentId1", "contentId2", "contentId3"},
|
||||
new long[] {DURATION_1_MS, MediaInfo.UNKNOWN_DURATION, MediaInfo.UNKNOWN_DURATION});
|
||||
|
||||
CastTimelineTracker tracker = new CastTimelineTracker();
|
||||
mediaInfo = getMediaInfo("contentId1", DURATION_1_MS);
|
||||
Mockito.when(status.getMediaInfo()).thenReturn(mediaInfo);
|
||||
TimelineAsserts.assertPeriodDurations(
|
||||
tracker.getCastTimeline(status), C.msToUs(DURATION_1_MS), C.TIME_UNSET, C.TIME_UNSET);
|
||||
|
||||
mediaInfo = getMediaInfo("contentId3", DURATION_3_MS);
|
||||
Mockito.when(status.getMediaInfo()).thenReturn(mediaInfo);
|
||||
TimelineAsserts.assertPeriodDurations(
|
||||
tracker.getCastTimeline(status),
|
||||
C.msToUs(DURATION_1_MS),
|
||||
C.TIME_UNSET,
|
||||
C.msToUs(DURATION_3_MS));
|
||||
|
||||
mediaInfo = getMediaInfo("contentId2", DURATION_2_MS);
|
||||
Mockito.when(status.getMediaInfo()).thenReturn(mediaInfo);
|
||||
TimelineAsserts.assertPeriodDurations(
|
||||
tracker.getCastTimeline(status),
|
||||
C.msToUs(DURATION_1_MS),
|
||||
C.msToUs(DURATION_2_MS),
|
||||
C.msToUs(DURATION_3_MS));
|
||||
|
||||
MediaStatus newStatus =
|
||||
mockMediaStatus(
|
||||
new int[] {4, 1, 5, 3},
|
||||
new String[] {"contentId4", "contentId1", "contentId5", "contentId3"},
|
||||
new long[] {
|
||||
MediaInfo.UNKNOWN_DURATION,
|
||||
MediaInfo.UNKNOWN_DURATION,
|
||||
DURATION_5_MS,
|
||||
MediaInfo.UNKNOWN_DURATION
|
||||
});
|
||||
mediaInfo = getMediaInfo("contentId5", DURATION_5_MS);
|
||||
Mockito.when(newStatus.getMediaInfo()).thenReturn(mediaInfo);
|
||||
TimelineAsserts.assertPeriodDurations(
|
||||
tracker.getCastTimeline(newStatus),
|
||||
C.TIME_UNSET,
|
||||
C.msToUs(DURATION_1_MS),
|
||||
C.msToUs(DURATION_5_MS),
|
||||
C.msToUs(DURATION_3_MS));
|
||||
|
||||
mediaInfo = getMediaInfo("contentId3", DURATION_3_MS);
|
||||
Mockito.when(newStatus.getMediaInfo()).thenReturn(mediaInfo);
|
||||
TimelineAsserts.assertPeriodDurations(
|
||||
tracker.getCastTimeline(newStatus),
|
||||
C.TIME_UNSET,
|
||||
C.msToUs(DURATION_1_MS),
|
||||
C.msToUs(DURATION_5_MS),
|
||||
C.msToUs(DURATION_3_MS));
|
||||
|
||||
mediaInfo = getMediaInfo("contentId4", DURATION_4_MS);
|
||||
Mockito.when(newStatus.getMediaInfo()).thenReturn(mediaInfo);
|
||||
TimelineAsserts.assertPeriodDurations(
|
||||
tracker.getCastTimeline(newStatus),
|
||||
C.msToUs(DURATION_4_MS),
|
||||
C.msToUs(DURATION_1_MS),
|
||||
C.msToUs(DURATION_5_MS),
|
||||
C.msToUs(DURATION_3_MS));
|
||||
}
|
||||
|
||||
private static MediaStatus mockMediaStatus(
|
||||
int[] itemIds, String[] contentIds, long[] durationsMs) {
|
||||
ArrayList<MediaQueueItem> items = new ArrayList<>();
|
||||
for (int i = 0; i < contentIds.length; i++) {
|
||||
MediaInfo mediaInfo = getMediaInfo(contentIds[i], durationsMs[i]);
|
||||
MediaQueueItem item = Mockito.mock(MediaQueueItem.class);
|
||||
Mockito.when(item.getMedia()).thenReturn(mediaInfo);
|
||||
Mockito.when(item.getItemId()).thenReturn(itemIds[i]);
|
||||
items.add(item);
|
||||
}
|
||||
MediaStatus status = Mockito.mock(MediaStatus.class);
|
||||
Mockito.when(status.getQueueItems()).thenReturn(items);
|
||||
return status;
|
||||
}
|
||||
|
||||
private static MediaInfo getMediaInfo(String contentId, long durationMs) {
|
||||
return new MediaInfo.Builder(contentId)
|
||||
.setStreamDuration(durationMs)
|
||||
.setContentType(MimeTypes.APPLICATION_MP4)
|
||||
.setStreamType(MediaInfo.STREAM_TYPE_NONE)
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
manifest=src/test/AndroidManifest.xml
|
||||
|
|
@ -19,10 +19,20 @@ and enable the extension:
|
|||
1. Copy the three jar files into the `libs` directory of this extension
|
||||
1. Copy the content of the downloaded `libs` directory into the `jniLibs`
|
||||
directory of this extension
|
||||
|
||||
* In your `settings.gradle` file, add
|
||||
`gradle.ext.exoplayerIncludeCronetExtension = true` before the line that
|
||||
applies `core_settings.gradle`.
|
||||
1. In your `settings.gradle` file, add
|
||||
`gradle.ext.exoplayerIncludeCronetExtension = true` before the line that
|
||||
applies `core_settings.gradle`.
|
||||
1. In all `build.gradle` files where this extension is linked as a dependency,
|
||||
add
|
||||
```
|
||||
android {
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
}
|
||||
```
|
||||
to enable Java 8 features required by the Cronet library.
|
||||
|
||||
[top level README]: https://github.com/google/ExoPlayer/blob/release-v2/README.md
|
||||
[here]: https://console.cloud.google.com/storage/browser/chromium-cronet/android
|
||||
|
|
|
|||
|
|
@ -21,24 +21,26 @@ android {
|
|||
defaultConfig {
|
||||
minSdkVersion project.ext.minSdkVersion
|
||||
targetSdkVersion project.ext.targetSdkVersion
|
||||
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
|
||||
sourceSets.main {
|
||||
jniLibs.srcDirs = ['jniLibs']
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
compile project(modulePrefix + 'library-core')
|
||||
compile files('libs/cronet_api.jar')
|
||||
compile files('libs/cronet_impl_common_java.jar')
|
||||
compile files('libs/cronet_impl_native_java.jar')
|
||||
androidTestCompile project(modulePrefix + 'library')
|
||||
androidTestCompile 'com.google.dexmaker:dexmaker:' + dexmakerVersion
|
||||
androidTestCompile 'com.google.dexmaker:dexmaker-mockito:' + dexmakerVersion
|
||||
androidTestCompile 'org.mockito:mockito-core:' + mockitoVersion
|
||||
androidTestCompile 'com.android.support.test:runner:' + testSupportLibraryVersion
|
||||
api files('libs/cronet_api.jar')
|
||||
implementation files('libs/cronet_impl_common_java.jar')
|
||||
implementation files('libs/cronet_impl_native_java.jar')
|
||||
implementation project(modulePrefix + 'library-core')
|
||||
implementation 'com.android.support:support-annotations:' + supportLibraryVersion
|
||||
testImplementation project(modulePrefix + 'library')
|
||||
testImplementation project(modulePrefix + 'testutils-robolectric')
|
||||
}
|
||||
|
||||
ext {
|
||||
|
|
|
|||
|
|
@ -1,33 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- 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.
|
||||
-->
|
||||
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
package="com.google.android.exoplayer.ext.cronet">
|
||||
|
||||
<uses-sdk android:minSdkVersion="14" android:targetSdkVersion="26"/>
|
||||
|
||||
<application android:debuggable="true"
|
||||
android:allowBackup="false"
|
||||
tools:ignore="MissingApplicationIcon,HardcodedDebugMode">
|
||||
<uses-library android:name="android.test.runner" />
|
||||
</application>
|
||||
|
||||
<instrumentation
|
||||
android:name="android.test.InstrumentationTestRunner"
|
||||
android:targetPackage="com.google.android.exoplayer.ext.cronet"/>
|
||||
|
||||
</manifest>
|
||||
|
|
@ -14,7 +14,7 @@
|
|||
-->
|
||||
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.google.android.exoplayer.ext.cronet">
|
||||
package="com.google.android.exoplayer2.ext.cronet">
|
||||
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
|
||||
|
||||
|
|
|
|||
|
|
@ -16,7 +16,6 @@
|
|||
package com.google.android.exoplayer2.ext.cronet;
|
||||
|
||||
import android.net.Uri;
|
||||
import android.os.ConditionVariable;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
import com.google.android.exoplayer2.C;
|
||||
|
|
@ -27,6 +26,7 @@ import com.google.android.exoplayer2.upstream.HttpDataSource;
|
|||
import com.google.android.exoplayer2.upstream.TransferListener;
|
||||
import com.google.android.exoplayer2.util.Assertions;
|
||||
import com.google.android.exoplayer2.util.Clock;
|
||||
import com.google.android.exoplayer2.util.ConditionVariable;
|
||||
import com.google.android.exoplayer2.util.Predicate;
|
||||
import java.io.IOException;
|
||||
import java.net.SocketTimeoutException;
|
||||
|
|
@ -74,6 +74,14 @@ public class CronetDataSource extends UrlRequest.Callback implements HttpDataSou
|
|||
|
||||
}
|
||||
|
||||
/** Thrown on catching an InterruptedException. */
|
||||
public static final class InterruptedIOException extends IOException {
|
||||
|
||||
public InterruptedIOException(InterruptedException e) {
|
||||
super(e);
|
||||
}
|
||||
}
|
||||
|
||||
static {
|
||||
ExoPlayerLibraryInfo.registerModule("goog.exo.cronet");
|
||||
}
|
||||
|
|
@ -89,6 +97,9 @@ public class CronetDataSource extends UrlRequest.Callback implements HttpDataSou
|
|||
|
||||
private static final String TAG = "CronetDataSource";
|
||||
private static final String CONTENT_TYPE = "Content-Type";
|
||||
private static final String SET_COOKIE = "Set-Cookie";
|
||||
private static final String COOKIE = "Cookie";
|
||||
|
||||
private static final Pattern CONTENT_RANGE_HEADER_PATTERN =
|
||||
Pattern.compile("^bytes (\\d+)-(\\d+)/(\\d+)$");
|
||||
// The size of read buffer passed to cronet UrlRequest.read().
|
||||
|
|
@ -101,6 +112,7 @@ public class CronetDataSource extends UrlRequest.Callback implements HttpDataSou
|
|||
private final int connectTimeoutMs;
|
||||
private final int readTimeoutMs;
|
||||
private final boolean resetTimeoutOnRedirects;
|
||||
private final boolean handleSetCookieRequests;
|
||||
private final RequestProperties defaultRequestProperties;
|
||||
private final RequestProperties requestProperties;
|
||||
private final ConditionVariable operation;
|
||||
|
|
@ -144,7 +156,7 @@ public class CronetDataSource extends UrlRequest.Callback implements HttpDataSou
|
|||
public CronetDataSource(CronetEngine cronetEngine, Executor executor,
|
||||
Predicate<String> contentTypePredicate, TransferListener<? super CronetDataSource> listener) {
|
||||
this(cronetEngine, executor, contentTypePredicate, listener, DEFAULT_CONNECT_TIMEOUT_MILLIS,
|
||||
DEFAULT_READ_TIMEOUT_MILLIS, false, null);
|
||||
DEFAULT_READ_TIMEOUT_MILLIS, false, null, false);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -168,13 +180,40 @@ public class CronetDataSource extends UrlRequest.Callback implements HttpDataSou
|
|||
int connectTimeoutMs, int readTimeoutMs, boolean resetTimeoutOnRedirects,
|
||||
RequestProperties defaultRequestProperties) {
|
||||
this(cronetEngine, executor, contentTypePredicate, listener, connectTimeoutMs,
|
||||
readTimeoutMs, resetTimeoutOnRedirects, Clock.DEFAULT, defaultRequestProperties);
|
||||
readTimeoutMs, resetTimeoutOnRedirects, Clock.DEFAULT, defaultRequestProperties, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param cronetEngine A CronetEngine.
|
||||
* @param executor The {@link java.util.concurrent.Executor} that will handle responses.
|
||||
* This may be a direct executor (i.e. executes tasks on the calling thread) in order
|
||||
* to avoid a thread hop from Cronet's internal network thread to the response handling
|
||||
* thread. However, to avoid slowing down overall network performance, care must be taken
|
||||
* to make sure response handling is a fast operation when using a direct executor.
|
||||
* @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the
|
||||
* predicate then an {@link InvalidContentTypeException} is thrown from
|
||||
* {@link #open(DataSpec)}.
|
||||
* @param listener An optional listener.
|
||||
* @param connectTimeoutMs The connection timeout, in milliseconds.
|
||||
* @param readTimeoutMs The read timeout, in milliseconds.
|
||||
* @param resetTimeoutOnRedirects Whether the connect timeout is reset when a redirect occurs.
|
||||
* @param defaultRequestProperties The default request properties to be used.
|
||||
* @param handleSetCookieRequests Whether "Set-Cookie" requests on redirect should be forwarded to
|
||||
* the redirect url in the "Cookie" header.
|
||||
*/
|
||||
public CronetDataSource(CronetEngine cronetEngine, Executor executor,
|
||||
Predicate<String> contentTypePredicate, TransferListener<? super CronetDataSource> listener,
|
||||
int connectTimeoutMs, int readTimeoutMs, boolean resetTimeoutOnRedirects,
|
||||
RequestProperties defaultRequestProperties, boolean handleSetCookieRequests) {
|
||||
this(cronetEngine, executor, contentTypePredicate, listener, connectTimeoutMs,
|
||||
readTimeoutMs, resetTimeoutOnRedirects, Clock.DEFAULT, defaultRequestProperties,
|
||||
handleSetCookieRequests);
|
||||
}
|
||||
|
||||
/* package */ CronetDataSource(CronetEngine cronetEngine, Executor executor,
|
||||
Predicate<String> contentTypePredicate, TransferListener<? super CronetDataSource> listener,
|
||||
int connectTimeoutMs, int readTimeoutMs, boolean resetTimeoutOnRedirects, Clock clock,
|
||||
RequestProperties defaultRequestProperties) {
|
||||
RequestProperties defaultRequestProperties, boolean handleSetCookieRequests) {
|
||||
this.cronetEngine = Assertions.checkNotNull(cronetEngine);
|
||||
this.executor = Assertions.checkNotNull(executor);
|
||||
this.contentTypePredicate = contentTypePredicate;
|
||||
|
|
@ -184,6 +223,7 @@ public class CronetDataSource extends UrlRequest.Callback implements HttpDataSou
|
|||
this.resetTimeoutOnRedirects = resetTimeoutOnRedirects;
|
||||
this.clock = Assertions.checkNotNull(clock);
|
||||
this.defaultRequestProperties = defaultRequestProperties;
|
||||
this.handleSetCookieRequests = handleSetCookieRequests;
|
||||
requestProperties = new RequestProperties();
|
||||
operation = new ConditionVariable();
|
||||
}
|
||||
|
|
@ -223,15 +263,25 @@ public class CronetDataSource extends UrlRequest.Callback implements HttpDataSou
|
|||
operation.close();
|
||||
resetConnectTimeout();
|
||||
currentDataSpec = dataSpec;
|
||||
currentUrlRequest = buildRequest(dataSpec);
|
||||
try {
|
||||
currentUrlRequest = buildRequestBuilder(dataSpec).build();
|
||||
} catch (IOException e) {
|
||||
throw new OpenException(e, currentDataSpec, Status.IDLE);
|
||||
}
|
||||
currentUrlRequest.start();
|
||||
boolean requestStarted = blockUntilConnectTimeout();
|
||||
|
||||
if (exception != null) {
|
||||
throw new OpenException(exception, currentDataSpec, getStatus(currentUrlRequest));
|
||||
} else if (!requestStarted) {
|
||||
// The timeout was reached before the connection was opened.
|
||||
throw new OpenException(new SocketTimeoutException(), dataSpec, getStatus(currentUrlRequest));
|
||||
try {
|
||||
boolean connectionOpened = blockUntilConnectTimeout();
|
||||
if (exception != null) {
|
||||
throw new OpenException(exception, currentDataSpec, getStatus(currentUrlRequest));
|
||||
} else if (!connectionOpened) {
|
||||
// The timeout was reached before the connection was opened.
|
||||
throw new OpenException(
|
||||
new SocketTimeoutException(), dataSpec, getStatus(currentUrlRequest));
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
throw new OpenException(new InterruptedIOException(e), dataSpec, Status.INVALID);
|
||||
}
|
||||
|
||||
// Check for a valid response code.
|
||||
|
|
@ -299,17 +349,29 @@ public class CronetDataSource extends UrlRequest.Callback implements HttpDataSou
|
|||
operation.close();
|
||||
readBuffer.clear();
|
||||
currentUrlRequest.read(readBuffer);
|
||||
if (!operation.block(readTimeoutMs)) {
|
||||
// We're timing out, but since the operation is still ongoing we'll need to replace
|
||||
// readBuffer to avoid the possibility of it being written to by this operation during a
|
||||
// subsequent request.
|
||||
try {
|
||||
if (!operation.block(readTimeoutMs)) {
|
||||
throw new SocketTimeoutException();
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
// The operation is ongoing so replace readBuffer to avoid it being written to by this
|
||||
// operation during a subsequent request.
|
||||
readBuffer = null;
|
||||
Thread.currentThread().interrupt();
|
||||
throw new HttpDataSourceException(
|
||||
new SocketTimeoutException(), currentDataSpec, HttpDataSourceException.TYPE_READ);
|
||||
} else if (exception != null) {
|
||||
new InterruptedIOException(e), currentDataSpec, HttpDataSourceException.TYPE_READ);
|
||||
} catch (SocketTimeoutException e) {
|
||||
// The operation is ongoing so replace readBuffer to avoid it being written to by this
|
||||
// operation during a subsequent request.
|
||||
readBuffer = null;
|
||||
throw new HttpDataSourceException(e, currentDataSpec, HttpDataSourceException.TYPE_READ);
|
||||
}
|
||||
|
||||
if (exception != null) {
|
||||
throw new HttpDataSourceException(exception, currentDataSpec,
|
||||
HttpDataSourceException.TYPE_READ);
|
||||
} else if (finished) {
|
||||
bytesRemaining = 0;
|
||||
return C.RESULT_END_OF_INPUT;
|
||||
} else {
|
||||
// The operation didn't time out, fail or finish, and therefore data must have been read.
|
||||
|
|
@ -379,7 +441,28 @@ public class CronetDataSource extends UrlRequest.Callback implements HttpDataSou
|
|||
if (resetTimeoutOnRedirects) {
|
||||
resetConnectTimeout();
|
||||
}
|
||||
request.followRedirect();
|
||||
|
||||
Map<String, List<String>> headers = info.getAllHeaders();
|
||||
if (!handleSetCookieRequests || isEmpty(headers.get(SET_COOKIE))) {
|
||||
request.followRedirect();
|
||||
} else {
|
||||
currentUrlRequest.cancel();
|
||||
DataSpec redirectUrlDataSpec = new DataSpec(Uri.parse(newLocationUrl),
|
||||
currentDataSpec.postBody, currentDataSpec.absoluteStreamPosition,
|
||||
currentDataSpec.position, currentDataSpec.length, currentDataSpec.key,
|
||||
currentDataSpec.flags);
|
||||
UrlRequest.Builder requestBuilder;
|
||||
try {
|
||||
requestBuilder = buildRequestBuilder(redirectUrlDataSpec);
|
||||
} catch (IOException e) {
|
||||
exception = e;
|
||||
return;
|
||||
}
|
||||
String cookieHeadersValue = parseCookies(headers.get(SET_COOKIE));
|
||||
attachCookies(requestBuilder, cookieHeadersValue);
|
||||
currentUrlRequest = requestBuilder.build();
|
||||
currentUrlRequest.start();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -427,7 +510,7 @@ public class CronetDataSource extends UrlRequest.Callback implements HttpDataSou
|
|||
|
||||
// Internal methods.
|
||||
|
||||
private UrlRequest buildRequest(DataSpec dataSpec) throws OpenException {
|
||||
private UrlRequest.Builder buildRequestBuilder(DataSpec dataSpec) throws IOException {
|
||||
UrlRequest.Builder requestBuilder = cronetEngine.newUrlRequestBuilder(
|
||||
dataSpec.uri.toString(), this, executor).allowDirectExecutor();
|
||||
// Set the headers.
|
||||
|
|
@ -446,20 +529,25 @@ public class CronetDataSource extends UrlRequest.Callback implements HttpDataSou
|
|||
requestBuilder.addHeader(key, headerEntry.getValue());
|
||||
}
|
||||
if (dataSpec.postBody != null && dataSpec.postBody.length != 0 && !isContentTypeHeaderSet) {
|
||||
throw new OpenException("POST request with non-empty body must set Content-Type", dataSpec,
|
||||
Status.IDLE);
|
||||
throw new IOException("POST request with non-empty body must set Content-Type");
|
||||
}
|
||||
// Set the Range header.
|
||||
if (currentDataSpec.position != 0 || currentDataSpec.length != C.LENGTH_UNSET) {
|
||||
if (dataSpec.position != 0 || dataSpec.length != C.LENGTH_UNSET) {
|
||||
StringBuilder rangeValue = new StringBuilder();
|
||||
rangeValue.append("bytes=");
|
||||
rangeValue.append(currentDataSpec.position);
|
||||
rangeValue.append(dataSpec.position);
|
||||
rangeValue.append("-");
|
||||
if (currentDataSpec.length != C.LENGTH_UNSET) {
|
||||
rangeValue.append(currentDataSpec.position + currentDataSpec.length - 1);
|
||||
if (dataSpec.length != C.LENGTH_UNSET) {
|
||||
rangeValue.append(dataSpec.position + dataSpec.length - 1);
|
||||
}
|
||||
requestBuilder.addHeader("Range", rangeValue.toString());
|
||||
}
|
||||
// TODO: Uncomment when https://bugs.chromium.org/p/chromium/issues/detail?id=767025 is fixed
|
||||
// (adjusting the code as necessary).
|
||||
// Force identity encoding unless gzip is allowed.
|
||||
// if (!dataSpec.isFlagSet(DataSpec.FLAG_ALLOW_GZIP)) {
|
||||
// requestBuilder.addHeader("Accept-Encoding", "identity");
|
||||
// }
|
||||
// Set the method and (if non-empty) the body.
|
||||
if (dataSpec.postBody != null) {
|
||||
requestBuilder.setHttpMethod("POST");
|
||||
|
|
@ -468,10 +556,10 @@ public class CronetDataSource extends UrlRequest.Callback implements HttpDataSou
|
|||
executor);
|
||||
}
|
||||
}
|
||||
return requestBuilder.build();
|
||||
return requestBuilder;
|
||||
}
|
||||
|
||||
private boolean blockUntilConnectTimeout() {
|
||||
private boolean blockUntilConnectTimeout() throws InterruptedException {
|
||||
long now = clock.elapsedRealtime();
|
||||
boolean opened = false;
|
||||
while (!opened && now < currentConnectTimeoutMs) {
|
||||
|
|
@ -538,7 +626,18 @@ public class CronetDataSource extends UrlRequest.Callback implements HttpDataSou
|
|||
return contentLength;
|
||||
}
|
||||
|
||||
private static int getStatus(UrlRequest request) {
|
||||
private static String parseCookies(List<String> setCookieHeaders) {
|
||||
return TextUtils.join(";", setCookieHeaders);
|
||||
}
|
||||
|
||||
private static void attachCookies(UrlRequest.Builder requestBuilder, String cookies) {
|
||||
if (TextUtils.isEmpty(cookies)) {
|
||||
return;
|
||||
}
|
||||
requestBuilder.addHeader(COOKIE, cookies);
|
||||
}
|
||||
|
||||
private static int getStatus(UrlRequest request) throws InterruptedException {
|
||||
final ConditionVariable conditionVariable = new ConditionVariable();
|
||||
final int[] statusHolder = new int[1];
|
||||
request.getStatus(new UrlRequest.StatusListener() {
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ import android.util.Log;
|
|||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
|
|
@ -86,7 +87,7 @@ public final class CronetEngineWrapper {
|
|||
public CronetEngineWrapper(Context context, boolean preferGMSCoreCronet) {
|
||||
CronetEngine cronetEngine = null;
|
||||
@CronetEngineSource int cronetEngineSource = SOURCE_UNAVAILABLE;
|
||||
List<CronetProvider> cronetProviders = CronetProvider.getAllProviders(context);
|
||||
List<CronetProvider> cronetProviders = new ArrayList<>(CronetProvider.getAllProviders(context));
|
||||
// Remove disabled and fallback Cronet providers from list
|
||||
for (int i = cronetProviders.size() - 1; i >= 0; i--) {
|
||||
if (!cronetProviders.get(i).isEnabled()
|
||||
|
|
|
|||
|
|
@ -13,12 +13,5 @@
|
|||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
<resources>
|
||||
|
||||
<style name="ExoMediaButton">
|
||||
<item name="android:background">?android:attr/selectableItemBackground</item>
|
||||
<item name="android:layout_width">@dimen/exo_media_button_width</item>
|
||||
<item name="android:layout_height">@dimen/exo_media_button_height</item>
|
||||
</style>
|
||||
|
||||
</resources>
|
||||
<manifest package="com.google.android.exoplayer2.ext.cronet"/>
|
||||
|
|
@ -15,14 +15,10 @@
|
|||
*/
|
||||
package com.google.android.exoplayer2.ext.cronet;
|
||||
|
||||
import static org.junit.Assert.assertArrayEquals;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.MockitoAnnotations.initMocks;
|
||||
|
||||
import android.support.test.InstrumentationRegistry;
|
||||
import android.support.test.runner.AndroidJUnit4;
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.Arrays;
|
||||
|
|
@ -31,11 +27,11 @@ import org.junit.Before;
|
|||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.MockitoAnnotations;
|
||||
import org.robolectric.RobolectricTestRunner;
|
||||
|
||||
/**
|
||||
* Tests for {@link ByteArrayUploadDataProvider}.
|
||||
*/
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
/** Tests for {@link ByteArrayUploadDataProvider}. */
|
||||
@RunWith(RobolectricTestRunner.class)
|
||||
public final class ByteArrayUploadDataProviderTest {
|
||||
|
||||
private static final byte[] TEST_DATA = new byte[] {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
|
||||
|
|
@ -46,22 +42,20 @@ public final class ByteArrayUploadDataProviderTest {
|
|||
|
||||
@Before
|
||||
public void setUp() {
|
||||
System.setProperty("dexmaker.dexcache",
|
||||
InstrumentationRegistry.getTargetContext().getCacheDir().getPath());
|
||||
initMocks(this);
|
||||
MockitoAnnotations.initMocks(this);
|
||||
byteBuffer = ByteBuffer.allocate(TEST_DATA.length);
|
||||
byteArrayUploadDataProvider = new ByteArrayUploadDataProvider(TEST_DATA);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetLength() {
|
||||
assertEquals(TEST_DATA.length, byteArrayUploadDataProvider.getLength());
|
||||
assertThat(byteArrayUploadDataProvider.getLength()).isEqualTo(TEST_DATA.length);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testReadFullBuffer() throws IOException {
|
||||
byteArrayUploadDataProvider.read(mockUploadDataSink, byteBuffer);
|
||||
assertArrayEquals(TEST_DATA, byteBuffer.array());
|
||||
assertThat(byteBuffer.array()).isEqualTo(TEST_DATA);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -71,12 +65,12 @@ public final class ByteArrayUploadDataProviderTest {
|
|||
byteBuffer = ByteBuffer.allocate(TEST_DATA.length / 2);
|
||||
// Read half of the data.
|
||||
byteArrayUploadDataProvider.read(mockUploadDataSink, byteBuffer);
|
||||
assertArrayEquals(firstHalf, byteBuffer.array());
|
||||
assertThat(byteBuffer.array()).isEqualTo(firstHalf);
|
||||
|
||||
// Read the second half of the data.
|
||||
byteBuffer.rewind();
|
||||
byteArrayUploadDataProvider.read(mockUploadDataSink, byteBuffer);
|
||||
assertArrayEquals(secondHalf, byteBuffer.array());
|
||||
assertThat(byteBuffer.array()).isEqualTo(secondHalf);
|
||||
verify(mockUploadDataSink, times(2)).onReadSucceeded(false);
|
||||
}
|
||||
|
||||
|
|
@ -84,14 +78,13 @@ public final class ByteArrayUploadDataProviderTest {
|
|||
public void testRewind() throws IOException {
|
||||
// Read all the data.
|
||||
byteArrayUploadDataProvider.read(mockUploadDataSink, byteBuffer);
|
||||
assertArrayEquals(TEST_DATA, byteBuffer.array());
|
||||
assertThat(byteBuffer.array()).isEqualTo(TEST_DATA);
|
||||
|
||||
// Rewind and make sure it can be read again.
|
||||
byteBuffer.clear();
|
||||
byteArrayUploadDataProvider.rewind(mockUploadDataSink);
|
||||
byteArrayUploadDataProvider.read(mockUploadDataSink, byteBuffer);
|
||||
assertArrayEquals(TEST_DATA, byteBuffer.array());
|
||||
assertThat(byteBuffer.array()).isEqualTo(TEST_DATA);
|
||||
verify(mockUploadDataSink).onRewindSucceeded();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
manifest=src/test/AndroidManifest.xml
|
||||
|
|
@ -3,6 +3,14 @@
|
|||
The FFmpeg extension provides `FfmpegAudioRenderer`, which uses FFmpeg for
|
||||
decoding and can render audio encoded in a variety of formats.
|
||||
|
||||
## License note ##
|
||||
|
||||
Please note that whilst the code in this repository is licensed under
|
||||
[Apache 2.0][], using this extension also requires building and including one or
|
||||
more external libraries as described below. These are licensed separately.
|
||||
|
||||
[Apache 2.0]: https://github.com/google/ExoPlayer/blob/release-v2/LICENSE
|
||||
|
||||
## Build instructions ##
|
||||
|
||||
To use this extension you need to clone the ExoPlayer repository and depend on
|
||||
|
|
@ -21,7 +29,8 @@ EXOPLAYER_ROOT="$(pwd)"
|
|||
FFMPEG_EXT_PATH="${EXOPLAYER_ROOT}/extensions/ffmpeg/src/main"
|
||||
```
|
||||
|
||||
* Download the [Android NDK][] and set its location in an environment variable:
|
||||
* Download the [Android NDK][] and set its location in an environment variable.
|
||||
Only versions up to NDK 15c are supported currently.
|
||||
|
||||
```
|
||||
NDK_PATH="<path to Android NDK>"
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ android {
|
|||
}
|
||||
|
||||
dependencies {
|
||||
compile project(modulePrefix + 'library-core')
|
||||
implementation project(modulePrefix + 'library-core')
|
||||
}
|
||||
|
||||
ext {
|
||||
|
|
|
|||
|
|
@ -21,7 +21,10 @@ import com.google.android.exoplayer2.ExoPlaybackException;
|
|||
import com.google.android.exoplayer2.Format;
|
||||
import com.google.android.exoplayer2.audio.AudioProcessor;
|
||||
import com.google.android.exoplayer2.audio.AudioRendererEventListener;
|
||||
import com.google.android.exoplayer2.audio.AudioSink;
|
||||
import com.google.android.exoplayer2.audio.DefaultAudioSink;
|
||||
import com.google.android.exoplayer2.audio.SimpleDecoderAudioRenderer;
|
||||
import com.google.android.exoplayer2.drm.DrmSessionManager;
|
||||
import com.google.android.exoplayer2.drm.ExoMediaCrypto;
|
||||
import com.google.android.exoplayer2.util.MimeTypes;
|
||||
|
||||
|
|
@ -40,6 +43,8 @@ public final class FfmpegAudioRenderer extends SimpleDecoderAudioRenderer {
|
|||
*/
|
||||
private static final int INITIAL_INPUT_BUFFER_SIZE = 960 * 6;
|
||||
|
||||
private final boolean enableFloatOutput;
|
||||
|
||||
private FfmpegDecoder decoder;
|
||||
|
||||
public FfmpegAudioRenderer() {
|
||||
|
|
@ -54,17 +59,43 @@ public final class FfmpegAudioRenderer extends SimpleDecoderAudioRenderer {
|
|||
*/
|
||||
public FfmpegAudioRenderer(Handler eventHandler, AudioRendererEventListener eventListener,
|
||||
AudioProcessor... audioProcessors) {
|
||||
super(eventHandler, eventListener, audioProcessors);
|
||||
this(eventHandler, eventListener, new DefaultAudioSink(null, audioProcessors), false);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param eventHandler A handler to use when delivering events to {@code eventListener}. May be
|
||||
* null if delivery of events is not required.
|
||||
* @param eventListener A listener of events. May be null if delivery of events is not required.
|
||||
* @param audioSink The sink to which audio will be output.
|
||||
* @param enableFloatOutput Whether to enable 32-bit float audio format, if supported on the
|
||||
* device/build and if the input format may have bit depth higher than 16-bit. When using
|
||||
* 32-bit float output, any audio processing will be disabled, including playback speed/pitch
|
||||
* adjustment.
|
||||
*/
|
||||
public FfmpegAudioRenderer(Handler eventHandler, AudioRendererEventListener eventListener,
|
||||
AudioSink audioSink, boolean enableFloatOutput) {
|
||||
super(
|
||||
eventHandler,
|
||||
eventListener,
|
||||
/* drmSessionManager= */ null,
|
||||
/* playClearSamplesWithoutKeys= */ false,
|
||||
audioSink);
|
||||
this.enableFloatOutput = enableFloatOutput;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int supportsFormatInternal(Format format) {
|
||||
if (!FfmpegLibrary.isAvailable()) {
|
||||
protected int supportsFormatInternal(DrmSessionManager<ExoMediaCrypto> drmSessionManager,
|
||||
Format format) {
|
||||
String sampleMimeType = format.sampleMimeType;
|
||||
if (!FfmpegLibrary.isAvailable() || !MimeTypes.isAudio(sampleMimeType)) {
|
||||
return FORMAT_UNSUPPORTED_TYPE;
|
||||
} else if (!FfmpegLibrary.supportsFormat(sampleMimeType) || !isOutputSupported(format)) {
|
||||
return FORMAT_UNSUPPORTED_SUBTYPE;
|
||||
} else if (!supportsFormatDrm(drmSessionManager, format.drmInitData)) {
|
||||
return FORMAT_UNSUPPORTED_DRM;
|
||||
} else {
|
||||
return FORMAT_HANDLED;
|
||||
}
|
||||
String mimeType = format.sampleMimeType;
|
||||
return FfmpegLibrary.supportsFormat(mimeType) ? FORMAT_HANDLED
|
||||
: MimeTypes.isAudio(mimeType) ? FORMAT_UNSUPPORTED_SUBTYPE : FORMAT_UNSUPPORTED_TYPE;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -76,7 +107,7 @@ public final class FfmpegAudioRenderer extends SimpleDecoderAudioRenderer {
|
|||
protected FfmpegDecoder createDecoder(Format format, ExoMediaCrypto mediaCrypto)
|
||||
throws FfmpegDecoderException {
|
||||
decoder = new FfmpegDecoder(NUM_BUFFERS, NUM_BUFFERS, INITIAL_INPUT_BUFFER_SIZE,
|
||||
format.sampleMimeType, format.initializationData);
|
||||
format.sampleMimeType, format.initializationData, shouldUseFloatOutput(format));
|
||||
return decoder;
|
||||
}
|
||||
|
||||
|
|
@ -84,8 +115,32 @@ public final class FfmpegAudioRenderer extends SimpleDecoderAudioRenderer {
|
|||
public Format getOutputFormat() {
|
||||
int channelCount = decoder.getChannelCount();
|
||||
int sampleRate = decoder.getSampleRate();
|
||||
@C.PcmEncoding int encoding = decoder.getEncoding();
|
||||
return Format.createAudioSampleFormat(null, MimeTypes.AUDIO_RAW, null, Format.NO_VALUE,
|
||||
Format.NO_VALUE, channelCount, sampleRate, C.ENCODING_PCM_16BIT, null, null, 0, null);
|
||||
Format.NO_VALUE, channelCount, sampleRate, encoding, null, null, 0, null);
|
||||
}
|
||||
|
||||
private boolean isOutputSupported(Format inputFormat) {
|
||||
return shouldUseFloatOutput(inputFormat) || supportsOutputEncoding(C.ENCODING_PCM_16BIT);
|
||||
}
|
||||
|
||||
private boolean shouldUseFloatOutput(Format inputFormat) {
|
||||
if (!enableFloatOutput || !supportsOutputEncoding(C.ENCODING_PCM_FLOAT)) {
|
||||
return false;
|
||||
}
|
||||
switch (inputFormat.sampleMimeType) {
|
||||
case MimeTypes.AUDIO_RAW:
|
||||
// For raw audio, output in 32-bit float encoding if the bit depth is > 16-bit.
|
||||
return inputFormat.pcmEncoding == C.ENCODING_PCM_24BIT
|
||||
|| inputFormat.pcmEncoding == C.ENCODING_PCM_32BIT
|
||||
|| inputFormat.pcmEncoding == C.ENCODING_PCM_FLOAT;
|
||||
case MimeTypes.AUDIO_AC3:
|
||||
// AC-3 is always 16-bit, so there is no point outputting in 32-bit float encoding.
|
||||
return false;
|
||||
default:
|
||||
// For all other formats, assume that it's worth using 32-bit float encoding.
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@
|
|||
*/
|
||||
package com.google.android.exoplayer2.ext.ffmpeg;
|
||||
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.decoder.DecoderInputBuffer;
|
||||
import com.google.android.exoplayer2.decoder.SimpleDecoder;
|
||||
import com.google.android.exoplayer2.decoder.SimpleOutputBuffer;
|
||||
|
|
@ -29,11 +30,15 @@ import java.util.List;
|
|||
/* package */ final class FfmpegDecoder extends
|
||||
SimpleDecoder<DecoderInputBuffer, SimpleOutputBuffer, FfmpegDecoderException> {
|
||||
|
||||
// Space for 64 ms of 6 channel 48 kHz 16-bit PCM audio.
|
||||
private static final int OUTPUT_BUFFER_SIZE = 1536 * 6 * 2 * 2;
|
||||
// Space for 64 ms of 48 kHz 8 channel 16-bit PCM audio.
|
||||
private static final int OUTPUT_BUFFER_SIZE_16BIT = 64 * 48 * 8 * 2;
|
||||
// Space for 64 ms of 48 KhZ 8 channel 32-bit PCM audio.
|
||||
private static final int OUTPUT_BUFFER_SIZE_32BIT = OUTPUT_BUFFER_SIZE_16BIT * 2;
|
||||
|
||||
private final String codecName;
|
||||
private final byte[] extraData;
|
||||
private final @C.Encoding int encoding;
|
||||
private final int outputBufferSize;
|
||||
|
||||
private long nativeContext; // May be reassigned on resetting the codec.
|
||||
private boolean hasOutputFormat;
|
||||
|
|
@ -41,14 +46,17 @@ import java.util.List;
|
|||
private volatile int sampleRate;
|
||||
|
||||
public FfmpegDecoder(int numInputBuffers, int numOutputBuffers, int initialInputBufferSize,
|
||||
String mimeType, List<byte[]> initializationData) throws FfmpegDecoderException {
|
||||
String mimeType, List<byte[]> initializationData, boolean outputFloat)
|
||||
throws FfmpegDecoderException {
|
||||
super(new DecoderInputBuffer[numInputBuffers], new SimpleOutputBuffer[numOutputBuffers]);
|
||||
if (!FfmpegLibrary.isAvailable()) {
|
||||
throw new FfmpegDecoderException("Failed to load decoder native libraries.");
|
||||
}
|
||||
codecName = FfmpegLibrary.getCodecName(mimeType);
|
||||
extraData = getExtraData(mimeType, initializationData);
|
||||
nativeContext = ffmpegInitialize(codecName, extraData);
|
||||
encoding = outputFloat ? C.ENCODING_PCM_FLOAT : C.ENCODING_PCM_16BIT;
|
||||
outputBufferSize = outputFloat ? OUTPUT_BUFFER_SIZE_32BIT : OUTPUT_BUFFER_SIZE_16BIT;
|
||||
nativeContext = ffmpegInitialize(codecName, extraData, outputFloat);
|
||||
if (nativeContext == 0) {
|
||||
throw new FfmpegDecoderException("Initialization failed.");
|
||||
}
|
||||
|
|
@ -61,18 +69,23 @@ import java.util.List;
|
|||
}
|
||||
|
||||
@Override
|
||||
public DecoderInputBuffer createInputBuffer() {
|
||||
protected DecoderInputBuffer createInputBuffer() {
|
||||
return new DecoderInputBuffer(DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_DIRECT);
|
||||
}
|
||||
|
||||
@Override
|
||||
public SimpleOutputBuffer createOutputBuffer() {
|
||||
protected SimpleOutputBuffer createOutputBuffer() {
|
||||
return new SimpleOutputBuffer(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public FfmpegDecoderException decode(DecoderInputBuffer inputBuffer,
|
||||
SimpleOutputBuffer outputBuffer, boolean reset) {
|
||||
protected FfmpegDecoderException createUnexpectedDecodeException(Throwable error) {
|
||||
return new FfmpegDecoderException("Unexpected decode error", error);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected FfmpegDecoderException decode(
|
||||
DecoderInputBuffer inputBuffer, SimpleOutputBuffer outputBuffer, boolean reset) {
|
||||
if (reset) {
|
||||
nativeContext = ffmpegReset(nativeContext, extraData);
|
||||
if (nativeContext == 0) {
|
||||
|
|
@ -81,8 +94,8 @@ import java.util.List;
|
|||
}
|
||||
ByteBuffer inputData = inputBuffer.data;
|
||||
int inputSize = inputData.limit();
|
||||
ByteBuffer outputData = outputBuffer.init(inputBuffer.timeUs, OUTPUT_BUFFER_SIZE);
|
||||
int result = ffmpegDecode(nativeContext, inputData, inputSize, outputData, OUTPUT_BUFFER_SIZE);
|
||||
ByteBuffer outputData = outputBuffer.init(inputBuffer.timeUs, outputBufferSize);
|
||||
int result = ffmpegDecode(nativeContext, inputData, inputSize, outputData, outputBufferSize);
|
||||
if (result < 0) {
|
||||
return new FfmpegDecoderException("Error decoding (see logcat). Code: " + result);
|
||||
}
|
||||
|
|
@ -124,6 +137,13 @@ import java.util.List;
|
|||
return sampleRate;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the encoding of output audio.
|
||||
*/
|
||||
public @C.Encoding int getEncoding() {
|
||||
return encoding;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns FFmpeg-compatible codec-specific initialization data ("extra data"), or {@code null} if
|
||||
* not required.
|
||||
|
|
@ -153,7 +173,7 @@ import java.util.List;
|
|||
}
|
||||
}
|
||||
|
||||
private native long ffmpegInitialize(String codecName, byte[] extraData);
|
||||
private native long ffmpegInitialize(String codecName, byte[] extraData, boolean outputFloat);
|
||||
private native int ffmpegDecode(long context, ByteBuffer inputData, int inputSize,
|
||||
ByteBuffer outputData, int outputSize);
|
||||
private native int ffmpegGetChannelCount(long context);
|
||||
|
|
|
|||
|
|
@ -26,4 +26,7 @@ public final class FfmpegDecoderException extends AudioDecoderException {
|
|||
super(message);
|
||||
}
|
||||
|
||||
/* package */ FfmpegDecoderException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -57,8 +57,10 @@ extern "C" {
|
|||
|
||||
#define ERROR_STRING_BUFFER_LENGTH 256
|
||||
|
||||
// Request a format corresponding to AudioFormat.ENCODING_PCM_16BIT.
|
||||
static const AVSampleFormat OUTPUT_FORMAT = AV_SAMPLE_FMT_S16;
|
||||
// Output format corresponding to AudioFormat.ENCODING_PCM_16BIT.
|
||||
static const AVSampleFormat OUTPUT_FORMAT_PCM_16BIT = AV_SAMPLE_FMT_S16;
|
||||
// Output format corresponding to AudioFormat.ENCODING_PCM_FLOAT.
|
||||
static const AVSampleFormat OUTPUT_FORMAT_PCM_FLOAT = AV_SAMPLE_FMT_FLT;
|
||||
|
||||
/**
|
||||
* Returns the AVCodec with the specified name, or NULL if it is not available.
|
||||
|
|
@ -71,7 +73,7 @@ AVCodec *getCodecByName(JNIEnv* env, jstring codecName);
|
|||
* Returns the created context.
|
||||
*/
|
||||
AVCodecContext *createContext(JNIEnv *env, AVCodec *codec,
|
||||
jbyteArray extraData);
|
||||
jbyteArray extraData, jboolean outputFloat);
|
||||
|
||||
/**
|
||||
* Decodes the packet into the output buffer, returning the number of bytes
|
||||
|
|
@ -107,13 +109,14 @@ LIBRARY_FUNC(jboolean, ffmpegHasDecoder, jstring codecName) {
|
|||
return getCodecByName(env, codecName) != NULL;
|
||||
}
|
||||
|
||||
DECODER_FUNC(jlong, ffmpegInitialize, jstring codecName, jbyteArray extraData) {
|
||||
DECODER_FUNC(jlong, ffmpegInitialize, jstring codecName, jbyteArray extraData,
|
||||
jboolean outputFloat) {
|
||||
AVCodec *codec = getCodecByName(env, codecName);
|
||||
if (!codec) {
|
||||
LOGE("Codec not found.");
|
||||
return 0L;
|
||||
}
|
||||
return (jlong) createContext(env, codec, extraData);
|
||||
return (jlong) createContext(env, codec, extraData, outputFloat);
|
||||
}
|
||||
|
||||
DECODER_FUNC(jint, ffmpegDecode, jlong context, jobject inputData,
|
||||
|
|
@ -177,7 +180,8 @@ DECODER_FUNC(jlong, ffmpegReset, jlong jContext, jbyteArray extraData) {
|
|||
LOGE("Unexpected error finding codec %d.", codecId);
|
||||
return 0L;
|
||||
}
|
||||
return (jlong) createContext(env, codec, extraData);
|
||||
return (jlong) createContext(env, codec, extraData,
|
||||
context->request_sample_fmt == OUTPUT_FORMAT_PCM_FLOAT);
|
||||
}
|
||||
|
||||
avcodec_flush_buffers(context);
|
||||
|
|
@ -201,13 +205,14 @@ AVCodec *getCodecByName(JNIEnv* env, jstring codecName) {
|
|||
}
|
||||
|
||||
AVCodecContext *createContext(JNIEnv *env, AVCodec *codec,
|
||||
jbyteArray extraData) {
|
||||
jbyteArray extraData, jboolean outputFloat) {
|
||||
AVCodecContext *context = avcodec_alloc_context3(codec);
|
||||
if (!context) {
|
||||
LOGE("Failed to allocate context.");
|
||||
return NULL;
|
||||
}
|
||||
context->request_sample_fmt = OUTPUT_FORMAT;
|
||||
context->request_sample_fmt =
|
||||
outputFloat ? OUTPUT_FORMAT_PCM_FLOAT : OUTPUT_FORMAT_PCM_16BIT;
|
||||
if (extraData) {
|
||||
jsize size = env->GetArrayLength(extraData);
|
||||
context->extradata_size = size;
|
||||
|
|
@ -275,7 +280,9 @@ int decodePacket(AVCodecContext *context, AVPacket *packet,
|
|||
av_opt_set_int(resampleContext, "in_sample_rate", sampleRate, 0);
|
||||
av_opt_set_int(resampleContext, "out_sample_rate", sampleRate, 0);
|
||||
av_opt_set_int(resampleContext, "in_sample_fmt", sampleFormat, 0);
|
||||
av_opt_set_int(resampleContext, "out_sample_fmt", OUTPUT_FORMAT, 0);
|
||||
// The output format is always the requested format.
|
||||
av_opt_set_int(resampleContext, "out_sample_fmt",
|
||||
context->request_sample_fmt, 0);
|
||||
result = avresample_open(resampleContext);
|
||||
if (result < 0) {
|
||||
logError("avresample_open", result);
|
||||
|
|
@ -285,7 +292,7 @@ int decodePacket(AVCodecContext *context, AVPacket *packet,
|
|||
context->opaque = resampleContext;
|
||||
}
|
||||
int inSampleSize = av_get_bytes_per_sample(sampleFormat);
|
||||
int outSampleSize = av_get_bytes_per_sample(OUTPUT_FORMAT);
|
||||
int outSampleSize = av_get_bytes_per_sample(context->request_sample_fmt);
|
||||
int outSamples = avresample_get_out_samples(resampleContext, sampleCount);
|
||||
int bufferOutSize = outSampleSize * channelCount * outSamples;
|
||||
if (outSize + bufferOutSize > outputSize) {
|
||||
|
|
|
|||
|
|
@ -3,6 +3,14 @@
|
|||
The Flac extension provides `FlacExtractor` and `LibflacAudioRenderer`, which
|
||||
use libFLAC (the Flac decoding library) to extract and decode FLAC audio.
|
||||
|
||||
## License note ##
|
||||
|
||||
Please note that whilst the code in this repository is licensed under
|
||||
[Apache 2.0][], using this extension also requires building and including one or
|
||||
more external libraries as described below. These are licensed separately.
|
||||
|
||||
[Apache 2.0]: https://github.com/google/ExoPlayer/blob/release-v2/LICENSE
|
||||
|
||||
## Build instructions ##
|
||||
|
||||
To use this extension you need to clone the ExoPlayer repository and depend on
|
||||
|
|
@ -30,7 +38,7 @@ NDK_PATH="<path to Android NDK>"
|
|||
|
||||
```
|
||||
cd "${FLAC_EXT_PATH}/jni" && \
|
||||
curl http://downloads.xiph.org/releases/flac/flac-1.3.1.tar.xz | tar xJ && \
|
||||
curl https://ftp.osuosl.org/pub/xiph/releases/flac/flac-1.3.1.tar.xz | tar xJ && \
|
||||
mv flac-1.3.1 flac
|
||||
```
|
||||
|
||||
|
|
|
|||
|
|
@ -31,8 +31,10 @@ android {
|
|||
}
|
||||
|
||||
dependencies {
|
||||
compile project(modulePrefix + 'library-core')
|
||||
androidTestCompile project(modulePrefix + 'testutils')
|
||||
implementation 'com.android.support:support-annotations:' + supportLibraryVersion
|
||||
implementation project(modulePrefix + 'library-core')
|
||||
androidTestImplementation project(modulePrefix + 'testutils')
|
||||
testImplementation project(modulePrefix + 'testutils-robolectric')
|
||||
}
|
||||
|
||||
ext {
|
||||
|
|
|
|||
|
|
@ -18,8 +18,6 @@
|
|||
xmlns:tools="http://schemas.android.com/tools"
|
||||
package="com.google.android.exoplayer2.ext.flac.test">
|
||||
|
||||
<uses-sdk android:minSdkVersion="14" android:targetSdkVersion="26"/>
|
||||
|
||||
<application android:debuggable="true"
|
||||
android:allowBackup="false"
|
||||
tools:ignore="MissingApplicationIcon,HardcodedDebugMode">
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
seekMap:
|
||||
isSeekable = true
|
||||
duration = 2741000
|
||||
getPosition(0) = 8880
|
||||
getPosition(0) = [[timeUs=0, position=8880]]
|
||||
numberOfTracks = 1
|
||||
track 0:
|
||||
format:
|
||||
|
|
@ -9,22 +9,23 @@ track 0:
|
|||
id = null
|
||||
containerMimeType = null
|
||||
sampleMimeType = audio/raw
|
||||
maxInputSize = -1
|
||||
maxInputSize = 16384
|
||||
width = -1
|
||||
height = -1
|
||||
frameRate = -1.0
|
||||
rotationDegrees = -1
|
||||
pixelWidthHeightRatio = -1.0
|
||||
rotationDegrees = 0
|
||||
pixelWidthHeightRatio = 1.0
|
||||
channelCount = 2
|
||||
sampleRate = 48000
|
||||
pcmEncoding = 2
|
||||
encoderDelay = -1
|
||||
encoderPadding = -1
|
||||
encoderDelay = 0
|
||||
encoderPadding = 0
|
||||
subsampleOffsetUs = 9223372036854775807
|
||||
selectionFlags = 0
|
||||
language = null
|
||||
drmInitData = -
|
||||
initializationData:
|
||||
total output bytes = 526272
|
||||
sample count = 33
|
||||
sample 0:
|
||||
time = 0
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
seekMap:
|
||||
isSeekable = true
|
||||
duration = 2741000
|
||||
getPosition(0) = 8880
|
||||
getPosition(0) = [[timeUs=0, position=8880]]
|
||||
numberOfTracks = 1
|
||||
track 0:
|
||||
format:
|
||||
|
|
@ -9,22 +9,23 @@ track 0:
|
|||
id = null
|
||||
containerMimeType = null
|
||||
sampleMimeType = audio/raw
|
||||
maxInputSize = -1
|
||||
maxInputSize = 16384
|
||||
width = -1
|
||||
height = -1
|
||||
frameRate = -1.0
|
||||
rotationDegrees = -1
|
||||
pixelWidthHeightRatio = -1.0
|
||||
rotationDegrees = 0
|
||||
pixelWidthHeightRatio = 1.0
|
||||
channelCount = 2
|
||||
sampleRate = 48000
|
||||
pcmEncoding = 2
|
||||
encoderDelay = -1
|
||||
encoderPadding = -1
|
||||
encoderDelay = 0
|
||||
encoderPadding = 0
|
||||
subsampleOffsetUs = 9223372036854775807
|
||||
selectionFlags = 0
|
||||
language = null
|
||||
drmInitData = -
|
||||
initializationData:
|
||||
total output bytes = 362432
|
||||
sample count = 23
|
||||
sample 0:
|
||||
time = 853333
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
seekMap:
|
||||
isSeekable = true
|
||||
duration = 2741000
|
||||
getPosition(0) = 8880
|
||||
getPosition(0) = [[timeUs=0, position=8880]]
|
||||
numberOfTracks = 1
|
||||
track 0:
|
||||
format:
|
||||
|
|
@ -9,22 +9,23 @@ track 0:
|
|||
id = null
|
||||
containerMimeType = null
|
||||
sampleMimeType = audio/raw
|
||||
maxInputSize = -1
|
||||
maxInputSize = 16384
|
||||
width = -1
|
||||
height = -1
|
||||
frameRate = -1.0
|
||||
rotationDegrees = -1
|
||||
pixelWidthHeightRatio = -1.0
|
||||
rotationDegrees = 0
|
||||
pixelWidthHeightRatio = 1.0
|
||||
channelCount = 2
|
||||
sampleRate = 48000
|
||||
pcmEncoding = 2
|
||||
encoderDelay = -1
|
||||
encoderPadding = -1
|
||||
encoderDelay = 0
|
||||
encoderPadding = 0
|
||||
subsampleOffsetUs = 9223372036854775807
|
||||
selectionFlags = 0
|
||||
language = null
|
||||
drmInitData = -
|
||||
initializationData:
|
||||
total output bytes = 182208
|
||||
sample count = 12
|
||||
sample 0:
|
||||
time = 1792000
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
seekMap:
|
||||
isSeekable = true
|
||||
duration = 2741000
|
||||
getPosition(0) = 8880
|
||||
getPosition(0) = [[timeUs=0, position=8880]]
|
||||
numberOfTracks = 1
|
||||
track 0:
|
||||
format:
|
||||
|
|
@ -9,22 +9,23 @@ track 0:
|
|||
id = null
|
||||
containerMimeType = null
|
||||
sampleMimeType = audio/raw
|
||||
maxInputSize = -1
|
||||
maxInputSize = 16384
|
||||
width = -1
|
||||
height = -1
|
||||
frameRate = -1.0
|
||||
rotationDegrees = -1
|
||||
pixelWidthHeightRatio = -1.0
|
||||
rotationDegrees = 0
|
||||
pixelWidthHeightRatio = 1.0
|
||||
channelCount = 2
|
||||
sampleRate = 48000
|
||||
pcmEncoding = 2
|
||||
encoderDelay = -1
|
||||
encoderPadding = -1
|
||||
encoderDelay = 0
|
||||
encoderPadding = 0
|
||||
subsampleOffsetUs = 9223372036854775807
|
||||
selectionFlags = 0
|
||||
language = null
|
||||
drmInitData = -
|
||||
initializationData:
|
||||
total output bytes = 18368
|
||||
sample count = 2
|
||||
sample 0:
|
||||
time = 2645333
|
||||
|
|
|
|||
BIN
extensions/flac/src/androidTest/assets/bear_with_id3.flac
Normal file
162
extensions/flac/src/androidTest/assets/bear_with_id3.flac.0.dump
Normal file
|
|
@ -0,0 +1,162 @@
|
|||
seekMap:
|
||||
isSeekable = true
|
||||
duration = 2741000
|
||||
getPosition(0) = [[timeUs=0, position=55284]]
|
||||
numberOfTracks = 1
|
||||
track 0:
|
||||
format:
|
||||
bitrate = 768000
|
||||
id = null
|
||||
containerMimeType = null
|
||||
sampleMimeType = audio/raw
|
||||
maxInputSize = 16384
|
||||
width = -1
|
||||
height = -1
|
||||
frameRate = -1.0
|
||||
rotationDegrees = 0
|
||||
pixelWidthHeightRatio = 1.0
|
||||
channelCount = 2
|
||||
sampleRate = 48000
|
||||
pcmEncoding = 2
|
||||
encoderDelay = 0
|
||||
encoderPadding = 0
|
||||
subsampleOffsetUs = 9223372036854775807
|
||||
selectionFlags = 0
|
||||
language = null
|
||||
drmInitData = -
|
||||
initializationData:
|
||||
total output bytes = 526272
|
||||
sample count = 33
|
||||
sample 0:
|
||||
time = 0
|
||||
flags = 1
|
||||
data = length 16384, hash 61D2C5C2
|
||||
sample 1:
|
||||
time = 85333
|
||||
flags = 1
|
||||
data = length 16384, hash E6D7F214
|
||||
sample 2:
|
||||
time = 170666
|
||||
flags = 1
|
||||
data = length 16384, hash 59BF0D5D
|
||||
sample 3:
|
||||
time = 256000
|
||||
flags = 1
|
||||
data = length 16384, hash 3625F468
|
||||
sample 4:
|
||||
time = 341333
|
||||
flags = 1
|
||||
data = length 16384, hash F66A323
|
||||
sample 5:
|
||||
time = 426666
|
||||
flags = 1
|
||||
data = length 16384, hash CDBAE629
|
||||
sample 6:
|
||||
time = 512000
|
||||
flags = 1
|
||||
data = length 16384, hash 536F3A91
|
||||
sample 7:
|
||||
time = 597333
|
||||
flags = 1
|
||||
data = length 16384, hash D4F35C9C
|
||||
sample 8:
|
||||
time = 682666
|
||||
flags = 1
|
||||
data = length 16384, hash EE04CEBF
|
||||
sample 9:
|
||||
time = 768000
|
||||
flags = 1
|
||||
data = length 16384, hash 647E2A67
|
||||
sample 10:
|
||||
time = 853333
|
||||
flags = 1
|
||||
data = length 16384, hash 31583F2C
|
||||
sample 11:
|
||||
time = 938666
|
||||
flags = 1
|
||||
data = length 16384, hash E433A93D
|
||||
sample 12:
|
||||
time = 1024000
|
||||
flags = 1
|
||||
data = length 16384, hash 5E1C7051
|
||||
sample 13:
|
||||
time = 1109333
|
||||
flags = 1
|
||||
data = length 16384, hash 43E6E358
|
||||
sample 14:
|
||||
time = 1194666
|
||||
flags = 1
|
||||
data = length 16384, hash 5DC1B256
|
||||
sample 15:
|
||||
time = 1280000
|
||||
flags = 1
|
||||
data = length 16384, hash 3D9D95CF
|
||||
sample 16:
|
||||
time = 1365333
|
||||
flags = 1
|
||||
data = length 16384, hash 2A5BD2C0
|
||||
sample 17:
|
||||
time = 1450666
|
||||
flags = 1
|
||||
data = length 16384, hash 93E25061
|
||||
sample 18:
|
||||
time = 1536000
|
||||
flags = 1
|
||||
data = length 16384, hash B81793D8
|
||||
sample 19:
|
||||
time = 1621333
|
||||
flags = 1
|
||||
data = length 16384, hash 1A3BD49F
|
||||
sample 20:
|
||||
time = 1706666
|
||||
flags = 1
|
||||
data = length 16384, hash FB672FF1
|
||||
sample 21:
|
||||
time = 1792000
|
||||
flags = 1
|
||||
data = length 16384, hash 48AB8B45
|
||||
sample 22:
|
||||
time = 1877333
|
||||
flags = 1
|
||||
data = length 16384, hash 13C9640A
|
||||
sample 23:
|
||||
time = 1962666
|
||||
flags = 1
|
||||
data = length 16384, hash 499E4A0B
|
||||
sample 24:
|
||||
time = 2048000
|
||||
flags = 1
|
||||
data = length 16384, hash F9A783E6
|
||||
sample 25:
|
||||
time = 2133333
|
||||
flags = 1
|
||||
data = length 16384, hash D2B77598
|
||||
sample 26:
|
||||
time = 2218666
|
||||
flags = 1
|
||||
data = length 16384, hash CE5B826C
|
||||
sample 27:
|
||||
time = 2304000
|
||||
flags = 1
|
||||
data = length 16384, hash E99EE956
|
||||
sample 28:
|
||||
time = 2389333
|
||||
flags = 1
|
||||
data = length 16384, hash F2DB1486
|
||||
sample 29:
|
||||
time = 2474666
|
||||
flags = 1
|
||||
data = length 16384, hash 1636EAB
|
||||
sample 30:
|
||||
time = 2560000
|
||||
flags = 1
|
||||
data = length 16384, hash 23457C08
|
||||
sample 31:
|
||||
time = 2645333
|
||||
flags = 1
|
||||
data = length 16384, hash 30EB8381
|
||||
sample 32:
|
||||
time = 2730666
|
||||
flags = 1
|
||||
data = length 1984, hash 59CFDE1B
|
||||
tracksEnded = true
|
||||
122
extensions/flac/src/androidTest/assets/bear_with_id3.flac.1.dump
Normal file
|
|
@ -0,0 +1,122 @@
|
|||
seekMap:
|
||||
isSeekable = true
|
||||
duration = 2741000
|
||||
getPosition(0) = [[timeUs=0, position=55284]]
|
||||
numberOfTracks = 1
|
||||
track 0:
|
||||
format:
|
||||
bitrate = 768000
|
||||
id = null
|
||||
containerMimeType = null
|
||||
sampleMimeType = audio/raw
|
||||
maxInputSize = 16384
|
||||
width = -1
|
||||
height = -1
|
||||
frameRate = -1.0
|
||||
rotationDegrees = 0
|
||||
pixelWidthHeightRatio = 1.0
|
||||
channelCount = 2
|
||||
sampleRate = 48000
|
||||
pcmEncoding = 2
|
||||
encoderDelay = 0
|
||||
encoderPadding = 0
|
||||
subsampleOffsetUs = 9223372036854775807
|
||||
selectionFlags = 0
|
||||
language = null
|
||||
drmInitData = -
|
||||
initializationData:
|
||||
total output bytes = 362432
|
||||
sample count = 23
|
||||
sample 0:
|
||||
time = 853333
|
||||
flags = 1
|
||||
data = length 16384, hash 31583F2C
|
||||
sample 1:
|
||||
time = 938666
|
||||
flags = 1
|
||||
data = length 16384, hash E433A93D
|
||||
sample 2:
|
||||
time = 1024000
|
||||
flags = 1
|
||||
data = length 16384, hash 5E1C7051
|
||||
sample 3:
|
||||
time = 1109333
|
||||
flags = 1
|
||||
data = length 16384, hash 43E6E358
|
||||
sample 4:
|
||||
time = 1194666
|
||||
flags = 1
|
||||
data = length 16384, hash 5DC1B256
|
||||
sample 5:
|
||||
time = 1280000
|
||||
flags = 1
|
||||
data = length 16384, hash 3D9D95CF
|
||||
sample 6:
|
||||
time = 1365333
|
||||
flags = 1
|
||||
data = length 16384, hash 2A5BD2C0
|
||||
sample 7:
|
||||
time = 1450666
|
||||
flags = 1
|
||||
data = length 16384, hash 93E25061
|
||||
sample 8:
|
||||
time = 1536000
|
||||
flags = 1
|
||||
data = length 16384, hash B81793D8
|
||||
sample 9:
|
||||
time = 1621333
|
||||
flags = 1
|
||||
data = length 16384, hash 1A3BD49F
|
||||
sample 10:
|
||||
time = 1706666
|
||||
flags = 1
|
||||
data = length 16384, hash FB672FF1
|
||||
sample 11:
|
||||
time = 1792000
|
||||
flags = 1
|
||||
data = length 16384, hash 48AB8B45
|
||||
sample 12:
|
||||
time = 1877333
|
||||
flags = 1
|
||||
data = length 16384, hash 13C9640A
|
||||
sample 13:
|
||||
time = 1962666
|
||||
flags = 1
|
||||
data = length 16384, hash 499E4A0B
|
||||
sample 14:
|
||||
time = 2048000
|
||||
flags = 1
|
||||
data = length 16384, hash F9A783E6
|
||||
sample 15:
|
||||
time = 2133333
|
||||
flags = 1
|
||||
data = length 16384, hash D2B77598
|
||||
sample 16:
|
||||
time = 2218666
|
||||
flags = 1
|
||||
data = length 16384, hash CE5B826C
|
||||
sample 17:
|
||||
time = 2304000
|
||||
flags = 1
|
||||
data = length 16384, hash E99EE956
|
||||
sample 18:
|
||||
time = 2389333
|
||||
flags = 1
|
||||
data = length 16384, hash F2DB1486
|
||||
sample 19:
|
||||
time = 2474666
|
||||
flags = 1
|
||||
data = length 16384, hash 1636EAB
|
||||
sample 20:
|
||||
time = 2560000
|
||||
flags = 1
|
||||
data = length 16384, hash 23457C08
|
||||
sample 21:
|
||||
time = 2645333
|
||||
flags = 1
|
||||
data = length 16384, hash 30EB8381
|
||||
sample 22:
|
||||
time = 2730666
|
||||
flags = 1
|
||||
data = length 1984, hash 59CFDE1B
|
||||
tracksEnded = true
|
||||
|
|
@ -0,0 +1,78 @@
|
|||
seekMap:
|
||||
isSeekable = true
|
||||
duration = 2741000
|
||||
getPosition(0) = [[timeUs=0, position=55284]]
|
||||
numberOfTracks = 1
|
||||
track 0:
|
||||
format:
|
||||
bitrate = 768000
|
||||
id = null
|
||||
containerMimeType = null
|
||||
sampleMimeType = audio/raw
|
||||
maxInputSize = 16384
|
||||
width = -1
|
||||
height = -1
|
||||
frameRate = -1.0
|
||||
rotationDegrees = 0
|
||||
pixelWidthHeightRatio = 1.0
|
||||
channelCount = 2
|
||||
sampleRate = 48000
|
||||
pcmEncoding = 2
|
||||
encoderDelay = 0
|
||||
encoderPadding = 0
|
||||
subsampleOffsetUs = 9223372036854775807
|
||||
selectionFlags = 0
|
||||
language = null
|
||||
drmInitData = -
|
||||
initializationData:
|
||||
total output bytes = 182208
|
||||
sample count = 12
|
||||
sample 0:
|
||||
time = 1792000
|
||||
flags = 1
|
||||
data = length 16384, hash 48AB8B45
|
||||
sample 1:
|
||||
time = 1877333
|
||||
flags = 1
|
||||
data = length 16384, hash 13C9640A
|
||||
sample 2:
|
||||
time = 1962666
|
||||
flags = 1
|
||||
data = length 16384, hash 499E4A0B
|
||||
sample 3:
|
||||
time = 2048000
|
||||
flags = 1
|
||||
data = length 16384, hash F9A783E6
|
||||
sample 4:
|
||||
time = 2133333
|
||||
flags = 1
|
||||
data = length 16384, hash D2B77598
|
||||
sample 5:
|
||||
time = 2218666
|
||||
flags = 1
|
||||
data = length 16384, hash CE5B826C
|
||||
sample 6:
|
||||
time = 2304000
|
||||
flags = 1
|
||||
data = length 16384, hash E99EE956
|
||||
sample 7:
|
||||
time = 2389333
|
||||
flags = 1
|
||||
data = length 16384, hash F2DB1486
|
||||
sample 8:
|
||||
time = 2474666
|
||||
flags = 1
|
||||
data = length 16384, hash 1636EAB
|
||||
sample 9:
|
||||
time = 2560000
|
||||
flags = 1
|
||||
data = length 16384, hash 23457C08
|
||||
sample 10:
|
||||
time = 2645333
|
||||
flags = 1
|
||||
data = length 16384, hash 30EB8381
|
||||
sample 11:
|
||||
time = 2730666
|
||||
flags = 1
|
||||
data = length 1984, hash 59CFDE1B
|
||||
tracksEnded = true
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
seekMap:
|
||||
isSeekable = true
|
||||
duration = 2741000
|
||||
getPosition(0) = [[timeUs=0, position=55284]]
|
||||
numberOfTracks = 1
|
||||
track 0:
|
||||
format:
|
||||
bitrate = 768000
|
||||
id = null
|
||||
containerMimeType = null
|
||||
sampleMimeType = audio/raw
|
||||
maxInputSize = 16384
|
||||
width = -1
|
||||
height = -1
|
||||
frameRate = -1.0
|
||||
rotationDegrees = 0
|
||||
pixelWidthHeightRatio = 1.0
|
||||
channelCount = 2
|
||||
sampleRate = 48000
|
||||
pcmEncoding = 2
|
||||
encoderDelay = 0
|
||||
encoderPadding = 0
|
||||
subsampleOffsetUs = 9223372036854775807
|
||||
selectionFlags = 0
|
||||
language = null
|
||||
drmInitData = -
|
||||
initializationData:
|
||||
total output bytes = 18368
|
||||
sample count = 2
|
||||
sample 0:
|
||||
time = 2645333
|
||||
flags = 1
|
||||
data = length 16384, hash 30EB8381
|
||||
sample 1:
|
||||
time = 2730666
|
||||
flags = 1
|
||||
data = length 1984, hash 59CFDE1B
|
||||
tracksEnded = true
|
||||
|
|
@ -25,12 +25,35 @@ import com.google.android.exoplayer2.testutil.ExtractorAsserts.ExtractorFactory;
|
|||
*/
|
||||
public class FlacExtractorTest extends InstrumentationTestCase {
|
||||
|
||||
public void testSample() throws Exception {
|
||||
ExtractorAsserts.assertBehavior(new ExtractorFactory() {
|
||||
@Override
|
||||
public Extractor create() {
|
||||
return new FlacExtractor();
|
||||
}
|
||||
}, "bear.flac", getInstrumentation());
|
||||
@Override
|
||||
protected void setUp() throws Exception {
|
||||
super.setUp();
|
||||
if (!FlacLibrary.isAvailable()) {
|
||||
fail("Flac library not available.");
|
||||
}
|
||||
}
|
||||
|
||||
public void testExtractFlacSample() throws Exception {
|
||||
ExtractorAsserts.assertBehavior(
|
||||
new ExtractorFactory() {
|
||||
@Override
|
||||
public Extractor create() {
|
||||
return new FlacExtractor();
|
||||
}
|
||||
},
|
||||
"bear.flac",
|
||||
getInstrumentation().getContext());
|
||||
}
|
||||
|
||||
public void testExtractFlacSampleWithId3Header() throws Exception {
|
||||
ExtractorAsserts.assertBehavior(
|
||||
new ExtractorFactory() {
|
||||
@Override
|
||||
public Extractor create() {
|
||||
return new FlacExtractor();
|
||||
}
|
||||
},
|
||||
"bear_with_id3.flac",
|
||||
getInstrumentation().getContext());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,15 +22,12 @@ import android.test.InstrumentationTestCase;
|
|||
import com.google.android.exoplayer2.ExoPlaybackException;
|
||||
import com.google.android.exoplayer2.ExoPlayer;
|
||||
import com.google.android.exoplayer2.ExoPlayerFactory;
|
||||
import com.google.android.exoplayer2.PlaybackParameters;
|
||||
import com.google.android.exoplayer2.Player;
|
||||
import com.google.android.exoplayer2.Renderer;
|
||||
import com.google.android.exoplayer2.Timeline;
|
||||
import com.google.android.exoplayer2.extractor.mkv.MatroskaExtractor;
|
||||
import com.google.android.exoplayer2.source.ExtractorMediaSource;
|
||||
import com.google.android.exoplayer2.source.TrackGroupArray;
|
||||
import com.google.android.exoplayer2.source.MediaSource;
|
||||
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
|
||||
import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
|
||||
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
|
||||
|
||||
/**
|
||||
|
|
@ -40,25 +37,35 @@ public class FlacPlaybackTest extends InstrumentationTestCase {
|
|||
|
||||
private static final String BEAR_FLAC_URI = "asset:///bear-flac.mka";
|
||||
|
||||
@Override
|
||||
protected void setUp() throws Exception {
|
||||
super.setUp();
|
||||
if (!FlacLibrary.isAvailable()) {
|
||||
fail("Flac library not available.");
|
||||
}
|
||||
}
|
||||
|
||||
public void testBasicPlayback() throws ExoPlaybackException {
|
||||
playUri(BEAR_FLAC_URI);
|
||||
}
|
||||
|
||||
private void playUri(String uri) throws ExoPlaybackException {
|
||||
TestPlaybackThread thread = new TestPlaybackThread(Uri.parse(uri),
|
||||
TestPlaybackRunnable testPlaybackRunnable = new TestPlaybackRunnable(Uri.parse(uri),
|
||||
getInstrumentation().getContext());
|
||||
Thread thread = new Thread(testPlaybackRunnable);
|
||||
thread.start();
|
||||
try {
|
||||
thread.join();
|
||||
} catch (InterruptedException e) {
|
||||
fail(); // Should never happen.
|
||||
}
|
||||
if (thread.playbackException != null) {
|
||||
throw thread.playbackException;
|
||||
if (testPlaybackRunnable.playbackException != null) {
|
||||
throw testPlaybackRunnable.playbackException;
|
||||
}
|
||||
}
|
||||
|
||||
private static class TestPlaybackThread extends Thread implements Player.EventListener {
|
||||
private static class TestPlaybackRunnable extends Player.DefaultEventListener
|
||||
implements Runnable {
|
||||
|
||||
private final Context context;
|
||||
private final Uri uri;
|
||||
|
|
@ -66,7 +73,7 @@ public class FlacPlaybackTest extends InstrumentationTestCase {
|
|||
private ExoPlayer player;
|
||||
private ExoPlaybackException playbackException;
|
||||
|
||||
public TestPlaybackThread(Uri uri, Context context) {
|
||||
public TestPlaybackRunnable(Uri uri, Context context) {
|
||||
this.uri = uri;
|
||||
this.context = context;
|
||||
}
|
||||
|
|
@ -78,42 +85,16 @@ public class FlacPlaybackTest extends InstrumentationTestCase {
|
|||
DefaultTrackSelector trackSelector = new DefaultTrackSelector();
|
||||
player = ExoPlayerFactory.newInstance(new Renderer[] {audioRenderer}, trackSelector);
|
||||
player.addListener(this);
|
||||
ExtractorMediaSource mediaSource = new ExtractorMediaSource(
|
||||
uri,
|
||||
new DefaultDataSourceFactory(context, "ExoPlayerExtFlacTest"),
|
||||
MatroskaExtractor.FACTORY,
|
||||
null,
|
||||
null);
|
||||
MediaSource mediaSource =
|
||||
new ExtractorMediaSource.Factory(
|
||||
new DefaultDataSourceFactory(context, "ExoPlayerExtFlacTest"))
|
||||
.setExtractorsFactory(MatroskaExtractor.FACTORY)
|
||||
.createMediaSource(uri);
|
||||
player.prepare(mediaSource);
|
||||
player.setPlayWhenReady(true);
|
||||
Looper.loop();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoadingChanged(boolean isLoading) {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray trackSelections) {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPositionDiscontinuity() {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPlaybackParametersChanged(PlaybackParameters playbackParameters) {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTimelineChanged(Timeline timeline, Object manifest) {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPlayerError(ExoPlaybackException error) {
|
||||
playbackException = error;
|
||||
|
|
@ -123,25 +104,10 @@ public class FlacPlaybackTest extends InstrumentationTestCase {
|
|||
public void onPlayerStateChanged(boolean playWhenReady, int playbackState) {
|
||||
if (playbackState == Player.STATE_ENDED
|
||||
|| (playbackState == Player.STATE_IDLE && playbackException != null)) {
|
||||
releasePlayerAndQuitLooper();
|
||||
player.release();
|
||||
Looper.myLooper().quit();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRepeatModeChanged(int repeatMode) {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onShuffleModeEnabledChanged(boolean shuffleModeEnabled) {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
private void releasePlayerAndQuitLooper() {
|
||||
player.release();
|
||||
Looper.myLooper().quit();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -70,18 +70,23 @@ import java.util.List;
|
|||
}
|
||||
|
||||
@Override
|
||||
public DecoderInputBuffer createInputBuffer() {
|
||||
protected DecoderInputBuffer createInputBuffer() {
|
||||
return new DecoderInputBuffer(DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_NORMAL);
|
||||
}
|
||||
|
||||
@Override
|
||||
public SimpleOutputBuffer createOutputBuffer() {
|
||||
protected SimpleOutputBuffer createOutputBuffer() {
|
||||
return new SimpleOutputBuffer(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public FlacDecoderException decode(DecoderInputBuffer inputBuffer,
|
||||
SimpleOutputBuffer outputBuffer, boolean reset) {
|
||||
protected FlacDecoderException createUnexpectedDecodeException(Throwable error) {
|
||||
return new FlacDecoderException("Unexpected decode error", error);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected FlacDecoderException decode(
|
||||
DecoderInputBuffer inputBuffer, SimpleOutputBuffer outputBuffer, boolean reset) {
|
||||
if (reset) {
|
||||
decoderJni.flush();
|
||||
}
|
||||
|
|
|
|||