mirror of
https://github.com/samsonjs/media.git
synced 2026-03-30 10:15:48 +00:00
Merge remote-tracking branch 'upstream/dev-v2' into dev-v2
This commit is contained in:
commit
8bed008934
1610 changed files with 55036 additions and 20120 deletions
|
|
@ -65,6 +65,7 @@ extensions/vp9/src/main/jni/libyuv
|
|||
|
||||
# AV1 extension
|
||||
extensions/av1/src/main/jni/libgav1
|
||||
extensions/av1/src/main/jni/cpu_features
|
||||
|
||||
# Opus extension
|
||||
extensions/opus/src/main/jni/libopus
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
# ExoPlayer #
|
||||
# ExoPlayer <img src="https://img.shields.io/github/v/release/google/ExoPlayer.svg?label=latest"/> #
|
||||
|
||||
ExoPlayer is an application level media player for Android. It provides an
|
||||
alternative to Android’s MediaPlayer API for playing audio and video both
|
||||
|
|
|
|||
625
RELEASENOTES.md
625
RELEASENOTES.md
|
|
@ -2,142 +2,428 @@
|
|||
|
||||
### dev-v2 (not yet released)
|
||||
|
||||
* Track selection:
|
||||
* Add option to specify multiple preferred audio or text languages.
|
||||
* Data sources:
|
||||
* Add support for `android.resource` URI scheme in `RawResourceDataSource`
|
||||
([#7866](https://github.com/google/ExoPlayer/issues/7866)).
|
||||
* Core library:
|
||||
* Added `TextComponent.getCurrentCues` because the current cues are no
|
||||
longer forwarded to a new `TextOutput` in `SimpleExoPlayer`
|
||||
automatically.
|
||||
* Add opt-in to verify correct thread usage with
|
||||
`SimpleExoPlayer.setThrowsWhenUsingWrongThread(true)`
|
||||
([#4463](https://github.com/google/ExoPlayer/issues/4463)).
|
||||
* Fix bug where `PlayerMessages` throw an exception after `MediaSources`
|
||||
are removed from the playlist
|
||||
([#7278](https://github.com/google/ExoPlayer/issues/7278)).
|
||||
* Add playbackPositionUs parameter to 'LoadControl.shouldContinueLoading'.
|
||||
* The `DefaultLoadControl` default minimum buffer is set to 50 seconds,
|
||||
equal to the default maximum buffer. `DefaultLoadControl` applies the
|
||||
same behavior for audio and video.
|
||||
* Add API in `AnalyticsListener` to report video frame processing offset.
|
||||
`MediaCodecVideoRenderer` reports the event.
|
||||
* Add fields `videoFrameProcessingOffsetUsSum` and
|
||||
`videoFrameProcessingOffsetUsCount` in `DecoderCounters` to compute the
|
||||
average video frame processing offset.
|
||||
* Add playlist API
|
||||
([#6161](https://github.com/google/ExoPlayer/issues/6161)).
|
||||
* Add `play` and `pause` methods to `Player`.
|
||||
* Add `Player.getCurrentLiveOffset` to conveniently return the live
|
||||
offset.
|
||||
* Add `Player.onPlayWhenReadyChanged` with reasons.
|
||||
* Add `Player.onPlaybackStateChanged` and deprecate
|
||||
`Player.onPlayerStateChanged`.
|
||||
* Add `Player.setAudioSessionId` to set the session ID attached to the
|
||||
`AudioTrack`.
|
||||
* Deprecate and rename `getPlaybackError` to `getPlayerError` for
|
||||
consistency.
|
||||
* Deprecate and rename `onLoadingChanged` to `onIsLoadingChanged` for
|
||||
consistency.
|
||||
* Deprecate `onSeekProcessed` because all seek changes happen instantly
|
||||
now and listening to `onPositionDiscontinuity` is sufficient.
|
||||
* Add `ExoPlayer.setPauseAtEndOfMediaItems` to let the player pause at the
|
||||
end of each media item
|
||||
([#5660](https://github.com/google/ExoPlayer/issues/5660)).
|
||||
* Split `setPlaybackParameter` into `setPlaybackSpeed` and
|
||||
`AudioComponent.setSkipSilenceEnabled` with callbacks
|
||||
`onPlaybackSpeedChanged` and
|
||||
`AudioListener.onSkipSilenceEnabledChanged`.
|
||||
* Make `MediaSourceEventListener.LoadEventInfo` and
|
||||
`MediaSourceEventListener.MediaLoadData` top-level classes.
|
||||
* Rename `MediaCodecRenderer.onOutputFormatChanged` to
|
||||
`MediaCodecRenderer.onOutputMediaFormatChanged`, further clarifying the
|
||||
distinction between `Format` and `MediaFormat`.
|
||||
* Improve `Format` propagation within the media codec renderer
|
||||
([#6646](https://github.com/google/ExoPlayer/issues/6646)).
|
||||
* Move player message-related constants from `C` to `Renderer`, to avoid
|
||||
having the constants class depend on player/renderer classes.
|
||||
* Split out `common` and `extractor` submodules.
|
||||
* Allow to explicitly send `PlayerMessage`s at the end of a stream.
|
||||
* Add `DataSpec.Builder` and deprecate most `DataSpec` constructors.
|
||||
* Add `DataSpec.customData` to allow applications to pass custom data
|
||||
through `DataSource` chains.
|
||||
* Add a `Format.Builder` and deprecate all `Format.create*` methods and
|
||||
most `Format.copyWith*` methods.
|
||||
* Split `Format.bitrate` into `Format.averageBitrate` and
|
||||
`Format.peakBitrate`
|
||||
([#2863](https://github.com/google/ExoPlayer/issues/2863)).
|
||||
* Add option to `MergingMediaSource` to adjust the time offsets between
|
||||
the merged sources
|
||||
* Suppress Guava-related ProGuard/R8 warnings
|
||||
([#7904](https://github.com/google/ExoPlayer/issues/7904)).
|
||||
|
||||
### 2.12.0 (2020-09-11) ###
|
||||
|
||||
* Core library:
|
||||
* `Player`:
|
||||
* Add a top level playlist API based on a new `MediaItem` class
|
||||
([#6161](https://github.com/google/ExoPlayer/issues/6161)). The
|
||||
new methods for playlist manipulation are `setMediaItem(s)`,
|
||||
`addMediaItem(s)`, `moveMediaItem(s)`, `removeMediaItem(s)` and
|
||||
`clearMediaItems`. The playlist can be queried using
|
||||
`getMediaItemCount` and `getMediaItemAt`. This API should be used
|
||||
instead of `ConcatenatingMediaSource` in most cases.
|
||||
* Add `getCurrentMediaItem` for getting the currently playing item
|
||||
in the playlist.
|
||||
* Add `EventListener.onMediaItemTransition` to report when
|
||||
playback transitions from one item to another in the playlist.
|
||||
* Add `play` and `pause` convenience methods. They are equivalent to
|
||||
`setPlayWhenReady(true)` and `setPlayWhenReady(false)` respectively.
|
||||
* Add `getCurrentLiveOffset` for getting the offset of the current
|
||||
playback position from the live edge of a live stream.
|
||||
* Add `getTrackSelector` for getting the `TrackSelector` used by the
|
||||
player.
|
||||
* Add `AudioComponent.setAudioSessionId` to set the audio session ID.
|
||||
This method is also available on `SimpleExoPlayer`.
|
||||
* Add `TextComponent.getCurrentCues` to get the current cues. This
|
||||
method is also available on `SimpleExoPlayer`. The current cues are
|
||||
no longer automatically forwarded to a `TextOutput` when it's added
|
||||
to a `SimpleExoPlayer`.
|
||||
* Add `Player.DeviceComponent` to query and control the device volume.
|
||||
`SimpleExoPlayer` implements this interface.
|
||||
* Deprecate and rename `getPlaybackError` to `getPlayerError` for
|
||||
consistency.
|
||||
* Deprecate and rename `onLoadingChanged` to `onIsLoadingChanged` for
|
||||
consistency.
|
||||
* Deprecate `EventListener.onPlayerStateChanged`, replacing it with
|
||||
`EventListener.onPlayWhenReadyChanged` and
|
||||
`EventListener.onPlaybackStateChanged`.
|
||||
* Deprecate `EventListener.onSeekProcessed` because seek changes now
|
||||
happen instantly and listening to `onPositionDiscontinuity` is
|
||||
sufficient.
|
||||
* `ExoPlayer`:
|
||||
* Add `setMediaSource(s)` and `addMediaSource(s)` to `ExoPlayer`, for
|
||||
adding `MediaSource` instances directly to the playlist.
|
||||
* Add `ExoPlayer.setPauseAtEndOfMediaItems` to let the player pause at
|
||||
the end of each media item
|
||||
([#5660](https://github.com/google/ExoPlayer/issues/5660)).
|
||||
* Allow passing `C.TIME_END_OF_SOURCE` to `PlayerMessage.setPosition`
|
||||
to send a `PlayerMessage` at the end of a stream.
|
||||
* `SimpleExoPlayer`:
|
||||
* `SimpleExoPlayer` implements the new `MediaItem` based playlist API,
|
||||
using a `MediaSourceFactory` to convert `MediaItem` instances to
|
||||
playable `MediaSource` instances. A `DefaultMediaSourceFactory` is
|
||||
used by default. `Builder.setMediaSourceFactory` allows setting a
|
||||
custom factory.
|
||||
* Add additional options to `Builder` that were previously only
|
||||
accessible via setters.
|
||||
* Add opt-in to verify correct thread usage with
|
||||
`setThrowsWhenUsingWrongThread(true)`
|
||||
([#4463](https://github.com/google/ExoPlayer/issues/4463)).
|
||||
* `Format`:
|
||||
* Add a `Builder` and deprecate all `create` methods and most
|
||||
`Format.copyWith` methods.
|
||||
* Split `bitrate` into `averageBitrate` and `peakBitrate`
|
||||
([#2863](https://github.com/google/ExoPlayer/issues/2863)).
|
||||
* `LoadControl`:
|
||||
* Add a `playbackPositionUs` parameter to `shouldContinueLoading`.
|
||||
* Set the default minimum buffer duration in `DefaultLoadControl` to
|
||||
50 seconds (equal to the default maximum buffer), and treat audio
|
||||
and video the same.
|
||||
* Add a `MetadataRetriever` API for retrieving track information and
|
||||
static metadata for a media item
|
||||
([#3609](https://github.com/google/ExoPlayer/issues/3609)).
|
||||
* Attach an identifier and extra information to load error events passed
|
||||
to `LoadErrorHandlingPolicy`
|
||||
([#7309](https://github.com/google/ExoPlayer/issues/7309)).
|
||||
`LoadErrorHandlingPolicy` implementations should migrate to implementing
|
||||
the non-deprecated methods of the interface.
|
||||
* Add an option to `MergingMediaSource` to adjust the time offsets
|
||||
between the merged sources
|
||||
([#6103](https://github.com/google/ExoPlayer/issues/6103)).
|
||||
* `SimpleDecoderVideoRenderer` and `SimpleDecoderAudioRenderer` renamed to
|
||||
* Move `MediaSourceEventListener.LoadEventInfo` and
|
||||
`MediaSourceEventListener.MediaLoadData` to be top-level classes in
|
||||
`com.google.android.exoplayer2.source`.
|
||||
* Move `SimpleDecoderVideoRenderer` and `SimpleDecoderAudioRenderer` to
|
||||
`DecoderVideoRenderer` and `DecoderAudioRenderer` respectively, and
|
||||
generalized to work with `Decoder` rather than `SimpleDecoder`.
|
||||
* Add media item based playlist API to Player.
|
||||
* Remove deprecated members in `DefaultTrackSelector`.
|
||||
* Add `Player.DeviceComponent` and implement it for `SimpleExoPlayer` so
|
||||
that the device volume can be controlled by player.
|
||||
* Avoid throwing an exception while parsing fragmented MP4 default sample
|
||||
values where the most-significant bit is set
|
||||
([#7207](https://github.com/google/ExoPlayer/issues/7207)).
|
||||
* Add `SilenceMediaSource.Factory` to support tags
|
||||
([PR #7245](https://github.com/google/ExoPlayer/pull/7245)).
|
||||
* Fix `AdsMediaSource` child `MediaSource`s not being released.
|
||||
* Parse track titles from Matroska files
|
||||
([#7247](https://github.com/google/ExoPlayer/pull/7247)).
|
||||
* Replace `CacheDataSinkFactory` and `CacheDataSourceFactory` with
|
||||
`CacheDataSink.Factory` and `CacheDataSource.Factory` respectively.
|
||||
* Video: Pass frame rate hint to `Surface.setFrameRate` on Android R devices.
|
||||
generalize them to work with `Decoder` rather than `SimpleDecoder`.
|
||||
* Deprecate `C.MSG_*` constants, replacing them with constants in
|
||||
`Renderer`.
|
||||
* Split the `library-core` module into `library-core`,
|
||||
`library-common` and `library-extractor`. The `library-core` module
|
||||
has an API dependency on both of the new modules, so this change
|
||||
should be transparent to developers including ExoPlayer using Gradle
|
||||
dependencies.
|
||||
* Add a dependency on Guava.
|
||||
* Video:
|
||||
* Pass frame rate hint to `Surface.setFrameRate` on Android 11.
|
||||
* Fix incorrect aspect ratio when transitioning from one video to another
|
||||
with the same resolution, but a different pixel aspect ratio
|
||||
([#6646](https://github.com/google/ExoPlayer/issues/6646)).
|
||||
* Audio:
|
||||
* Add experimental support for power efficient playback using audio
|
||||
offload.
|
||||
* Add support for using framework audio speed adjustment instead of
|
||||
ExoPlayer's implementation
|
||||
([#7502](https://github.com/google/ExoPlayer/issues/7502)). This option
|
||||
can be set using
|
||||
`DefaultRenderersFactory.setEnableAudioTrackPlaybackParams`.
|
||||
* Add an event for the audio position starting to advance, to make it
|
||||
easier for apps to determine when audio playout started
|
||||
([#7577](https://github.com/google/ExoPlayer/issues/7577)).
|
||||
* Generalize support for floating point audio.
|
||||
* Add an option to `DefaultAudioSink` for enabling floating point
|
||||
output. This option can also be set using
|
||||
`DefaultRenderersFactory.setEnableAudioFloatOutput`.
|
||||
* Add floating point output capability to `MediaCodecAudioRenderer`
|
||||
and `LibopusAudioRenderer`, which is enabled automatically if the
|
||||
audio sink supports floating point output and if it makes sense for
|
||||
the content being played.
|
||||
* Enable the floating point output capability of `FfmpegAudioRenderer`
|
||||
automatically if the audio sink supports floating point output and
|
||||
if it makes sense for the content being played. The option to
|
||||
manually enable floating point output has been removed, since this
|
||||
now done with the generalized option on `DefaultAudioSink`.
|
||||
* In `MediaCodecAudioRenderer`, stop passing audio samples through
|
||||
`MediaCodec` when playing PCM audio or encoded audio using passthrough
|
||||
mode.
|
||||
* Reuse audio decoders when transitioning through playlists of gapless
|
||||
audio, rather than reinstantiating them.
|
||||
* Check `DefaultAudioSink` supports passthrough, in addition to checking
|
||||
the `AudioCapabilities`
|
||||
([#7404](https://github.com/google/ExoPlayer/issues/7404)).
|
||||
* Text:
|
||||
* Parse `<ruby>` and `<rt>` tags in WebVTT subtitles (rendering is coming
|
||||
later).
|
||||
* Parse `text-combine-upright` CSS property (i.e. tate-chu-yoko) in WebVTT
|
||||
subtitles (rendering is coming later).
|
||||
* Parse `tts:combineText` property (i.e. tate-chu-yoko) in TTML subtitles
|
||||
(rendering is coming later).
|
||||
* Fix `SubtitlePainter` to render `EDGE_TYPE_OUTLINE` using the correct
|
||||
color ([#6724](https://github.com/google/ExoPlayer/pull/6724)).
|
||||
* Add support for WebVTT default
|
||||
[text](https://www.w3.org/TR/webvtt1/#default-text-color) and
|
||||
[background](https://www.w3.org/TR/webvtt1/#default-text-background)
|
||||
colors ([PR #4178](https://github.com/google/ExoPlayer/pull/4178),
|
||||
[issue #6581](https://github.com/google/ExoPlayer/issues/6581)).
|
||||
* Parse `tts:ruby` and `tts:rubyPosition` properties in TTML subtitles
|
||||
(rendering is coming later).
|
||||
* Update WebVTT position alignment parsing to recognise `line-left`,
|
||||
`center` and `line-right` as per the
|
||||
[released spec](https://www.w3.org/TR/webvtt1/#webvtt-position-cue-setting)
|
||||
(a
|
||||
[previous draft](https://www.w3.org/TR/2014/WD-webvtt1-20141111/#dfn-webvtt-text-position-cue-setting)
|
||||
used `start`, `middle` and `end`).
|
||||
* Use anti-aliasing and bitmap filtering when displaying bitmap subtitles
|
||||
([#6950](https://github.com/google/ExoPlayer/pull/6950)).
|
||||
* Implement timing-out of stuck CEA-608 captions (as permitted by
|
||||
ANSI/CTA-608-E R-2014 Annex C.9) and set the default timeout to 16
|
||||
seconds ([#7181](https://github.com/google/ExoPlayer/issues/7181)).
|
||||
* Add special-case positioning behaviour for vertical cues being rendered
|
||||
horizontally.
|
||||
* Implement steps 4-10 of the
|
||||
[WebVTT line computation algorithm](https://www.w3.org/TR/webvtt1/#cue-computed-line).
|
||||
* DRM:
|
||||
* Add support for attaching DRM sessions to clear content in the demo app.
|
||||
* Remove `DrmSessionManager` references from all renderers.
|
||||
`DrmSessionManager` must be injected into the MediaSources using the
|
||||
MediaSources factories.
|
||||
* Add option to inject a custom `DefaultDrmSessionManager` into
|
||||
`OfflineLicenseHelper`
|
||||
([#7078](https://github.com/google/ExoPlayer/issues/7078)).
|
||||
* Remove generics from DRM components.
|
||||
* Add a WebView-based output option to `SubtitleView`. This can display
|
||||
some features not supported by the existing Canvas-based output such as
|
||||
vertical text and rubies. It can be enabled by calling
|
||||
`SubtitleView#setViewType(VIEW_TYPE_WEB)`.
|
||||
* Recreate the decoder when handling and swallowing decode errors in
|
||||
`TextRenderer`. This fixes a case where playback would never end when
|
||||
playing content with malformed subtitles
|
||||
([#7590](https://github.com/google/ExoPlayer/issues/7590)).
|
||||
* Only apply `CaptionManager` font scaling in
|
||||
`SubtitleView.setUserDefaultTextSize` if the `CaptionManager` is
|
||||
enabled.
|
||||
* Improve positioning of vertical cues when rendered horizontally.
|
||||
* Redefine `Cue.lineType=LINE_TYPE_NUMBER` in terms of aligning the cue
|
||||
text lines to grid of viewport lines. Only consider `Cue.lineAnchor`
|
||||
when `Cue.lineType=LINE_TYPE_FRACTION`.
|
||||
* WebVTT
|
||||
* Add support for default
|
||||
[text](https://www.w3.org/TR/webvtt1/#default-text-color) and
|
||||
[background](https://www.w3.org/TR/webvtt1/#default-text-background)
|
||||
colors ([#6581](https://github.com/google/ExoPlayer/issues/6581)).
|
||||
* Update position alignment parsing to recognise `line-left`, `center`
|
||||
and `line-right`.
|
||||
* Implement steps 4-10 of the
|
||||
[WebVTT line computation algorithm](https://www.w3.org/TR/webvtt1/#cue-computed-line).
|
||||
* Stop parsing unsupported CSS properties. The spec provides an
|
||||
[exhaustive list](https://www.w3.org/TR/webvtt1/#the-cue-pseudo-element)
|
||||
of which properties are supported.
|
||||
* Parse the `ruby-position` CSS property.
|
||||
* Parse the `text-combine-upright` CSS property (i.e., tate-chu-yoko).
|
||||
* Parse the `<ruby>` and `<rt>` tags.
|
||||
* TTML
|
||||
* Parse the `tts:combineText` property (i.e., tate-chu-yoko).
|
||||
* Parse t`tts:ruby` and `tts:rubyPosition` properties.
|
||||
* CEA-608
|
||||
* Implement timing-out of stuck captions, as permitted by
|
||||
ANSI/CTA-608-E R-2014 Annex C.9. The default timeout is set to 16
|
||||
seconds ([#7181](https://github.com/google/ExoPlayer/issues/7181)).
|
||||
* Trim lines that exceed the maximum length of 32 characters
|
||||
([#7341](https://github.com/google/ExoPlayer/issues/7341)).
|
||||
* Fix positioning of roll-up captions in the top half of the screen
|
||||
([#7475](https://github.com/google/ExoPlayer/issues/7475)).
|
||||
* Stop automatically generating a CEA-608 track when playing
|
||||
standalone MPEG-TS files. The previous behavior can still be
|
||||
obtained by manually injecting a customized
|
||||
`DefaultTsPayloadReaderFactory` into `TsExtractor`.
|
||||
* Metadata: Add minimal DVB Application Information Table (AIT) support.
|
||||
* DASH:
|
||||
* Add support for canceling in-progress segment fetches
|
||||
([#2848](https://github.com/google/ExoPlayer/issues/2848)).
|
||||
* Add support for CEA-708 embedded in FMP4.
|
||||
* SmoothStreaming:
|
||||
* Add support for canceling in-progress segment fetches
|
||||
([#2848](https://github.com/google/ExoPlayer/issues/2848)).
|
||||
* HLS:
|
||||
* Add support for discarding buffered media (e.g., to allow faster
|
||||
adaptation to a higher quality variant)
|
||||
([#6322](https://github.com/google/ExoPlayer/issues/6322)).
|
||||
* Add support for canceling in-progress segment fetches
|
||||
([#2848](https://github.com/google/ExoPlayer/issues/2848)).
|
||||
* Respect 33-bit PTS wrapping when applying `X-TIMESTAMP-MAP` to WebVTT
|
||||
timestamps ([#7464](https://github.com/google/ExoPlayer/issues/7464)).
|
||||
* Extractors:
|
||||
* Optimize the `Extractor` sniffing order to reduce start-up latency in
|
||||
`DefaultExtractorsFactory` and `DefaultHlsExtractorsFactory`
|
||||
([#6410](https://github.com/google/ExoPlayer/issues/6410)).
|
||||
* Use filename extensions and response header MIME types to further
|
||||
optimize `Extractor` sniffing order on a per-media basis.
|
||||
* MP3: Add `IndexSeeker` for accurate seeks in VBR MP3 streams
|
||||
([#6787](https://github.com/google/ExoPlayer/issues/6787)). This seeker
|
||||
can be enabled by passing `FLAG_ENABLE_INDEX_SEEKING` to the
|
||||
`Mp3Extractor`. A significant portion of the file may need to be scanned
|
||||
when a seek is performed, which may be costly for large files.
|
||||
* MP4: Fix playback of MP4 streams that contain Opus audio.
|
||||
* FMP4: Add support for partially fragmented MP4s
|
||||
([#7308](https://github.com/google/ExoPlayer/issues/7308)).
|
||||
* Matroska:
|
||||
* Support Dolby Vision
|
||||
([#7267](https://github.com/google/ExoPlayer/issues/7267)).
|
||||
* Populate `Format.label` with track titles.
|
||||
* Remove support for the `Invisible` block header flag.
|
||||
* MPEG-TS: Add support for MPEG-4 Part 2 and H.263
|
||||
([#1603](https://github.com/google/ExoPlayer/issues/1603),
|
||||
[#5107](https://github.com/google/ExoPlayer/issues/5107)).
|
||||
* Ogg: Fix handling of non-contiguous pages
|
||||
([#7230](https://github.com/google/ExoPlayer/issues/7230)).
|
||||
* UI:
|
||||
* Add `StyledPlayerView` and `StyledPlayerControlView`, which provide a
|
||||
more polished user experience than `PlayerView` and `PlayerControlView`
|
||||
at the cost of decreased customizability.
|
||||
* Remove the previously deprecated `SimpleExoPlayerView` and
|
||||
`PlaybackControlView` classes, along with the corresponding
|
||||
`exo_simple_player_view.xml` and `exo_playback_control_view.xml` layout
|
||||
resources. Use the equivalent `PlayerView`, `PlayerControlView`,
|
||||
`exo_player_view.xml` and `exo_player_control_view.xml` instead.
|
||||
* Add setter methods to `PlayerView` and `PlayerControlView` to set
|
||||
whether the rewind, fast forward, previous and next buttons are shown
|
||||
([#7410](https://github.com/google/ExoPlayer/issues/7410)).
|
||||
* Update `TrackSelectionDialogBuilder` to use the AndroidX app compat
|
||||
`AlertDialog` rather than the platform version, if available
|
||||
([#7357](https://github.com/google/ExoPlayer/issues/7357)).
|
||||
* Make UI components dispatch previous, next, fast forward and rewind
|
||||
actions via their `ControlDispatcher`
|
||||
([#6926](https://github.com/google/ExoPlayer/issues/6926)).
|
||||
* Downloads and caching:
|
||||
* Merge downloads in `SegmentDownloader` to improve overall download speed
|
||||
* Add `DownloadRequest.Builder`.
|
||||
* Add `DownloadRequest.keySetId` to make it easier to store an offline
|
||||
license keyset identifier alongside the other information that's
|
||||
persisted in `DownloadIndex`.
|
||||
* Support passing an `Executor` to `DefaultDownloaderFactory` on which
|
||||
data downloads are performed.
|
||||
* Parallelize and merge downloads in `SegmentDownloader` to improve
|
||||
download speeds
|
||||
([#5978](https://github.com/google/ExoPlayer/issues/5978)).
|
||||
* Replace `CacheDataSinkFactory` and `CacheDataSourceFactory` with
|
||||
`CacheDataSink.Factory` and `CacheDataSource.Factory` respectively.
|
||||
* Remove `DownloadConstructorHelper` and use `CacheDataSource.Factory`
|
||||
directly instead.
|
||||
* Update `CachedContentIndex` to use `SecureRandom` for generating the
|
||||
initialization vector used to encrypt the cache contents.
|
||||
* Audio:
|
||||
* Add a sample count parameter to `MediaCodecRenderer.processOutputBuffer`
|
||||
and `AudioSink.handleBuffer` to allow batching multiple encoded frames
|
||||
in one buffer.
|
||||
* No longer use a `MediaCodec` in audio passthrough mode.
|
||||
* Remove `DownloadConstructorHelper` and instead use
|
||||
`CacheDataSource.Factory` directly.
|
||||
* Add `Requirements.DEVICE_STORAGE_NOT_LOW`, which can be specified as a
|
||||
requirement to a `DownloadManager` for it to proceed with downloading.
|
||||
* For failed downloads, propagate the `Exception` that caused the failure
|
||||
to `DownloadManager.Listener.onDownloadChanged`.
|
||||
* Support multiple non-overlapping write locks for the same key in
|
||||
`SimpleCache`.
|
||||
* Remove `CacheUtil`. Equivalent functionality is provided by a new
|
||||
`CacheWriter` class, `Cache.getCachedBytes`, `Cache.removeResource` and
|
||||
`CacheKeyFactory.DEFAULT`.
|
||||
* DRM:
|
||||
* Remove previously deprecated APIs to inject `DrmSessionManager` into
|
||||
`Renderer` instances. `DrmSessionManager` must now be injected into
|
||||
`MediaSource` instances via the `MediaSource` factories.
|
||||
* Add the ability to inject a custom `DefaultDrmSessionManager` into
|
||||
`OfflineLicenseHelper`
|
||||
([#7078](https://github.com/google/ExoPlayer/issues/7078)).
|
||||
* Keep DRM sessions alive for a short time before fully releasing them
|
||||
([#7011](https://github.com/google/ExoPlayer/issues/7011),
|
||||
[#6725](https://github.com/google/ExoPlayer/issues/6725),
|
||||
[#7066](https://github.com/google/ExoPlayer/issues/7066)).
|
||||
* Remove support for `cbc1` and `cens` encrytion schemes. Support for
|
||||
these schemes was removed from the Android platform from API level 30,
|
||||
and the range of API levels for which they are supported is too small to
|
||||
be useful.
|
||||
* Remove generic types from DRM components.
|
||||
* Track selection:
|
||||
* Add `TrackSelection.shouldCancelMediaChunkLoad` to check whether an
|
||||
ongoing load should be canceled
|
||||
([#2848](https://github.com/google/ExoPlayer/issues/2848)).
|
||||
* Add `DefaultTrackSelector` constraints for minimum video resolution,
|
||||
bitrate and frame rate
|
||||
([#4511](https://github.com/google/ExoPlayer/issues/4511)).
|
||||
* Remove previously deprecated `DefaultTrackSelector` members.
|
||||
* Data sources:
|
||||
* Add `HttpDataSource.InvalidResponseCodeException#responseBody` field
|
||||
([#6853](https://github.com/google/ExoPlayer/issues/6853)).
|
||||
* Add `DataSpec.Builder` and deprecate most `DataSpec` constructors.
|
||||
* Add `DataSpec.customData` to allow applications to pass custom data
|
||||
through `DataSource` chains.
|
||||
* Deprecate `CacheDataSinkFactory` and `CacheDataSourceFactory`, which are
|
||||
replaced by `CacheDataSink.Factory` and `CacheDataSource.Factory`
|
||||
respectively.
|
||||
* Analytics:
|
||||
* Extend `EventTime` with more details about the current player state
|
||||
([#7332](https://github.com/google/ExoPlayer/issues/7332)).
|
||||
* Add `AnalyticsListener.onVideoFrameProcessingOffset` to report how
|
||||
early or late video frames are processed relative to them needing to be
|
||||
presented. Video frame processing offset fields are also added to
|
||||
`DecoderCounters`.
|
||||
* Fix incorrect `MediaPeriodId` for some renderer errors reported by
|
||||
`AnalyticsListener.onPlayerError`.
|
||||
* Remove `onMediaPeriodCreated`, `onMediaPeriodReleased` and
|
||||
`onReadingStarted` from `AnalyticsListener`.
|
||||
* Test utils: Add `TestExoPlayer`, a utility class with APIs to create
|
||||
`SimpleExoPlayer` instances with fake components for testing.
|
||||
* Media2 extension: This is a new extension that makes it easy to use
|
||||
ExoPlayer together with AndroidX Media2.
|
||||
* Cast extension: Implement playlist API and deprecate the old queue
|
||||
manipulation API.
|
||||
* IMA extension:
|
||||
* Migrate to new 'friendly obstruction' IMA SDK APIs, and allow apps to
|
||||
register a purpose and detail reason for overlay views via
|
||||
`AdsLoader.AdViewProvider`.
|
||||
* Add support for audio-only ads display containers by returning `null`
|
||||
from `AdsLoader.AdViewProvider.getAdViewGroup`, and allow skipping
|
||||
audio-only ads via `ImaAdsLoader.skipAd`
|
||||
([#7689](https://github.com/google/ExoPlayer/issues/7689)).
|
||||
* Add `ImaAdsLoader.Builder.setCompanionAdSlots` so it's possible to set
|
||||
companion ad slots without accessing the `AdDisplayContainer`.
|
||||
* Add missing notification of `VideoAdPlayerCallback.onLoaded`.
|
||||
* Fix handling of incompatible VPAID ads
|
||||
([#7832](https://github.com/google/ExoPlayer/issues/7832)).
|
||||
* Fix handling of empty ads at non-integer cue points
|
||||
([#7889](https://github.com/google/ExoPlayer/issues/7889)).
|
||||
* Demo app:
|
||||
* Replace the `extensions` variant with `decoderExtensions` and update the
|
||||
demo app use the Cronet and IMA extensions by default.
|
||||
* Expand the `exolist.json` schema, as well the structure of intents that
|
||||
can be used to launch `PlayerActivity`. See the
|
||||
[Demo application page](https://exoplayer.dev/demo-application.html#playing-your-own-content)
|
||||
for the latest versions. Changes include:
|
||||
* Add `drm_session_for_clear_content` to allow attaching DRM sessions
|
||||
to clear audio and video tracks.
|
||||
* Add `clip_start_position_ms` and `clip_end_position_ms` to allow
|
||||
clipped samples.
|
||||
* Use `StyledPlayerControlView` rather than `PlayerView`.
|
||||
* Remove support for media tunneling, random ABR and playback of
|
||||
spherical video. Developers wishing to experiment with these features
|
||||
can enable them by modifying the demo app source code.
|
||||
* Add support for downloading DRM-protected content using offline
|
||||
Widevine licenses.
|
||||
|
||||
### 2.11.8 (2020-08-25) ###
|
||||
|
||||
* Fix distorted playback of floating point audio when samples exceed the
|
||||
`[-1, 1]` nominal range.
|
||||
* MP4:
|
||||
* Add support for `piff` and `isml` brands
|
||||
([#7584](https://github.com/google/ExoPlayer/issues/7584)).
|
||||
* Fix playback of very short MP4 files.
|
||||
* FMP4:
|
||||
* Fix `saiz` and `senc` sample count checks, resolving a "length
|
||||
mismatch" `ParserException` when playing certain protected FMP4 streams
|
||||
([#7592](https://github.com/google/ExoPlayer/issues/7592)).
|
||||
* Fix handling of `traf` boxes containing multiple `sbgp` or `sgpd`
|
||||
boxes.
|
||||
* FLV: Ignore `SCRIPTDATA` segments with invalid name types, rather than
|
||||
failing playback ([#7675](https://github.com/google/ExoPlayer/issues/7675)).
|
||||
* Better infer the content type of `.ism` and `.isml` streaming URLs.
|
||||
* Workaround an issue on Broadcom based devices where playbacks would not
|
||||
transition to `STATE_ENDED` when using video tunneling mode
|
||||
([#7647](https://github.com/google/ExoPlayer/issues/7647)).
|
||||
* IMA extension: Upgrade to IMA SDK 3.19.4, bringing in a fix for setting the
|
||||
media load timeout
|
||||
([#7170](https://github.com/google/ExoPlayer/issues/7170)).
|
||||
* Demo app: Fix playback of ClearKey protected content on API level 26 and
|
||||
earlier ([#7735](https://github.com/google/ExoPlayer/issues/7735)).
|
||||
|
||||
### 2.11.7 (2020-06-29) ###
|
||||
|
||||
* IMA extension: Fix the way postroll "content complete" notifications are
|
||||
handled to avoid repeatedly refreshing the timeline after playback ends.
|
||||
|
||||
### 2.11.6 (2020-06-19) ###
|
||||
|
||||
* UI: Prevent `PlayerView` from temporarily hiding the video surface when
|
||||
seeking to an unprepared period within the current window. For example when
|
||||
seeking over an ad group, or to the next period in a multi-period DASH
|
||||
stream ([#5507](https://github.com/google/ExoPlayer/issues/5507)).
|
||||
* IMA extension:
|
||||
* Add option to skip ads before the start position.
|
||||
* Catch unexpected errors in `stopAd` to avoid a crash
|
||||
([#7492](https://github.com/google/ExoPlayer/issues/7492)).
|
||||
* Fix a bug that caused playback to be stuck buffering on resuming from
|
||||
the background after all ads had played to the end
|
||||
([#7508](https://github.com/google/ExoPlayer/issues/7508)).
|
||||
* Fix a bug where the number of ads in an ad group couldn't change
|
||||
([#7477](https://github.com/google/ExoPlayer/issues/7477)).
|
||||
* Work around unexpected `pauseAd`/`stopAd` for ads that have preloaded
|
||||
on seeking to another position
|
||||
([#7492](https://github.com/google/ExoPlayer/issues/7492)).
|
||||
* Fix incorrect rounding of ad cue points.
|
||||
* Fix handling of postrolls preloading
|
||||
([#7518](https://github.com/google/ExoPlayer/issues/7518)).
|
||||
|
||||
### 2.11.5 (2020-06-05) ###
|
||||
|
||||
* Improve the smoothness of video playback immediately after starting, seeking
|
||||
or resuming a playback
|
||||
([#6901](https://github.com/google/ExoPlayer/issues/6901)).
|
||||
* Add `SilenceMediaSource.Factory` to support tags.
|
||||
* Enable the configuration of `SilenceSkippingAudioProcessor`
|
||||
([#6705](https://github.com/google/ExoPlayer/issues/6705)).
|
||||
* Fix bug where `PlayerMessages` throw an exception after `MediaSources`
|
||||
are removed from the playlist
|
||||
([#7278](https://github.com/google/ExoPlayer/issues/7278)).
|
||||
* Fix "Not allowed to start service" `IllegalStateException` in
|
||||
`DownloadService`
|
||||
([#7306](https://github.com/google/ExoPlayer/issues/7306)).
|
||||
* Fix issue in `AudioTrackPositionTracker` that could cause negative positions
|
||||
to be reported at the start of playback and immediately after seeking
|
||||
([#7456](https://github.com/google/ExoPlayer/issues/7456)).
|
||||
* Fix further cases where downloads would sometimes not resume after their
|
||||
network requirements are met
|
||||
([#7453](https://github.com/google/ExoPlayer/issues/7453)).
|
||||
* DASH:
|
||||
* Merge trick play adaptation sets (i.e., adaptation sets marked with
|
||||
`http://dashif.org/guidelines/trickmode`) into the same `TrackGroup` as
|
||||
|
|
@ -145,45 +431,57 @@
|
|||
marked with the `C.ROLE_FLAG_TRICK_PLAY` flag.
|
||||
* Fix assertion failure in `SampleQueue` when playing DASH streams with
|
||||
EMSG tracks ([#7273](https://github.com/google/ExoPlayer/issues/7273)).
|
||||
* MP3: Add `IndexSeeker` for accurate seeks in VBR streams
|
||||
([#6787](https://github.com/google/ExoPlayer/issues/6787)). This seeker is
|
||||
enabled by passing `FLAG_ENABLE_INDEX_SEEKING` to the `Mp3Extractor`. It may
|
||||
require to scan a significant portion of the file for seeking, which may be
|
||||
costly on large files.
|
||||
* MP4: Store the Android capture frame rate only in `Format.metadata`.
|
||||
`Format.frameRate` now stores the calculated frame rate.
|
||||
* FMP4: Avoid throwing an exception while parsing default sample values whose
|
||||
most significant bits are set
|
||||
([#7207](https://github.com/google/ExoPlayer/issues/7207)).
|
||||
* MP3: Fix issue parsing the XING headers belonging to files larger than 2GB
|
||||
([#7337](https://github.com/google/ExoPlayer/issues/7337)).
|
||||
* MPEG-TS: Fix issue where SEI NAL units were incorrectly dropped from H.265
|
||||
samples ([#7113](https://github.com/google/ExoPlayer/issues/7113)).
|
||||
* Testing
|
||||
* Add `TestExoPlayer`, a utility class with APIs to create
|
||||
`SimpleExoPlayer` instances with fake components for testing.
|
||||
* Upgrade Truth dependency from 0.44 to 1.0.
|
||||
* Upgrade to JUnit 4.13-rc-2.
|
||||
* UI
|
||||
* Remove deperecated `exo_simple_player_view.xml` and
|
||||
`exo_playback_control_view.xml` from resource.
|
||||
* Add `showScrubber` and `hideScrubber` methods to DefaultTimeBar.
|
||||
* Move logic of prev, next, fast forward and rewind to ControlDispatcher
|
||||
([#6926](https://github.com/google/ExoPlayer/issues/6926)).
|
||||
* Metadata: Add minimal DVB Application Information Table (AIT) support
|
||||
([#6922](https://github.com/google/ExoPlayer/pull/6922)).
|
||||
* Cast extension: Implement playlist API and deprecate the old queue
|
||||
manipulation API.
|
||||
* Demo app: Retain previous position in list of samples.
|
||||
* Change the order of extractors for sniffing to reduce start-up latency in
|
||||
`DefaultExtractorsFactory` and `DefaultHlsExtractorsFactory`
|
||||
([#6410](https://github.com/google/ExoPlayer/issues/6410)).
|
||||
* Add missing `@Nullable` annotations to `MediaSessionConnector`
|
||||
([#7234](https://github.com/google/ExoPlayer/issues/7234)).
|
||||
* AV1 extension: Add a heuristic to determine the default number of threads
|
||||
used for AV1 playback using the extension.
|
||||
* IMA extension: Upgrade to IMA SDK version 3.18.1, and migrate to new
|
||||
preloading APIs ([#6429](https://github.com/google/ExoPlayer/issues/6429)).
|
||||
* OkHttp extension: Upgrade OkHttp dependency to 3.12.11.
|
||||
* UI:
|
||||
* Fix `DefaultTimeBar` to respect touch transformations
|
||||
([#7303](https://github.com/google/ExoPlayer/issues/7303)).
|
||||
* Add `showScrubber` and `hideScrubber` methods to `DefaultTimeBar`.
|
||||
* Text:
|
||||
* Use anti-aliasing and bitmap filtering when displaying bitmap
|
||||
subtitles.
|
||||
* Fix `SubtitlePainter` to render `EDGE_TYPE_OUTLINE` using the correct
|
||||
color.
|
||||
* IMA extension:
|
||||
* Upgrade to IMA SDK version 3.19.0, and migrate to new
|
||||
preloading APIs
|
||||
([#6429](https://github.com/google/ExoPlayer/issues/6429)). This fixes
|
||||
several issues involving preloading and handling of ad loading error
|
||||
cases: ([#4140](https://github.com/google/ExoPlayer/issues/4140),
|
||||
[#5006](https://github.com/google/ExoPlayer/issues/5006),
|
||||
[#6030](https://github.com/google/ExoPlayer/issues/6030),
|
||||
[#6097](https://github.com/google/ExoPlayer/issues/6097),
|
||||
[#6425](https://github.com/google/ExoPlayer/issues/6425),
|
||||
[#6967](https://github.com/google/ExoPlayer/issues/6967),
|
||||
[#7041](https://github.com/google/ExoPlayer/issues/7041),
|
||||
[#7161](https://github.com/google/ExoPlayer/issues/7161),
|
||||
[#7212](https://github.com/google/ExoPlayer/issues/7212),
|
||||
[#7340](https://github.com/google/ExoPlayer/issues/7340)).
|
||||
* Add support for timing out ad preloading, to avoid playback getting
|
||||
stuck if an ad group unexpectedly fails to load
|
||||
([#5444](https://github.com/google/ExoPlayer/issues/5444),
|
||||
[#5966](https://github.com/google/ExoPlayer/issues/5966),
|
||||
[#7002](https://github.com/google/ExoPlayer/issues/7002)).
|
||||
* Fix `AdsMediaSource` child `MediaSource`s not being released.
|
||||
* Cronet extension: Default to using the Cronet implementation in Google Play
|
||||
Services rather than Cronet Embedded. This allows Cronet to be used with a
|
||||
negligible increase in application size, compared to approximately 8MB when
|
||||
embedding the library.
|
||||
* OkHttp extension: Upgrade OkHttp dependency to 3.12.11.
|
||||
* MediaSession extension:
|
||||
* Only set the playback state to `BUFFERING` if `playWhenReady` is true
|
||||
([#7206](https://github.com/google/ExoPlayer/issues/7206)).
|
||||
* Add missing `@Nullable` annotations to `MediaSessionConnector`
|
||||
([#7234](https://github.com/google/ExoPlayer/issues/7234)).
|
||||
* AV1 extension: Add a heuristic to determine the default number of threads
|
||||
used for AV1 playback using the extension.
|
||||
|
||||
### 2.11.4 (2020-04-08)
|
||||
|
||||
|
|
@ -206,11 +504,12 @@
|
|||
to the `DefaultAudioSink` constructor
|
||||
([#7134](https://github.com/google/ExoPlayer/issues/7134)).
|
||||
* Workaround issue that could cause slower than realtime playback of AAC
|
||||
on Android 10 ([#6671](https://github.com/google/ExoPlayer/issues/6671).
|
||||
on Android 10
|
||||
([#6671](https://github.com/google/ExoPlayer/issues/6671)).
|
||||
* Fix case where another app spuriously holding transient audio focus
|
||||
could prevent ExoPlayer from acquiring audio focus for an indefinite
|
||||
period of time
|
||||
([#7182](https://github.com/google/ExoPlayer/issues/7182).
|
||||
([#7182](https://github.com/google/ExoPlayer/issues/7182)).
|
||||
* Fix case where the player volume could be permanently ducked if audio
|
||||
focus was released whilst ducking.
|
||||
* Fix playback of WAV files with trailing non-media bytes
|
||||
|
|
@ -1159,7 +1458,7 @@
|
|||
([#4492](https://github.com/google/ExoPlayer/issues/4492) and
|
||||
[#4634](https://github.com/google/ExoPlayer/issues/4634)).
|
||||
* Fix issue where removing looping media from a playlist throws an exception
|
||||
([#4871](https://github.com/google/ExoPlayer/issues/4871).
|
||||
([#4871](https://github.com/google/ExoPlayer/issues/4871)).
|
||||
* Fix issue where the preferred audio or text track would not be selected if
|
||||
mapped onto a secondary renderer of the corresponding type
|
||||
([#4711](http://github.com/google/ExoPlayer/issues/4711)).
|
||||
|
|
@ -1590,7 +1889,7 @@
|
|||
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).
|
||||
([#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.
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ buildscript {
|
|||
jcenter()
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:3.6.3'
|
||||
classpath 'com.android.tools.build:gradle:4.0.0'
|
||||
classpath 'com.novoda:bintray-release:0.9.1'
|
||||
classpath 'com.google.android.gms:strict-version-matcher-plugin:1.2.1'
|
||||
}
|
||||
|
|
@ -26,6 +26,7 @@ allprojects {
|
|||
repositories {
|
||||
google()
|
||||
jcenter()
|
||||
maven { url "https://oss.sonatype.org/content/repositories/snapshots" }
|
||||
}
|
||||
project.ext {
|
||||
exoplayerPublishEnabled = false
|
||||
|
|
|
|||
34
common_library_config.gradle
Normal file
34
common_library_config.gradle
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
// Copyright (C) 2020 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: "$gradle.ext.exoplayerSettingsDir/constants.gradle"
|
||||
apply plugin: 'com.android.library'
|
||||
|
||||
android {
|
||||
compileSdkVersion project.ext.compileSdkVersion
|
||||
|
||||
defaultConfig {
|
||||
minSdkVersion project.ext.minSdkVersion
|
||||
targetSdkVersion project.ext.targetSdkVersion
|
||||
consumerProguardFiles 'proguard-rules.txt'
|
||||
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
|
||||
testOptions.unitTests.includeAndroidResources = true
|
||||
}
|
||||
|
|
@ -13,24 +13,28 @@
|
|||
// limitations under the License.
|
||||
project.ext {
|
||||
// ExoPlayer version and version code.
|
||||
releaseVersion = '2.11.4'
|
||||
releaseVersionCode = 2011004
|
||||
releaseVersion = '2.12.0'
|
||||
releaseVersionCode = 2012000
|
||||
minSdkVersion = 16
|
||||
appTargetSdkVersion = 29
|
||||
targetSdkVersion = 28 // TODO: Bump once b/143232359 is resolved. Also fix TODOs in UtilTest.
|
||||
compileSdkVersion = 29
|
||||
dexmakerVersion = '2.21.0'
|
||||
junitVersion = '4.13-rc-2'
|
||||
guavaVersion = '28.2-android'
|
||||
mockitoVersion = '2.25.0'
|
||||
robolectricVersion = '4.3.1'
|
||||
checkerframeworkVersion = '2.5.0'
|
||||
guavaVersion = '27.1-android'
|
||||
mockitoVersion = '2.28.2'
|
||||
mockWebServerVersion = '3.12.0'
|
||||
robolectricVersion = '4.4'
|
||||
checkerframeworkVersion = '3.3.0'
|
||||
checkerframeworkCompatVersion = '2.5.0'
|
||||
jsr305Version = '3.0.2'
|
||||
kotlinAnnotationsVersion = '1.3.70'
|
||||
androidxAnnotationVersion = '1.1.0'
|
||||
androidxAppCompatVersion = '1.1.0'
|
||||
androidxCollectionVersion = '1.1.0'
|
||||
androidxMediaVersion = '1.0.1'
|
||||
androidxMultidexVersion = '2.0.0'
|
||||
androidxRecyclerViewVersion = '1.1.0'
|
||||
androidxTestCoreVersion = '1.2.0'
|
||||
androidxTestJUnitVersion = '1.1.1'
|
||||
androidxTestRunnerVersion = '1.2.0'
|
||||
|
|
|
|||
|
|
@ -12,6 +12,10 @@
|
|||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
def rootDir = gradle.ext.exoplayerRoot
|
||||
if (!gradle.ext.has('exoplayerSettingsDir')) {
|
||||
gradle.ext.exoplayerSettingsDir =
|
||||
new File(rootDir.toString()).getCanonicalPath()
|
||||
}
|
||||
def modulePrefix = ':'
|
||||
if (gradle.ext.has('exoplayerModulePrefix')) {
|
||||
modulePrefix += gradle.ext.exoplayerModulePrefix
|
||||
|
|
@ -35,6 +39,7 @@ include modulePrefix + 'extension-ima'
|
|||
include modulePrefix + 'extension-cast'
|
||||
include modulePrefix + 'extension-cronet'
|
||||
include modulePrefix + 'extension-mediasession'
|
||||
include modulePrefix + 'extension-media2'
|
||||
include modulePrefix + 'extension-okhttp'
|
||||
include modulePrefix + 'extension-opus'
|
||||
include modulePrefix + 'extension-vp9'
|
||||
|
|
@ -61,6 +66,7 @@ project(modulePrefix + 'extension-ima').projectDir = new File(rootDir, 'extensio
|
|||
project(modulePrefix + 'extension-cast').projectDir = new File(rootDir, 'extensions/cast')
|
||||
project(modulePrefix + 'extension-cronet').projectDir = new File(rootDir, 'extensions/cronet')
|
||||
project(modulePrefix + 'extension-mediasession').projectDir = new File(rootDir, 'extensions/mediasession')
|
||||
project(modulePrefix + 'extension-media2').projectDir = new File(rootDir, 'extensions/media2')
|
||||
project(modulePrefix + 'extension-okhttp').projectDir = new File(rootDir, 'extensions/okhttp')
|
||||
project(modulePrefix + 'extension-opus').projectDir = new File(rootDir, 'extensions/opus')
|
||||
project(modulePrefix + 'extension-vp9').projectDir = new File(rootDir, 'extensions/vp9')
|
||||
|
|
|
|||
|
|
@ -2,3 +2,24 @@
|
|||
|
||||
This directory contains applications that demonstrate how to use ExoPlayer.
|
||||
Browse the individual demos and their READMEs to learn more.
|
||||
|
||||
## Running a demo ##
|
||||
|
||||
### From Android Studio ###
|
||||
|
||||
* File -> New -> Import Project -> Specify the root ExoPlayer folder.
|
||||
* Choose the demo from the run configuration dropdown list.
|
||||
* Click Run.
|
||||
|
||||
### Using gradle from the command line: ###
|
||||
|
||||
* Open a Terminal window at the root ExoPlayer folder.
|
||||
* Run `./gradlew projects` to show all projects. Demo projects start with `demo`.
|
||||
* Run `./gradlew :<demo name>:tasks` to view the list of available tasks for
|
||||
the demo project. Choose an install option from the `Install tasks` section.
|
||||
* Run `./gradlew :<demo name>:<install task>`.
|
||||
|
||||
**Example**:
|
||||
|
||||
`./gradlew :demo:installNoExtensionsDebug` installs the main ExoPlayer demo app
|
||||
in debug mode with no extensions.
|
||||
|
|
|
|||
|
|
@ -2,3 +2,6 @@
|
|||
|
||||
This folder contains a demo application that showcases ExoPlayer integration
|
||||
with Google Cast.
|
||||
|
||||
Please see the [demos README](../README.md) for instructions on how to build and
|
||||
run this demo.
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ android {
|
|||
versionCode project.ext.releaseVersionCode
|
||||
minSdkVersion project.ext.minSdkVersion
|
||||
targetSdkVersion project.ext.appTargetSdkVersion
|
||||
multiDexEnabled true
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
|
|
@ -57,8 +58,9 @@ dependencies {
|
|||
implementation project(modulePrefix + 'library-ui')
|
||||
implementation project(modulePrefix + 'extension-cast')
|
||||
implementation 'androidx.appcompat:appcompat:' + androidxAppCompatVersion
|
||||
implementation 'androidx.multidex:multidex:' + androidxMultidexVersion
|
||||
implementation 'androidx.recyclerview:recyclerview:1.1.0'
|
||||
implementation 'com.google.android.material:material:1.1.0'
|
||||
implementation 'com.google.android.material:material:1.2.1'
|
||||
}
|
||||
|
||||
apply plugin: 'com.google.android.gms.strict-version-matcher-plugin'
|
||||
|
|
|
|||
|
|
@ -13,21 +13,11 @@
|
|||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.google.android.exoplayer2.util;
|
||||
package com.google.android.exoplayer2.castdemo;
|
||||
|
||||
/**
|
||||
* A functional interface representing a function taking one argument and returning a result.
|
||||
*
|
||||
* @param <T> The input type of the function.
|
||||
* @param <R> The output type of the function.
|
||||
*/
|
||||
public interface Function<T, R> {
|
||||
import androidx.multidex.MultiDexApplication;
|
||||
|
||||
/**
|
||||
* Applies this function to the given argument.
|
||||
*
|
||||
* @param t The function argument.
|
||||
* @return The function result, which may be {@code null}.
|
||||
*/
|
||||
R apply(T t);
|
||||
}
|
||||
// Note: Multidex is enabled in code not AndroidManifest.xml because the internal build system
|
||||
// doesn't dejetify MultiDexApplication in AndroidManifest.xml.
|
||||
/** Application for multidex support. */
|
||||
public final class DemoApplication extends MultiDexApplication {}
|
||||
|
|
@ -29,7 +29,6 @@ import com.google.android.exoplayer2.SimpleExoPlayer;
|
|||
import com.google.android.exoplayer2.Timeline;
|
||||
import com.google.android.exoplayer2.ext.cast.CastPlayer;
|
||||
import com.google.android.exoplayer2.ext.cast.SessionAvailabilityListener;
|
||||
import com.google.android.exoplayer2.source.DefaultMediaSourceFactory;
|
||||
import com.google.android.exoplayer2.source.TrackGroupArray;
|
||||
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
|
||||
import com.google.android.exoplayer2.trackselection.MappingTrackSelector;
|
||||
|
|
@ -61,7 +60,6 @@ import java.util.ArrayList;
|
|||
private static final DefaultHttpDataSourceFactory DATA_SOURCE_FACTORY =
|
||||
new DefaultHttpDataSourceFactory(USER_AGENT);
|
||||
|
||||
private final DefaultMediaSourceFactory defaultMediaSourceFactory;
|
||||
private final PlayerView localPlayerView;
|
||||
private final PlayerControlView castControlView;
|
||||
private final DefaultTrackSelector trackSelector;
|
||||
|
|
@ -97,7 +95,6 @@ import java.util.ArrayList;
|
|||
|
||||
trackSelector = new DefaultTrackSelector(context);
|
||||
exoPlayer = new SimpleExoPlayer.Builder(context).setTrackSelector(trackSelector).build();
|
||||
defaultMediaSourceFactory = DefaultMediaSourceFactory.newInstance(context, DATA_SOURCE_FACTORY);
|
||||
exoPlayer.addListener(this);
|
||||
localPlayerView.setPlayer(exoPlayer);
|
||||
|
||||
|
|
|
|||
|
|
@ -13,16 +13,7 @@
|
|||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
@NonNullApi
|
||||
package com.google.android.exoplayer2.castdemo;
|
||||
|
||||
package com.google.android.exoplayer2.util;
|
||||
|
||||
/**
|
||||
* A functional interface representing a supplier of results.
|
||||
*
|
||||
* @param <T> The type of results supplied by this supplier.
|
||||
*/
|
||||
public interface Supplier<T> {
|
||||
|
||||
/** Gets a result. */
|
||||
T get();
|
||||
}
|
||||
import com.google.android.exoplayer2.util.NonNullApi;
|
||||
|
|
@ -8,4 +8,7 @@ drawn using an Android canvas, and includes the current frame's presentation
|
|||
timestamp, to show how to get the timestamp of the frame currently in the
|
||||
off-screen surface texture.
|
||||
|
||||
Please see the [demos README](../README.md) for instructions on how to build and
|
||||
run this demo.
|
||||
|
||||
[GLSurfaceView]: https://developer.android.com/reference/android/opengl/GLSurfaceView
|
||||
|
|
|
|||
|
|
@ -49,5 +49,5 @@ dependencies {
|
|||
implementation project(modulePrefix + 'library-dash')
|
||||
implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion
|
||||
compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion
|
||||
compileOnly 'org.checkerframework:checker-compat-qual:' + checkerframeworkVersion
|
||||
compileOnly 'org.checkerframework:checker-compat-qual:' + checkerframeworkCompatVersion
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,4 +32,3 @@ void main() {
|
|||
gl_FragColor = videoColor * (1.0 - overlayColor.a)
|
||||
+ overlayColor * overlayColor.a;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -11,11 +11,10 @@
|
|||
// 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.
|
||||
attribute vec4 a_position;
|
||||
attribute vec3 a_texcoord;
|
||||
attribute vec2 a_position;
|
||||
attribute vec2 a_texcoord;
|
||||
varying vec2 v_texcoord;
|
||||
void main() {
|
||||
gl_Position = a_position;
|
||||
v_texcoord = a_texcoord.xy;
|
||||
gl_Position = vec4(a_position.x, a_position.y, 0, 1);
|
||||
v_texcoord = a_texcoord;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -88,18 +88,9 @@ import javax.microedition.khronos.opengles.GL10;
|
|||
GlUtil.Uniform[] uniforms = GlUtil.getUniforms(program);
|
||||
for (GlUtil.Attribute attribute : attributes) {
|
||||
if (attribute.name.equals("a_position")) {
|
||||
attribute.setBuffer(
|
||||
new float[] {
|
||||
-1.0f, -1.0f, 0.0f, 1.0f, 1.0f, -1.0f, 0.0f, 1.0f, -1.0f, 1.0f, 0.0f, 1.0f, 1.0f,
|
||||
1.0f, 0.0f, 1.0f,
|
||||
},
|
||||
4);
|
||||
attribute.setBuffer(new float[] {-1, -1, 1, -1, -1, 1, 1, 1}, 2);
|
||||
} else if (attribute.name.equals("a_texcoord")) {
|
||||
attribute.setBuffer(
|
||||
new float[] {
|
||||
0.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 1.0f,
|
||||
},
|
||||
3);
|
||||
attribute.setBuffer(new float[] {0, 1, 1, 1, 0, 0, 1, 0}, 2);
|
||||
}
|
||||
}
|
||||
this.attributes = attributes;
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ import android.widget.FrameLayout;
|
|||
import android.widget.Toast;
|
||||
import androidx.annotation.Nullable;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.MediaItem;
|
||||
import com.google.android.exoplayer2.Player;
|
||||
import com.google.android.exoplayer2.SimpleExoPlayer;
|
||||
import com.google.android.exoplayer2.drm.DefaultDrmSessionManager;
|
||||
|
|
@ -138,13 +139,12 @@ public final class MainActivity extends Activity {
|
|||
ACTION_VIEW.equals(action)
|
||||
? Assertions.checkNotNull(intent.getData())
|
||||
: Uri.parse(DEFAULT_MEDIA_URI);
|
||||
String userAgent = Util.getUserAgent(this, getString(R.string.application_name));
|
||||
DrmSessionManager drmSessionManager;
|
||||
if (Util.SDK_INT >= 18 && intent.hasExtra(DRM_SCHEME_EXTRA)) {
|
||||
String drmScheme = Assertions.checkNotNull(intent.getStringExtra(DRM_SCHEME_EXTRA));
|
||||
String drmLicenseUrl = Assertions.checkNotNull(intent.getStringExtra(DRM_LICENSE_URL_EXTRA));
|
||||
UUID drmSchemeUuid = Assertions.checkNotNull(Util.getDrmUuid(drmScheme));
|
||||
HttpDataSource.Factory licenseDataSourceFactory = new DefaultHttpDataSourceFactory(userAgent);
|
||||
HttpDataSource.Factory licenseDataSourceFactory = new DefaultHttpDataSourceFactory();
|
||||
HttpMediaDrmCallback drmCallback =
|
||||
new HttpMediaDrmCallback(drmLicenseUrl, licenseDataSourceFactory);
|
||||
drmSessionManager =
|
||||
|
|
@ -155,21 +155,19 @@ public final class MainActivity extends Activity {
|
|||
drmSessionManager = DrmSessionManager.getDummyDrmSessionManager();
|
||||
}
|
||||
|
||||
DataSource.Factory dataSourceFactory =
|
||||
new DefaultDataSourceFactory(
|
||||
this, Util.getUserAgent(this, getString(R.string.application_name)));
|
||||
DataSource.Factory dataSourceFactory = new DefaultDataSourceFactory(this);
|
||||
MediaSource mediaSource;
|
||||
@C.ContentType int type = Util.inferContentType(uri, intent.getStringExtra(EXTENSION_EXTRA));
|
||||
if (type == C.TYPE_DASH) {
|
||||
mediaSource =
|
||||
new DashMediaSource.Factory(dataSourceFactory)
|
||||
.setDrmSessionManager(drmSessionManager)
|
||||
.createMediaSource(uri);
|
||||
.createMediaSource(MediaItem.fromUri(uri));
|
||||
} else if (type == C.TYPE_OTHER) {
|
||||
mediaSource =
|
||||
new ProgressiveMediaSource.Factory(dataSourceFactory)
|
||||
.setDrmSessionManager(drmSessionManager)
|
||||
.createMediaSource(uri);
|
||||
.createMediaSource(MediaItem.fromUri(uri));
|
||||
} else {
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,19 @@
|
|||
/*
|
||||
* Copyright (C) 2020 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.
|
||||
*/
|
||||
@NonNullApi
|
||||
package com.google.android.exoplayer2.gldemo;
|
||||
|
||||
import com.google.android.exoplayer2.util.NonNullApi;
|
||||
|
|
@ -27,4 +27,3 @@
|
|||
app:surface_type="none"/>
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
|
|
|
|||
|
|
@ -3,3 +3,6 @@
|
|||
This is the main ExoPlayer demo application. It uses ExoPlayer to play a number
|
||||
of test streams. It can be used as a starting point or reference project when
|
||||
developing other applications that make use of the ExoPlayer library.
|
||||
|
||||
Please see the [demos README](../README.md) for instructions on how to build and
|
||||
run this demo.
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ android {
|
|||
versionCode project.ext.releaseVersionCode
|
||||
minSdkVersion project.ext.minSdkVersion
|
||||
targetSdkVersion project.ext.appTargetSdkVersion
|
||||
multiDexEnabled true
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
|
|
@ -49,34 +50,46 @@ android {
|
|||
disable 'GoogleAppIndexingWarning','MissingTranslation','IconDensities'
|
||||
}
|
||||
|
||||
flavorDimensions "extensions"
|
||||
flavorDimensions "decoderExtensions"
|
||||
|
||||
productFlavors {
|
||||
noExtensions {
|
||||
dimension "extensions"
|
||||
noDecoderExtensions {
|
||||
dimension "decoderExtensions"
|
||||
buildConfigField "boolean", "USE_DECODER_EXTENSIONS", "false"
|
||||
}
|
||||
withExtensions {
|
||||
dimension "extensions"
|
||||
withDecoderExtensions {
|
||||
dimension "decoderExtensions"
|
||||
buildConfigField "boolean", "USE_DECODER_EXTENSIONS", "true"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion
|
||||
implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion
|
||||
implementation 'androidx.appcompat:appcompat:' + androidxAppCompatVersion
|
||||
implementation 'com.google.android.material:material:1.1.0'
|
||||
implementation 'androidx.multidex:multidex:' + androidxMultidexVersion
|
||||
implementation 'com.google.android.material:material:1.2.1'
|
||||
implementation ('com.google.guava:guava:' + guavaVersion) {
|
||||
exclude group: 'com.google.code.findbugs', module: 'jsr305'
|
||||
exclude group: 'org.checkerframework', module: 'checker-compat-qual'
|
||||
exclude group: 'com.google.errorprone', module: 'error_prone_annotations'
|
||||
exclude group: 'com.google.j2objc', module: 'j2objc-annotations'
|
||||
exclude group: 'org.codehaus.mojo', module: 'animal-sniffer-annotations'
|
||||
}
|
||||
implementation project(modulePrefix + 'library-core')
|
||||
implementation project(modulePrefix + 'library-dash')
|
||||
implementation project(modulePrefix + 'library-hls')
|
||||
implementation project(modulePrefix + 'library-smoothstreaming')
|
||||
implementation project(modulePrefix + 'library-ui')
|
||||
withExtensionsImplementation project(path: modulePrefix + 'extension-av1')
|
||||
withExtensionsImplementation project(path: modulePrefix + 'extension-ffmpeg')
|
||||
withExtensionsImplementation project(path: modulePrefix + 'extension-flac')
|
||||
withExtensionsImplementation project(path: modulePrefix + 'extension-ima')
|
||||
withExtensionsImplementation project(path: modulePrefix + 'extension-opus')
|
||||
withExtensionsImplementation project(path: modulePrefix + 'extension-vp9')
|
||||
withExtensionsImplementation project(path: modulePrefix + 'extension-rtmp')
|
||||
implementation project(modulePrefix + 'extension-cronet')
|
||||
implementation project(modulePrefix + 'extension-ima')
|
||||
withDecoderExtensionsImplementation project(modulePrefix + 'extension-av1')
|
||||
withDecoderExtensionsImplementation project(modulePrefix + 'extension-ffmpeg')
|
||||
withDecoderExtensionsImplementation project(modulePrefix + 'extension-flac')
|
||||
withDecoderExtensionsImplementation project(modulePrefix + 'extension-opus')
|
||||
withDecoderExtensionsImplementation project(modulePrefix + 'extension-vp9')
|
||||
withDecoderExtensionsImplementation project(modulePrefix + 'extension-rtmp')
|
||||
}
|
||||
|
||||
apply plugin: 'com.google.android.gms.strict-version-matcher-plugin'
|
||||
|
|
|
|||
|
|
@ -1,7 +1,2 @@
|
|||
# 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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -35,8 +35,8 @@
|
|||
android:largeHeap="true"
|
||||
android:allowBackup="false"
|
||||
android:requestLegacyExternalStorage="true"
|
||||
android:name="com.google.android.exoplayer2.demo.DemoApplication"
|
||||
tools:ignore="UnusedAttribute">
|
||||
android:name="androidx.multidex.MultiDexApplication"
|
||||
tools:targetApi="29">
|
||||
|
||||
<activity android:name="com.google.android.exoplayer2.demo.SampleChooserActivity"
|
||||
android:configChanges="keyboardHidden"
|
||||
|
|
|
|||
|
|
@ -3,332 +3,228 @@
|
|||
"name": "YouTube DASH",
|
||||
"samples": [
|
||||
{
|
||||
"name": "Google Glass (MP4,H264)",
|
||||
"name": "Google Glass H264 (MP4)",
|
||||
"uri": "https://www.youtube.com/api/manifest/dash/id/bf5bb2419360daf1/source/youtube?as=fmp4_audio_clear,fmp4_sd_hd_clear&sparams=ip,ipbits,expire,source,id,as&ip=0.0.0.0&ipbits=0&expire=19000000000&signature=51AF5F39AB0CEC3E5497CD9C900EBFEAECCCB5C7.8506521BFC350652163895D4C26DEE124209AA9E&key=ik0",
|
||||
"extension": "mpd"
|
||||
},
|
||||
{
|
||||
"name": "Google Play (MP4,H264)",
|
||||
"name": "Google Play H264 (MP4)",
|
||||
"uri": "https://www.youtube.com/api/manifest/dash/id/3aa39fa2cc27967f/source/youtube?as=fmp4_audio_clear,fmp4_sd_hd_clear&sparams=ip,ipbits,expire,source,id,as&ip=0.0.0.0&ipbits=0&expire=19000000000&signature=A2716F75795F5D2AF0E88962FFCD10DB79384F29.84308FF04844498CE6FBCE4731507882B8307798&key=ik0",
|
||||
"extension": "mpd"
|
||||
},
|
||||
{
|
||||
"name": "Google Glass (WebM,VP9)",
|
||||
"name": "Google Glass VP9 (WebM)",
|
||||
"uri": "https://www.youtube.com/api/manifest/dash/id/bf5bb2419360daf1/source/youtube?as=fmp4_audio_clear,webm2_sd_hd_clear&sparams=ip,ipbits,expire,source,id,as&ip=0.0.0.0&ipbits=0&expire=19000000000&signature=249B04F79E984D7F86B4D8DB48AE6FAF41C17AB3.7B9F0EC0505E1566E59B8E488E9419F253DDF413&key=ik0",
|
||||
"extension": "mpd"
|
||||
},
|
||||
{
|
||||
"name": "Google Play (WebM,VP9)",
|
||||
"name": "Google Play VP9 (WebM)",
|
||||
"uri": "https://www.youtube.com/api/manifest/dash/id/3aa39fa2cc27967f/source/youtube?as=fmp4_audio_clear,webm2_sd_hd_clear&sparams=ip,ipbits,expire,source,id,as&ip=0.0.0.0&ipbits=0&expire=19000000000&signature=B1C2A74783AC1CC4865EB312D7DD2D48230CC9FD.BD153B9882175F1F94BFE5141A5482313EA38E8D&key=ik0",
|
||||
"extension": "mpd"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Widevine DASH Policy Tests (GTS)",
|
||||
"name": "Widevine GTS policy tests",
|
||||
"samples": [
|
||||
{
|
||||
"name": "WV: HDCP not specified",
|
||||
"name": "SW secure crypto (L3)",
|
||||
"uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd",
|
||||
"drm_scheme": "widevine",
|
||||
"drm_license_url": "https://proxy.uat.widevine.com/proxy?video_id=d286538032258a1c&provider=widevine_test"
|
||||
"drm_license_uri": "https://proxy.uat.widevine.com/proxy?video_id=GTS_SW_SECURE_CRYPTO&provider=widevine_test"
|
||||
},
|
||||
{
|
||||
"name": "WV: HDCP not required",
|
||||
"name": "SW secure decode",
|
||||
"uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd",
|
||||
"drm_scheme": "widevine",
|
||||
"drm_license_url": "https://proxy.uat.widevine.com/proxy?video_id=48fcc369939ac96c&provider=widevine_test"
|
||||
"drm_license_uri": "https://proxy.uat.widevine.com/proxy?video_id=GTS_SW_SECURE_DECODE&provider=widevine_test"
|
||||
},
|
||||
{
|
||||
"name": "WV: HDCP required",
|
||||
"name": "HW secure crypto",
|
||||
"uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd",
|
||||
"drm_scheme": "widevine",
|
||||
"drm_license_url": "https://proxy.uat.widevine.com/proxy?video_id=e06c39f1151da3df&provider=widevine_test"
|
||||
"drm_license_uri": "https://proxy.uat.widevine.com/proxy?video_id=GTS_HW_SECURE_CRYPTO&provider=widevine_test"
|
||||
},
|
||||
{
|
||||
"name": "WV: Secure video path required (MP4,H264)",
|
||||
"name": "HW secure decode",
|
||||
"uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd",
|
||||
"drm_scheme": "widevine",
|
||||
"drm_license_url": "https://proxy.uat.widevine.com/proxy?video_id=0894c7c8719b28a0&provider=widevine_test"
|
||||
"drm_license_uri": "https://proxy.uat.widevine.com/proxy?video_id=GTS_HW_SECURE_DECODE&provider=widevine_test"
|
||||
},
|
||||
{
|
||||
"name": "WV: Secure video path required (WebM,VP9)",
|
||||
"uri": "https://storage.googleapis.com/wvmedia/cenc/vp9/tears/tears.mpd",
|
||||
"drm_scheme": "widevine",
|
||||
"drm_license_url": "https://proxy.uat.widevine.com/proxy?video_id=0894c7c8719b28a0&provider=widevine_test"
|
||||
},
|
||||
{
|
||||
"name": "WV: Secure video path required (MP4,H265)",
|
||||
"uri": "https://storage.googleapis.com/wvmedia/cenc/hevc/tears/tears.mpd",
|
||||
"drm_scheme": "widevine",
|
||||
"drm_license_url": "https://proxy.uat.widevine.com/proxy?video_id=0894c7c8719b28a0&provider=widevine_test"
|
||||
},
|
||||
{
|
||||
"name": "WV: HDCP + secure video path required",
|
||||
"name": "HW secure all (L1)",
|
||||
"uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd",
|
||||
"drm_scheme": "widevine",
|
||||
"drm_license_url": "https://proxy.uat.widevine.com/proxy?video_id=efd045b1eb61888a&provider=widevine_test"
|
||||
"drm_license_uri": "https://proxy.uat.widevine.com/proxy?video_id=GTS_HW_SECURE_ALL&provider=widevine_test"
|
||||
},
|
||||
{
|
||||
"name": "WV: 30s license duration (fails at ~30s)",
|
||||
"name": "30s license (fails at ~30s)",
|
||||
"uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd",
|
||||
"drm_scheme": "widevine",
|
||||
"drm_license_url": "https://proxy.uat.widevine.com/proxy?video_id=f9a34cab7b05881a&provider=widevine_test"
|
||||
"drm_license_uri": "https://proxy.uat.widevine.com/proxy?video_id=GTS_CAN_RENEW_FALSE_LICENSE_30S_PLAYBACK_30S&provider=widevine_test"
|
||||
},
|
||||
{
|
||||
"name": "HDCP not required",
|
||||
"uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd",
|
||||
"drm_scheme": "widevine",
|
||||
"drm_license_uri": "https://proxy.uat.widevine.com/proxy?video_id=GTS_SW_SECURE_CRYPTO_HDCP_NONE&provider=widevine_test"
|
||||
},
|
||||
{
|
||||
"name": "HDCP 1.0 required",
|
||||
"uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd",
|
||||
"drm_scheme": "widevine",
|
||||
"drm_license_uri": "https://proxy.uat.widevine.com/proxy?video_id=GTS_SW_SECURE_CRYPTO_HDCP_V1&provider=widevine_test"
|
||||
},
|
||||
{
|
||||
"name": "HDCP 2.0 required",
|
||||
"uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd",
|
||||
"drm_scheme": "widevine",
|
||||
"drm_license_uri": "https://proxy.uat.widevine.com/proxy?video_id=GTS_SW_SECURE_CRYPTO_HDCP_V2&provider=widevine_test"
|
||||
},
|
||||
{
|
||||
"name": "HDCP 2.1 required",
|
||||
"uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd",
|
||||
"drm_scheme": "widevine",
|
||||
"drm_license_uri": "https://proxy.uat.widevine.com/proxy?video_id=GTS_SW_SECURE_CRYPTO_HDCP_V2_1&provider=widevine_test"
|
||||
},
|
||||
{
|
||||
"name": "HDCP 2.2 required",
|
||||
"uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd",
|
||||
"drm_scheme": "widevine",
|
||||
"drm_license_uri": "https://proxy.uat.widevine.com/proxy?video_id=GTS_SW_SECURE_CRYPTO_HDCP_V2_2&provider=widevine_test"
|
||||
},
|
||||
{
|
||||
"name": "HDCP no digital output",
|
||||
"uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd",
|
||||
"drm_scheme": "widevine",
|
||||
"drm_license_uri": "https://proxy.uat.widevine.com/proxy?video_id=GTS_SW_SECURE_CRYPTO_HDCP_NO_DIGITAL_OUTPUT&provider=widevine_test"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Widevine HDCP Capabilities Tests",
|
||||
"name": "Widevine DASH H264 (MP4)",
|
||||
"samples": [
|
||||
{
|
||||
"name": "WV: HDCP: None (not required)",
|
||||
"uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd",
|
||||
"drm_scheme": "widevine",
|
||||
"drm_license_url": "https://proxy.uat.widevine.com/proxy?video_id=HDCP_None&provider=widevine_test"
|
||||
},
|
||||
{
|
||||
"name": "WV: HDCP: 1.0 required",
|
||||
"uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd",
|
||||
"drm_scheme": "widevine",
|
||||
"drm_license_url": "https://proxy.uat.widevine.com/proxy?video_id=HDCP_V1&provider=widevine_test"
|
||||
},
|
||||
{
|
||||
"name": "WV: HDCP: 2.0 required",
|
||||
"uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd",
|
||||
"drm_scheme": "widevine",
|
||||
"drm_license_url": "https://proxy.uat.widevine.com/proxy?video_id=HDCP_V2&provider=widevine_test"
|
||||
},
|
||||
{
|
||||
"name": "WV: HDCP: 2.1 required",
|
||||
"uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd",
|
||||
"drm_scheme": "widevine",
|
||||
"drm_license_url": "https://proxy.uat.widevine.com/proxy?video_id=HDCP_V2_1&provider=widevine_test"
|
||||
},
|
||||
{
|
||||
"name": "WV: HDCP: 2.2 required",
|
||||
"uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd",
|
||||
"drm_scheme": "widevine",
|
||||
"drm_license_url": "https://proxy.uat.widevine.com/proxy?video_id=HDCP_V2_2&provider=widevine_test"
|
||||
},
|
||||
{
|
||||
"name": "WV: HDCP: No digital output",
|
||||
"uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd",
|
||||
"drm_scheme": "widevine",
|
||||
"drm_license_url": "https://proxy.uat.widevine.com/proxy?video_id=HDCP_NO_DIGTAL_OUTPUT&provider=widevine_test"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Widevine DASH: MP4,H264",
|
||||
"samples": [
|
||||
{
|
||||
"name": "WV: Clear SD & HD (MP4,H264)",
|
||||
"name": "Clear",
|
||||
"uri": "https://storage.googleapis.com/wvmedia/clear/h264/tears/tears.mpd"
|
||||
},
|
||||
{
|
||||
"name": "WV: Clear SD (MP4,H264)",
|
||||
"uri": "https://storage.googleapis.com/wvmedia/clear/h264/tears/tears_sd.mpd"
|
||||
},
|
||||
{
|
||||
"name": "WV: Clear HD (MP4,H264)",
|
||||
"uri": "https://storage.googleapis.com/wvmedia/clear/h264/tears/tears_hd.mpd"
|
||||
},
|
||||
{
|
||||
"name": "WV: Clear UHD (MP4,H264)",
|
||||
"name": "Clear UHD",
|
||||
"uri": "https://storage.googleapis.com/wvmedia/clear/h264/tears/tears_uhd.mpd"
|
||||
},
|
||||
{
|
||||
"name": "WV: Secure SD & HD (cenc,MP4,H264)",
|
||||
"name": "Secure (cenc)",
|
||||
"uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd",
|
||||
"drm_scheme": "widevine",
|
||||
"drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
|
||||
"drm_license_uri": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
|
||||
},
|
||||
{
|
||||
"name": "WV: Secure SD (cenc,MP4,H264)",
|
||||
"uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears_sd.mpd",
|
||||
"drm_scheme": "widevine",
|
||||
"drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
|
||||
},
|
||||
{
|
||||
"name": "WV: Secure HD (cenc,MP4,H264)",
|
||||
"uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears_hd.mpd",
|
||||
"drm_scheme": "widevine",
|
||||
"drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
|
||||
},
|
||||
{
|
||||
"name": "WV: Secure UHD (cenc,MP4,H264)",
|
||||
"name": "Secure UHD (cenc)",
|
||||
"uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears_uhd.mpd",
|
||||
"drm_scheme": "widevine",
|
||||
"drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
|
||||
"drm_license_uri": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
|
||||
},
|
||||
{
|
||||
"name": "WV: Secure SD & HD (cbc1,MP4,H264)",
|
||||
"uri": "https://storage.googleapis.com/wvmedia/cbc1/h264/tears/tears_aes_cbc1.mpd",
|
||||
"drm_scheme": "widevine",
|
||||
"drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
|
||||
},
|
||||
{
|
||||
"name": "WV: Secure SD (cbc1,MP4,H264)",
|
||||
"uri": "https://storage.googleapis.com/wvmedia/cbc1/h264/tears/tears_aes_cbc1_sd.mpd",
|
||||
"drm_scheme": "widevine",
|
||||
"drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
|
||||
},
|
||||
{
|
||||
"name": "WV: Secure HD (cbc1,MP4,H264)",
|
||||
"uri": "https://storage.googleapis.com/wvmedia/cbc1/h264/tears/tears_aes_cbc1_hd.mpd",
|
||||
"drm_scheme": "widevine",
|
||||
"drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
|
||||
},
|
||||
{
|
||||
"name": "WV: Secure UHD (cbc1,MP4,H264)",
|
||||
"uri": "https://storage.googleapis.com/wvmedia/cbc1/h264/tears/tears_aes_cbc1_uhd.mpd",
|
||||
"drm_scheme": "widevine",
|
||||
"drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
|
||||
},
|
||||
{
|
||||
"name": "WV: Secure SD & HD (cbcs,MP4,H264)",
|
||||
"name": "Secure (cbcs)",
|
||||
"uri": "https://storage.googleapis.com/wvmedia/cbcs/h264/tears/tears_aes_cbcs.mpd",
|
||||
"drm_scheme": "widevine",
|
||||
"drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
|
||||
"drm_license_uri": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
|
||||
},
|
||||
{
|
||||
"name": "WV: Secure SD (cbcs,MP4,H264)",
|
||||
"uri": "https://storage.googleapis.com/wvmedia/cbcs/h264/tears/tears_aes_cbcs_sd.mpd",
|
||||
"drm_scheme": "widevine",
|
||||
"drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
|
||||
},
|
||||
{
|
||||
"name": "WV: Secure HD (cbcs,MP4,H264)",
|
||||
"uri": "https://storage.googleapis.com/wvmedia/cbcs/h264/tears/tears_aes_cbcs_hd.mpd",
|
||||
"drm_scheme": "widevine",
|
||||
"drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
|
||||
},
|
||||
{
|
||||
"name": "WV: Secure UHD (cbcs,MP4,H264)",
|
||||
"name": "Secure UHD (cbcs)",
|
||||
"uri": "https://storage.googleapis.com/wvmedia/cbcs/h264/tears/tears_aes_cbcs_uhd.mpd",
|
||||
"drm_scheme": "widevine",
|
||||
"drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
|
||||
"drm_license_uri": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
|
||||
},
|
||||
{
|
||||
"name": "WV: Secure and Clear SD & HD (cenc,MP4,H264)",
|
||||
"name": "Secure -> Clear -> Secure (cenc)",
|
||||
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/widevine/tears_enc_clear_enc.mpd",
|
||||
"drm_scheme": "widevine",
|
||||
"drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test",
|
||||
"drm_session_for_clear_types": ["audio", "video"]
|
||||
"drm_license_uri": "https://proxy.uat.widevine.com/proxy?provider=widevine_test",
|
||||
"drm_session_for_clear_content": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Widevine DASH: WebM,VP9",
|
||||
"name": "Widevine DASH VP9 (WebM)",
|
||||
"samples": [
|
||||
{
|
||||
"name": "WV: Clear SD & HD (WebM,VP9)",
|
||||
"name": "Clear",
|
||||
"uri": "https://storage.googleapis.com/wvmedia/clear/vp9/tears/tears.mpd"
|
||||
},
|
||||
{
|
||||
"name": "WV: Clear SD (WebM,VP9)",
|
||||
"uri": "https://storage.googleapis.com/wvmedia/clear/vp9/tears/tears_sd.mpd"
|
||||
},
|
||||
{
|
||||
"name": "WV: Clear HD (WebM,VP9)",
|
||||
"uri": "https://storage.googleapis.com/wvmedia/clear/vp9/tears/tears_hd.mpd"
|
||||
},
|
||||
{
|
||||
"name": "WV: Clear UHD (WebM,VP9)",
|
||||
"name": "Clear UHD",
|
||||
"uri": "https://storage.googleapis.com/wvmedia/clear/vp9/tears/tears_uhd.mpd"
|
||||
},
|
||||
{
|
||||
"name": "WV: Secure Fullsample SD & HD (WebM,VP9)",
|
||||
"name": "Secure (full-sample)",
|
||||
"uri": "https://storage.googleapis.com/wvmedia/cenc/vp9/tears/tears.mpd",
|
||||
"drm_scheme": "widevine",
|
||||
"drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
|
||||
"drm_license_uri": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
|
||||
},
|
||||
{
|
||||
"name": "WV: Secure Fullsample SD (WebM,VP9)",
|
||||
"uri": "https://storage.googleapis.com/wvmedia/cenc/vp9/tears/tears_sd.mpd",
|
||||
"drm_scheme": "widevine",
|
||||
"drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
|
||||
},
|
||||
{
|
||||
"name": "WV: Secure Fullsample HD (WebM,VP9)",
|
||||
"uri": "https://storage.googleapis.com/wvmedia/cenc/vp9/tears/tears_hd.mpd",
|
||||
"drm_scheme": "widevine",
|
||||
"drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
|
||||
},
|
||||
{
|
||||
"name": "WV: Secure Fullsample UHD (WebM,VP9)",
|
||||
"name": "Secure UHD (full-sample)",
|
||||
"uri": "https://storage.googleapis.com/wvmedia/cenc/vp9/tears/tears_uhd.mpd",
|
||||
"drm_scheme": "widevine",
|
||||
"drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
|
||||
"drm_license_uri": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
|
||||
},
|
||||
{
|
||||
"name": "WV: Secure Subsample SD & HD (WebM,VP9)",
|
||||
"name": "Secure (sub-sample)",
|
||||
"uri": "https://storage.googleapis.com/wvmedia/cenc/vp9/subsample/24fps/tears/tears.mpd",
|
||||
"drm_scheme": "widevine",
|
||||
"drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
|
||||
"drm_license_uri": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
|
||||
},
|
||||
{
|
||||
"name": "WV: Secure Subsample SD (WebM,VP9)",
|
||||
"uri": "https://storage.googleapis.com/wvmedia/cenc/vp9/subsample/24fps/tears/tears_sd.mpd",
|
||||
"drm_scheme": "widevine",
|
||||
"drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
|
||||
},
|
||||
{
|
||||
"name": "WV: Secure Subsample HD (WebM,VP9)",
|
||||
"uri": "https://storage.googleapis.com/wvmedia/cenc/vp9/subsample/24fps/tears/tears_hd.mpd",
|
||||
"drm_scheme": "widevine",
|
||||
"drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
|
||||
},
|
||||
{
|
||||
"name": "WV: Secure Subsample UHD (WebM,VP9)",
|
||||
"name": "Secure UHD (sub-sample)",
|
||||
"uri": "https://storage.googleapis.com/wvmedia/cenc/vp9/subsample/24fps/tears/tears_uhd.mpd",
|
||||
"drm_scheme": "widevine",
|
||||
"drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
|
||||
"drm_license_uri": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Widevine DASH: MP4,H265",
|
||||
"name": "Widevine DASH H265 (MP4)",
|
||||
"samples": [
|
||||
{
|
||||
"name": "WV: Clear SD & HD (MP4,H265)",
|
||||
"name": "Clear",
|
||||
"uri": "https://storage.googleapis.com/wvmedia/clear/hevc/tears/tears.mpd"
|
||||
},
|
||||
{
|
||||
"name": "WV: Clear SD (MP4,H265)",
|
||||
"uri": "https://storage.googleapis.com/wvmedia/clear/hevc/tears/tears_sd.mpd"
|
||||
},
|
||||
{
|
||||
"name": "WV: Clear HD (MP4,H265)",
|
||||
"uri": "https://storage.googleapis.com/wvmedia/clear/hevc/tears/tears_hd.mpd"
|
||||
},
|
||||
{
|
||||
"name": "WV: Clear UHD (MP4,H265)",
|
||||
"name": "Clear UHD",
|
||||
"uri": "https://storage.googleapis.com/wvmedia/clear/hevc/tears/tears_uhd.mpd"
|
||||
},
|
||||
{
|
||||
"name": "WV: Secure SD & HD (MP4,H265)",
|
||||
"name": "Secure",
|
||||
"uri": "https://storage.googleapis.com/wvmedia/cenc/hevc/tears/tears.mpd",
|
||||
"drm_scheme": "widevine",
|
||||
"drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
|
||||
"drm_license_uri": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
|
||||
},
|
||||
{
|
||||
"name": "WV: Secure SD (MP4,H265)",
|
||||
"uri": "https://storage.googleapis.com/wvmedia/cenc/hevc/tears/tears_sd.mpd",
|
||||
"drm_scheme": "widevine",
|
||||
"drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
|
||||
},
|
||||
{
|
||||
"name": "WV: Secure HD (MP4,H265)",
|
||||
"uri": "https://storage.googleapis.com/wvmedia/cenc/hevc/tears/tears_hd.mpd",
|
||||
"drm_scheme": "widevine",
|
||||
"drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
|
||||
},
|
||||
{
|
||||
"name": "WV: Secure UHD (MP4,H265)",
|
||||
"name": "Secure UHD",
|
||||
"uri": "https://storage.googleapis.com/wvmedia/cenc/hevc/tears/tears_uhd.mpd",
|
||||
"drm_scheme": "widevine",
|
||||
"drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
|
||||
"drm_license_uri": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Widevine AV1 (WebM)",
|
||||
"samples": [
|
||||
{
|
||||
"name": "Clear",
|
||||
"uri": "https://storage.googleapis.com/wvmedia/2019/clear/av1/24/webm/llama_av1_480p_400.webm"
|
||||
},
|
||||
{
|
||||
"name": "Secure L3",
|
||||
"uri": "https://storage.googleapis.com/wvmedia/2019/cenc/av1/24/webm/llama_av1_480p_400.webm",
|
||||
"drm_scheme": "widevine",
|
||||
"drm_license_uri": "https://proxy.uat.widevine.com/proxy?video_id=GTS_SW_SECURE_CRYPTO&provider=widevine_test"
|
||||
},
|
||||
{
|
||||
"name": "Secure L1",
|
||||
"uri": "https://storage.googleapis.com/wvmedia/2019/cenc/av1/24/webm/llama_av1_480p_400.webm",
|
||||
"drm_scheme": "widevine",
|
||||
"drm_license_uri": "https://proxy.uat.widevine.com/proxy?video_id=GTS_HW_SECURE_ALL&provider=widevine_test"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
|
@ -362,7 +258,7 @@
|
|||
"uri": "https://devstreaming-cdn.apple.com/videos/streaming/examples/img_bipbop_adv_example_ts/master.m3u8"
|
||||
},
|
||||
{
|
||||
"name": "Apple master playlist advanced (fMP4)",
|
||||
"name": "Apple master playlist advanced (FMP4)",
|
||||
"uri": "https://devstreaming-cdn.apple.com/videos/streaming/examples/img_bipbop_adv_example_fmp4/master.m3u8"
|
||||
},
|
||||
{
|
||||
|
|
@ -429,6 +325,10 @@
|
|||
{
|
||||
"name": "Big Buck Bunny 480p video (MP4,AV1)",
|
||||
"uri": "https://storage.googleapis.com/downloads.webmproject.org/av1/exoplayer/bbb-av1-480p.mp4"
|
||||
},
|
||||
{
|
||||
"name": "One hour frame counter (MP4)",
|
||||
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/mp4/frame-counter-one-hour.mp4"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
|
@ -469,7 +369,7 @@
|
|||
{
|
||||
"uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears_sd.mpd",
|
||||
"drm_scheme": "widevine",
|
||||
"drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
|
||||
"drm_license_uri": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
|
||||
},
|
||||
{
|
||||
"uri": "https://html5demos.com/assets/dizzy.mp4"
|
||||
|
|
@ -477,12 +377,29 @@
|
|||
{
|
||||
"uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears_sd.mpd",
|
||||
"drm_scheme": "widevine",
|
||||
"drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
|
||||
"drm_license_uri": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
|
||||
},
|
||||
{
|
||||
"uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears_sd.mpd",
|
||||
"drm_scheme": "widevine",
|
||||
"drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
|
||||
"drm_license_uri": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Manual ad insertion",
|
||||
"playlist": [
|
||||
{
|
||||
"uri": "https://html5demos.com/assets/dizzy.mp4",
|
||||
"clip_end_position_ms": 10000
|
||||
},
|
||||
{
|
||||
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/mp4/frame-counter-one-hour.mp4",
|
||||
"clip_end_position_ms": 5000
|
||||
},
|
||||
{
|
||||
"uri": "https://html5demos.com/assets/dizzy.mp4",
|
||||
"clip_start_position_ms": 10000
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -575,26 +492,11 @@
|
|||
"name": "VMAP full, empty, full midrolls",
|
||||
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv",
|
||||
"ad_tag_uri": "https://vastsynthesizer.appspot.com/empty-midroll-2"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "360",
|
||||
"samples": [
|
||||
{
|
||||
"name": "Congo (360 top-bottom stereo)",
|
||||
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/360/congo.mp4",
|
||||
"spherical_stereo_mode": "top_bottom"
|
||||
},
|
||||
{
|
||||
"name": "Sphericalv2 (180 top-bottom stereo)",
|
||||
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/360/sphericalv2.mp4",
|
||||
"spherical_stereo_mode": "top_bottom"
|
||||
},
|
||||
{
|
||||
"name": "Iceland (360 top-bottom stereo ts)",
|
||||
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/360/iceland0.ts",
|
||||
"spherical_stereo_mode": "top_bottom"
|
||||
"name": "VMAP midroll at 1765 s",
|
||||
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/mp4/frame-counter-one-hour.mp4",
|
||||
"ad_tag_uri": "https://vastsynthesizer.appspot.com/midroll-large"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
|
@ -608,12 +510,23 @@
|
|||
"subtitle_mime_type": "application/ttml+xml",
|
||||
"subtitle_language": "en"
|
||||
},
|
||||
{
|
||||
"name": "WebVTT line positioning",
|
||||
"uri": "https://html5demos.com/assets/dizzy.mp4",
|
||||
"subtitle_uri": "https://storage.googleapis.com/exoplayer-test-media-1/webvtt/numeric-lines.vtt",
|
||||
"subtitle_mime_type": "text/vtt",
|
||||
"subtitle_language": "en"
|
||||
},
|
||||
{
|
||||
"name": "SSA/ASS position & alignment",
|
||||
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/gen-3/screens/dash-vod-single-segment/video-avc-baseline-480.mp4",
|
||||
"subtitle_uri": "https://storage.googleapis.com/exoplayer-test-media-1/ssa/test-subs-position.ass",
|
||||
"subtitle_mime_type": "text/x-ssa",
|
||||
"subtitle_language": "en"
|
||||
},
|
||||
{
|
||||
"name": "MPEG-4 Timed Text (tx3g, mov_text)",
|
||||
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/mp4/dizzy-with-tx3g.mp4"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
|
@ -632,13 +545,13 @@
|
|||
"name": "Big Buck Bunny (DASH,H264,1080p,Widevine)",
|
||||
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/60fps/bbb-wv-1080/manifest.mpd",
|
||||
"drm_scheme": "widevine",
|
||||
"drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
|
||||
"drm_license_uri": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
|
||||
},
|
||||
{
|
||||
"name": "Big Buck Bunny (DASH,H264,4K,Widevine)",
|
||||
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/60fps/bbb-wv-2160/manifest.mpd",
|
||||
"drm_scheme": "widevine",
|
||||
"drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
|
||||
"drm_license_uri": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,177 +0,0 @@
|
|||
/*
|
||||
* Copyright (C) 2016 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.google.android.exoplayer2.demo;
|
||||
|
||||
import android.app.Application;
|
||||
import com.google.android.exoplayer2.DefaultRenderersFactory;
|
||||
import com.google.android.exoplayer2.RenderersFactory;
|
||||
import com.google.android.exoplayer2.database.DatabaseProvider;
|
||||
import com.google.android.exoplayer2.database.ExoDatabaseProvider;
|
||||
import com.google.android.exoplayer2.offline.ActionFileUpgradeUtil;
|
||||
import com.google.android.exoplayer2.offline.DefaultDownloadIndex;
|
||||
import com.google.android.exoplayer2.offline.DownloadManager;
|
||||
import com.google.android.exoplayer2.ui.DownloadNotificationHelper;
|
||||
import com.google.android.exoplayer2.upstream.DataSource;
|
||||
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
|
||||
import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory;
|
||||
import com.google.android.exoplayer2.upstream.HttpDataSource;
|
||||
import com.google.android.exoplayer2.upstream.cache.Cache;
|
||||
import com.google.android.exoplayer2.upstream.cache.CacheDataSource;
|
||||
import com.google.android.exoplayer2.upstream.cache.NoOpCacheEvictor;
|
||||
import com.google.android.exoplayer2.upstream.cache.SimpleCache;
|
||||
import com.google.android.exoplayer2.util.Log;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* Placeholder application to facilitate overriding Application methods for debugging and testing.
|
||||
*/
|
||||
public class DemoApplication extends Application {
|
||||
|
||||
public static final String DOWNLOAD_NOTIFICATION_CHANNEL_ID = "download_channel";
|
||||
|
||||
private static final String TAG = "DemoApplication";
|
||||
private static final String DOWNLOAD_ACTION_FILE = "actions";
|
||||
private static final String DOWNLOAD_TRACKER_ACTION_FILE = "tracked_actions";
|
||||
private static final String DOWNLOAD_CONTENT_DIRECTORY = "downloads";
|
||||
|
||||
protected String userAgent;
|
||||
|
||||
private DatabaseProvider databaseProvider;
|
||||
private File downloadDirectory;
|
||||
private Cache downloadCache;
|
||||
private DownloadManager downloadManager;
|
||||
private DownloadTracker downloadTracker;
|
||||
private DownloadNotificationHelper downloadNotificationHelper;
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
userAgent = Util.getUserAgent(this, "ExoPlayerDemo");
|
||||
}
|
||||
|
||||
/** Returns a {@link DataSource.Factory}. */
|
||||
public DataSource.Factory buildDataSourceFactory() {
|
||||
DefaultDataSourceFactory upstreamFactory =
|
||||
new DefaultDataSourceFactory(this, buildHttpDataSourceFactory());
|
||||
return buildReadOnlyCacheDataSource(upstreamFactory, getDownloadCache());
|
||||
}
|
||||
|
||||
/** Returns a {@link HttpDataSource.Factory}. */
|
||||
public HttpDataSource.Factory buildHttpDataSourceFactory() {
|
||||
return new DefaultHttpDataSourceFactory(userAgent);
|
||||
}
|
||||
|
||||
/** Returns whether extension renderers should be used. */
|
||||
public boolean useExtensionRenderers() {
|
||||
return "withExtensions".equals(BuildConfig.FLAVOR);
|
||||
}
|
||||
|
||||
public RenderersFactory buildRenderersFactory(boolean preferExtensionRenderer) {
|
||||
@DefaultRenderersFactory.ExtensionRendererMode
|
||||
int extensionRendererMode =
|
||||
useExtensionRenderers()
|
||||
? (preferExtensionRenderer
|
||||
? DefaultRenderersFactory.EXTENSION_RENDERER_MODE_PREFER
|
||||
: DefaultRenderersFactory.EXTENSION_RENDERER_MODE_ON)
|
||||
: DefaultRenderersFactory.EXTENSION_RENDERER_MODE_OFF;
|
||||
return new DefaultRenderersFactory(/* context= */ this)
|
||||
.setExtensionRendererMode(extensionRendererMode);
|
||||
}
|
||||
|
||||
public DownloadNotificationHelper getDownloadNotificationHelper() {
|
||||
if (downloadNotificationHelper == null) {
|
||||
downloadNotificationHelper =
|
||||
new DownloadNotificationHelper(this, DOWNLOAD_NOTIFICATION_CHANNEL_ID);
|
||||
}
|
||||
return downloadNotificationHelper;
|
||||
}
|
||||
|
||||
public DownloadManager getDownloadManager() {
|
||||
initDownloadManager();
|
||||
return downloadManager;
|
||||
}
|
||||
|
||||
public DownloadTracker getDownloadTracker() {
|
||||
initDownloadManager();
|
||||
return downloadTracker;
|
||||
}
|
||||
|
||||
protected synchronized Cache getDownloadCache() {
|
||||
if (downloadCache == null) {
|
||||
File downloadContentDirectory = new File(getDownloadDirectory(), DOWNLOAD_CONTENT_DIRECTORY);
|
||||
downloadCache =
|
||||
new SimpleCache(downloadContentDirectory, new NoOpCacheEvictor(), getDatabaseProvider());
|
||||
}
|
||||
return downloadCache;
|
||||
}
|
||||
|
||||
private synchronized void initDownloadManager() {
|
||||
if (downloadManager == null) {
|
||||
DefaultDownloadIndex downloadIndex = new DefaultDownloadIndex(getDatabaseProvider());
|
||||
upgradeActionFile(
|
||||
DOWNLOAD_ACTION_FILE, downloadIndex, /* addNewDownloadsAsCompleted= */ false);
|
||||
upgradeActionFile(
|
||||
DOWNLOAD_TRACKER_ACTION_FILE, downloadIndex, /* addNewDownloadsAsCompleted= */ true);
|
||||
downloadManager =
|
||||
new DownloadManager(
|
||||
this, getDatabaseProvider(), getDownloadCache(), buildHttpDataSourceFactory());
|
||||
downloadTracker =
|
||||
new DownloadTracker(/* context= */ this, buildDataSourceFactory(), downloadManager);
|
||||
}
|
||||
}
|
||||
|
||||
private void upgradeActionFile(
|
||||
String fileName, DefaultDownloadIndex downloadIndex, boolean addNewDownloadsAsCompleted) {
|
||||
try {
|
||||
ActionFileUpgradeUtil.upgradeAndDelete(
|
||||
new File(getDownloadDirectory(), fileName),
|
||||
/* downloadIdProvider= */ null,
|
||||
downloadIndex,
|
||||
/* deleteOnFailure= */ true,
|
||||
addNewDownloadsAsCompleted);
|
||||
} catch (IOException e) {
|
||||
Log.e(TAG, "Failed to upgrade action file: " + fileName, e);
|
||||
}
|
||||
}
|
||||
|
||||
private DatabaseProvider getDatabaseProvider() {
|
||||
if (databaseProvider == null) {
|
||||
databaseProvider = new ExoDatabaseProvider(this);
|
||||
}
|
||||
return databaseProvider;
|
||||
}
|
||||
|
||||
private File getDownloadDirectory() {
|
||||
if (downloadDirectory == null) {
|
||||
downloadDirectory = getExternalFilesDir(null);
|
||||
if (downloadDirectory == null) {
|
||||
downloadDirectory = getFilesDir();
|
||||
}
|
||||
}
|
||||
return downloadDirectory;
|
||||
}
|
||||
|
||||
protected static CacheDataSource.Factory buildReadOnlyCacheDataSource(
|
||||
DataSource.Factory upstreamFactory, Cache cache) {
|
||||
return new CacheDataSource.Factory()
|
||||
.setCache(cache)
|
||||
.setUpstreamDataSourceFactory(upstreamFactory)
|
||||
.setCacheWriteDataSinkFactory(null)
|
||||
.setFlags(CacheDataSource.FLAG_IGNORE_CACHE_ON_ERROR);
|
||||
}
|
||||
}
|
||||
|
|
@ -15,11 +15,12 @@
|
|||
*/
|
||||
package com.google.android.exoplayer2.demo;
|
||||
|
||||
import static com.google.android.exoplayer2.demo.DemoApplication.DOWNLOAD_NOTIFICATION_CHANNEL_ID;
|
||||
import static com.google.android.exoplayer2.demo.DemoUtil.DOWNLOAD_NOTIFICATION_CHANNEL_ID;
|
||||
|
||||
import android.app.Notification;
|
||||
import android.content.Context;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import com.google.android.exoplayer2.offline.Download;
|
||||
import com.google.android.exoplayer2.offline.DownloadManager;
|
||||
import com.google.android.exoplayer2.offline.DownloadService;
|
||||
|
|
@ -49,10 +50,9 @@ public class DemoDownloadService extends DownloadService {
|
|||
protected DownloadManager getDownloadManager() {
|
||||
// This will only happen once, because getDownloadManager is guaranteed to be called only once
|
||||
// in the life cycle of the process.
|
||||
DemoApplication application = (DemoApplication) getApplication();
|
||||
DownloadManager downloadManager = application.getDownloadManager();
|
||||
DownloadManager downloadManager = DemoUtil.getDownloadManager(/* context= */ this);
|
||||
DownloadNotificationHelper downloadNotificationHelper =
|
||||
application.getDownloadNotificationHelper();
|
||||
DemoUtil.getDownloadNotificationHelper(/* context= */ this);
|
||||
downloadManager.addListener(
|
||||
new TerminalStateNotificationHelper(
|
||||
this, downloadNotificationHelper, FOREGROUND_NOTIFICATION_ID + 1));
|
||||
|
|
@ -67,10 +67,13 @@ public class DemoDownloadService extends DownloadService {
|
|||
@Override
|
||||
@NonNull
|
||||
protected Notification getForegroundNotification(@NonNull List<Download> downloads) {
|
||||
return ((DemoApplication) getApplication())
|
||||
.getDownloadNotificationHelper()
|
||||
return DemoUtil.getDownloadNotificationHelper(/* context= */ this)
|
||||
.buildProgressNotification(
|
||||
R.drawable.ic_download, /* contentIntent= */ null, /* message= */ null, downloads);
|
||||
/* context= */ this,
|
||||
R.drawable.ic_download,
|
||||
/* contentIntent= */ null,
|
||||
/* message= */ null,
|
||||
downloads);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -94,17 +97,20 @@ public class DemoDownloadService extends DownloadService {
|
|||
}
|
||||
|
||||
@Override
|
||||
public void onDownloadChanged(@NonNull DownloadManager manager, @NonNull Download download) {
|
||||
public void onDownloadChanged(
|
||||
DownloadManager downloadManager, Download download, @Nullable Exception finalException) {
|
||||
Notification notification;
|
||||
if (download.state == Download.STATE_COMPLETED) {
|
||||
notification =
|
||||
notificationHelper.buildDownloadCompletedNotification(
|
||||
context,
|
||||
R.drawable.ic_download_done,
|
||||
/* contentIntent= */ null,
|
||||
Util.fromUtf8Bytes(download.request.data));
|
||||
} else if (download.state == Download.STATE_FAILED) {
|
||||
notification =
|
||||
notificationHelper.buildDownloadFailedNotification(
|
||||
context,
|
||||
R.drawable.ic_download_done,
|
||||
/* contentIntent= */ null,
|
||||
Util.fromUtf8Bytes(download.request.data));
|
||||
|
|
|
|||
|
|
@ -0,0 +1,196 @@
|
|||
/*
|
||||
* Copyright (C) 2016 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.google.android.exoplayer2.demo;
|
||||
|
||||
import android.content.Context;
|
||||
import com.google.android.exoplayer2.DefaultRenderersFactory;
|
||||
import com.google.android.exoplayer2.RenderersFactory;
|
||||
import com.google.android.exoplayer2.database.DatabaseProvider;
|
||||
import com.google.android.exoplayer2.database.ExoDatabaseProvider;
|
||||
import com.google.android.exoplayer2.ext.cronet.CronetDataSourceFactory;
|
||||
import com.google.android.exoplayer2.ext.cronet.CronetEngineWrapper;
|
||||
import com.google.android.exoplayer2.offline.ActionFileUpgradeUtil;
|
||||
import com.google.android.exoplayer2.offline.DefaultDownloadIndex;
|
||||
import com.google.android.exoplayer2.offline.DownloadManager;
|
||||
import com.google.android.exoplayer2.ui.DownloadNotificationHelper;
|
||||
import com.google.android.exoplayer2.upstream.DataSource;
|
||||
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
|
||||
import com.google.android.exoplayer2.upstream.HttpDataSource;
|
||||
import com.google.android.exoplayer2.upstream.cache.Cache;
|
||||
import com.google.android.exoplayer2.upstream.cache.CacheDataSource;
|
||||
import com.google.android.exoplayer2.upstream.cache.NoOpCacheEvictor;
|
||||
import com.google.android.exoplayer2.upstream.cache.SimpleCache;
|
||||
import com.google.android.exoplayer2.util.Log;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.concurrent.Executors;
|
||||
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
||||
|
||||
/** Utility methods for the demo app. */
|
||||
public final class DemoUtil {
|
||||
|
||||
public static final String DOWNLOAD_NOTIFICATION_CHANNEL_ID = "download_channel";
|
||||
|
||||
private static final String TAG = "DemoUtil";
|
||||
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 DataSource.@MonotonicNonNull Factory dataSourceFactory;
|
||||
private static HttpDataSource.@MonotonicNonNull Factory httpDataSourceFactory;
|
||||
private static @MonotonicNonNull DatabaseProvider databaseProvider;
|
||||
private static @MonotonicNonNull File downloadDirectory;
|
||||
private static @MonotonicNonNull Cache downloadCache;
|
||||
private static @MonotonicNonNull DownloadManager downloadManager;
|
||||
private static @MonotonicNonNull DownloadTracker downloadTracker;
|
||||
private static @MonotonicNonNull DownloadNotificationHelper downloadNotificationHelper;
|
||||
|
||||
/** Returns whether extension renderers should be used. */
|
||||
public static boolean useExtensionRenderers() {
|
||||
return BuildConfig.USE_DECODER_EXTENSIONS;
|
||||
}
|
||||
|
||||
public static RenderersFactory buildRenderersFactory(
|
||||
Context context, boolean preferExtensionRenderer) {
|
||||
@DefaultRenderersFactory.ExtensionRendererMode
|
||||
int extensionRendererMode =
|
||||
useExtensionRenderers()
|
||||
? (preferExtensionRenderer
|
||||
? DefaultRenderersFactory.EXTENSION_RENDERER_MODE_PREFER
|
||||
: DefaultRenderersFactory.EXTENSION_RENDERER_MODE_ON)
|
||||
: DefaultRenderersFactory.EXTENSION_RENDERER_MODE_OFF;
|
||||
return new DefaultRenderersFactory(context.getApplicationContext())
|
||||
.setExtensionRendererMode(extensionRendererMode);
|
||||
}
|
||||
|
||||
public static synchronized HttpDataSource.Factory getHttpDataSourceFactory(Context context) {
|
||||
if (httpDataSourceFactory == null) {
|
||||
context = context.getApplicationContext();
|
||||
CronetEngineWrapper cronetEngineWrapper = new CronetEngineWrapper(context);
|
||||
httpDataSourceFactory =
|
||||
new CronetDataSourceFactory(cronetEngineWrapper, Executors.newSingleThreadExecutor());
|
||||
}
|
||||
return httpDataSourceFactory;
|
||||
}
|
||||
|
||||
/** Returns a {@link DataSource.Factory}. */
|
||||
public static synchronized DataSource.Factory getDataSourceFactory(Context context) {
|
||||
if (dataSourceFactory == null) {
|
||||
context = context.getApplicationContext();
|
||||
DefaultDataSourceFactory upstreamFactory =
|
||||
new DefaultDataSourceFactory(context, getHttpDataSourceFactory(context));
|
||||
dataSourceFactory = buildReadOnlyCacheDataSource(upstreamFactory, getDownloadCache(context));
|
||||
}
|
||||
return dataSourceFactory;
|
||||
}
|
||||
|
||||
public static synchronized DownloadNotificationHelper getDownloadNotificationHelper(
|
||||
Context context) {
|
||||
if (downloadNotificationHelper == null) {
|
||||
downloadNotificationHelper =
|
||||
new DownloadNotificationHelper(context, DOWNLOAD_NOTIFICATION_CHANNEL_ID);
|
||||
}
|
||||
return downloadNotificationHelper;
|
||||
}
|
||||
|
||||
public static synchronized DownloadManager getDownloadManager(Context context) {
|
||||
ensureDownloadManagerInitialized(context);
|
||||
return downloadManager;
|
||||
}
|
||||
|
||||
public static synchronized DownloadTracker getDownloadTracker(Context context) {
|
||||
ensureDownloadManagerInitialized(context);
|
||||
return downloadTracker;
|
||||
}
|
||||
|
||||
private static synchronized Cache getDownloadCache(Context context) {
|
||||
if (downloadCache == null) {
|
||||
File downloadContentDirectory =
|
||||
new File(getDownloadDirectory(context), DOWNLOAD_CONTENT_DIRECTORY);
|
||||
downloadCache =
|
||||
new SimpleCache(
|
||||
downloadContentDirectory, new NoOpCacheEvictor(), getDatabaseProvider(context));
|
||||
}
|
||||
return downloadCache;
|
||||
}
|
||||
|
||||
private static synchronized void ensureDownloadManagerInitialized(Context context) {
|
||||
if (downloadManager == null) {
|
||||
DefaultDownloadIndex downloadIndex = new DefaultDownloadIndex(getDatabaseProvider(context));
|
||||
upgradeActionFile(
|
||||
context, DOWNLOAD_ACTION_FILE, downloadIndex, /* addNewDownloadsAsCompleted= */ false);
|
||||
upgradeActionFile(
|
||||
context,
|
||||
DOWNLOAD_TRACKER_ACTION_FILE,
|
||||
downloadIndex,
|
||||
/* addNewDownloadsAsCompleted= */ true);
|
||||
downloadManager =
|
||||
new DownloadManager(
|
||||
context,
|
||||
getDatabaseProvider(context),
|
||||
getDownloadCache(context),
|
||||
getHttpDataSourceFactory(context),
|
||||
Executors.newFixedThreadPool(/* nThreads= */ 6));
|
||||
downloadTracker =
|
||||
new DownloadTracker(context, getHttpDataSourceFactory(context), downloadManager);
|
||||
}
|
||||
}
|
||||
|
||||
private static synchronized void upgradeActionFile(
|
||||
Context context,
|
||||
String fileName,
|
||||
DefaultDownloadIndex downloadIndex,
|
||||
boolean addNewDownloadsAsCompleted) {
|
||||
try {
|
||||
ActionFileUpgradeUtil.upgradeAndDelete(
|
||||
new File(getDownloadDirectory(context), fileName),
|
||||
/* downloadIdProvider= */ null,
|
||||
downloadIndex,
|
||||
/* deleteOnFailure= */ true,
|
||||
addNewDownloadsAsCompleted);
|
||||
} catch (IOException e) {
|
||||
Log.e(TAG, "Failed to upgrade action file: " + fileName, e);
|
||||
}
|
||||
}
|
||||
|
||||
private static synchronized DatabaseProvider getDatabaseProvider(Context context) {
|
||||
if (databaseProvider == null) {
|
||||
databaseProvider = new ExoDatabaseProvider(context);
|
||||
}
|
||||
return databaseProvider;
|
||||
}
|
||||
|
||||
private static synchronized File getDownloadDirectory(Context context) {
|
||||
if (downloadDirectory == null) {
|
||||
downloadDirectory = context.getExternalFilesDir(/* type= */ null);
|
||||
if (downloadDirectory == null) {
|
||||
downloadDirectory = context.getFilesDir();
|
||||
}
|
||||
}
|
||||
return downloadDirectory;
|
||||
}
|
||||
|
||||
private static CacheDataSource.Factory buildReadOnlyCacheDataSource(
|
||||
DataSource.Factory upstreamFactory, Cache cache) {
|
||||
return new CacheDataSource.Factory()
|
||||
.setCache(cache)
|
||||
.setUpstreamDataSourceFactory(upstreamFactory)
|
||||
.setCacheWriteDataSinkFactory(null)
|
||||
.setFlags(CacheDataSource.FLAG_IGNORE_CACHE_ON_ERROR);
|
||||
}
|
||||
|
||||
private DemoUtil() {}
|
||||
}
|
||||
|
|
@ -16,27 +16,37 @@
|
|||
package com.google.android.exoplayer2.demo;
|
||||
|
||||
import static com.google.android.exoplayer2.util.Assertions.checkNotNull;
|
||||
import static com.google.android.exoplayer2.util.Assertions.checkStateNotNull;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.net.Uri;
|
||||
import android.os.AsyncTask;
|
||||
import android.widget.Toast;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.RequiresApi;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.Format;
|
||||
import com.google.android.exoplayer2.MediaItem;
|
||||
import com.google.android.exoplayer2.RenderersFactory;
|
||||
import com.google.android.exoplayer2.drm.DrmInitData;
|
||||
import com.google.android.exoplayer2.drm.DrmSession;
|
||||
import com.google.android.exoplayer2.drm.DrmSessionEventListener;
|
||||
import com.google.android.exoplayer2.drm.OfflineLicenseHelper;
|
||||
import com.google.android.exoplayer2.offline.Download;
|
||||
import com.google.android.exoplayer2.offline.DownloadCursor;
|
||||
import com.google.android.exoplayer2.offline.DownloadHelper;
|
||||
import com.google.android.exoplayer2.offline.DownloadHelper.LiveContentUnsupportedException;
|
||||
import com.google.android.exoplayer2.offline.DownloadIndex;
|
||||
import com.google.android.exoplayer2.offline.DownloadManager;
|
||||
import com.google.android.exoplayer2.offline.DownloadRequest;
|
||||
import com.google.android.exoplayer2.offline.DownloadService;
|
||||
import com.google.android.exoplayer2.source.TrackGroup;
|
||||
import com.google.android.exoplayer2.source.TrackGroupArray;
|
||||
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
|
||||
import com.google.android.exoplayer2.trackselection.MappingTrackSelector.MappedTrackInfo;
|
||||
import com.google.android.exoplayer2.upstream.DataSource;
|
||||
import com.google.android.exoplayer2.upstream.HttpDataSource;
|
||||
import com.google.android.exoplayer2.util.Log;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
import java.io.IOException;
|
||||
|
|
@ -56,7 +66,7 @@ public class DownloadTracker {
|
|||
private static final String TAG = "DownloadTracker";
|
||||
|
||||
private final Context context;
|
||||
private final DataSource.Factory dataSourceFactory;
|
||||
private final HttpDataSource.Factory httpDataSourceFactory;
|
||||
private final CopyOnWriteArraySet<Listener> listeners;
|
||||
private final HashMap<Uri, Download> downloads;
|
||||
private final DownloadIndex downloadIndex;
|
||||
|
|
@ -65,9 +75,11 @@ public class DownloadTracker {
|
|||
@Nullable private StartDownloadDialogHelper startDownloadDialogHelper;
|
||||
|
||||
public DownloadTracker(
|
||||
Context context, DataSource.Factory dataSourceFactory, DownloadManager downloadManager) {
|
||||
Context context,
|
||||
HttpDataSource.Factory httpDataSourceFactory,
|
||||
DownloadManager downloadManager) {
|
||||
this.context = context.getApplicationContext();
|
||||
this.dataSourceFactory = dataSourceFactory;
|
||||
this.httpDataSourceFactory = httpDataSourceFactory;
|
||||
listeners = new CopyOnWriteArraySet<>();
|
||||
downloads = new HashMap<>();
|
||||
downloadIndex = downloadManager.getDownloadIndex();
|
||||
|
|
@ -77,6 +89,7 @@ public class DownloadTracker {
|
|||
}
|
||||
|
||||
public void addListener(Listener listener) {
|
||||
checkNotNull(listener);
|
||||
listeners.add(listener);
|
||||
}
|
||||
|
||||
|
|
@ -89,6 +102,7 @@ public class DownloadTracker {
|
|||
return download != null && download.state != Download.STATE_FAILED;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public DownloadRequest getDownloadRequest(Uri uri) {
|
||||
Download download = downloads.get(uri);
|
||||
return download != null && download.state != Download.STATE_FAILED ? download.request : null;
|
||||
|
|
@ -106,7 +120,10 @@ public class DownloadTracker {
|
|||
}
|
||||
startDownloadDialogHelper =
|
||||
new StartDownloadDialogHelper(
|
||||
fragmentManager, getDownloadHelper(mediaItem, renderersFactory), mediaItem);
|
||||
fragmentManager,
|
||||
DownloadHelper.forMediaItem(
|
||||
context, mediaItem, renderersFactory, httpDataSourceFactory),
|
||||
mediaItem);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -121,33 +138,13 @@ public class DownloadTracker {
|
|||
}
|
||||
}
|
||||
|
||||
private DownloadHelper getDownloadHelper(MediaItem mediaItem, RenderersFactory renderersFactory) {
|
||||
MediaItem.PlaybackProperties playbackProperties = checkNotNull(mediaItem.playbackProperties);
|
||||
@C.ContentType
|
||||
int type =
|
||||
Util.inferContentTypeWithMimeType(playbackProperties.uri, playbackProperties.mimeType);
|
||||
switch (type) {
|
||||
case C.TYPE_DASH:
|
||||
return DownloadHelper.forDash(
|
||||
context, playbackProperties.uri, dataSourceFactory, renderersFactory);
|
||||
case C.TYPE_SS:
|
||||
return DownloadHelper.forSmoothStreaming(
|
||||
context, playbackProperties.uri, dataSourceFactory, renderersFactory);
|
||||
case C.TYPE_HLS:
|
||||
return DownloadHelper.forHls(
|
||||
context, playbackProperties.uri, dataSourceFactory, renderersFactory);
|
||||
case C.TYPE_OTHER:
|
||||
return DownloadHelper.forProgressive(context, playbackProperties.uri);
|
||||
default:
|
||||
throw new IllegalStateException("Unsupported type: " + type);
|
||||
}
|
||||
}
|
||||
|
||||
private class DownloadManagerListener implements DownloadManager.Listener {
|
||||
|
||||
@Override
|
||||
public void onDownloadChanged(
|
||||
@NonNull DownloadManager downloadManager, @NonNull Download download) {
|
||||
@NonNull DownloadManager downloadManager,
|
||||
@NonNull Download download,
|
||||
@Nullable Exception finalException) {
|
||||
downloads.put(download.request.uri, download);
|
||||
for (Listener listener : listeners) {
|
||||
listener.onDownloadsChanged();
|
||||
|
|
@ -175,6 +172,8 @@ public class DownloadTracker {
|
|||
|
||||
private TrackSelectionDialog trackSelectionDialog;
|
||||
private MappedTrackInfo mappedTrackInfo;
|
||||
private WidevineOfflineLicenseFetchTask widevineOfflineLicenseFetchTask;
|
||||
@Nullable private byte[] keySetId;
|
||||
|
||||
public StartDownloadDialogHelper(
|
||||
FragmentManager fragmentManager, DownloadHelper downloadHelper, MediaItem mediaItem) {
|
||||
|
|
@ -189,46 +188,57 @@ public class DownloadTracker {
|
|||
if (trackSelectionDialog != null) {
|
||||
trackSelectionDialog.dismiss();
|
||||
}
|
||||
if (widevineOfflineLicenseFetchTask != null) {
|
||||
widevineOfflineLicenseFetchTask.cancel(false);
|
||||
}
|
||||
}
|
||||
|
||||
// DownloadHelper.Callback implementation.
|
||||
|
||||
@Override
|
||||
public void onPrepared(@NonNull DownloadHelper helper) {
|
||||
if (helper.getPeriodCount() == 0) {
|
||||
Log.d(TAG, "No periods found. Downloading entire stream.");
|
||||
startDownload();
|
||||
downloadHelper.release();
|
||||
@Nullable Format format = getFirstFormatWithDrmInitData(helper);
|
||||
if (format == null) {
|
||||
onDownloadPrepared(helper);
|
||||
return;
|
||||
}
|
||||
mappedTrackInfo = downloadHelper.getMappedTrackInfo(/* periodIndex= */ 0);
|
||||
if (!TrackSelectionDialog.willHaveContent(mappedTrackInfo)) {
|
||||
Log.d(TAG, "No dialog content. Downloading entire stream.");
|
||||
startDownload();
|
||||
downloadHelper.release();
|
||||
|
||||
// The content is DRM protected. We need to acquire an offline license.
|
||||
if (Util.SDK_INT < 18) {
|
||||
Toast.makeText(context, R.string.error_drm_unsupported_before_api_18, Toast.LENGTH_LONG)
|
||||
.show();
|
||||
Log.e(TAG, "Downloading DRM protected content is not supported on API versions below 18");
|
||||
return;
|
||||
}
|
||||
trackSelectionDialog =
|
||||
TrackSelectionDialog.createForMappedTrackInfoAndParameters(
|
||||
/* titleId= */ R.string.exo_download_description,
|
||||
mappedTrackInfo,
|
||||
trackSelectorParameters,
|
||||
/* allowAdaptiveSelections =*/ false,
|
||||
/* allowMultipleOverrides= */ true,
|
||||
/* onClickListener= */ this,
|
||||
/* onDismissListener= */ this);
|
||||
trackSelectionDialog.show(fragmentManager, /* tag= */ null);
|
||||
// TODO(internal b/163107948): Support cases where DrmInitData are not in the manifest.
|
||||
if (!hasSchemaData(format.drmInitData)) {
|
||||
Toast.makeText(context, R.string.download_start_error_offline_license, Toast.LENGTH_LONG)
|
||||
.show();
|
||||
Log.e(
|
||||
TAG,
|
||||
"Downloading content where DRM scheme data is not located in the manifest is not"
|
||||
+ " supported");
|
||||
return;
|
||||
}
|
||||
widevineOfflineLicenseFetchTask =
|
||||
new WidevineOfflineLicenseFetchTask(
|
||||
format,
|
||||
mediaItem.playbackProperties.drmConfiguration.licenseUri,
|
||||
httpDataSourceFactory,
|
||||
/* dialogHelper= */ this,
|
||||
helper);
|
||||
widevineOfflineLicenseFetchTask.execute();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPrepareError(@NonNull DownloadHelper helper, @NonNull IOException e) {
|
||||
Toast.makeText(context, R.string.download_start_error, Toast.LENGTH_LONG).show();
|
||||
Log.e(
|
||||
TAG,
|
||||
e instanceof DownloadHelper.LiveContentUnsupportedException
|
||||
? "Downloading live content unsupported"
|
||||
: "Failed to start download",
|
||||
e);
|
||||
boolean isLiveContent = e instanceof LiveContentUnsupportedException;
|
||||
int toastStringId =
|
||||
isLiveContent ? R.string.download_live_unsupported : R.string.download_start_error;
|
||||
String logMessage =
|
||||
isLiveContent ? "Downloading live content unsupported" : "Failed to start download";
|
||||
Toast.makeText(context, toastStringId, Toast.LENGTH_LONG).show();
|
||||
Log.e(TAG, logMessage, e);
|
||||
}
|
||||
|
||||
// DialogInterface.OnClickListener implementation.
|
||||
|
|
@ -265,6 +275,83 @@ public class DownloadTracker {
|
|||
|
||||
// Internal methods.
|
||||
|
||||
/**
|
||||
* Returns the first {@link Format} with a non-null {@link Format#drmInitData} found in the
|
||||
* content's tracks, or null if none is found.
|
||||
*/
|
||||
@Nullable
|
||||
private Format getFirstFormatWithDrmInitData(DownloadHelper helper) {
|
||||
for (int periodIndex = 0; periodIndex < helper.getPeriodCount(); periodIndex++) {
|
||||
MappedTrackInfo mappedTrackInfo = helper.getMappedTrackInfo(periodIndex);
|
||||
for (int rendererIndex = 0;
|
||||
rendererIndex < mappedTrackInfo.getRendererCount();
|
||||
rendererIndex++) {
|
||||
TrackGroupArray trackGroups = mappedTrackInfo.getTrackGroups(rendererIndex);
|
||||
for (int trackGroupIndex = 0; trackGroupIndex < trackGroups.length; trackGroupIndex++) {
|
||||
TrackGroup trackGroup = trackGroups.get(trackGroupIndex);
|
||||
for (int formatIndex = 0; formatIndex < trackGroup.length; formatIndex++) {
|
||||
Format format = trackGroup.getFormat(formatIndex);
|
||||
if (format.drmInitData != null) {
|
||||
return format;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private void onOfflineLicenseFetched(DownloadHelper helper, byte[] keySetId) {
|
||||
this.keySetId = keySetId;
|
||||
onDownloadPrepared(helper);
|
||||
}
|
||||
|
||||
private void onOfflineLicenseFetchedError(DrmSession.DrmSessionException e) {
|
||||
Toast.makeText(context, R.string.download_start_error_offline_license, Toast.LENGTH_LONG)
|
||||
.show();
|
||||
Log.e(TAG, "Failed to fetch offline DRM license", e);
|
||||
}
|
||||
|
||||
private void onDownloadPrepared(DownloadHelper helper) {
|
||||
if (helper.getPeriodCount() == 0) {
|
||||
Log.d(TAG, "No periods found. Downloading entire stream.");
|
||||
startDownload();
|
||||
downloadHelper.release();
|
||||
return;
|
||||
}
|
||||
|
||||
mappedTrackInfo = downloadHelper.getMappedTrackInfo(/* periodIndex= */ 0);
|
||||
if (!TrackSelectionDialog.willHaveContent(mappedTrackInfo)) {
|
||||
Log.d(TAG, "No dialog content. Downloading entire stream.");
|
||||
startDownload();
|
||||
downloadHelper.release();
|
||||
return;
|
||||
}
|
||||
trackSelectionDialog =
|
||||
TrackSelectionDialog.createForMappedTrackInfoAndParameters(
|
||||
/* titleId= */ R.string.exo_download_description,
|
||||
mappedTrackInfo,
|
||||
trackSelectorParameters,
|
||||
/* allowAdaptiveSelections =*/ false,
|
||||
/* allowMultipleOverrides= */ true,
|
||||
/* onClickListener= */ this,
|
||||
/* onDismissListener= */ this);
|
||||
trackSelectionDialog.show(fragmentManager, /* tag= */ null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether any the {@link DrmInitData.SchemeData} contained in {@code drmInitData} has
|
||||
* non-null {@link DrmInitData.SchemeData#data}.
|
||||
*/
|
||||
private boolean hasSchemaData(DrmInitData drmInitData) {
|
||||
for (int i = 0; i < drmInitData.schemeDataCount; i++) {
|
||||
if (drmInitData.get(i).hasData()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private void startDownload() {
|
||||
startDownload(buildDownloadRequest());
|
||||
}
|
||||
|
|
@ -275,8 +362,62 @@ public class DownloadTracker {
|
|||
}
|
||||
|
||||
private DownloadRequest buildDownloadRequest() {
|
||||
return downloadHelper.getDownloadRequest(
|
||||
Util.getUtf8Bytes(checkNotNull(mediaItem.mediaMetadata.title)));
|
||||
return downloadHelper
|
||||
.getDownloadRequest(Util.getUtf8Bytes(checkNotNull(mediaItem.mediaMetadata.title)))
|
||||
.copyWithKeySetId(keySetId);
|
||||
}
|
||||
}
|
||||
|
||||
/** Downloads a Widevine offline license in a background thread. */
|
||||
@RequiresApi(18)
|
||||
private static final class WidevineOfflineLicenseFetchTask extends AsyncTask<Void, Void, Void> {
|
||||
|
||||
private final Format format;
|
||||
private final Uri licenseUri;
|
||||
private final HttpDataSource.Factory httpDataSourceFactory;
|
||||
private final StartDownloadDialogHelper dialogHelper;
|
||||
private final DownloadHelper downloadHelper;
|
||||
|
||||
@Nullable private byte[] keySetId;
|
||||
@Nullable private DrmSession.DrmSessionException drmSessionException;
|
||||
|
||||
public WidevineOfflineLicenseFetchTask(
|
||||
Format format,
|
||||
Uri licenseUri,
|
||||
HttpDataSource.Factory httpDataSourceFactory,
|
||||
StartDownloadDialogHelper dialogHelper,
|
||||
DownloadHelper downloadHelper) {
|
||||
this.format = format;
|
||||
this.licenseUri = licenseUri;
|
||||
this.httpDataSourceFactory = httpDataSourceFactory;
|
||||
this.dialogHelper = dialogHelper;
|
||||
this.downloadHelper = downloadHelper;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Void doInBackground(Void... voids) {
|
||||
OfflineLicenseHelper offlineLicenseHelper =
|
||||
OfflineLicenseHelper.newWidevineInstance(
|
||||
licenseUri.toString(),
|
||||
httpDataSourceFactory,
|
||||
new DrmSessionEventListener.EventDispatcher());
|
||||
try {
|
||||
keySetId = offlineLicenseHelper.downloadLicense(format);
|
||||
} catch (DrmSession.DrmSessionException e) {
|
||||
drmSessionException = e;
|
||||
} finally {
|
||||
offlineLicenseHelper.release();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(Void aVoid) {
|
||||
if (drmSessionException != null) {
|
||||
dialogHelper.onOfflineLicenseFetchedError(drmSessionException);
|
||||
} else {
|
||||
dialogHelper.onOfflineLicenseFetched(downloadHelper, checkStateNotNull(keySetId));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,35 +23,18 @@ import android.net.Uri;
|
|||
import androidx.annotation.Nullable;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.MediaItem;
|
||||
import com.google.android.exoplayer2.offline.DownloadRequest;
|
||||
import com.google.android.exoplayer2.util.Assertions;
|
||||
import com.google.android.exoplayer2.util.MimeTypes;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/** Util to read from and populate an intent. */
|
||||
public class IntentUtil {
|
||||
|
||||
/** A tag to hold custom playback configuration attributes. */
|
||||
public static class Tag {
|
||||
|
||||
/** Whether the stream is a live stream. */
|
||||
public final boolean isLive;
|
||||
/** The spherical stereo mode or null. */
|
||||
@Nullable public final String sphericalStereoMode;
|
||||
|
||||
/** Creates an instance. */
|
||||
public Tag(boolean isLive, @Nullable String sphericalStereoMode) {
|
||||
this.isLive = isLive;
|
||||
this.sphericalStereoMode = sphericalStereoMode;
|
||||
}
|
||||
}
|
||||
|
||||
// Actions.
|
||||
|
||||
public static final String ACTION_VIEW = "com.google.android.exoplayer.demo.action.VIEW";
|
||||
|
|
@ -60,61 +43,41 @@ public class IntentUtil {
|
|||
|
||||
// Activity extras.
|
||||
|
||||
public static final String SPHERICAL_STEREO_MODE_EXTRA = "spherical_stereo_mode";
|
||||
public static final String SPHERICAL_STEREO_MODE_MONO = "mono";
|
||||
public static final String SPHERICAL_STEREO_MODE_TOP_BOTTOM = "top_bottom";
|
||||
public static final String SPHERICAL_STEREO_MODE_LEFT_RIGHT = "left_right";
|
||||
|
||||
// Player configuration extras.
|
||||
|
||||
public static final String ABR_ALGORITHM_EXTRA = "abr_algorithm";
|
||||
public static final String ABR_ALGORITHM_DEFAULT = "default";
|
||||
public static final String ABR_ALGORITHM_RANDOM = "random";
|
||||
public static final String PREFER_EXTENSION_DECODERS_EXTRA = "prefer_extension_decoders";
|
||||
|
||||
// Media item configuration extras.
|
||||
|
||||
public static final String URI_EXTRA = "uri";
|
||||
public static final String IS_LIVE_EXTRA = "is_live";
|
||||
public static final String MIME_TYPE_EXTRA = "mime_type";
|
||||
// For backwards compatibility only.
|
||||
public static final String EXTENSION_EXTRA = "extension";
|
||||
public static final String CLIP_START_POSITION_MS_EXTRA = "clip_start_position_ms";
|
||||
public static final String CLIP_END_POSITION_MS_EXTRA = "clip_end_position_ms";
|
||||
|
||||
public static final String AD_TAG_URI_EXTRA = "ad_tag_uri";
|
||||
|
||||
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_LICENSE_URI_EXTRA = "drm_license_uri";
|
||||
public static final String DRM_KEY_REQUEST_PROPERTIES_EXTRA = "drm_key_request_properties";
|
||||
public static final String DRM_SESSION_FOR_CLEAR_TYPES_EXTRA = "drm_session_for_clear_types";
|
||||
public static final String DRM_SESSION_FOR_CLEAR_CONTENT = "drm_session_for_clear_content";
|
||||
public static final String DRM_MULTI_SESSION_EXTRA = "drm_multi_session";
|
||||
public static final String AD_TAG_URI_EXTRA = "ad_tag_uri";
|
||||
public static final String DRM_FORCE_DEFAULT_LICENSE_URI_EXTRA = "drm_force_default_license_uri";
|
||||
|
||||
public static final String SUBTITLE_URI_EXTRA = "subtitle_uri";
|
||||
public static final String SUBTITLE_MIME_TYPE_EXTRA = "subtitle_mime_type";
|
||||
public static final String SUBTITLE_LANGUAGE_EXTRA = "subtitle_language";
|
||||
// For backwards compatibility only.
|
||||
public static final String DRM_SCHEME_UUID_EXTRA = "drm_scheme_uuid";
|
||||
|
||||
public static final String PREFER_EXTENSION_DECODERS_EXTRA = "prefer_extension_decoders";
|
||||
public static final String TUNNELING_EXTRA = "tunneling";
|
||||
|
||||
/** Creates a list of {@link MediaItem media items} from an {@link Intent}. */
|
||||
public static List<MediaItem> createMediaItemsFromIntent(
|
||||
Intent intent, DownloadTracker downloadTracker) {
|
||||
public static List<MediaItem> createMediaItemsFromIntent(Intent intent) {
|
||||
List<MediaItem> mediaItems = new ArrayList<>();
|
||||
if (ACTION_VIEW_LIST.equals(intent.getAction())) {
|
||||
int index = 0;
|
||||
while (intent.hasExtra(URI_EXTRA + "_" + index)) {
|
||||
Uri uri = Uri.parse(intent.getStringExtra(URI_EXTRA + "_" + index));
|
||||
mediaItems.add(
|
||||
createMediaItemFromIntent(
|
||||
uri,
|
||||
intent,
|
||||
/* extrasKeySuffix= */ "_" + index,
|
||||
downloadTracker.getDownloadRequest(uri)));
|
||||
mediaItems.add(createMediaItemFromIntent(uri, intent, /* extrasKeySuffix= */ "_" + index));
|
||||
index++;
|
||||
}
|
||||
} else {
|
||||
Uri uri = intent.getData();
|
||||
mediaItems.add(
|
||||
createMediaItemFromIntent(
|
||||
uri, intent, /* extrasKeySuffix= */ "", downloadTracker.getDownloadRequest(uri)));
|
||||
mediaItems.add(createMediaItemFromIntent(uri, intent, /* extrasKeySuffix= */ ""));
|
||||
}
|
||||
return mediaItems;
|
||||
}
|
||||
|
|
@ -123,54 +86,41 @@ public class IntentUtil {
|
|||
public static void addToIntent(List<MediaItem> mediaItems, Intent intent) {
|
||||
Assertions.checkArgument(!mediaItems.isEmpty());
|
||||
if (mediaItems.size() == 1) {
|
||||
MediaItem.PlaybackProperties playbackProperties =
|
||||
checkNotNull(mediaItems.get(0).playbackProperties);
|
||||
intent.setAction(IntentUtil.ACTION_VIEW).setData(playbackProperties.uri);
|
||||
MediaItem mediaItem = mediaItems.get(0);
|
||||
MediaItem.PlaybackProperties playbackProperties = checkNotNull(mediaItem.playbackProperties);
|
||||
intent.setAction(ACTION_VIEW).setData(mediaItem.playbackProperties.uri);
|
||||
addPlaybackPropertiesToIntent(playbackProperties, intent, /* extrasKeySuffix= */ "");
|
||||
addClippingPropertiesToIntent(
|
||||
mediaItem.clippingProperties, intent, /* extrasKeySuffix= */ "");
|
||||
} else {
|
||||
intent.setAction(IntentUtil.ACTION_VIEW_LIST);
|
||||
intent.setAction(ACTION_VIEW_LIST);
|
||||
for (int i = 0; i < mediaItems.size(); i++) {
|
||||
MediaItem mediaItem = mediaItems.get(i);
|
||||
MediaItem.PlaybackProperties playbackProperties =
|
||||
checkNotNull(mediaItems.get(i).playbackProperties);
|
||||
intent.putExtra(IntentUtil.URI_EXTRA + ("_" + i), playbackProperties.uri.toString());
|
||||
checkNotNull(mediaItem.playbackProperties);
|
||||
intent.putExtra(URI_EXTRA + ("_" + i), playbackProperties.uri.toString());
|
||||
addPlaybackPropertiesToIntent(playbackProperties, intent, /* extrasKeySuffix= */ "_" + i);
|
||||
addClippingPropertiesToIntent(
|
||||
mediaItem.clippingProperties, intent, /* extrasKeySuffix= */ "_" + i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Makes a best guess to infer the MIME type from a {@link Uri} and an optional extension. */
|
||||
@Nullable
|
||||
public static String inferAdaptiveStreamMimeType(Uri uri, @Nullable String extension) {
|
||||
@C.ContentType int contentType = Util.inferContentType(uri, extension);
|
||||
switch (contentType) {
|
||||
case C.TYPE_DASH:
|
||||
return MimeTypes.APPLICATION_MPD;
|
||||
case C.TYPE_HLS:
|
||||
return MimeTypes.APPLICATION_M3U8;
|
||||
case C.TYPE_SS:
|
||||
return MimeTypes.APPLICATION_SS;
|
||||
case C.TYPE_OTHER:
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static MediaItem createMediaItemFromIntent(
|
||||
Uri uri, Intent intent, String extrasKeySuffix, @Nullable DownloadRequest downloadRequest) {
|
||||
String mimeType = intent.getStringExtra(MIME_TYPE_EXTRA + extrasKeySuffix);
|
||||
if (mimeType == null) {
|
||||
// Try to use extension for backwards compatibility.
|
||||
String extension = intent.getStringExtra(EXTENSION_EXTRA + extrasKeySuffix);
|
||||
mimeType = inferAdaptiveStreamMimeType(uri, extension);
|
||||
}
|
||||
Uri uri, Intent intent, String extrasKeySuffix) {
|
||||
@Nullable String mimeType = intent.getStringExtra(MIME_TYPE_EXTRA + extrasKeySuffix);
|
||||
MediaItem.Builder builder =
|
||||
new MediaItem.Builder()
|
||||
.setUri(uri)
|
||||
.setStreamKeys(downloadRequest != null ? downloadRequest.streamKeys : null)
|
||||
.setCustomCacheKey(downloadRequest != null ? downloadRequest.customCacheKey : null)
|
||||
.setMimeType(mimeType)
|
||||
.setAdTagUri(intent.getStringExtra(AD_TAG_URI_EXTRA + extrasKeySuffix))
|
||||
.setSubtitles(createSubtitlesFromIntent(intent, extrasKeySuffix));
|
||||
.setSubtitles(createSubtitlesFromIntent(intent, extrasKeySuffix))
|
||||
.setClipStartPositionMs(
|
||||
intent.getLongExtra(CLIP_START_POSITION_MS_EXTRA + extrasKeySuffix, 0))
|
||||
.setClipEndPositionMs(
|
||||
intent.getLongExtra(
|
||||
CLIP_END_POSITION_MS_EXTRA + extrasKeySuffix, C.TIME_END_OF_SOURCE));
|
||||
|
||||
return populateDrmPropertiesFromIntent(builder, intent, extrasKeySuffix).build();
|
||||
}
|
||||
|
||||
|
|
@ -190,17 +140,12 @@ public class IntentUtil {
|
|||
private static MediaItem.Builder populateDrmPropertiesFromIntent(
|
||||
MediaItem.Builder builder, Intent intent, String extrasKeySuffix) {
|
||||
String schemeKey = DRM_SCHEME_EXTRA + extrasKeySuffix;
|
||||
String schemeUuidKey = DRM_SCHEME_UUID_EXTRA + extrasKeySuffix;
|
||||
if (!intent.hasExtra(schemeKey) && !intent.hasExtra(schemeUuidKey)) {
|
||||
@Nullable String drmSchemeExtra = intent.getStringExtra(schemeKey);
|
||||
if (drmSchemeExtra == null) {
|
||||
return builder;
|
||||
}
|
||||
String drmSchemeExtra =
|
||||
intent.hasExtra(schemeKey)
|
||||
? intent.getStringExtra(schemeKey)
|
||||
: intent.getStringExtra(schemeUuidKey);
|
||||
String[] drmSessionForClearTypesExtra =
|
||||
intent.getStringArrayExtra(DRM_SESSION_FOR_CLEAR_TYPES_EXTRA + extrasKeySuffix);
|
||||
Map<String, String> headers = new HashMap<>();
|
||||
@Nullable
|
||||
String[] keyRequestPropertiesArray =
|
||||
intent.getStringArrayExtra(DRM_KEY_REQUEST_PROPERTIES_EXTRA + extrasKeySuffix);
|
||||
if (keyRequestPropertiesArray != null) {
|
||||
|
|
@ -210,50 +155,25 @@ public class IntentUtil {
|
|||
}
|
||||
builder
|
||||
.setDrmUuid(Util.getDrmUuid(Util.castNonNull(drmSchemeExtra)))
|
||||
.setDrmLicenseUri(intent.getStringExtra(DRM_LICENSE_URL_EXTRA + extrasKeySuffix))
|
||||
.setDrmSessionForClearTypes(toTrackTypeList(drmSessionForClearTypesExtra))
|
||||
.setDrmLicenseUri(intent.getStringExtra(DRM_LICENSE_URI_EXTRA + extrasKeySuffix))
|
||||
.setDrmMultiSession(
|
||||
intent.getBooleanExtra(DRM_MULTI_SESSION_EXTRA + extrasKeySuffix, false))
|
||||
.setDrmForceDefaultLicenseUri(
|
||||
intent.getBooleanExtra(DRM_FORCE_DEFAULT_LICENSE_URI_EXTRA + extrasKeySuffix, false))
|
||||
.setDrmLicenseRequestHeaders(headers);
|
||||
if (intent.getBooleanExtra(DRM_SESSION_FOR_CLEAR_CONTENT + extrasKeySuffix, false)) {
|
||||
builder.setDrmSessionForClearTypes(ImmutableList.of(C.TRACK_TYPE_VIDEO, C.TRACK_TYPE_AUDIO));
|
||||
}
|
||||
return builder;
|
||||
}
|
||||
|
||||
private static List<Integer> toTrackTypeList(@Nullable String[] trackTypeStringsArray) {
|
||||
if (trackTypeStringsArray == null) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
HashSet<Integer> trackTypes = new HashSet<>();
|
||||
for (String trackTypeString : trackTypeStringsArray) {
|
||||
switch (Util.toLowerInvariant(trackTypeString)) {
|
||||
case "audio":
|
||||
trackTypes.add(C.TRACK_TYPE_AUDIO);
|
||||
break;
|
||||
case "video":
|
||||
trackTypes.add(C.TRACK_TYPE_VIDEO);
|
||||
break;
|
||||
default:
|
||||
throw new IllegalArgumentException("Invalid track type: " + trackTypeString);
|
||||
}
|
||||
}
|
||||
return new ArrayList<>(trackTypes);
|
||||
}
|
||||
|
||||
private static void addPlaybackPropertiesToIntent(
|
||||
MediaItem.PlaybackProperties playbackProperties, Intent intent, String extrasKeySuffix) {
|
||||
boolean isLive = false;
|
||||
String sphericalStereoMode = null;
|
||||
if (playbackProperties.tag instanceof Tag) {
|
||||
Tag tag = (Tag) playbackProperties.tag;
|
||||
isLive = tag.isLive;
|
||||
sphericalStereoMode = tag.sphericalStereoMode;
|
||||
}
|
||||
intent
|
||||
.putExtra(MIME_TYPE_EXTRA + extrasKeySuffix, playbackProperties.mimeType)
|
||||
.putExtra(
|
||||
AD_TAG_URI_EXTRA + extrasKeySuffix,
|
||||
playbackProperties.adTagUri != null ? playbackProperties.adTagUri.toString() : null)
|
||||
.putExtra(IS_LIVE_EXTRA + extrasKeySuffix, isLive)
|
||||
.putExtra(SPHERICAL_STEREO_MODE_EXTRA, sphericalStereoMode);
|
||||
playbackProperties.adTagUri != null ? playbackProperties.adTagUri.toString() : null);
|
||||
if (playbackProperties.drmConfiguration != null) {
|
||||
addDrmConfigurationToIntent(playbackProperties.drmConfiguration, intent, extrasKeySuffix);
|
||||
}
|
||||
|
|
@ -270,9 +190,12 @@ public class IntentUtil {
|
|||
MediaItem.DrmConfiguration drmConfiguration, Intent intent, String extrasKeySuffix) {
|
||||
intent.putExtra(DRM_SCHEME_EXTRA + extrasKeySuffix, drmConfiguration.uuid.toString());
|
||||
intent.putExtra(
|
||||
DRM_LICENSE_URL_EXTRA + extrasKeySuffix,
|
||||
checkNotNull(drmConfiguration.licenseUri).toString());
|
||||
DRM_LICENSE_URI_EXTRA + extrasKeySuffix,
|
||||
drmConfiguration.licenseUri != null ? drmConfiguration.licenseUri.toString() : null);
|
||||
intent.putExtra(DRM_MULTI_SESSION_EXTRA + extrasKeySuffix, drmConfiguration.multiSession);
|
||||
intent.putExtra(
|
||||
DRM_FORCE_DEFAULT_LICENSE_URI_EXTRA + extrasKeySuffix,
|
||||
drmConfiguration.forceDefaultLicenseUri);
|
||||
|
||||
String[] drmKeyRequestProperties = new String[drmConfiguration.requestHeaders.size() * 2];
|
||||
int index = 0;
|
||||
|
|
@ -282,13 +205,26 @@ public class IntentUtil {
|
|||
}
|
||||
intent.putExtra(DRM_KEY_REQUEST_PROPERTIES_EXTRA + extrasKeySuffix, drmKeyRequestProperties);
|
||||
|
||||
ArrayList<String> typeStrings = new ArrayList<>();
|
||||
for (int type : drmConfiguration.sessionForClearTypes) {
|
||||
// Only audio and video are supported.
|
||||
Assertions.checkState(type == C.TRACK_TYPE_AUDIO || type == C.TRACK_TYPE_VIDEO);
|
||||
typeStrings.add(type == C.TRACK_TYPE_AUDIO ? "audio" : "video");
|
||||
List<Integer> drmSessionForClearTypes = drmConfiguration.sessionForClearTypes;
|
||||
if (!drmSessionForClearTypes.isEmpty()) {
|
||||
// Only video and audio together are supported.
|
||||
Assertions.checkState(
|
||||
drmSessionForClearTypes.size() == 2
|
||||
&& drmSessionForClearTypes.contains(C.TRACK_TYPE_VIDEO)
|
||||
&& drmSessionForClearTypes.contains(C.TRACK_TYPE_AUDIO));
|
||||
intent.putExtra(DRM_SESSION_FOR_CLEAR_CONTENT + extrasKeySuffix, true);
|
||||
}
|
||||
}
|
||||
|
||||
private static void addClippingPropertiesToIntent(
|
||||
MediaItem.ClippingProperties clippingProperties, Intent intent, String extrasKeySuffix) {
|
||||
if (clippingProperties.startPositionMs != 0) {
|
||||
intent.putExtra(
|
||||
CLIP_START_POSITION_MS_EXTRA + extrasKeySuffix, clippingProperties.startPositionMs);
|
||||
}
|
||||
if (clippingProperties.endPositionMs != C.TIME_END_OF_SOURCE) {
|
||||
intent.putExtra(
|
||||
CLIP_END_POSITION_MS_EXTRA + extrasKeySuffix, clippingProperties.endPositionMs);
|
||||
}
|
||||
intent.putExtra(
|
||||
DRM_SESSION_FOR_CLEAR_TYPES_EXTRA + extrasKeySuffix, typeStrings.toArray(new String[0]));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,9 +15,10 @@
|
|||
*/
|
||||
package com.google.android.exoplayer2.demo;
|
||||
|
||||
import static com.google.android.exoplayer2.util.Assertions.checkNotNull;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.media.MediaDrm;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.util.Pair;
|
||||
|
|
@ -39,37 +40,36 @@ import com.google.android.exoplayer2.Player;
|
|||
import com.google.android.exoplayer2.RenderersFactory;
|
||||
import com.google.android.exoplayer2.SimpleExoPlayer;
|
||||
import com.google.android.exoplayer2.audio.AudioAttributes;
|
||||
import com.google.android.exoplayer2.drm.FrameworkMediaDrm;
|
||||
import com.google.android.exoplayer2.ext.ima.ImaAdsLoader;
|
||||
import com.google.android.exoplayer2.mediacodec.MediaCodecRenderer.DecoderInitializationException;
|
||||
import com.google.android.exoplayer2.mediacodec.MediaCodecUtil.DecoderQueryException;
|
||||
import com.google.android.exoplayer2.offline.DownloadRequest;
|
||||
import com.google.android.exoplayer2.source.BehindLiveWindowException;
|
||||
import com.google.android.exoplayer2.source.DefaultMediaSourceFactory;
|
||||
import com.google.android.exoplayer2.source.MediaSourceFactory;
|
||||
import com.google.android.exoplayer2.source.TrackGroupArray;
|
||||
import com.google.android.exoplayer2.source.ads.AdsLoader;
|
||||
import com.google.android.exoplayer2.trackselection.AdaptiveTrackSelection;
|
||||
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
|
||||
import com.google.android.exoplayer2.trackselection.MappingTrackSelector.MappedTrackInfo;
|
||||
import com.google.android.exoplayer2.trackselection.RandomTrackSelection;
|
||||
import com.google.android.exoplayer2.trackselection.TrackSelection;
|
||||
import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
|
||||
import com.google.android.exoplayer2.ui.DebugTextViewHelper;
|
||||
import com.google.android.exoplayer2.ui.PlayerControlView;
|
||||
import com.google.android.exoplayer2.ui.PlayerView;
|
||||
import com.google.android.exoplayer2.ui.spherical.SphericalGLSurfaceView;
|
||||
import com.google.android.exoplayer2.ui.StyledPlayerControlView;
|
||||
import com.google.android.exoplayer2.ui.StyledPlayerView;
|
||||
import com.google.android.exoplayer2.upstream.DataSource;
|
||||
import com.google.android.exoplayer2.util.Assertions;
|
||||
import com.google.android.exoplayer2.util.ErrorMessageProvider;
|
||||
import com.google.android.exoplayer2.util.EventLogger;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
import java.lang.reflect.Constructor;
|
||||
import java.net.CookieHandler;
|
||||
import java.net.CookieManager;
|
||||
import java.net.CookiePolicy;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
/** An activity that plays media using {@link SimpleExoPlayer}. */
|
||||
public class PlayerActivity extends AppCompatActivity
|
||||
implements OnClickListener, PlaybackPreparer, PlayerControlView.VisibilityListener {
|
||||
implements OnClickListener, PlaybackPreparer, StyledPlayerControlView.VisibilityListener {
|
||||
|
||||
// Saved instance state keys.
|
||||
|
||||
|
|
@ -85,14 +85,14 @@ public class PlayerActivity extends AppCompatActivity
|
|||
DEFAULT_COOKIE_MANAGER.setCookiePolicy(CookiePolicy.ACCEPT_ORIGINAL_SERVER);
|
||||
}
|
||||
|
||||
private PlayerView playerView;
|
||||
private LinearLayout debugRootView;
|
||||
private Button selectTracksButton;
|
||||
private TextView debugTextView;
|
||||
private boolean isShowingTrackSelectionDialog;
|
||||
protected StyledPlayerView playerView;
|
||||
protected LinearLayout debugRootView;
|
||||
protected TextView debugTextView;
|
||||
protected SimpleExoPlayer player;
|
||||
|
||||
private boolean isShowingTrackSelectionDialog;
|
||||
private Button selectTracksButton;
|
||||
private DataSource.Factory dataSourceFactory;
|
||||
private SimpleExoPlayer player;
|
||||
private List<MediaItem> mediaItems;
|
||||
private DefaultTrackSelector trackSelector;
|
||||
private DefaultTrackSelector.Parameters trackSelectorParameters;
|
||||
|
|
@ -111,18 +111,13 @@ public class PlayerActivity extends AppCompatActivity
|
|||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
Intent intent = getIntent();
|
||||
String sphericalStereoMode = intent.getStringExtra(IntentUtil.SPHERICAL_STEREO_MODE_EXTRA);
|
||||
if (sphericalStereoMode != null) {
|
||||
setTheme(R.style.PlayerTheme_Spherical);
|
||||
}
|
||||
super.onCreate(savedInstanceState);
|
||||
dataSourceFactory = buildDataSourceFactory();
|
||||
dataSourceFactory = DemoUtil.getDataSourceFactory(/* context= */ this);
|
||||
if (CookieHandler.getDefault() != DEFAULT_COOKIE_MANAGER) {
|
||||
CookieHandler.setDefault(DEFAULT_COOKIE_MANAGER);
|
||||
}
|
||||
|
||||
setContentView(R.layout.player_activity);
|
||||
setContentView();
|
||||
debugRootView = findViewById(R.id.controls_root);
|
||||
debugTextView = findViewById(R.id.debug_text_view);
|
||||
selectTracksButton = findViewById(R.id.select_tracks_button);
|
||||
|
|
@ -132,21 +127,6 @@ public class PlayerActivity extends AppCompatActivity
|
|||
playerView.setControllerVisibilityListener(this);
|
||||
playerView.setErrorMessageProvider(new PlayerErrorMessageProvider());
|
||||
playerView.requestFocus();
|
||||
if (sphericalStereoMode != null) {
|
||||
int stereoMode;
|
||||
if (IntentUtil.SPHERICAL_STEREO_MODE_MONO.equals(sphericalStereoMode)) {
|
||||
stereoMode = C.STEREO_MODE_MONO;
|
||||
} else if (IntentUtil.SPHERICAL_STEREO_MODE_TOP_BOTTOM.equals(sphericalStereoMode)) {
|
||||
stereoMode = C.STEREO_MODE_TOP_BOTTOM;
|
||||
} else if (IntentUtil.SPHERICAL_STEREO_MODE_LEFT_RIGHT.equals(sphericalStereoMode)) {
|
||||
stereoMode = C.STEREO_MODE_LEFT_RIGHT;
|
||||
} else {
|
||||
showToast(R.string.error_unrecognized_stereo_mode);
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
((SphericalGLSurfaceView) playerView.getVideoSurfaceView()).setDefaultStereoMode(stereoMode);
|
||||
}
|
||||
|
||||
if (savedInstanceState != null) {
|
||||
trackSelectorParameters = savedInstanceState.getParcelable(KEY_TRACK_SELECTOR_PARAMETERS);
|
||||
|
|
@ -156,10 +136,6 @@ public class PlayerActivity extends AppCompatActivity
|
|||
} else {
|
||||
DefaultTrackSelector.ParametersBuilder builder =
|
||||
new DefaultTrackSelector.ParametersBuilder(/* context= */ this);
|
||||
boolean tunneling = intent.getBooleanExtra(IntentUtil.TUNNELING_EXTRA, false);
|
||||
if (Util.SDK_INT >= 21 && tunneling) {
|
||||
builder.setTunnelingAudioSessionId(C.generateAudioSessionIdV21(/* context= */ this));
|
||||
}
|
||||
trackSelectorParameters = builder.build();
|
||||
clearStartPosition();
|
||||
}
|
||||
|
|
@ -276,14 +252,14 @@ public class PlayerActivity extends AppCompatActivity
|
|||
}
|
||||
}
|
||||
|
||||
// PlaybackControlView.PlaybackPreparer implementation
|
||||
// PlaybackPreparer implementation
|
||||
|
||||
@Override
|
||||
public void preparePlayback() {
|
||||
player.prepare();
|
||||
}
|
||||
|
||||
// PlaybackControlView.VisibilityListener implementation
|
||||
// PlayerControlView.VisibilityListener implementation
|
||||
|
||||
@Override
|
||||
public void onVisibilityChange(int visibility) {
|
||||
|
|
@ -292,47 +268,41 @@ public class PlayerActivity extends AppCompatActivity
|
|||
|
||||
// Internal methods
|
||||
|
||||
private void initializePlayer() {
|
||||
protected void setContentView() {
|
||||
setContentView(R.layout.player_activity);
|
||||
}
|
||||
|
||||
/** @return Whether initialization was successful. */
|
||||
protected boolean initializePlayer() {
|
||||
if (player == null) {
|
||||
Intent intent = getIntent();
|
||||
|
||||
mediaItems = createMediaItems(intent);
|
||||
if (mediaItems.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
TrackSelection.Factory trackSelectionFactory;
|
||||
String abrAlgorithm = intent.getStringExtra(IntentUtil.ABR_ALGORITHM_EXTRA);
|
||||
if (abrAlgorithm == null || IntentUtil.ABR_ALGORITHM_DEFAULT.equals(abrAlgorithm)) {
|
||||
trackSelectionFactory = new AdaptiveTrackSelection.Factory();
|
||||
} else if (IntentUtil.ABR_ALGORITHM_RANDOM.equals(abrAlgorithm)) {
|
||||
trackSelectionFactory = new RandomTrackSelection.Factory();
|
||||
} else {
|
||||
showToast(R.string.error_unrecognized_abr_algorithm);
|
||||
finish();
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
boolean preferExtensionDecoders =
|
||||
intent.getBooleanExtra(IntentUtil.PREFER_EXTENSION_DECODERS_EXTRA, false);
|
||||
RenderersFactory renderersFactory =
|
||||
((DemoApplication) getApplication()).buildRenderersFactory(preferExtensionDecoders);
|
||||
DemoUtil.buildRenderersFactory(/* context= */ this, preferExtensionDecoders);
|
||||
MediaSourceFactory mediaSourceFactory =
|
||||
new DefaultMediaSourceFactory(dataSourceFactory)
|
||||
.setAdsLoaderProvider(this::getAdsLoader)
|
||||
.setAdViewProvider(playerView);
|
||||
|
||||
trackSelector = new DefaultTrackSelector(/* context= */ this, trackSelectionFactory);
|
||||
trackSelector = new DefaultTrackSelector(/* context= */ this);
|
||||
trackSelector.setParameters(trackSelectorParameters);
|
||||
lastSeenTrackGroupArray = null;
|
||||
|
||||
player =
|
||||
new SimpleExoPlayer.Builder(/* context= */ this, renderersFactory)
|
||||
.setMediaSourceFactory(
|
||||
new DefaultMediaSourceFactory(
|
||||
/* context= */ this, dataSourceFactory, new AdSupportProvider()))
|
||||
.setMediaSourceFactory(mediaSourceFactory)
|
||||
.setTrackSelector(trackSelector)
|
||||
.build();
|
||||
player.addListener(new PlayerEventListener());
|
||||
player.addAnalyticsListener(new EventLogger(trackSelector));
|
||||
player.setAudioAttributes(AudioAttributes.DEFAULT, /* handleAudioFocus= */ true);
|
||||
player.setPlayWhenReady(startAutoPlay);
|
||||
player.addAnalyticsListener(new EventLogger(trackSelector));
|
||||
playerView.setPlayer(player);
|
||||
playerView.setPlaybackPreparer(this);
|
||||
debugViewHelper = new DebugTextViewHelper(player, debugTextView);
|
||||
|
|
@ -345,6 +315,7 @@ public class PlayerActivity extends AppCompatActivity
|
|||
player.setMediaItems(mediaItems, /* resetPosition= */ !haveStartPosition);
|
||||
player.prepare();
|
||||
updateButtonVisibility();
|
||||
return true;
|
||||
}
|
||||
|
||||
private List<MediaItem> createMediaItems(Intent intent) {
|
||||
|
|
@ -357,8 +328,7 @@ public class PlayerActivity extends AppCompatActivity
|
|||
}
|
||||
|
||||
List<MediaItem> mediaItems =
|
||||
IntentUtil.createMediaItemsFromIntent(
|
||||
intent, ((DemoApplication) getApplication()).getDownloadTracker());
|
||||
createMediaItems(intent, DemoUtil.getDownloadTracker(/* context= */ this));
|
||||
boolean hasAds = false;
|
||||
for (int i = 0; i < mediaItems.size(); i++) {
|
||||
MediaItem mediaItem = mediaItems.get(i);
|
||||
|
|
@ -373,13 +343,13 @@ public class PlayerActivity extends AppCompatActivity
|
|||
}
|
||||
|
||||
MediaItem.DrmConfiguration drmConfiguration =
|
||||
Assertions.checkNotNull(mediaItem.playbackProperties).drmConfiguration;
|
||||
checkNotNull(mediaItem.playbackProperties).drmConfiguration;
|
||||
if (drmConfiguration != null) {
|
||||
if (Util.SDK_INT < 18) {
|
||||
showToast(R.string.error_drm_unsupported_before_api_18);
|
||||
finish();
|
||||
return Collections.emptyList();
|
||||
} else if (!MediaDrm.isCryptoSchemeSupported(drmConfiguration.uuid)) {
|
||||
} else if (!FrameworkMediaDrm.isCryptoSchemeSupported(drmConfiguration.uuid)) {
|
||||
showToast(R.string.error_drm_unsupported_scheme);
|
||||
finish();
|
||||
return Collections.emptyList();
|
||||
|
|
@ -393,7 +363,25 @@ public class PlayerActivity extends AppCompatActivity
|
|||
return mediaItems;
|
||||
}
|
||||
|
||||
private void releasePlayer() {
|
||||
private AdsLoader getAdsLoader(Uri adTagUri) {
|
||||
if (mediaItems.size() > 1) {
|
||||
showToast(R.string.unsupported_ads_in_playlist);
|
||||
releaseAdsLoader();
|
||||
return null;
|
||||
}
|
||||
if (!adTagUri.equals(loadedAdTagUri)) {
|
||||
releaseAdsLoader();
|
||||
loadedAdTagUri = adTagUri;
|
||||
}
|
||||
// The ads loader is reused for multiple playbacks, so that ad playback can resume.
|
||||
if (adsLoader == null) {
|
||||
adsLoader = new ImaAdsLoader(/* context= */ PlayerActivity.this, adTagUri);
|
||||
}
|
||||
adsLoader.setPlayer(player);
|
||||
return adsLoader;
|
||||
}
|
||||
|
||||
protected void releasePlayer() {
|
||||
if (player != null) {
|
||||
updateTrackSelectorParameters();
|
||||
updateStartPosition();
|
||||
|
|
@ -432,42 +420,12 @@ public class PlayerActivity extends AppCompatActivity
|
|||
}
|
||||
}
|
||||
|
||||
private void clearStartPosition() {
|
||||
protected void clearStartPosition() {
|
||||
startAutoPlay = true;
|
||||
startWindow = C.INDEX_UNSET;
|
||||
startPosition = C.TIME_UNSET;
|
||||
}
|
||||
|
||||
/** Returns a new DataSource factory. */
|
||||
private DataSource.Factory buildDataSourceFactory() {
|
||||
return ((DemoApplication) getApplication()).buildDataSourceFactory();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an ads loader for the Interactive Media Ads SDK if found in the classpath, or null
|
||||
* otherwise.
|
||||
*/
|
||||
@Nullable
|
||||
private AdsLoader maybeCreateAdsLoader(Uri adTagUri) {
|
||||
// Load the extension source using reflection so the demo app doesn't have to depend on it.
|
||||
try {
|
||||
Class<?> loaderClass = Class.forName("com.google.android.exoplayer2.ext.ima.ImaAdsLoader");
|
||||
// Full class names used so the lint 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)
|
||||
return loaderConstructor.newInstance(this, adTagUri);
|
||||
} catch (ClassNotFoundException e) {
|
||||
// IMA extension not loaded.
|
||||
return null;
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
// User controls
|
||||
|
||||
private void updateButtonVisibility() {
|
||||
|
|
@ -579,35 +537,26 @@ public class PlayerActivity extends AppCompatActivity
|
|||
}
|
||||
}
|
||||
|
||||
private class AdSupportProvider implements DefaultMediaSourceFactory.AdSupportProvider {
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public AdsLoader getAdsLoader(Uri adTagUri) {
|
||||
if (mediaItems.size() > 1) {
|
||||
showToast(R.string.unsupported_ads_in_concatenation);
|
||||
releaseAdsLoader();
|
||||
return null;
|
||||
}
|
||||
if (!adTagUri.equals(loadedAdTagUri)) {
|
||||
releaseAdsLoader();
|
||||
loadedAdTagUri = adTagUri;
|
||||
}
|
||||
// The ads loader is reused for multiple playbacks, so that ad playback can resume.
|
||||
if (adsLoader == null) {
|
||||
adsLoader = maybeCreateAdsLoader(adTagUri);
|
||||
}
|
||||
if (adsLoader != null) {
|
||||
adsLoader.setPlayer(player);
|
||||
private static List<MediaItem> createMediaItems(Intent intent, DownloadTracker downloadTracker) {
|
||||
List<MediaItem> mediaItems = new ArrayList<>();
|
||||
for (MediaItem item : IntentUtil.createMediaItemsFromIntent(intent)) {
|
||||
@Nullable
|
||||
DownloadRequest downloadRequest =
|
||||
downloadTracker.getDownloadRequest(checkNotNull(item.playbackProperties).uri);
|
||||
if (downloadRequest != null) {
|
||||
MediaItem.Builder builder = item.buildUpon();
|
||||
builder
|
||||
.setMediaId(downloadRequest.id)
|
||||
.setUri(downloadRequest.uri)
|
||||
.setCustomCacheKey(downloadRequest.customCacheKey)
|
||||
.setMimeType(downloadRequest.mimeType)
|
||||
.setStreamKeys(downloadRequest.streamKeys)
|
||||
.setDrmKeySetId(downloadRequest.keySetId);
|
||||
mediaItems.add(builder.build());
|
||||
} else {
|
||||
showToast(R.string.ima_not_loaded);
|
||||
mediaItems.add(item);
|
||||
}
|
||||
return adsLoader;
|
||||
}
|
||||
|
||||
@Override
|
||||
public AdsLoader.AdViewProvider getAdViewProvider() {
|
||||
return Assertions.checkNotNull(playerView);
|
||||
}
|
||||
return mediaItems;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,7 +15,9 @@
|
|||
*/
|
||||
package com.google.android.exoplayer2.demo;
|
||||
|
||||
import static com.google.android.exoplayer2.util.Assertions.checkArgument;
|
||||
import static com.google.android.exoplayer2.util.Assertions.checkNotNull;
|
||||
import static com.google.android.exoplayer2.util.Assertions.checkState;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
|
|
@ -51,10 +53,9 @@ import com.google.android.exoplayer2.offline.DownloadService;
|
|||
import com.google.android.exoplayer2.upstream.DataSource;
|
||||
import com.google.android.exoplayer2.upstream.DataSourceInputStream;
|
||||
import com.google.android.exoplayer2.upstream.DataSpec;
|
||||
import com.google.android.exoplayer2.upstream.DefaultDataSource;
|
||||
import com.google.android.exoplayer2.util.Assertions;
|
||||
import com.google.android.exoplayer2.util.Log;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
|
|
@ -62,7 +63,6 @@ import java.util.ArrayList;
|
|||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
|
|
@ -71,16 +71,14 @@ public class SampleChooserActivity extends AppCompatActivity
|
|||
implements DownloadTracker.Listener, OnChildClickListener {
|
||||
|
||||
private static final String TAG = "SampleChooserActivity";
|
||||
private static final String GROUP_POSITION_PREFERENCE_KEY = "SAMPLE_CHOOSER_GROUP_POSITION";
|
||||
private static final String CHILD_POSITION_PREFERENCE_KEY = "SAMPLE_CHOOSER_CHILD_POSITION";
|
||||
private static final String GROUP_POSITION_PREFERENCE_KEY = "sample_chooser_group_position";
|
||||
private static final String CHILD_POSITION_PREFERENCE_KEY = "sample_chooser_child_position";
|
||||
|
||||
private String[] uris;
|
||||
private boolean useExtensionRenderers;
|
||||
private DownloadTracker downloadTracker;
|
||||
private SampleAdapter sampleAdapter;
|
||||
private MenuItem preferExtensionDecodersMenuItem;
|
||||
private MenuItem randomAbrMenuItem;
|
||||
private MenuItem tunnelingMenuItem;
|
||||
private ExpandableListView sampleListView;
|
||||
|
||||
@Override
|
||||
|
|
@ -115,9 +113,8 @@ public class SampleChooserActivity extends AppCompatActivity
|
|||
Arrays.sort(uris);
|
||||
}
|
||||
|
||||
DemoApplication application = (DemoApplication) getApplication();
|
||||
useExtensionRenderers = application.useExtensionRenderers();
|
||||
downloadTracker = application.getDownloadTracker();
|
||||
useExtensionRenderers = DemoUtil.useExtensionRenderers();
|
||||
downloadTracker = DemoUtil.getDownloadTracker(/* context= */ this);
|
||||
loadSample();
|
||||
|
||||
// Start the download service if it should be running but it's not currently.
|
||||
|
|
@ -137,11 +134,6 @@ public class SampleChooserActivity extends AppCompatActivity
|
|||
inflater.inflate(R.menu.sample_chooser_menu, menu);
|
||||
preferExtensionDecodersMenuItem = menu.findItem(R.id.prefer_extension_decoders);
|
||||
preferExtensionDecodersMenuItem.setVisible(useExtensionRenderers);
|
||||
randomAbrMenuItem = menu.findItem(R.id.random_abr);
|
||||
tunnelingMenuItem = menu.findItem(R.id.tunneling);
|
||||
if (Util.SDK_INT < 21) {
|
||||
tunnelingMenuItem.setEnabled(false);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
@ -209,16 +201,13 @@ public class SampleChooserActivity extends AppCompatActivity
|
|||
sampleAdapter.setPlaylistGroups(groups);
|
||||
|
||||
SharedPreferences preferences = getPreferences(MODE_PRIVATE);
|
||||
|
||||
int groupPosition = -1;
|
||||
int childPosition = -1;
|
||||
try {
|
||||
groupPosition = preferences.getInt(GROUP_POSITION_PREFERENCE_KEY, /* defValue= */ -1);
|
||||
childPosition = preferences.getInt(CHILD_POSITION_PREFERENCE_KEY, /* defValue= */ -1);
|
||||
} catch (ClassCastException e) {
|
||||
Log.w(TAG, "Saved position is not an int. Will not restore position.", e);
|
||||
}
|
||||
if (groupPosition != -1 && childPosition != -1) {
|
||||
int groupPosition = preferences.getInt(GROUP_POSITION_PREFERENCE_KEY, /* defValue= */ -1);
|
||||
int childPosition = preferences.getInt(CHILD_POSITION_PREFERENCE_KEY, /* defValue= */ -1);
|
||||
// Clear the group and child position if either are unset or if either are out of bounds.
|
||||
if (groupPosition != -1
|
||||
&& childPosition != -1
|
||||
&& groupPosition < groups.size()
|
||||
&& childPosition < groups.get(groupPosition).playlists.size()) {
|
||||
sampleListView.expandGroup(groupPosition); // shouldExpandGroup does not work without this.
|
||||
sampleListView.setSelectedChild(groupPosition, childPosition, /* shouldExpandGroup= */ true);
|
||||
}
|
||||
|
|
@ -238,12 +227,6 @@ public class SampleChooserActivity extends AppCompatActivity
|
|||
intent.putExtra(
|
||||
IntentUtil.PREFER_EXTENSION_DECODERS_EXTRA,
|
||||
isNonNullAndChecked(preferExtensionDecodersMenuItem));
|
||||
String abrAlgorithm =
|
||||
isNonNullAndChecked(randomAbrMenuItem)
|
||||
? IntentUtil.ABR_ALGORITHM_RANDOM
|
||||
: IntentUtil.ABR_ALGORITHM_DEFAULT;
|
||||
intent.putExtra(IntentUtil.ABR_ALGORITHM_EXTRA, abrAlgorithm);
|
||||
intent.putExtra(IntentUtil.TUNNELING_EXTRA, isNonNullAndChecked(tunnelingMenuItem));
|
||||
IntentUtil.addToIntent(playlistHolder.mediaItems, intent);
|
||||
startActivity(intent);
|
||||
return true;
|
||||
|
|
@ -256,8 +239,8 @@ public class SampleChooserActivity extends AppCompatActivity
|
|||
.show();
|
||||
} else {
|
||||
RenderersFactory renderersFactory =
|
||||
((DemoApplication) getApplication())
|
||||
.buildRenderersFactory(isNonNullAndChecked(preferExtensionDecodersMenuItem));
|
||||
DemoUtil.buildRenderersFactory(
|
||||
/* context= */ this, isNonNullAndChecked(preferExtensionDecodersMenuItem));
|
||||
downloadTracker.toggleDownload(
|
||||
getSupportFragmentManager(), playlistHolder.mediaItems.get(0), renderersFactory);
|
||||
}
|
||||
|
|
@ -269,12 +252,6 @@ public class SampleChooserActivity extends AppCompatActivity
|
|||
}
|
||||
MediaItem.PlaybackProperties playbackProperties =
|
||||
checkNotNull(playlistHolder.mediaItems.get(0).playbackProperties);
|
||||
if (playbackProperties.drmConfiguration != null) {
|
||||
return R.string.download_drm_unsupported;
|
||||
}
|
||||
if (((IntentUtil.Tag) checkNotNull(playbackProperties.tag)).isLive) {
|
||||
return R.string.download_live_unsupported;
|
||||
}
|
||||
if (playbackProperties.adTagUri != null) {
|
||||
return R.string.download_ads_unsupported;
|
||||
}
|
||||
|
|
@ -298,9 +275,7 @@ public class SampleChooserActivity extends AppCompatActivity
|
|||
protected List<PlaylistGroup> doInBackground(String... uris) {
|
||||
List<PlaylistGroup> result = new ArrayList<>();
|
||||
Context context = getApplicationContext();
|
||||
String userAgent = Util.getUserAgent(context, "ExoPlayerDemo");
|
||||
DataSource dataSource =
|
||||
new DefaultDataSource(context, userAgent, /* allowCrossProtocolRedirects= */ false);
|
||||
DataSource dataSource = DemoUtil.getDataSourceFactory(context).createDataSource();
|
||||
for (String uri : uris) {
|
||||
DataSpec dataSpec = new DataSpec(Uri.parse(uri));
|
||||
InputStream inputStream = new DataSourceInputStream(dataSource, dataSpec);
|
||||
|
|
@ -366,8 +341,6 @@ public class SampleChooserActivity extends AppCompatActivity
|
|||
Uri uri = null;
|
||||
String extension = null;
|
||||
String title = null;
|
||||
boolean isLive = false;
|
||||
String sphericalStereoMode = null;
|
||||
ArrayList<PlaylistHolder> children = null;
|
||||
Uri subtitleUri = null;
|
||||
String subtitleMimeType = null;
|
||||
|
|
@ -387,13 +360,20 @@ public class SampleChooserActivity extends AppCompatActivity
|
|||
case "extension":
|
||||
extension = reader.nextString();
|
||||
break;
|
||||
case "clip_start_position_ms":
|
||||
mediaItem.setClipStartPositionMs(reader.nextLong());
|
||||
break;
|
||||
case "clip_end_position_ms":
|
||||
mediaItem.setClipEndPositionMs(reader.nextLong());
|
||||
break;
|
||||
case "ad_tag_uri":
|
||||
mediaItem.setAdTagUri(reader.nextString());
|
||||
break;
|
||||
case "drm_scheme":
|
||||
mediaItem.setDrmUuid(Util.getDrmUuid(reader.nextString()));
|
||||
break;
|
||||
case "is_live":
|
||||
isLive = reader.nextBoolean();
|
||||
break;
|
||||
case "drm_license_url":
|
||||
case "drm_license_uri":
|
||||
case "drm_license_url": // For backward compatibility only.
|
||||
mediaItem.setDrmLicenseUri(reader.nextString());
|
||||
break;
|
||||
case "drm_key_request_properties":
|
||||
|
|
@ -405,34 +385,17 @@ public class SampleChooserActivity extends AppCompatActivity
|
|||
reader.endObject();
|
||||
mediaItem.setDrmLicenseRequestHeaders(requestHeaders);
|
||||
break;
|
||||
case "drm_session_for_clear_types":
|
||||
HashSet<Integer> drmSessionForClearTypes = new HashSet<>();
|
||||
reader.beginArray();
|
||||
while (reader.hasNext()) {
|
||||
drmSessionForClearTypes.add(toTrackType(reader.nextString()));
|
||||
case "drm_session_for_clear_content":
|
||||
if (reader.nextBoolean()) {
|
||||
mediaItem.setDrmSessionForClearTypes(
|
||||
ImmutableList.of(C.TRACK_TYPE_VIDEO, C.TRACK_TYPE_AUDIO));
|
||||
}
|
||||
reader.endArray();
|
||||
mediaItem.setDrmSessionForClearTypes(new ArrayList<>(drmSessionForClearTypes));
|
||||
break;
|
||||
case "drm_multi_session":
|
||||
mediaItem.setDrmMultiSession(reader.nextBoolean());
|
||||
break;
|
||||
case "playlist":
|
||||
Assertions.checkState(!insidePlaylist, "Invalid nesting of playlists");
|
||||
children = new ArrayList<>();
|
||||
reader.beginArray();
|
||||
while (reader.hasNext()) {
|
||||
children.add(readEntry(reader, /* insidePlaylist= */ true));
|
||||
}
|
||||
reader.endArray();
|
||||
break;
|
||||
case "ad_tag_uri":
|
||||
mediaItem.setAdTagUri(reader.nextString());
|
||||
break;
|
||||
case "spherical_stereo_mode":
|
||||
Assertions.checkState(
|
||||
!insidePlaylist, "Invalid attribute on nested item: spherical_stereo_mode");
|
||||
sphericalStereoMode = reader.nextString();
|
||||
case "drm_force_default_license_uri":
|
||||
mediaItem.setDrmForceDefaultLicenseUri(reader.nextBoolean());
|
||||
break;
|
||||
case "subtitle_uri":
|
||||
subtitleUri = Uri.parse(reader.nextString());
|
||||
|
|
@ -443,6 +406,15 @@ public class SampleChooserActivity extends AppCompatActivity
|
|||
case "subtitle_language":
|
||||
subtitleLanguage = reader.nextString();
|
||||
break;
|
||||
case "playlist":
|
||||
checkState(!insidePlaylist, "Invalid nesting of playlists");
|
||||
children = new ArrayList<>();
|
||||
reader.beginArray();
|
||||
while (reader.hasNext()) {
|
||||
children.add(readEntry(reader, /* insidePlaylist= */ true));
|
||||
}
|
||||
reader.endArray();
|
||||
break;
|
||||
default:
|
||||
throw new ParserException("Unsupported attribute name: " + name);
|
||||
}
|
||||
|
|
@ -456,11 +428,13 @@ public class SampleChooserActivity extends AppCompatActivity
|
|||
}
|
||||
return new PlaylistHolder(title, mediaItems);
|
||||
} else {
|
||||
@Nullable
|
||||
String adaptiveMimeType =
|
||||
Util.getAdaptiveMimeTypeForContentType(Util.inferContentType(uri, extension));
|
||||
mediaItem
|
||||
.setUri(uri)
|
||||
.setMediaMetadata(new MediaMetadata.Builder().setTitle(title).build())
|
||||
.setMimeType(IntentUtil.inferAdaptiveStreamMimeType(uri, extension))
|
||||
.setTag(new IntentUtil.Tag(isLive, sphericalStereoMode));
|
||||
.setMimeType(adaptiveMimeType);
|
||||
if (subtitleUri != null) {
|
||||
MediaItem.Subtitle subtitle =
|
||||
new MediaItem.Subtitle(
|
||||
|
|
@ -484,17 +458,6 @@ public class SampleChooserActivity extends AppCompatActivity
|
|||
groups.add(group);
|
||||
return group;
|
||||
}
|
||||
|
||||
private int toTrackType(String trackTypeString) {
|
||||
switch (Util.toLowerInvariant(trackTypeString)) {
|
||||
case "audio":
|
||||
return C.TRACK_TYPE_AUDIO;
|
||||
case "video":
|
||||
return C.TRACK_TYPE_VIDEO;
|
||||
default:
|
||||
throw new IllegalArgumentException("Invalid track type: " + trackTypeString);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private final class SampleAdapter extends BaseExpandableListAdapter implements OnClickListener {
|
||||
|
|
@ -609,7 +572,7 @@ public class SampleChooserActivity extends AppCompatActivity
|
|||
public final List<MediaItem> mediaItems;
|
||||
|
||||
private PlaylistHolder(String title, List<MediaItem> mediaItems) {
|
||||
Assertions.checkArgument(!mediaItems.isEmpty());
|
||||
checkArgument(!mediaItems.isEmpty());
|
||||
this.title = title;
|
||||
this.mediaItems = Collections.unmodifiableList(new ArrayList<>(mediaItems));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -286,7 +286,7 @@ public final class TrackSelectionDialog extends DialogFragment {
|
|||
private final class FragmentAdapter extends FragmentPagerAdapter {
|
||||
|
||||
public FragmentAdapter(FragmentManager fragmentManager) {
|
||||
super(fragmentManager);
|
||||
super(fragmentManager, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
|||
|
|
@ -0,0 +1,19 @@
|
|||
/*
|
||||
* Copyright (C) 2020 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.
|
||||
*/
|
||||
@NonNullApi
|
||||
package com.google.android.exoplayer2.demo;
|
||||
|
||||
import com.google.android.exoplayer2.util.NonNullApi;
|
||||
|
|
@ -15,14 +15,17 @@
|
|||
-->
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:id="@+id/root"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:keepScreenOn="true">
|
||||
|
||||
<com.google.android.exoplayer2.ui.PlayerView android:id="@+id/player_view"
|
||||
<com.google.android.exoplayer2.ui.StyledPlayerView android:id="@+id/player_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"/>
|
||||
android:layout_height="match_parent"
|
||||
app:show_shuffle_button="true"
|
||||
app:show_subtitle_button="true"/>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
|
|
|
|||
|
|
@ -19,12 +19,4 @@
|
|||
android:title="@string/prefer_extension_decoders"
|
||||
android:checkable="true"
|
||||
app:showAsAction="never"/>
|
||||
<item android:id="@+id/random_abr"
|
||||
android:title="@string/random_abr"
|
||||
android:checkable="true"
|
||||
app:showAsAction="never"/>
|
||||
<item android:id="@+id/tunneling"
|
||||
android:title="@string/tunneling"
|
||||
android:checkable="true"
|
||||
app:showAsAction="never"/>
|
||||
</menu>
|
||||
|
|
|
|||
|
|
@ -25,16 +25,10 @@
|
|||
|
||||
<string name="error_generic">Playback failed</string>
|
||||
|
||||
<string name="error_unrecognized_abr_algorithm">Unrecognized ABR algorithm</string>
|
||||
|
||||
<string name="error_unrecognized_stereo_mode">Unrecognized stereo mode</string>
|
||||
|
||||
<string name="error_drm_unsupported_before_api_18">Protected content not supported on API levels below 18</string>
|
||||
<string name="error_drm_unsupported_before_api_18">DRM content not supported on API levels below 18</string>
|
||||
|
||||
<string name="error_drm_unsupported_scheme">This device does not support the required DRM scheme</string>
|
||||
|
||||
<string name="error_drm_unknown">An unknown DRM error occurred</string>
|
||||
|
||||
<string name="error_no_decoder">This device does not provide a decoder for <xliff:g id="mime_type">%1$s</xliff:g></string>
|
||||
|
||||
<string name="error_no_secure_decoder">This device does not provide a secure decoder for <xliff:g id="mime_type">%1$s</xliff:g></string>
|
||||
|
|
@ -51,15 +45,13 @@
|
|||
|
||||
<string name="sample_list_load_error">One or more sample lists failed to load</string>
|
||||
|
||||
<string name="ima_not_loaded">Playing sample without ads, as the IMA extension was not loaded</string>
|
||||
|
||||
<string name="unsupported_ads_in_concatenation">Playing sample without ads, as ads are not supported in concatenations</string>
|
||||
<string name="unsupported_ads_in_playlist">Playing without ads, as ads are not supported in playlists</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_start_error_offline_license">Failed to obtain offline license</string>
|
||||
|
||||
<string name="download_drm_unsupported">This demo app does not support downloading protected content</string>
|
||||
<string name="download_playlist_unsupported">This demo app does not support downloading playlists</string>
|
||||
|
||||
<string name="download_scheme_unsupported">This demo app only supports downloading http streams</string>
|
||||
|
||||
|
|
@ -69,8 +61,4 @@
|
|||
|
||||
<string name="prefer_extension_decoders">Prefer extension decoders</string>
|
||||
|
||||
<string name="random_abr">Enable random ABR</string>
|
||||
|
||||
<string name="tunneling">Request multimedia tunneling</string>
|
||||
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -23,8 +23,4 @@
|
|||
<item name="android:windowBackground">@android:color/black</item>
|
||||
</style>
|
||||
|
||||
<style name="PlayerTheme.Spherical">
|
||||
<item name="surface_type">spherical_gl_surface_view</item>
|
||||
</style>
|
||||
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -18,4 +18,7 @@ called, and because you can move output off-screen easily (`setOutputSurface`
|
|||
can't take a `null` surface, so the player has to use a `DummySurface`, which
|
||||
doesn't handle protected output on all devices).
|
||||
|
||||
Please see the [demos README](../README.md) for instructions on how to build and
|
||||
run this demo.
|
||||
|
||||
[SurfaceControl]: https://developer.android.com/reference/android/view/SurfaceControl
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ import android.widget.Button;
|
|||
import android.widget.GridLayout;
|
||||
import androidx.annotation.Nullable;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.MediaItem;
|
||||
import com.google.android.exoplayer2.Player;
|
||||
import com.google.android.exoplayer2.SimpleExoPlayer;
|
||||
import com.google.android.exoplayer2.drm.DefaultDrmSessionManager;
|
||||
|
|
@ -183,13 +184,12 @@ public final class MainActivity extends Activity {
|
|||
ACTION_VIEW.equals(action)
|
||||
? Assertions.checkNotNull(intent.getData())
|
||||
: Uri.parse(DEFAULT_MEDIA_URI);
|
||||
String userAgent = Util.getUserAgent(this, getString(R.string.application_name));
|
||||
DrmSessionManager drmSessionManager;
|
||||
if (intent.hasExtra(DRM_SCHEME_EXTRA)) {
|
||||
String drmScheme = Assertions.checkNotNull(intent.getStringExtra(DRM_SCHEME_EXTRA));
|
||||
String drmLicenseUrl = Assertions.checkNotNull(intent.getStringExtra(DRM_LICENSE_URL_EXTRA));
|
||||
UUID drmSchemeUuid = Assertions.checkNotNull(Util.getDrmUuid(drmScheme));
|
||||
HttpDataSource.Factory licenseDataSourceFactory = new DefaultHttpDataSourceFactory(userAgent);
|
||||
HttpDataSource.Factory licenseDataSourceFactory = new DefaultHttpDataSourceFactory();
|
||||
HttpMediaDrmCallback drmCallback =
|
||||
new HttpMediaDrmCallback(drmLicenseUrl, licenseDataSourceFactory);
|
||||
drmSessionManager =
|
||||
|
|
@ -200,21 +200,19 @@ public final class MainActivity extends Activity {
|
|||
drmSessionManager = DrmSessionManager.getDummyDrmSessionManager();
|
||||
}
|
||||
|
||||
DataSource.Factory dataSourceFactory =
|
||||
new DefaultDataSourceFactory(
|
||||
this, Util.getUserAgent(this, getString(R.string.application_name)));
|
||||
DataSource.Factory dataSourceFactory = new DefaultDataSourceFactory(this);
|
||||
MediaSource mediaSource;
|
||||
@C.ContentType int type = Util.inferContentType(uri, intent.getStringExtra(EXTENSION_EXTRA));
|
||||
if (type == C.TYPE_DASH) {
|
||||
mediaSource =
|
||||
new DashMediaSource.Factory(dataSourceFactory)
|
||||
.setDrmSessionManager(drmSessionManager)
|
||||
.createMediaSource(uri);
|
||||
.createMediaSource(MediaItem.fromUri(uri));
|
||||
} else if (type == C.TYPE_OTHER) {
|
||||
mediaSource =
|
||||
new ProgressiveMediaSource.Factory(dataSourceFactory)
|
||||
.setDrmSessionManager(drmSessionManager)
|
||||
.createMediaSource(uri);
|
||||
.createMediaSource(MediaItem.fromUri(uri));
|
||||
} else {
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,19 @@
|
|||
/*
|
||||
* Copyright (C) 2020 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.
|
||||
*/
|
||||
@NonNullApi
|
||||
package com.google.android.exoplayer2.surfacedemo;
|
||||
|
||||
import com.google.android.exoplayer2.util.NonNullApi;
|
||||
|
|
@ -39,7 +39,7 @@ git clone https://github.com/google/cpu_features
|
|||
|
||||
```
|
||||
cd "${AV1_EXT_PATH}/jni" && \
|
||||
git clone https://chromium.googlesource.com/codecs/libgav1 libgav1
|
||||
git clone https://chromium.googlesource.com/codecs/libgav1
|
||||
```
|
||||
|
||||
* Fetch Abseil:
|
||||
|
|
@ -109,19 +109,22 @@ To try out playback using the extension in the [demo application][], see
|
|||
There are two possibilities for rendering the output `Libgav1VideoRenderer`
|
||||
gets from the libgav1 decoder:
|
||||
|
||||
* GL rendering using GL shader for color space conversion
|
||||
* If you are using `SimpleExoPlayer` with `PlayerView`, enable this option by
|
||||
setting `surface_type` of `PlayerView` to be
|
||||
`video_decoder_gl_surface_view`.
|
||||
* Otherwise, enable this option by sending `Libgav1VideoRenderer` a message
|
||||
of type `C.MSG_SET_VIDEO_DECODER_OUTPUT_BUFFER_RENDERER` with an instance of
|
||||
`VideoDecoderOutputBufferRenderer` as its object.
|
||||
* GL rendering using GL shader for color space conversion
|
||||
|
||||
* Native rendering using `ANativeWindow`
|
||||
* If you are using `SimpleExoPlayer` with `PlayerView`, this option is enabled
|
||||
by default.
|
||||
* Otherwise, enable this option by sending `Libgav1VideoRenderer` a message of
|
||||
type `C.MSG_SET_SURFACE` with an instance of `SurfaceView` as its object.
|
||||
* If you are using `SimpleExoPlayer` with `PlayerView`, enable this option
|
||||
by setting `surface_type` of `PlayerView` to be
|
||||
`video_decoder_gl_surface_view`.
|
||||
* Otherwise, enable this option by sending `Libgav1VideoRenderer` a
|
||||
message of type `Renderer.MSG_SET_VIDEO_DECODER_OUTPUT_BUFFER_RENDERER`
|
||||
with an instance of `VideoDecoderOutputBufferRenderer` as its object.
|
||||
|
||||
* Native rendering using `ANativeWindow`
|
||||
|
||||
* If you are using `SimpleExoPlayer` with `PlayerView`, this option is
|
||||
enabled by default.
|
||||
* Otherwise, enable this option by sending `Libgav1VideoRenderer` a
|
||||
message of type `Renderer.MSG_SET_SURFACE` with an instance of
|
||||
`SurfaceView` as its object.
|
||||
|
||||
Note: Although the default option uses `ANativeWindow`, based on our testing the
|
||||
GL rendering mode has better performance, so should be preferred
|
||||
|
|
|
|||
|
|
@ -11,22 +11,10 @@
|
|||
// 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.library'
|
||||
apply from: "$gradle.ext.exoplayerSettingsDir/common_library_config.gradle"
|
||||
|
||||
android {
|
||||
compileSdkVersion project.ext.compileSdkVersion
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
minSdkVersion project.ext.minSdkVersion
|
||||
targetSdkVersion project.ext.targetSdkVersion
|
||||
consumerProguardFiles 'proguard-rules.txt'
|
||||
|
||||
externalNativeBuild {
|
||||
cmake {
|
||||
// Debug CMake build type causes video frames to drop,
|
||||
|
|
@ -36,30 +24,22 @@ android {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
// This option resolves the problem of finding libgav1JNI.so
|
||||
// on multiple paths. The first one found is picked.
|
||||
packagingOptions {
|
||||
pickFirst 'lib/arm64-v8a/libgav1JNI.so'
|
||||
pickFirst 'lib/armeabi-v7a/libgav1JNI.so'
|
||||
pickFirst 'lib/x86/libgav1JNI.so'
|
||||
pickFirst 'lib/x86_64/libgav1JNI.so'
|
||||
}
|
||||
|
||||
sourceSets.main {
|
||||
// As native JNI library build is invoked from gradle, this is
|
||||
// not needed. However, it exposes the built library and keeps
|
||||
// consistency with the other extensions.
|
||||
jniLibs.srcDir 'src/main/libs'
|
||||
}
|
||||
}
|
||||
|
||||
// Configure the native build only if libgav1 is present, to avoid gradle sync
|
||||
// failures if libgav1 hasn't been checked out according to the README and CMake
|
||||
// isn't installed.
|
||||
// Configure the native build only if libgav1 is present to avoid gradle sync
|
||||
// failures if libgav1 hasn't been built according to the README instructions.
|
||||
if (project.file('src/main/jni/libgav1').exists()) {
|
||||
android.externalNativeBuild.cmake.path = 'src/main/jni/CMakeLists.txt'
|
||||
android.externalNativeBuild.cmake.version = '3.7.1+'
|
||||
android.externalNativeBuild.cmake {
|
||||
path = 'src/main/jni/CMakeLists.txt'
|
||||
version = '3.7.1+'
|
||||
if (project.hasProperty('externalNativeBuildDir')) {
|
||||
if (!new File(externalNativeBuildDir).isAbsolute()) {
|
||||
ext.externalNativeBuildDir =
|
||||
new File(rootDir, it.externalNativeBuildDir)
|
||||
}
|
||||
buildStagingDirectory = "${externalNativeBuildDir}/${project.name}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ import static java.lang.Runtime.getRuntime;
|
|||
import android.view.Surface;
|
||||
import androidx.annotation.Nullable;
|
||||
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.util.Util;
|
||||
import com.google.android.exoplayer2.video.VideoDecoderInputBuffer;
|
||||
|
|
@ -83,18 +84,9 @@ import java.nio.ByteBuffer;
|
|||
return "libgav1";
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the output mode for frames rendered by the decoder.
|
||||
*
|
||||
* @param outputMode The output mode.
|
||||
*/
|
||||
public void setOutputMode(@C.VideoOutputMode int outputMode) {
|
||||
this.outputMode = outputMode;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected VideoDecoderInputBuffer createInputBuffer() {
|
||||
return new VideoDecoderInputBuffer();
|
||||
return new VideoDecoderInputBuffer(DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_DIRECT);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -128,7 +120,7 @@ import java.nio.ByteBuffer;
|
|||
outputBuffer.addFlag(C.BUFFER_FLAG_DECODE_ONLY);
|
||||
}
|
||||
if (!decodeOnly) {
|
||||
outputBuffer.colorInfo = inputBuffer.colorInfo;
|
||||
outputBuffer.format = inputBuffer.format;
|
||||
}
|
||||
|
||||
return null;
|
||||
|
|
@ -155,6 +147,15 @@ import java.nio.ByteBuffer;
|
|||
super.releaseOutputBuffer(buffer);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the output mode for frames rendered by the decoder.
|
||||
*
|
||||
* @param outputMode The output mode.
|
||||
*/
|
||||
public void setOutputMode(@C.VideoOutputMode int outputMode) {
|
||||
this.outputMode = outputMode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders output buffer to the given surface. Must only be called when in {@link
|
||||
* C#VIDEO_OUTPUT_MODE_SURFACE_YUV} mode.
|
||||
|
|
|
|||
|
|
@ -126,7 +126,7 @@ public class Libgav1VideoRenderer extends DecoderVideoRenderer {
|
|||
|| !Gav1Library.isAvailable()) {
|
||||
return RendererCapabilities.create(FORMAT_UNSUPPORTED_TYPE);
|
||||
}
|
||||
if (format.drmInitData != null && format.exoMediaCryptoType == null) {
|
||||
if (format.exoMediaCryptoType != null) {
|
||||
return RendererCapabilities.create(FORMAT_UNSUPPORTED_DRM);
|
||||
}
|
||||
return RendererCapabilities.create(FORMAT_HANDLED, ADAPTIVE_SEAMLESS, TUNNELING_NOT_SUPPORTED);
|
||||
|
|
|
|||
|
|
@ -1,7 +1,4 @@
|
|||
# libgav1JNI requires modern CMake.
|
||||
cmake_minimum_required(VERSION 3.7.1 FATAL_ERROR)
|
||||
|
||||
# libgav1JNI requires C++11.
|
||||
set(CMAKE_CXX_STANDARD 11)
|
||||
|
||||
project(libgav1JNI C CXX)
|
||||
|
|
@ -21,24 +18,13 @@ if(build_type MATCHES "^rel")
|
|||
endif()
|
||||
|
||||
set(libgav1_jni_root "${CMAKE_CURRENT_SOURCE_DIR}")
|
||||
set(libgav1_jni_build "${CMAKE_BINARY_DIR}")
|
||||
set(libgav1_jni_output_directory
|
||||
${libgav1_jni_root}/../libs/${ANDROID_ABI}/)
|
||||
|
||||
set(libgav1_root "${libgav1_jni_root}/libgav1")
|
||||
set(libgav1_build "${libgav1_jni_build}/libgav1")
|
||||
|
||||
set(cpu_features_root "${libgav1_jni_root}/cpu_features")
|
||||
set(cpu_features_build "${libgav1_jni_build}/cpu_features")
|
||||
|
||||
# Build cpu_features library.
|
||||
add_subdirectory("${cpu_features_root}"
|
||||
"${cpu_features_build}"
|
||||
add_subdirectory("${libgav1_jni_root}/cpu_features"
|
||||
EXCLUDE_FROM_ALL)
|
||||
|
||||
# Build libgav1.
|
||||
add_subdirectory("${libgav1_root}"
|
||||
"${libgav1_build}"
|
||||
add_subdirectory("${libgav1_jni_root}/libgav1"
|
||||
EXCLUDE_FROM_ALL)
|
||||
|
||||
# Build libgav1JNI.
|
||||
|
|
@ -58,7 +44,3 @@ target_link_libraries(gav1JNI
|
|||
PRIVATE libgav1_static
|
||||
PRIVATE ${android_log_lib})
|
||||
|
||||
# Specify output directory for libgav1JNI.
|
||||
set_target_properties(gav1JNI PROPERTIES
|
||||
LIBRARY_OUTPUT_DIRECTORY
|
||||
${libgav1_jni_output_directory})
|
||||
|
|
|
|||
|
|
@ -11,24 +11,7 @@
|
|||
// 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.library'
|
||||
|
||||
android {
|
||||
compileSdkVersion project.ext.compileSdkVersion
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
minSdkVersion project.ext.minSdkVersion
|
||||
targetSdkVersion project.ext.targetSdkVersion
|
||||
}
|
||||
|
||||
testOptions.unitTests.includeAndroidResources = true
|
||||
}
|
||||
apply from: "$gradle.ext.exoplayerSettingsDir/common_library_config.gradle"
|
||||
|
||||
dependencies {
|
||||
api 'com.google.android.gms:play-services-cast-framework:18.1.0'
|
||||
|
|
@ -36,7 +19,7 @@ dependencies {
|
|||
implementation project(modulePrefix + 'library-core')
|
||||
implementation project(modulePrefix + 'library-ui')
|
||||
compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion
|
||||
compileOnly 'org.checkerframework:checker-compat-qual:' + checkerframeworkVersion
|
||||
compileOnly 'org.checkerframework:checker-compat-qual:' + checkerframeworkCompatVersion
|
||||
compileOnly 'org.jetbrains.kotlin:kotlin-annotations-jvm:' + kotlinAnnotationsVersion
|
||||
testImplementation project(modulePrefix + 'testutils')
|
||||
testImplementation 'org.robolectric:robolectric:' + robolectricVersion
|
||||
|
|
|
|||
|
|
@ -15,6 +15,8 @@
|
|||
*/
|
||||
package com.google.android.exoplayer2.ext.cast;
|
||||
|
||||
import static java.lang.Math.min;
|
||||
|
||||
import android.os.Looper;
|
||||
import androidx.annotation.Nullable;
|
||||
import com.google.android.exoplayer2.BasePlayer;
|
||||
|
|
@ -30,6 +32,7 @@ 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.trackselection.TrackSelector;
|
||||
import com.google.android.exoplayer2.util.Assertions;
|
||||
import com.google.android.exoplayer2.util.Log;
|
||||
import com.google.android.exoplayer2.util.MimeTypes;
|
||||
|
|
@ -290,6 +293,7 @@ public final class CastPlayer extends BasePlayer {
|
|||
|
||||
@Override
|
||||
public void addListener(EventListener listener) {
|
||||
Assertions.checkNotNull(listener);
|
||||
listeners.addIfAbsent(new ListenerHolder(listener));
|
||||
}
|
||||
|
||||
|
|
@ -333,7 +337,7 @@ public final class CastPlayer extends BasePlayer {
|
|||
&& toIndex <= currentTimeline.getWindowCount()
|
||||
&& newIndex >= 0
|
||||
&& newIndex < currentTimeline.getWindowCount());
|
||||
newIndex = Math.min(newIndex, currentTimeline.getWindowCount() - (toIndex - fromIndex));
|
||||
newIndex = min(newIndex, currentTimeline.getWindowCount() - (toIndex - fromIndex));
|
||||
if (fromIndex == toIndex || fromIndex == newIndex) {
|
||||
// Do nothing.
|
||||
return;
|
||||
|
|
@ -426,6 +430,9 @@ public final class CastPlayer extends BasePlayer {
|
|||
return playWhenReady.value;
|
||||
}
|
||||
|
||||
// We still call EventListener#onSeekProcessed() for backwards compatibility with listeners that
|
||||
// don't implement onPositionDiscontinuity().
|
||||
@SuppressWarnings("deprecation")
|
||||
@Override
|
||||
public void seekTo(int windowIndex, long positionMs) {
|
||||
MediaStatus mediaStatus = getMediaStatus();
|
||||
|
|
@ -451,32 +458,16 @@ public final class CastPlayer extends BasePlayer {
|
|||
flushNotifications();
|
||||
}
|
||||
|
||||
/** @deprecated Use {@link #setPlaybackSpeed(float)} instead. */
|
||||
@SuppressWarnings("deprecation")
|
||||
@Deprecated
|
||||
@Override
|
||||
public void setPlaybackParameters(@Nullable PlaybackParameters playbackParameters) {
|
||||
// Unsupported by the RemoteMediaClient API. Do nothing.
|
||||
}
|
||||
|
||||
/** @deprecated Use {@link #getPlaybackSpeed()} instead. */
|
||||
@SuppressWarnings("deprecation")
|
||||
@Deprecated
|
||||
@Override
|
||||
public PlaybackParameters getPlaybackParameters() {
|
||||
return PlaybackParameters.DEFAULT;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setPlaybackSpeed(float playbackSpeed) {
|
||||
// Unsupported by the RemoteMediaClient API. Do nothing.
|
||||
}
|
||||
|
||||
@Override
|
||||
public float getPlaybackSpeed() {
|
||||
return Player.DEFAULT_PLAYBACK_SPEED;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop(boolean reset) {
|
||||
playbackState = STATE_IDLE;
|
||||
|
|
@ -513,6 +504,12 @@ public final class CastPlayer extends BasePlayer {
|
|||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public TrackSelector getTrackSelector() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setRepeatMode(@RepeatMode int repeatMode) {
|
||||
if (remoteMediaClient == null) {
|
||||
|
|
@ -800,7 +797,7 @@ public final class CastPlayer extends BasePlayer {
|
|||
}
|
||||
return remoteMediaClient.queueLoad(
|
||||
mediaQueueItems,
|
||||
Math.min(startWindowIndex, mediaQueueItems.length - 1),
|
||||
min(startWindowIndex, mediaQueueItems.length - 1),
|
||||
getCastRepeatMode(repeatMode),
|
||||
startPositionMs,
|
||||
/* customData= */ null);
|
||||
|
|
@ -874,7 +871,7 @@ public final class CastPlayer extends BasePlayer {
|
|||
return;
|
||||
}
|
||||
if (this.remoteMediaClient != null) {
|
||||
this.remoteMediaClient.removeListener(statusListener);
|
||||
this.remoteMediaClient.unregisterCallback(statusListener);
|
||||
this.remoteMediaClient.removeProgressListener(statusListener);
|
||||
}
|
||||
this.remoteMediaClient = remoteMediaClient;
|
||||
|
|
@ -882,7 +879,7 @@ public final class CastPlayer extends BasePlayer {
|
|||
if (sessionAvailabilityListener != null) {
|
||||
sessionAvailabilityListener.onCastSessionAvailable();
|
||||
}
|
||||
remoteMediaClient.addListener(statusListener);
|
||||
remoteMediaClient.registerCallback(statusListener);
|
||||
remoteMediaClient.addProgressListener(statusListener, PROGRESS_REPORT_PERIOD_MS);
|
||||
updateInternalStateAndNotifyIfChanged();
|
||||
} else {
|
||||
|
|
@ -996,10 +993,8 @@ public final class CastPlayer extends BasePlayer {
|
|||
|
||||
// Internal classes.
|
||||
|
||||
private final class StatusListener
|
||||
implements RemoteMediaClient.Listener,
|
||||
SessionManagerListener<CastSession>,
|
||||
RemoteMediaClient.ProgressListener {
|
||||
private final class StatusListener extends RemoteMediaClient.Callback
|
||||
implements SessionManagerListener<CastSession>, RemoteMediaClient.ProgressListener {
|
||||
|
||||
// RemoteMediaClient.ProgressListener implementation.
|
||||
|
||||
|
|
@ -1008,7 +1003,7 @@ public final class CastPlayer extends BasePlayer {
|
|||
lastReportedPositionMs = progressMs;
|
||||
}
|
||||
|
||||
// RemoteMediaClient.Listener implementation.
|
||||
// RemoteMediaClient.Callback implementation.
|
||||
|
||||
@Override
|
||||
public void onStatusUpdated() {
|
||||
|
|
@ -1085,6 +1080,9 @@ public final class CastPlayer extends BasePlayer {
|
|||
|
||||
private final class SeekResultCallback implements ResultCallback<MediaChannelResult> {
|
||||
|
||||
// We still call EventListener#onSeekProcessed() for backwards compatibility with listeners that
|
||||
// don't implement onPositionDiscontinuity().
|
||||
@SuppressWarnings("deprecation")
|
||||
@Override
|
||||
public void onResult(MediaChannelResult result) {
|
||||
int statusCode = result.getStatus().getStatusCode();
|
||||
|
|
|
|||
|
|
@ -15,10 +15,12 @@
|
|||
*/
|
||||
package com.google.android.exoplayer2.ext.cast;
|
||||
|
||||
import android.net.Uri;
|
||||
import android.util.SparseArray;
|
||||
import android.util.SparseIntArray;
|
||||
import androidx.annotation.Nullable;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.MediaItem;
|
||||
import com.google.android.exoplayer2.Timeline;
|
||||
import java.util.Arrays;
|
||||
|
||||
|
|
@ -126,7 +128,7 @@ import java.util.Arrays;
|
|||
boolean isDynamic = durationUs == C.TIME_UNSET;
|
||||
return window.set(
|
||||
/* uid= */ ids[windowIndex],
|
||||
/* tag= */ ids[windowIndex],
|
||||
/* mediaItem= */ new MediaItem.Builder().setUri(Uri.EMPTY).setTag(ids[windowIndex]).build(),
|
||||
/* manifest= */ null,
|
||||
/* presentationStartTimeMs= */ C.TIME_UNSET,
|
||||
/* windowStartTimeMs= */ C.TIME_UNSET,
|
||||
|
|
|
|||
|
|
@ -59,8 +59,7 @@ public class CastPlayerTest {
|
|||
|
||||
private CastPlayer castPlayer;
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
private RemoteMediaClient.Listener remoteMediaClientListener;
|
||||
private RemoteMediaClient.Callback remoteMediaClientCallback;
|
||||
|
||||
@Mock private RemoteMediaClient mockRemoteMediaClient;
|
||||
@Mock private MediaStatus mockMediaStatus;
|
||||
|
|
@ -76,7 +75,7 @@ public class CastPlayerTest {
|
|||
private ArgumentCaptor<ResultCallback<RemoteMediaClient.MediaChannelResult>>
|
||||
setResultCallbackArgumentCaptor;
|
||||
|
||||
@Captor private ArgumentCaptor<RemoteMediaClient.Listener> listenerArgumentCaptor;
|
||||
@Captor private ArgumentCaptor<RemoteMediaClient.Callback> callbackArgumentCaptor;
|
||||
|
||||
@Captor private ArgumentCaptor<MediaQueueItem[]> queueItemsArgumentCaptor;
|
||||
|
||||
|
|
@ -95,8 +94,8 @@ public class CastPlayerTest {
|
|||
when(mockMediaStatus.getQueueRepeatMode()).thenReturn(MediaStatus.REPEAT_MODE_REPEAT_OFF);
|
||||
castPlayer = new CastPlayer(mockCastContext);
|
||||
castPlayer.addListener(mockListener);
|
||||
verify(mockRemoteMediaClient).addListener(listenerArgumentCaptor.capture());
|
||||
remoteMediaClientListener = listenerArgumentCaptor.getValue();
|
||||
verify(mockRemoteMediaClient).registerCallback(callbackArgumentCaptor.capture());
|
||||
remoteMediaClientCallback = callbackArgumentCaptor.getValue();
|
||||
}
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
|
|
@ -113,7 +112,7 @@ public class CastPlayerTest {
|
|||
.onPlayWhenReadyChanged(true, Player.PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST);
|
||||
|
||||
// There is a status update in the middle, which should be hidden by masking.
|
||||
remoteMediaClientListener.onStatusUpdated();
|
||||
remoteMediaClientCallback.onStatusUpdated();
|
||||
verifyNoMoreInteractions(mockListener);
|
||||
|
||||
// Upon result, the remoteMediaClient has updated its state according to the play() call.
|
||||
|
|
@ -169,7 +168,7 @@ public class CastPlayerTest {
|
|||
public void playWhenReady_changesOnStatusUpdates() {
|
||||
assertThat(castPlayer.getPlayWhenReady()).isFalse();
|
||||
when(mockRemoteMediaClient.isPaused()).thenReturn(false);
|
||||
remoteMediaClientListener.onStatusUpdated();
|
||||
remoteMediaClientCallback.onStatusUpdated();
|
||||
verify(mockListener).onPlayerStateChanged(true, Player.STATE_IDLE);
|
||||
verify(mockListener).onPlayWhenReadyChanged(true, Player.PLAY_WHEN_READY_CHANGE_REASON_REMOTE);
|
||||
assertThat(castPlayer.getPlayWhenReady()).isTrue();
|
||||
|
|
@ -187,7 +186,7 @@ public class CastPlayerTest {
|
|||
|
||||
// There is a status update in the middle, which should be hidden by masking.
|
||||
when(mockMediaStatus.getQueueRepeatMode()).thenReturn(MediaStatus.REPEAT_MODE_REPEAT_ALL);
|
||||
remoteMediaClientListener.onStatusUpdated();
|
||||
remoteMediaClientCallback.onStatusUpdated();
|
||||
verifyNoMoreInteractions(mockListener);
|
||||
|
||||
// Upon result, the mediaStatus now exposes the new repeat mode.
|
||||
|
|
@ -209,7 +208,7 @@ public class CastPlayerTest {
|
|||
|
||||
// There is a status update in the middle, which should be hidden by masking.
|
||||
when(mockMediaStatus.getQueueRepeatMode()).thenReturn(MediaStatus.REPEAT_MODE_REPEAT_ALL);
|
||||
remoteMediaClientListener.onStatusUpdated();
|
||||
remoteMediaClientCallback.onStatusUpdated();
|
||||
verifyNoMoreInteractions(mockListener);
|
||||
|
||||
// Upon result, the repeat mode is ALL. The state should reflect that.
|
||||
|
|
@ -224,7 +223,7 @@ public class CastPlayerTest {
|
|||
public void repeatMode_changesOnStatusUpdates() {
|
||||
assertThat(castPlayer.getRepeatMode()).isEqualTo(Player.REPEAT_MODE_OFF);
|
||||
when(mockMediaStatus.getQueueRepeatMode()).thenReturn(MediaStatus.REPEAT_MODE_REPEAT_SINGLE);
|
||||
remoteMediaClientListener.onStatusUpdated();
|
||||
remoteMediaClientCallback.onStatusUpdated();
|
||||
verify(mockListener).onRepeatModeChanged(Player.REPEAT_MODE_ONE);
|
||||
assertThat(castPlayer.getRepeatMode()).isEqualTo(Player.REPEAT_MODE_ONE);
|
||||
}
|
||||
|
|
@ -494,6 +493,6 @@ public class CastPlayerTest {
|
|||
|
||||
castPlayer.addMediaItems(mediaItems);
|
||||
// Call listener to update the timeline of the player.
|
||||
remoteMediaClientListener.onQueueStatusUpdated();
|
||||
remoteMediaClientCallback.onQueueStatusUpdated();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,6 +15,8 @@
|
|||
*/
|
||||
package com.google.android.exoplayer2.ext.cast;
|
||||
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.testutil.TimelineAsserts;
|
||||
|
|
@ -105,18 +107,18 @@ public class CastTimelineTrackerTest {
|
|||
int[] itemIds, int currentItemId, long currentDurationMs) {
|
||||
RemoteMediaClient remoteMediaClient = Mockito.mock(RemoteMediaClient.class);
|
||||
MediaStatus status = Mockito.mock(MediaStatus.class);
|
||||
Mockito.when(status.getQueueItems()).thenReturn(Collections.emptyList());
|
||||
Mockito.when(remoteMediaClient.getMediaStatus()).thenReturn(status);
|
||||
Mockito.when(status.getMediaInfo()).thenReturn(getMediaInfo(currentDurationMs));
|
||||
Mockito.when(status.getCurrentItemId()).thenReturn(currentItemId);
|
||||
when(status.getQueueItems()).thenReturn(Collections.emptyList());
|
||||
when(remoteMediaClient.getMediaStatus()).thenReturn(status);
|
||||
when(status.getMediaInfo()).thenReturn(getMediaInfo(currentDurationMs));
|
||||
when(status.getCurrentItemId()).thenReturn(currentItemId);
|
||||
MediaQueue mediaQueue = mockMediaQueue(itemIds);
|
||||
Mockito.when(remoteMediaClient.getMediaQueue()).thenReturn(mediaQueue);
|
||||
when(remoteMediaClient.getMediaQueue()).thenReturn(mediaQueue);
|
||||
return remoteMediaClient;
|
||||
}
|
||||
|
||||
private static MediaQueue mockMediaQueue(int[] itemIds) {
|
||||
MediaQueue mediaQueue = Mockito.mock(MediaQueue.class);
|
||||
Mockito.when(mediaQueue.getItemIds()).thenReturn(itemIds);
|
||||
when(mediaQueue.getItemIds()).thenReturn(itemIds);
|
||||
return mediaQueue;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -11,29 +11,19 @@
|
|||
// 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.library'
|
||||
|
||||
android {
|
||||
compileSdkVersion project.ext.compileSdkVersion
|
||||
|
||||
defaultConfig {
|
||||
minSdkVersion project.ext.minSdkVersion
|
||||
targetSdkVersion project.ext.targetSdkVersion
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
|
||||
testOptions.unitTests.includeAndroidResources = true
|
||||
}
|
||||
apply from: "$gradle.ext.exoplayerSettingsDir/common_library_config.gradle"
|
||||
|
||||
dependencies {
|
||||
api "com.google.android.gms:play-services-cronet:17.0.0"
|
||||
implementation project(modulePrefix + 'library-core')
|
||||
implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion
|
||||
implementation ('com.google.guava:guava:' + guavaVersion) {
|
||||
exclude group: 'com.google.code.findbugs', module: 'jsr305'
|
||||
exclude group: 'org.checkerframework', module: 'checker-compat-qual'
|
||||
exclude group: 'com.google.errorprone', module: 'error_prone_annotations'
|
||||
exclude group: 'com.google.j2objc', module: 'j2objc-annotations'
|
||||
exclude group: 'org.codehaus.mojo', module: 'animal-sniffer-annotations'
|
||||
}
|
||||
compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion
|
||||
compileOnly 'org.jetbrains.kotlin:kotlin-annotations-jvm:' + kotlinAnnotationsVersion
|
||||
testImplementation project(modulePrefix + 'library')
|
||||
|
|
|
|||
|
|
@ -15,6 +15,8 @@
|
|||
*/
|
||||
package com.google.android.exoplayer2.ext.cronet;
|
||||
|
||||
import static java.lang.Math.min;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import org.chromium.net.UploadDataProvider;
|
||||
|
|
@ -40,7 +42,7 @@ import org.chromium.net.UploadDataSink;
|
|||
|
||||
@Override
|
||||
public void read(UploadDataSink uploadDataSink, ByteBuffer byteBuffer) throws IOException {
|
||||
int readLength = Math.min(byteBuffer.remaining(), data.length - position);
|
||||
int readLength = min(byteBuffer.remaining(), data.length - position);
|
||||
byteBuffer.put(data, position, readLength);
|
||||
position += readLength;
|
||||
uploadDataSink.onReadSucceeded(false);
|
||||
|
|
|
|||
|
|
@ -16,6 +16,8 @@
|
|||
package com.google.android.exoplayer2.ext.cronet;
|
||||
|
||||
import static com.google.android.exoplayer2.util.Util.castNonNull;
|
||||
import static java.lang.Math.max;
|
||||
import static java.lang.Math.min;
|
||||
|
||||
import android.net.Uri;
|
||||
import android.text.TextUtils;
|
||||
|
|
@ -30,12 +32,14 @@ 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.Log;
|
||||
import com.google.android.exoplayer2.util.Predicate;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
import com.google.common.base.Predicate;
|
||||
import java.io.IOException;
|
||||
import java.io.InterruptedIOException;
|
||||
import java.net.SocketTimeoutException;
|
||||
import java.net.UnknownHostException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
|
|
@ -146,6 +150,8 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
|
|||
private volatile long currentConnectTimeoutMs;
|
||||
|
||||
/**
|
||||
* Creates an instance.
|
||||
*
|
||||
* @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
|
||||
|
|
@ -164,6 +170,8 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
|
|||
}
|
||||
|
||||
/**
|
||||
* Creates an instance.
|
||||
*
|
||||
* @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
|
||||
|
|
@ -195,6 +203,8 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
|
|||
}
|
||||
|
||||
/**
|
||||
* Creates an instance.
|
||||
*
|
||||
* @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
|
||||
|
|
@ -229,6 +239,8 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
|
|||
}
|
||||
|
||||
/**
|
||||
* Creates an instance.
|
||||
*
|
||||
* @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
|
||||
|
|
@ -241,6 +253,7 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
|
|||
* @deprecated Use {@link #CronetDataSource(CronetEngine, Executor)} and {@link
|
||||
* #setContentTypePredicate(Predicate)}.
|
||||
*/
|
||||
@SuppressWarnings("deprecation")
|
||||
@Deprecated
|
||||
public CronetDataSource(
|
||||
CronetEngine cronetEngine,
|
||||
|
|
@ -257,6 +270,8 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
|
|||
}
|
||||
|
||||
/**
|
||||
* Creates an instance.
|
||||
*
|
||||
* @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
|
||||
|
|
@ -274,6 +289,7 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
|
|||
* @deprecated Use {@link #CronetDataSource(CronetEngine, Executor, int, int, boolean,
|
||||
* RequestProperties)} and {@link #setContentTypePredicate(Predicate)}.
|
||||
*/
|
||||
@SuppressWarnings("deprecation")
|
||||
@Deprecated
|
||||
public CronetDataSource(
|
||||
CronetEngine cronetEngine,
|
||||
|
|
@ -295,6 +311,8 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
|
|||
}
|
||||
|
||||
/**
|
||||
* Creates an instance.
|
||||
*
|
||||
* @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
|
||||
|
|
@ -440,12 +458,28 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
|
|||
UrlResponseInfo responseInfo = Assertions.checkNotNull(this.responseInfo);
|
||||
int responseCode = responseInfo.getHttpStatusCode();
|
||||
if (responseCode < 200 || responseCode > 299) {
|
||||
byte[] responseBody = Util.EMPTY_BYTE_ARRAY;
|
||||
ByteBuffer readBuffer = getOrCreateReadBuffer();
|
||||
while (!readBuffer.hasRemaining()) {
|
||||
operation.close();
|
||||
readBuffer.clear();
|
||||
readInternal(readBuffer);
|
||||
if (finished) {
|
||||
break;
|
||||
}
|
||||
readBuffer.flip();
|
||||
int existingResponseBodyEnd = responseBody.length;
|
||||
responseBody = Arrays.copyOf(responseBody, responseBody.length + readBuffer.remaining());
|
||||
readBuffer.get(responseBody, existingResponseBodyEnd, readBuffer.remaining());
|
||||
}
|
||||
|
||||
InvalidResponseCodeException exception =
|
||||
new InvalidResponseCodeException(
|
||||
responseCode,
|
||||
responseInfo.getHttpStatusText(),
|
||||
responseInfo.getAllHeaders(),
|
||||
dataSpec);
|
||||
dataSpec,
|
||||
responseBody);
|
||||
if (responseCode == 416) {
|
||||
exception.initCause(new DataSourceException(DataSourceException.POSITION_OUT_OF_RANGE));
|
||||
}
|
||||
|
|
@ -457,7 +491,7 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
|
|||
if (contentTypePredicate != null) {
|
||||
List<String> contentTypeHeaders = responseInfo.getAllHeaders().get(CONTENT_TYPE);
|
||||
String contentType = isEmpty(contentTypeHeaders) ? null : contentTypeHeaders.get(0);
|
||||
if (contentType != null && !contentTypePredicate.evaluate(contentType)) {
|
||||
if (contentType != null && !contentTypePredicate.apply(contentType)) {
|
||||
throw new InvalidContentTypeException(contentType, dataSpec);
|
||||
}
|
||||
}
|
||||
|
|
@ -496,17 +530,12 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
|
|||
return C.RESULT_END_OF_INPUT;
|
||||
}
|
||||
|
||||
ByteBuffer readBuffer = this.readBuffer;
|
||||
if (readBuffer == null) {
|
||||
readBuffer = ByteBuffer.allocateDirect(READ_BUFFER_SIZE_BYTES);
|
||||
readBuffer.limit(0);
|
||||
this.readBuffer = readBuffer;
|
||||
}
|
||||
ByteBuffer readBuffer = getOrCreateReadBuffer();
|
||||
while (!readBuffer.hasRemaining()) {
|
||||
// Fill readBuffer with more data from Cronet.
|
||||
operation.close();
|
||||
readBuffer.clear();
|
||||
readInternal(castNonNull(readBuffer));
|
||||
readInternal(readBuffer);
|
||||
|
||||
if (finished) {
|
||||
bytesRemaining = 0;
|
||||
|
|
@ -516,14 +545,14 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
|
|||
readBuffer.flip();
|
||||
Assertions.checkState(readBuffer.hasRemaining());
|
||||
if (bytesToSkip > 0) {
|
||||
int bytesSkipped = (int) Math.min(readBuffer.remaining(), bytesToSkip);
|
||||
int bytesSkipped = (int) min(readBuffer.remaining(), bytesToSkip);
|
||||
readBuffer.position(readBuffer.position() + bytesSkipped);
|
||||
bytesToSkip -= bytesSkipped;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int bytesRead = Math.min(readBuffer.remaining(), readLength);
|
||||
int bytesRead = min(readBuffer.remaining(), readLength);
|
||||
readBuffer.get(buffer, offset, bytesRead);
|
||||
|
||||
if (bytesRemaining != C.LENGTH_UNSET) {
|
||||
|
|
@ -603,11 +632,8 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
|
|||
operation.close();
|
||||
|
||||
if (!useCallerBuffer) {
|
||||
if (readBuffer == null) {
|
||||
readBuffer = ByteBuffer.allocateDirect(READ_BUFFER_SIZE_BYTES);
|
||||
} else {
|
||||
readBuffer.clear();
|
||||
}
|
||||
ByteBuffer readBuffer = getOrCreateReadBuffer();
|
||||
readBuffer.clear();
|
||||
if (bytesToSkip < READ_BUFFER_SIZE_BYTES) {
|
||||
readBuffer.limit((int) bytesToSkip);
|
||||
}
|
||||
|
|
@ -781,6 +807,14 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
|
|||
}
|
||||
}
|
||||
|
||||
private ByteBuffer getOrCreateReadBuffer() {
|
||||
if (readBuffer == null) {
|
||||
readBuffer = ByteBuffer.allocateDirect(READ_BUFFER_SIZE_BYTES);
|
||||
readBuffer.limit(0);
|
||||
}
|
||||
return readBuffer;
|
||||
}
|
||||
|
||||
private static boolean isCompressed(UrlResponseInfo info) {
|
||||
for (Map.Entry<String, String> entry : info.getAllHeadersAsList()) {
|
||||
if (entry.getKey().equalsIgnoreCase("Content-Encoding")) {
|
||||
|
|
@ -826,7 +860,7 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
|
|||
// would increase it.
|
||||
Log.w(TAG, "Inconsistent headers [" + contentLengthHeader + "] [" + contentRangeHeader
|
||||
+ "]");
|
||||
contentLength = Math.max(contentLength, contentLengthFromRange);
|
||||
contentLength = max(contentLength, contentLengthFromRange);
|
||||
}
|
||||
} catch (NumberFormatException e) {
|
||||
Log.e(TAG, "Unexpected Content-Range [" + contentRangeHeader + "]");
|
||||
|
|
@ -869,7 +903,7 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
|
|||
// Copy as much as possible from the src buffer into dst buffer.
|
||||
// Returns the number of bytes copied.
|
||||
private static int copyByteBuffer(ByteBuffer src, ByteBuffer dst) {
|
||||
int remaining = Math.min(src.remaining(), dst.remaining());
|
||||
int remaining = min(src.remaining(), dst.remaining());
|
||||
int limit = src.limit();
|
||||
src.limit(src.position() + remaining);
|
||||
dst.put(src);
|
||||
|
|
@ -893,7 +927,11 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
|
|||
if (responseCode == 307 || responseCode == 308) {
|
||||
exception =
|
||||
new InvalidResponseCodeException(
|
||||
responseCode, info.getHttpStatusText(), info.getAllHeaders(), dataSpec);
|
||||
responseCode,
|
||||
info.getHttpStatusText(),
|
||||
info.getAllHeaders(),
|
||||
dataSpec,
|
||||
/* responseBody= */ Util.EMPTY_BYTE_ARRAY);
|
||||
operation.open();
|
||||
return;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,6 +15,8 @@
|
|||
*/
|
||||
package com.google.android.exoplayer2.ext.cronet;
|
||||
|
||||
import static com.google.android.exoplayer2.ExoPlayerLibraryInfo.DEFAULT_USER_AGENT;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory;
|
||||
import com.google.android.exoplayer2.upstream.HttpDataSource;
|
||||
|
|
@ -50,14 +52,13 @@ public final class CronetDataSourceFactory extends BaseFactory {
|
|||
private final HttpDataSource.Factory fallbackFactory;
|
||||
|
||||
/**
|
||||
* Constructs a CronetDataSourceFactory.
|
||||
* Creates an instance.
|
||||
*
|
||||
* <p>If the {@link CronetEngineWrapper} fails to provide a {@link CronetEngine}, the provided
|
||||
* fallback {@link HttpDataSource.Factory} will be used instead.
|
||||
*
|
||||
* <p>Sets {@link CronetDataSource#DEFAULT_CONNECT_TIMEOUT_MILLIS} as the connection timeout,
|
||||
* {@link CronetDataSource#DEFAULT_READ_TIMEOUT_MILLIS} as the read timeout and disables
|
||||
* cross-protocol redirects.
|
||||
* {@link CronetDataSource#DEFAULT_READ_TIMEOUT_MILLIS} as the read timeout.
|
||||
*
|
||||
* @param cronetEngineWrapper A {@link CronetEngineWrapper}.
|
||||
* @param executor The {@link java.util.concurrent.Executor} that will perform the requests.
|
||||
|
|
@ -79,23 +80,36 @@ public final class CronetDataSourceFactory extends BaseFactory {
|
|||
}
|
||||
|
||||
/**
|
||||
* Constructs a CronetDataSourceFactory.
|
||||
* Creates an instance.
|
||||
*
|
||||
* <p>If the {@link CronetEngineWrapper} fails to provide a {@link CronetEngine}, a {@link
|
||||
* DefaultHttpDataSourceFactory} will be used instead.
|
||||
*
|
||||
* <p>Sets {@link CronetDataSource#DEFAULT_CONNECT_TIMEOUT_MILLIS} as the connection timeout,
|
||||
* {@link CronetDataSource#DEFAULT_READ_TIMEOUT_MILLIS} as the read timeout and disables
|
||||
* cross-protocol redirects.
|
||||
* {@link CronetDataSource#DEFAULT_READ_TIMEOUT_MILLIS} as the read timeout.
|
||||
*
|
||||
* @param cronetEngineWrapper A {@link CronetEngineWrapper}.
|
||||
* @param executor The {@link java.util.concurrent.Executor} that will perform the requests.
|
||||
*/
|
||||
public CronetDataSourceFactory(CronetEngineWrapper cronetEngineWrapper, Executor executor) {
|
||||
this(cronetEngineWrapper, executor, DEFAULT_USER_AGENT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an instance.
|
||||
*
|
||||
* <p>If the {@link CronetEngineWrapper} fails to provide a {@link CronetEngine}, a {@link
|
||||
* DefaultHttpDataSourceFactory} will be used instead.
|
||||
*
|
||||
* <p>Sets {@link CronetDataSource#DEFAULT_CONNECT_TIMEOUT_MILLIS} as the connection timeout,
|
||||
* {@link CronetDataSource#DEFAULT_READ_TIMEOUT_MILLIS} as the read timeout.
|
||||
*
|
||||
* @param cronetEngineWrapper A {@link CronetEngineWrapper}.
|
||||
* @param executor The {@link java.util.concurrent.Executor} that will perform the requests.
|
||||
* @param userAgent A user agent used to create a fallback HttpDataSource if needed.
|
||||
*/
|
||||
public CronetDataSourceFactory(
|
||||
CronetEngineWrapper cronetEngineWrapper,
|
||||
Executor executor,
|
||||
String userAgent) {
|
||||
CronetEngineWrapper cronetEngineWrapper, Executor executor, String userAgent) {
|
||||
this(
|
||||
cronetEngineWrapper,
|
||||
executor,
|
||||
|
|
@ -112,7 +126,7 @@ public final class CronetDataSourceFactory extends BaseFactory {
|
|||
}
|
||||
|
||||
/**
|
||||
* Constructs a CronetDataSourceFactory.
|
||||
* Creates an instance.
|
||||
*
|
||||
* <p>If the {@link CronetEngineWrapper} fails to provide a {@link CronetEngine}, a {@link
|
||||
* DefaultHttpDataSourceFactory} will be used instead.
|
||||
|
|
@ -147,7 +161,7 @@ public final class CronetDataSourceFactory extends BaseFactory {
|
|||
}
|
||||
|
||||
/**
|
||||
* Constructs a CronetDataSourceFactory.
|
||||
* Creates an instance.
|
||||
*
|
||||
* <p>If the {@link CronetEngineWrapper} fails to provide a {@link CronetEngine}, the provided
|
||||
* fallback {@link HttpDataSource.Factory} will be used instead.
|
||||
|
|
@ -178,14 +192,13 @@ public final class CronetDataSourceFactory extends BaseFactory {
|
|||
}
|
||||
|
||||
/**
|
||||
* Constructs a CronetDataSourceFactory.
|
||||
* Creates an instance.
|
||||
*
|
||||
* <p>If the {@link CronetEngineWrapper} fails to provide a {@link CronetEngine}, the provided
|
||||
* fallback {@link HttpDataSource.Factory} will be used instead.
|
||||
*
|
||||
* <p>Sets {@link CronetDataSource#DEFAULT_CONNECT_TIMEOUT_MILLIS} as the connection timeout,
|
||||
* {@link CronetDataSource#DEFAULT_READ_TIMEOUT_MILLIS} as the read timeout and disables
|
||||
* cross-protocol redirects.
|
||||
* {@link CronetDataSource#DEFAULT_READ_TIMEOUT_MILLIS} as the read timeout.
|
||||
*
|
||||
* @param cronetEngineWrapper A {@link CronetEngineWrapper}.
|
||||
* @param executor The {@link java.util.concurrent.Executor} that will perform the requests.
|
||||
|
|
@ -209,14 +222,33 @@ public final class CronetDataSourceFactory extends BaseFactory {
|
|||
}
|
||||
|
||||
/**
|
||||
* Constructs a CronetDataSourceFactory.
|
||||
* Creates an instance.
|
||||
*
|
||||
* <p>If the {@link CronetEngineWrapper} fails to provide a {@link CronetEngine}, a {@link
|
||||
* DefaultHttpDataSourceFactory} will be used instead.
|
||||
*
|
||||
* <p>Sets {@link CronetDataSource#DEFAULT_CONNECT_TIMEOUT_MILLIS} as the connection timeout,
|
||||
* {@link CronetDataSource#DEFAULT_READ_TIMEOUT_MILLIS} as the read timeout and disables
|
||||
* cross-protocol redirects.
|
||||
* {@link CronetDataSource#DEFAULT_READ_TIMEOUT_MILLIS} as the read timeout.
|
||||
*
|
||||
* @param cronetEngineWrapper A {@link CronetEngineWrapper}.
|
||||
* @param executor The {@link java.util.concurrent.Executor} that will perform the requests.
|
||||
* @param transferListener An optional listener.
|
||||
*/
|
||||
public CronetDataSourceFactory(
|
||||
CronetEngineWrapper cronetEngineWrapper,
|
||||
Executor executor,
|
||||
@Nullable TransferListener transferListener) {
|
||||
this(cronetEngineWrapper, executor, transferListener, DEFAULT_USER_AGENT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an instance.
|
||||
*
|
||||
* <p>If the {@link CronetEngineWrapper} fails to provide a {@link CronetEngine}, a {@link
|
||||
* DefaultHttpDataSourceFactory} will be used instead.
|
||||
*
|
||||
* <p>Sets {@link CronetDataSource#DEFAULT_CONNECT_TIMEOUT_MILLIS} as the connection timeout,
|
||||
* {@link CronetDataSource#DEFAULT_READ_TIMEOUT_MILLIS} as the read timeout.
|
||||
*
|
||||
* @param cronetEngineWrapper A {@link CronetEngineWrapper}.
|
||||
* @param executor The {@link java.util.concurrent.Executor} that will perform the requests.
|
||||
|
|
@ -244,7 +276,7 @@ public final class CronetDataSourceFactory extends BaseFactory {
|
|||
}
|
||||
|
||||
/**
|
||||
* Constructs a CronetDataSourceFactory.
|
||||
* Creates an instance.
|
||||
*
|
||||
* <p>If the {@link CronetEngineWrapper} fails to provide a {@link CronetEngine}, a {@link
|
||||
* DefaultHttpDataSourceFactory} will be used instead.
|
||||
|
|
@ -277,7 +309,7 @@ public final class CronetDataSourceFactory extends BaseFactory {
|
|||
}
|
||||
|
||||
/**
|
||||
* Constructs a CronetDataSourceFactory.
|
||||
* Creates an instance.
|
||||
*
|
||||
* <p>If the {@link CronetEngineWrapper} fails to provide a {@link CronetEngine}, the provided
|
||||
* fallback {@link HttpDataSource.Factory} will be used instead.
|
||||
|
|
|
|||
|
|
@ -15,6 +15,8 @@
|
|||
*/
|
||||
package com.google.android.exoplayer2.ext.cronet;
|
||||
|
||||
import static java.lang.Math.min;
|
||||
|
||||
import android.content.Context;
|
||||
import androidx.annotation.IntDef;
|
||||
import androidx.annotation.Nullable;
|
||||
|
|
@ -230,7 +232,7 @@ public final class CronetEngineWrapper {
|
|||
}
|
||||
String[] versionStringsLeft = Util.split(versionLeft, "\\.");
|
||||
String[] versionStringsRight = Util.split(versionRight, "\\.");
|
||||
int minLength = Math.min(versionStringsLeft.length, versionStringsRight.length);
|
||||
int minLength = min(versionStringsLeft.length, versionStringsRight.length);
|
||||
for (int i = 0; i < minLength; i++) {
|
||||
if (!versionStringsLeft[i].equals(versionStringsRight[i])) {
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@
|
|||
package com.google.android.exoplayer2.ext.cronet;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static java.lang.Math.min;
|
||||
import static org.junit.Assert.fail;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
|
|
@ -64,13 +65,10 @@ import org.junit.runner.RunWith;
|
|||
import org.mockito.ArgumentMatchers;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.MockitoAnnotations;
|
||||
import org.robolectric.annotation.LooperMode;
|
||||
import org.robolectric.annotation.LooperMode.Mode;
|
||||
import org.robolectric.shadows.ShadowLooper;
|
||||
|
||||
/** Tests for {@link CronetDataSource}. */
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
@LooperMode(Mode.PAUSED)
|
||||
public final class CronetDataSourceTest {
|
||||
|
||||
private static final int TEST_CONNECT_TIMEOUT_MS = 100;
|
||||
|
|
@ -378,15 +376,18 @@ public final class CronetDataSourceTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
public void requestOpenValidatesStatusCode() {
|
||||
public void requestOpenPropagatesFailureResponseBody() throws Exception {
|
||||
mockResponseStartSuccess();
|
||||
testUrlResponseInfo = createUrlResponseInfo(500); // statusCode
|
||||
// Use a size larger than CronetDataSource.READ_BUFFER_SIZE_BYTES
|
||||
int responseLength = 40 * 1024;
|
||||
mockReadSuccess(/* position= */ 0, /* length= */ responseLength);
|
||||
testUrlResponseInfo = createUrlResponseInfo(/* statusCode= */ 500);
|
||||
|
||||
try {
|
||||
dataSourceUnderTest.open(testDataSpec);
|
||||
fail("HttpDataSource.HttpDataSourceException expected");
|
||||
} catch (HttpDataSourceException e) {
|
||||
assertThat(e).isInstanceOf(HttpDataSource.InvalidResponseCodeException.class);
|
||||
fail("HttpDataSource.InvalidResponseCodeException expected");
|
||||
} catch (HttpDataSource.InvalidResponseCodeException e) {
|
||||
assertThat(e.responseBody).isEqualTo(buildTestDataArray(0, responseLength));
|
||||
// Check for connection not automatically closed.
|
||||
verify(mockUrlRequest, never()).cancel();
|
||||
verify(mockTransferListener, never())
|
||||
|
|
@ -1423,7 +1424,7 @@ public final class CronetDataSourceTest {
|
|||
mockUrlRequest, testUrlResponseInfo);
|
||||
} else {
|
||||
ByteBuffer inputBuffer = (ByteBuffer) invocation.getArguments()[0];
|
||||
int readLength = Math.min(positionAndRemaining[1], inputBuffer.remaining());
|
||||
int readLength = min(positionAndRemaining[1], inputBuffer.remaining());
|
||||
inputBuffer.put(buildTestDataBuffer(positionAndRemaining[0], readLength));
|
||||
positionAndRemaining[0] += readLength;
|
||||
positionAndRemaining[1] -= readLength;
|
||||
|
|
|
|||
|
|
@ -18,14 +18,15 @@ its modules locally. Instructions for doing this can be found in ExoPlayer's
|
|||
[top level README][]. The extension is not provided via JCenter (see [#2781][]
|
||||
for more information).
|
||||
|
||||
In addition, it's necessary to build the extension's native components as
|
||||
follows:
|
||||
In addition, it's necessary to manually build the FFmpeg library, so that gradle
|
||||
can bundle the FFmpeg binaries in the APK:
|
||||
|
||||
* Set the following shell variable:
|
||||
|
||||
```
|
||||
cd "<path to exoplayer checkout>"
|
||||
FFMPEG_EXT_PATH="$(pwd)/extensions/ffmpeg/src/main/jni"
|
||||
EXOPLAYER_ROOT="$(pwd)"
|
||||
FFMPEG_EXT_PATH="${EXOPLAYER_ROOT}/extensions/ffmpeg/src/main"
|
||||
```
|
||||
|
||||
* Download the [Android NDK][] and set its location in a shell variable.
|
||||
|
|
@ -41,6 +42,17 @@ NDK_PATH="<path to Android NDK>"
|
|||
HOST_PLATFORM="linux-x86_64"
|
||||
```
|
||||
|
||||
* Fetch FFmpeg and checkout an appropriate branch. We cannot guarantee
|
||||
compatibility with all versions of FFmpeg. We currently recommend version 4.2:
|
||||
|
||||
```
|
||||
cd "<preferred location for ffmpeg>" && \
|
||||
git clone git://source.ffmpeg.org/ffmpeg && \
|
||||
cd ffmpeg && \
|
||||
git checkout release/4.2 && \
|
||||
FFMPEG_PATH="$(pwd)"
|
||||
```
|
||||
|
||||
* Configure the decoders to include. See the [Supported formats][] page for
|
||||
details of the available decoders, and which formats they support.
|
||||
|
||||
|
|
@ -48,24 +60,23 @@ HOST_PLATFORM="linux-x86_64"
|
|||
ENABLED_DECODERS=(vorbis opus flac)
|
||||
```
|
||||
|
||||
* Fetch and build FFmpeg. Executing `build_ffmpeg.sh` will fetch and build
|
||||
FFmpeg 4.2 for `armeabi-v7a`, `arm64-v8a`, `x86` and `x86_64`. The script can
|
||||
be edited if you need to build for different architectures.
|
||||
* Add a link to the FFmpeg source code in the FFmpeg extension `jni` directory.
|
||||
|
||||
```
|
||||
cd "${FFMPEG_EXT_PATH}" && \
|
||||
cd "${FFMPEG_EXT_PATH}/jni" && \
|
||||
ln -s "$FFMPEG_PATH" ffmpeg
|
||||
```
|
||||
|
||||
* Execute `build_ffmpeg.sh` to build FFmpeg for `armeabi-v7a`, `arm64-v8a`,
|
||||
`x86` and `x86_64`. The script can be edited if you need to build for
|
||||
different architectures:
|
||||
|
||||
```
|
||||
cd "${FFMPEG_EXT_PATH}/jni" && \
|
||||
./build_ffmpeg.sh \
|
||||
"${FFMPEG_EXT_PATH}" "${NDK_PATH}" "${HOST_PLATFORM}" "${ENABLED_DECODERS[@]}"
|
||||
```
|
||||
|
||||
* Build the JNI native libraries, setting `APP_ABI` to include the architectures
|
||||
built in the previous step. For example:
|
||||
|
||||
```
|
||||
cd "${FFMPEG_EXT_PATH}" && \
|
||||
${NDK_PATH}/ndk-build APP_ABI="armeabi-v7a arm64-v8a x86 x86_64" -j4
|
||||
```
|
||||
|
||||
## Build instructions (Windows) ##
|
||||
|
||||
We do not provide support for building this extension on Windows, however it
|
||||
|
|
|
|||
|
|
@ -11,29 +11,13 @@
|
|||
// 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.library'
|
||||
apply from: "$gradle.ext.exoplayerSettingsDir/common_library_config.gradle"
|
||||
|
||||
android {
|
||||
compileSdkVersion project.ext.compileSdkVersion
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
minSdkVersion project.ext.minSdkVersion
|
||||
targetSdkVersion project.ext.targetSdkVersion
|
||||
consumerProguardFiles 'proguard-rules.txt'
|
||||
}
|
||||
|
||||
sourceSets.main {
|
||||
jniLibs.srcDir 'src/main/libs'
|
||||
jni.srcDirs = [] // Disable the automatic ndk-build call by Android Studio.
|
||||
}
|
||||
|
||||
testOptions.unitTests.includeAndroidResources = true
|
||||
// Configure the native build only if ffmpeg is present to avoid gradle sync
|
||||
// failures if ffmpeg hasn't been built according to the README instructions.
|
||||
if (project.file('src/main/jni/ffmpeg').exists()) {
|
||||
android.externalNativeBuild.cmake.path = 'src/main/jni/CMakeLists.txt'
|
||||
android.externalNativeBuild.cmake.version = '3.7.1+'
|
||||
}
|
||||
|
||||
dependencies {
|
||||
|
|
|
|||
|
|
@ -52,10 +52,10 @@ import java.util.List;
|
|||
private volatile int sampleRate;
|
||||
|
||||
public FfmpegAudioDecoder(
|
||||
Format format,
|
||||
int numInputBuffers,
|
||||
int numOutputBuffers,
|
||||
int initialInputBufferSize,
|
||||
Format format,
|
||||
boolean outputFloat)
|
||||
throws FfmpegDecoderException {
|
||||
super(new DecoderInputBuffer[numInputBuffers], new SimpleOutputBuffer[numOutputBuffers]);
|
||||
|
|
@ -82,7 +82,9 @@ import java.util.List;
|
|||
|
||||
@Override
|
||||
protected DecoderInputBuffer createInputBuffer() {
|
||||
return new DecoderInputBuffer(DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_DIRECT);
|
||||
return new DecoderInputBuffer(
|
||||
DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_DIRECT,
|
||||
FfmpegLibrary.getInputBufferPaddingSize());
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
|||
|
|
@ -15,6 +15,10 @@
|
|||
*/
|
||||
package com.google.android.exoplayer2.ext.ffmpeg;
|
||||
|
||||
import static com.google.android.exoplayer2.audio.AudioSink.SINK_FORMAT_SUPPORTED_DIRECTLY;
|
||||
import static com.google.android.exoplayer2.audio.AudioSink.SINK_FORMAT_SUPPORTED_WITH_TRANSCODING;
|
||||
import static com.google.android.exoplayer2.audio.AudioSink.SINK_FORMAT_UNSUPPORTED;
|
||||
|
||||
import android.os.Handler;
|
||||
import androidx.annotation.Nullable;
|
||||
import com.google.android.exoplayer2.C;
|
||||
|
|
@ -22,16 +26,17 @@ 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.AudioSink.SinkFormatSupport;
|
||||
import com.google.android.exoplayer2.audio.DecoderAudioRenderer;
|
||||
import com.google.android.exoplayer2.audio.DefaultAudioSink;
|
||||
import com.google.android.exoplayer2.drm.ExoMediaCrypto;
|
||||
import com.google.android.exoplayer2.util.Assertions;
|
||||
import com.google.android.exoplayer2.util.MimeTypes;
|
||||
import com.google.android.exoplayer2.util.TraceUtil;
|
||||
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
|
||||
/** Decodes and renders audio using FFmpeg. */
|
||||
public final class FfmpegAudioRenderer extends DecoderAudioRenderer {
|
||||
public final class FfmpegAudioRenderer extends DecoderAudioRenderer<FfmpegAudioDecoder> {
|
||||
|
||||
private static final String TAG = "FfmpegAudioRenderer";
|
||||
|
||||
|
|
@ -40,10 +45,6 @@ public final class FfmpegAudioRenderer extends DecoderAudioRenderer {
|
|||
/** The default input buffer size. */
|
||||
private static final int DEFAULT_INPUT_BUFFER_SIZE = 960 * 6;
|
||||
|
||||
private final boolean enableFloatOutput;
|
||||
|
||||
private @MonotonicNonNull FfmpegAudioDecoder decoder;
|
||||
|
||||
public FfmpegAudioRenderer() {
|
||||
this(/* eventHandler= */ null, /* eventListener= */ null);
|
||||
}
|
||||
|
|
@ -63,8 +64,7 @@ public final class FfmpegAudioRenderer extends DecoderAudioRenderer {
|
|||
this(
|
||||
eventHandler,
|
||||
eventListener,
|
||||
new DefaultAudioSink(/* audioCapabilities= */ null, audioProcessors),
|
||||
/* enableFloatOutput= */ false);
|
||||
new DefaultAudioSink(/* audioCapabilities= */ null, audioProcessors));
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -74,21 +74,15 @@ public final class FfmpegAudioRenderer extends DecoderAudioRenderer {
|
|||
* 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(
|
||||
@Nullable Handler eventHandler,
|
||||
@Nullable AudioRendererEventListener eventListener,
|
||||
AudioSink audioSink,
|
||||
boolean enableFloatOutput) {
|
||||
AudioSink audioSink) {
|
||||
super(
|
||||
eventHandler,
|
||||
eventListener,
|
||||
audioSink);
|
||||
this.enableFloatOutput = enableFloatOutput;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -102,9 +96,11 @@ public final class FfmpegAudioRenderer extends DecoderAudioRenderer {
|
|||
String mimeType = Assertions.checkNotNull(format.sampleMimeType);
|
||||
if (!FfmpegLibrary.isAvailable() || !MimeTypes.isAudio(mimeType)) {
|
||||
return FORMAT_UNSUPPORTED_TYPE;
|
||||
} else if (!FfmpegLibrary.supportsFormat(mimeType) || !isOutputSupported(format)) {
|
||||
} else if (!FfmpegLibrary.supportsFormat(mimeType)
|
||||
|| (!sinkSupportsFormat(format, C.ENCODING_PCM_16BIT)
|
||||
&& !sinkSupportsFormat(format, C.ENCODING_PCM_FLOAT))) {
|
||||
return FORMAT_UNSUPPORTED_SUBTYPE;
|
||||
} else if (format.drmInitData != null && format.exoMediaCryptoType == null) {
|
||||
} else if (format.exoMediaCryptoType != null) {
|
||||
return FORMAT_UNSUPPORTED_DRM;
|
||||
} else {
|
||||
return FORMAT_HANDLED;
|
||||
|
|
@ -123,15 +119,15 @@ public final class FfmpegAudioRenderer extends DecoderAudioRenderer {
|
|||
TraceUtil.beginSection("createFfmpegAudioDecoder");
|
||||
int initialInputBufferSize =
|
||||
format.maxInputSize != Format.NO_VALUE ? format.maxInputSize : DEFAULT_INPUT_BUFFER_SIZE;
|
||||
decoder =
|
||||
FfmpegAudioDecoder decoder =
|
||||
new FfmpegAudioDecoder(
|
||||
NUM_BUFFERS, NUM_BUFFERS, initialInputBufferSize, format, shouldUseFloatOutput(format));
|
||||
format, NUM_BUFFERS, NUM_BUFFERS, initialInputBufferSize, shouldOutputFloat(format));
|
||||
TraceUtil.endSection();
|
||||
return decoder;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Format getOutputFormat() {
|
||||
public Format getOutputFormat(FfmpegAudioDecoder decoder) {
|
||||
Assertions.checkNotNull(decoder);
|
||||
return new Format.Builder()
|
||||
.setSampleMimeType(MimeTypes.AUDIO_RAW)
|
||||
|
|
@ -141,31 +137,36 @@ public final class FfmpegAudioRenderer extends DecoderAudioRenderer {
|
|||
.build();
|
||||
}
|
||||
|
||||
private boolean isOutputSupported(Format inputFormat) {
|
||||
return shouldUseFloatOutput(inputFormat)
|
||||
|| supportsOutput(inputFormat.channelCount, inputFormat.sampleRate, C.ENCODING_PCM_16BIT);
|
||||
/**
|
||||
* Returns whether the renderer's {@link AudioSink} supports the PCM format that will be output
|
||||
* from the decoder for the given input format and requested output encoding.
|
||||
*/
|
||||
private boolean sinkSupportsFormat(Format inputFormat, @C.PcmEncoding int pcmEncoding) {
|
||||
return sinkSupportsFormat(
|
||||
Util.getPcmFormat(pcmEncoding, inputFormat.channelCount, inputFormat.sampleRate));
|
||||
}
|
||||
|
||||
private boolean shouldUseFloatOutput(Format inputFormat) {
|
||||
Assertions.checkNotNull(inputFormat.sampleMimeType);
|
||||
if (!enableFloatOutput
|
||||
|| !supportsOutput(
|
||||
inputFormat.channelCount, inputFormat.sampleRate, C.ENCODING_PCM_FLOAT)) {
|
||||
return false;
|
||||
private boolean shouldOutputFloat(Format inputFormat) {
|
||||
if (!sinkSupportsFormat(inputFormat, C.ENCODING_PCM_16BIT)) {
|
||||
// We have no choice because the sink doesn't support 16-bit integer PCM.
|
||||
return true;
|
||||
}
|
||||
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;
|
||||
|
||||
@SinkFormatSupport
|
||||
int formatSupport =
|
||||
getSinkFormatSupport(
|
||||
Util.getPcmFormat(
|
||||
C.ENCODING_PCM_FLOAT, inputFormat.channelCount, inputFormat.sampleRate));
|
||||
switch (formatSupport) {
|
||||
case SINK_FORMAT_SUPPORTED_DIRECTLY:
|
||||
// AC-3 is always 16-bit, so there's no point using floating point. Assume that it's worth
|
||||
// using for all other formats.
|
||||
return !MimeTypes.AUDIO_AC3.equals(inputFormat.sampleMimeType);
|
||||
case SINK_FORMAT_UNSUPPORTED:
|
||||
case SINK_FORMAT_SUPPORTED_WITH_TRANSCODING:
|
||||
default:
|
||||
// For all other formats, assume that it's worth using 32-bit float encoding.
|
||||
return true;
|
||||
// Always prefer 16-bit PCM if the sink does not provide direct support for floating point.
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,10 +16,12 @@
|
|||
package com.google.android.exoplayer2.ext.ffmpeg;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.ExoPlayerLibraryInfo;
|
||||
import com.google.android.exoplayer2.util.LibraryLoader;
|
||||
import com.google.android.exoplayer2.util.Log;
|
||||
import com.google.android.exoplayer2.util.MimeTypes;
|
||||
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
||||
|
||||
/**
|
||||
* Configures and queries the underlying native library.
|
||||
|
|
@ -33,7 +35,10 @@ public final class FfmpegLibrary {
|
|||
private static final String TAG = "FfmpegLibrary";
|
||||
|
||||
private static final LibraryLoader LOADER =
|
||||
new LibraryLoader("avutil", "swresample", "avcodec", "ffmpeg");
|
||||
new LibraryLoader("avutil", "swresample", "avcodec", "ffmpeg_jni");
|
||||
|
||||
private static @MonotonicNonNull String version;
|
||||
private static int inputBufferPaddingSize = C.LENGTH_UNSET;
|
||||
|
||||
private FfmpegLibrary() {}
|
||||
|
||||
|
|
@ -58,7 +63,27 @@ public final class FfmpegLibrary {
|
|||
/** Returns the version of the underlying library if available, or null otherwise. */
|
||||
@Nullable
|
||||
public static String getVersion() {
|
||||
return isAvailable() ? ffmpegGetVersion() : null;
|
||||
if (!isAvailable()) {
|
||||
return null;
|
||||
}
|
||||
if (version == null) {
|
||||
version = ffmpegGetVersion();
|
||||
}
|
||||
return version;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the required amount of padding for input buffers in bytes, or {@link C#LENGTH_UNSET} if
|
||||
* the underlying library is not available.
|
||||
*/
|
||||
public static int getInputBufferPaddingSize() {
|
||||
if (!isAvailable()) {
|
||||
return C.LENGTH_UNSET;
|
||||
}
|
||||
if (inputBufferPaddingSize == C.LENGTH_UNSET) {
|
||||
inputBufferPaddingSize = ffmpegGetInputBufferPaddingSize();
|
||||
}
|
||||
return inputBufferPaddingSize;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -130,6 +155,8 @@ public final class FfmpegLibrary {
|
|||
}
|
||||
|
||||
private static native String ffmpegGetVersion();
|
||||
private static native boolean ffmpegHasDecoder(String codecName);
|
||||
|
||||
private static native int ffmpegGetInputBufferPaddingSize();
|
||||
|
||||
private static native boolean ffmpegHasDecoder(String codecName);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ import com.google.android.exoplayer2.video.VideoRendererEventListener;
|
|||
*/
|
||||
public final class FfmpegVideoRenderer extends DecoderVideoRenderer {
|
||||
|
||||
private static final String TAG = "FfmpegAudioRenderer";
|
||||
private static final String TAG = "FfmpegVideoRenderer";
|
||||
|
||||
/**
|
||||
* Creates a new instance.
|
||||
|
|
@ -76,7 +76,7 @@ public final class FfmpegVideoRenderer extends DecoderVideoRenderer {
|
|||
return FORMAT_UNSUPPORTED_TYPE;
|
||||
} else if (!FfmpegLibrary.supportsFormat(format.sampleMimeType)) {
|
||||
return RendererCapabilities.create(FORMAT_UNSUPPORTED_SUBTYPE);
|
||||
} else if (format.drmInitData != null && format.exoMediaCryptoType == null) {
|
||||
} else if (format.exoMediaCryptoType != null) {
|
||||
return RendererCapabilities.create(FORMAT_UNSUPPORTED_DRM);
|
||||
} else {
|
||||
return RendererCapabilities.create(
|
||||
|
|
|
|||
|
|
@ -1,40 +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.
|
||||
#
|
||||
|
||||
LOCAL_PATH := $(call my-dir)
|
||||
|
||||
include $(CLEAR_VARS)
|
||||
LOCAL_MODULE := libavcodec
|
||||
LOCAL_SRC_FILES := ffmpeg/android-libs/$(TARGET_ARCH_ABI)/$(LOCAL_MODULE).so
|
||||
include $(PREBUILT_SHARED_LIBRARY)
|
||||
|
||||
include $(CLEAR_VARS)
|
||||
LOCAL_MODULE := libswresample
|
||||
LOCAL_SRC_FILES := ffmpeg/android-libs/$(TARGET_ARCH_ABI)/$(LOCAL_MODULE).so
|
||||
include $(PREBUILT_SHARED_LIBRARY)
|
||||
|
||||
include $(CLEAR_VARS)
|
||||
LOCAL_MODULE := libavutil
|
||||
LOCAL_SRC_FILES := ffmpeg/android-libs/$(TARGET_ARCH_ABI)/$(LOCAL_MODULE).so
|
||||
include $(PREBUILT_SHARED_LIBRARY)
|
||||
|
||||
include $(CLEAR_VARS)
|
||||
LOCAL_MODULE := ffmpeg
|
||||
LOCAL_SRC_FILES := ffmpeg_jni.cc
|
||||
LOCAL_C_INCLUDES := ffmpeg
|
||||
LOCAL_SHARED_LIBRARIES := libavcodec libswresample libavutil
|
||||
LOCAL_LDLIBS := -Lffmpeg/android-libs/$(TARGET_ARCH_ABI) -llog
|
||||
include $(BUILD_SHARED_LIBRARY)
|
||||
|
|
@ -1,20 +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.
|
||||
#
|
||||
|
||||
APP_OPTIM := release
|
||||
APP_STL := c++_static
|
||||
APP_CPPFLAGS := -frtti
|
||||
APP_PLATFORM := android-9
|
||||
36
extensions/ffmpeg/src/main/jni/CMakeLists.txt
Normal file
36
extensions/ffmpeg/src/main/jni/CMakeLists.txt
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
cmake_minimum_required(VERSION 3.7.1 FATAL_ERROR)
|
||||
|
||||
# Enable C++11 features.
|
||||
set(CMAKE_CXX_STANDARD 11)
|
||||
|
||||
project(libffmpeg_jni C CXX)
|
||||
|
||||
set(ffmpeg_location "${CMAKE_CURRENT_SOURCE_DIR}/ffmpeg")
|
||||
set(ffmpeg_binaries "${ffmpeg_location}/android-libs/${ANDROID_ABI}")
|
||||
|
||||
foreach(ffmpeg_lib avutil swresample avcodec)
|
||||
set(ffmpeg_lib_filename lib${ffmpeg_lib}.so)
|
||||
set(ffmpeg_lib_file_path ${ffmpeg_binaries}/${ffmpeg_lib_filename})
|
||||
add_library(
|
||||
${ffmpeg_lib}
|
||||
SHARED
|
||||
IMPORTED)
|
||||
set_target_properties(
|
||||
${ffmpeg_lib} PROPERTIES
|
||||
IMPORTED_LOCATION
|
||||
${ffmpeg_lib_file_path})
|
||||
endforeach()
|
||||
|
||||
include_directories(${ffmpeg_location})
|
||||
find_library(android_log_lib log)
|
||||
|
||||
add_library(ffmpeg_jni
|
||||
SHARED
|
||||
ffmpeg_jni.cc)
|
||||
|
||||
target_link_libraries(ffmpeg_jni
|
||||
PRIVATE android
|
||||
PRIVATE avutil
|
||||
PRIVATE swresample
|
||||
PRIVATE avcodec
|
||||
PRIVATE ${android_log_lib})
|
||||
|
|
@ -41,10 +41,7 @@ for decoder in "${ENABLED_DECODERS[@]}"
|
|||
do
|
||||
COMMON_OPTIONS="${COMMON_OPTIONS} --enable-decoder=${decoder}"
|
||||
done
|
||||
cd "${FFMPEG_EXT_PATH}"
|
||||
(git -C ffmpeg pull || git clone git://source.ffmpeg.org/ffmpeg ffmpeg)
|
||||
cd ffmpeg
|
||||
git checkout release/4.2
|
||||
cd "${FFMPEG_EXT_PATH}/jni/ffmpeg"
|
||||
./configure \
|
||||
--libdir=android-libs/armeabi-v7a \
|
||||
--arch=arm \
|
||||
|
|
|
|||
|
|
@ -113,6 +113,10 @@ LIBRARY_FUNC(jstring, ffmpegGetVersion) {
|
|||
return env->NewStringUTF(LIBAVCODEC_IDENT);
|
||||
}
|
||||
|
||||
LIBRARY_FUNC(jint, ffmpegGetInputBufferPaddingSize) {
|
||||
return (jint)AV_INPUT_BUFFER_PADDING_SIZE;
|
||||
}
|
||||
|
||||
LIBRARY_FUNC(jboolean, ffmpegHasDecoder, jstring codecName) {
|
||||
return getCodecByName(env, codecName) != NULL;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,13 +21,22 @@ import com.google.android.exoplayer2.testutil.DefaultRenderersFactoryAsserts;
|
|||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
/** Unit test for {@link DefaultRenderersFactoryTest} with {@link FfmpegAudioRenderer}. */
|
||||
/**
|
||||
* Unit test for {@link DefaultRenderersFactoryTest} with {@link FfmpegAudioRenderer} and {@link
|
||||
* FfmpegVideoRenderer}.
|
||||
*/
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public final class DefaultRenderersFactoryTest {
|
||||
|
||||
@Test
|
||||
public void createRenderers_instantiatesVpxRenderer() {
|
||||
public void createRenderers_instantiatesFfmpegAudioRenderer() {
|
||||
DefaultRenderersFactoryAsserts.assertExtensionRendererCreated(
|
||||
FfmpegAudioRenderer.class, C.TRACK_TYPE_AUDIO);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void createRenderers_instantiatesFfmpegVideoRenderer() {
|
||||
DefaultRenderersFactoryAsserts.assertExtensionRendererCreated(
|
||||
FfmpegVideoRenderer.class, C.TRACK_TYPE_VIDEO);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,24 +11,9 @@
|
|||
// 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.library'
|
||||
apply from: "$gradle.ext.exoplayerSettingsDir/common_library_config.gradle"
|
||||
|
||||
android {
|
||||
compileSdkVersion project.ext.compileSdkVersion
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
minSdkVersion project.ext.minSdkVersion
|
||||
targetSdkVersion project.ext.targetSdkVersion
|
||||
consumerProguardFiles 'proguard-rules.txt'
|
||||
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
main {
|
||||
jniLibs.srcDir 'src/main/libs'
|
||||
|
|
@ -36,8 +21,6 @@ android {
|
|||
}
|
||||
androidTest.assets.srcDir '../../testdata/src/test/assets/'
|
||||
}
|
||||
|
||||
testOptions.unitTests.includeAndroidResources = true
|
||||
}
|
||||
|
||||
dependencies {
|
||||
|
|
|
|||
|
|
@ -37,16 +37,16 @@ import org.junit.runner.RunWith;
|
|||
@RunWith(AndroidJUnit4.class)
|
||||
public final class FlacExtractorSeekTest {
|
||||
|
||||
private static final String TEST_FILE_SEEK_TABLE = "flac/bear.flac";
|
||||
private static final String TEST_FILE_BINARY_SEARCH = "flac/bear_one_metadata_block.flac";
|
||||
private static final String TEST_FILE_UNSEEKABLE = "flac/bear_no_seek_table_no_num_samples.flac";
|
||||
private static final String TEST_FILE_SEEK_TABLE = "media/flac/bear.flac";
|
||||
private static final String TEST_FILE_BINARY_SEARCH = "media/flac/bear_one_metadata_block.flac";
|
||||
private static final String TEST_FILE_UNSEEKABLE =
|
||||
"media/flac/bear_no_seek_table_no_num_samples.flac";
|
||||
private static final int DURATION_US = 2_741_000;
|
||||
|
||||
private FlacExtractor extractor = new FlacExtractor();
|
||||
private FakeExtractorOutput extractorOutput = new FakeExtractorOutput();
|
||||
private DefaultDataSource dataSource =
|
||||
new DefaultDataSourceFactory(ApplicationProvider.getApplicationContext(), "UserAgent")
|
||||
.createDataSource();
|
||||
new DefaultDataSourceFactory(ApplicationProvider.getApplicationContext()).createDataSource();
|
||||
|
||||
@Test
|
||||
public void flacExtractorReads_seekTable_returnSeekableSeekMap() throws IOException {
|
||||
|
|
|
|||
|
|
@ -39,78 +39,80 @@ public class FlacExtractorTest {
|
|||
@Test
|
||||
public void sample() throws Exception {
|
||||
ExtractorAsserts.assertAllBehaviors(
|
||||
FlacExtractor::new, /* file= */ "flac/bear.flac", /* dumpFilesPrefix= */ "flac/bear_raw");
|
||||
FlacExtractor::new,
|
||||
/* file= */ "media/flac/bear.flac",
|
||||
/* dumpFilesPrefix= */ "extractordumps/flac/bear_raw");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void sampleWithId3HeaderAndId3Enabled() throws Exception {
|
||||
ExtractorAsserts.assertAllBehaviors(
|
||||
FlacExtractor::new,
|
||||
/* file= */ "flac/bear_with_id3.flac",
|
||||
/* dumpFilesPrefix= */ "flac/bear_with_id3_enabled_raw");
|
||||
/* file= */ "media/flac/bear_with_id3.flac",
|
||||
/* dumpFilesPrefix= */ "extractordumps/flac/bear_with_id3_enabled_raw");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void sampleWithId3HeaderAndId3Disabled() throws Exception {
|
||||
ExtractorAsserts.assertAllBehaviors(
|
||||
() -> new FlacExtractor(FlacExtractor.FLAG_DISABLE_ID3_METADATA),
|
||||
/* file= */ "flac/bear_with_id3.flac",
|
||||
/* dumpFilesPrefix= */ "flac/bear_with_id3_disabled_raw");
|
||||
/* file= */ "media/flac/bear_with_id3.flac",
|
||||
/* dumpFilesPrefix= */ "extractordumps/flac/bear_with_id3_disabled_raw");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void sampleUnseekable() throws Exception {
|
||||
ExtractorAsserts.assertAllBehaviors(
|
||||
FlacExtractor::new,
|
||||
/* file= */ "flac/bear_no_seek_table_no_num_samples.flac",
|
||||
/* dumpFilesPrefix= */ "flac/bear_no_seek_table_no_num_samples_raw");
|
||||
/* file= */ "media/flac/bear_no_seek_table_no_num_samples.flac",
|
||||
/* dumpFilesPrefix= */ "extractordumps/flac/bear_no_seek_table_no_num_samples_raw");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void sampleWithVorbisComments() throws Exception {
|
||||
ExtractorAsserts.assertAllBehaviors(
|
||||
FlacExtractor::new,
|
||||
/* file= */ "flac/bear_with_vorbis_comments.flac",
|
||||
/* dumpFilesPrefix= */ "flac/bear_with_vorbis_comments_raw");
|
||||
/* file= */ "media/flac/bear_with_vorbis_comments.flac",
|
||||
/* dumpFilesPrefix= */ "extractordumps/flac/bear_with_vorbis_comments_raw");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void sampleWithPicture() throws Exception {
|
||||
ExtractorAsserts.assertAllBehaviors(
|
||||
FlacExtractor::new,
|
||||
/* file= */ "flac/bear_with_picture.flac",
|
||||
/* dumpFilesPrefix= */ "flac/bear_with_picture_raw");
|
||||
/* file= */ "media/flac/bear_with_picture.flac",
|
||||
/* dumpFilesPrefix= */ "extractordumps/flac/bear_with_picture_raw");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void oneMetadataBlock() throws Exception {
|
||||
ExtractorAsserts.assertAllBehaviors(
|
||||
FlacExtractor::new,
|
||||
/* file= */ "flac/bear_one_metadata_block.flac",
|
||||
/* dumpFilesPrefix= */ "flac/bear_one_metadata_block_raw");
|
||||
/* file= */ "media/flac/bear_one_metadata_block.flac",
|
||||
/* dumpFilesPrefix= */ "extractordumps/flac/bear_one_metadata_block_raw");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void noMinMaxFrameSize() throws Exception {
|
||||
ExtractorAsserts.assertAllBehaviors(
|
||||
FlacExtractor::new,
|
||||
/* file= */ "flac/bear_no_min_max_frame_size.flac",
|
||||
/* dumpFilesPrefix= */ "flac/bear_no_min_max_frame_size_raw");
|
||||
/* file= */ "media/flac/bear_no_min_max_frame_size.flac",
|
||||
/* dumpFilesPrefix= */ "extractordumps/flac/bear_no_min_max_frame_size_raw");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void noNumSamples() throws Exception {
|
||||
ExtractorAsserts.assertAllBehaviors(
|
||||
FlacExtractor::new,
|
||||
/* file= */ "flac/bear_no_num_samples.flac",
|
||||
/* dumpFilesPrefix= */ "flac/bear_no_num_samples_raw");
|
||||
/* file= */ "media/flac/bear_no_num_samples.flac",
|
||||
/* dumpFilesPrefix= */ "extractordumps/flac/bear_no_num_samples_raw");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void uncommonSampleRate() throws Exception {
|
||||
ExtractorAsserts.assertAllBehaviors(
|
||||
FlacExtractor::new,
|
||||
/* file= */ "flac/bear_uncommon_sample_rate.flac",
|
||||
/* dumpFilesPrefix= */ "flac/bear_uncommon_sample_rate_raw");
|
||||
/* file= */ "media/flac/bear_uncommon_sample_rate.flac",
|
||||
/* dumpFilesPrefix= */ "extractordumps/flac/bear_uncommon_sample_rate_raw");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ import androidx.test.core.app.ApplicationProvider;
|
|||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
import com.google.android.exoplayer2.ExoPlaybackException;
|
||||
import com.google.android.exoplayer2.ExoPlayer;
|
||||
import com.google.android.exoplayer2.MediaItem;
|
||||
import com.google.android.exoplayer2.Player;
|
||||
import com.google.android.exoplayer2.audio.AudioProcessor;
|
||||
import com.google.android.exoplayer2.audio.AudioSink;
|
||||
|
|
@ -33,6 +34,7 @@ import com.google.android.exoplayer2.extractor.mkv.MatroskaExtractor;
|
|||
import com.google.android.exoplayer2.source.MediaSource;
|
||||
import com.google.android.exoplayer2.source.ProgressiveMediaSource;
|
||||
import com.google.android.exoplayer2.testutil.CapturingAudioSink;
|
||||
import com.google.android.exoplayer2.testutil.DumpFileAsserts;
|
||||
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
|
@ -69,7 +71,7 @@ public class FlacPlaybackTest {
|
|||
|
||||
TestPlaybackRunnable testPlaybackRunnable =
|
||||
new TestPlaybackRunnable(
|
||||
Uri.parse("asset:///" + fileName),
|
||||
Uri.parse("asset:///media/" + fileName),
|
||||
ApplicationProvider.getApplicationContext(),
|
||||
audioSink);
|
||||
Thread thread = new Thread(testPlaybackRunnable);
|
||||
|
|
@ -79,8 +81,10 @@ public class FlacPlaybackTest {
|
|||
throw testPlaybackRunnable.playbackException;
|
||||
}
|
||||
|
||||
audioSink.assertOutput(
|
||||
ApplicationProvider.getApplicationContext(), fileName + ".audiosink.dump");
|
||||
DumpFileAsserts.assertOutput(
|
||||
ApplicationProvider.getApplicationContext(),
|
||||
audioSink,
|
||||
"audiosinkdumps/" + fileName + ".audiosink.dump");
|
||||
}
|
||||
|
||||
private static class TestPlaybackRunnable implements Player.EventListener, Runnable {
|
||||
|
|
@ -107,9 +111,8 @@ public class FlacPlaybackTest {
|
|||
player.addListener(this);
|
||||
MediaSource mediaSource =
|
||||
new ProgressiveMediaSource.Factory(
|
||||
new DefaultDataSourceFactory(context, "ExoPlayerExtFlacTest"),
|
||||
MatroskaExtractor.FACTORY)
|
||||
.createMediaSource(uri);
|
||||
new DefaultDataSourceFactory(context), MatroskaExtractor.FACTORY)
|
||||
.createMediaSource(MediaItem.fromUri(uri));
|
||||
player.setMediaSource(mediaSource);
|
||||
player.prepare();
|
||||
player.play();
|
||||
|
|
|
|||
|
|
@ -15,6 +15,8 @@
|
|||
*/
|
||||
package com.google.android.exoplayer2.ext.flac;
|
||||
|
||||
import static java.lang.Math.max;
|
||||
|
||||
import com.google.android.exoplayer2.extractor.BinarySearchSeeker;
|
||||
import com.google.android.exoplayer2.extractor.ExtractorInput;
|
||||
import com.google.android.exoplayer2.extractor.FlacStreamMetadata;
|
||||
|
|
@ -74,7 +76,7 @@ import java.nio.ByteBuffer;
|
|||
/* floorBytePosition= */ firstFramePosition,
|
||||
/* ceilingBytePosition= */ inputLength,
|
||||
/* approxBytesPerFrame= */ streamMetadata.getApproxBytesPerFrame(),
|
||||
/* minimumSearchRange= */ Math.max(
|
||||
/* minimumSearchRange= */ max(
|
||||
FlacConstants.MIN_FRAME_HEADER_SIZE, streamMetadata.minFrameSize));
|
||||
this.decoderJni = Assertions.checkNotNull(decoderJni);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,6 +15,8 @@
|
|||
*/
|
||||
package com.google.android.exoplayer2.ext.flac;
|
||||
|
||||
import static java.lang.Math.min;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.ParserException;
|
||||
|
|
@ -118,7 +120,7 @@ import java.nio.ByteBuffer;
|
|||
public int read(ByteBuffer target) throws IOException {
|
||||
int byteCount = target.remaining();
|
||||
if (byteBufferData != null) {
|
||||
byteCount = Math.min(byteCount, byteBufferData.remaining());
|
||||
byteCount = min(byteCount, byteBufferData.remaining());
|
||||
int originalLimit = byteBufferData.limit();
|
||||
byteBufferData.limit(byteBufferData.position() + byteCount);
|
||||
target.put(byteBufferData);
|
||||
|
|
@ -126,7 +128,7 @@ import java.nio.ByteBuffer;
|
|||
} else if (extractorInput != null) {
|
||||
ExtractorInput extractorInput = this.extractorInput;
|
||||
byte[] tempBuffer = Util.castNonNull(this.tempBuffer);
|
||||
byteCount = Math.min(byteCount, TEMP_BUFFER_SIZE);
|
||||
byteCount = min(byteCount, TEMP_BUFFER_SIZE);
|
||||
int read = readFromExtractorInput(extractorInput, tempBuffer, /* offset= */ 0, byteCount);
|
||||
if (read < 4) {
|
||||
// Reading less than 4 bytes, most of the time, happens because of getting the bytes left in
|
||||
|
|
|
|||
|
|
@ -53,6 +53,11 @@ public final class FlacExtractor implements Extractor {
|
|||
/** Factory that returns one extractor which is a {@link FlacExtractor}. */
|
||||
public static final ExtractorsFactory FACTORY = () -> new Extractor[] {new FlacExtractor()};
|
||||
|
||||
// LINT.IfChange
|
||||
/*
|
||||
* Flags in the two FLAC extractors should be kept in sync. If we ever change this then
|
||||
* DefaultExtractorsFactory will need modifying, because it currently assumes this is the case.
|
||||
*/
|
||||
/**
|
||||
* Flags controlling the behavior of the extractor. Possible flag value is {@link
|
||||
* #FLAG_DISABLE_ID3_METADATA}.
|
||||
|
|
@ -68,7 +73,9 @@ public final class FlacExtractor implements Extractor {
|
|||
* Flag to disable parsing of ID3 metadata. Can be set to save memory if ID3 metadata is not
|
||||
* required.
|
||||
*/
|
||||
public static final int FLAG_DISABLE_ID3_METADATA = 1;
|
||||
public static final int FLAG_DISABLE_ID3_METADATA =
|
||||
com.google.android.exoplayer2.extractor.flac.FlacExtractor.FLAG_DISABLE_ID3_METADATA;
|
||||
// LINT.ThenChange(../../../../../../../../../../../library/extractor/src/main/java/com/google/android/exoplayer2/extractor/flac/FlacExtractor.java)
|
||||
|
||||
private final ParsableByteArray outputBuffer;
|
||||
private final boolean id3MetadataDisabled;
|
||||
|
|
@ -203,7 +210,7 @@ public final class FlacExtractor implements Extractor {
|
|||
if (this.streamMetadata == null) {
|
||||
this.streamMetadata = streamMetadata;
|
||||
outputBuffer.reset(streamMetadata.getMaxDecodedFrameSize());
|
||||
outputFrameHolder = new OutputFrameHolder(ByteBuffer.wrap(outputBuffer.data));
|
||||
outputFrameHolder = new OutputFrameHolder(ByteBuffer.wrap(outputBuffer.getData()));
|
||||
binarySearchSeeker =
|
||||
outputSeekMap(
|
||||
flacDecoderJni,
|
||||
|
|
|
|||
|
|
@ -25,26 +25,24 @@ import com.google.android.exoplayer2.audio.AudioSink;
|
|||
import com.google.android.exoplayer2.audio.DecoderAudioRenderer;
|
||||
import com.google.android.exoplayer2.drm.ExoMediaCrypto;
|
||||
import com.google.android.exoplayer2.extractor.FlacStreamMetadata;
|
||||
import com.google.android.exoplayer2.util.Assertions;
|
||||
import com.google.android.exoplayer2.util.FlacConstants;
|
||||
import com.google.android.exoplayer2.util.MimeTypes;
|
||||
import com.google.android.exoplayer2.util.TraceUtil;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
||||
|
||||
/** Decodes and renders audio using the native Flac decoder. */
|
||||
public final class LibflacAudioRenderer extends DecoderAudioRenderer {
|
||||
public final class LibflacAudioRenderer extends DecoderAudioRenderer<FlacDecoder> {
|
||||
|
||||
private static final String TAG = "LibflacAudioRenderer";
|
||||
private static final int NUM_BUFFERS = 16;
|
||||
|
||||
private @MonotonicNonNull FlacStreamMetadata streamMetadata;
|
||||
|
||||
public LibflacAudioRenderer() {
|
||||
this(/* eventHandler= */ null, /* eventListener= */ null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an instance.
|
||||
*
|
||||
* @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.
|
||||
|
|
@ -58,6 +56,8 @@ public final class LibflacAudioRenderer extends DecoderAudioRenderer {
|
|||
}
|
||||
|
||||
/**
|
||||
* Creates an instance.
|
||||
*
|
||||
* @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.
|
||||
|
|
@ -85,24 +85,25 @@ public final class LibflacAudioRenderer extends DecoderAudioRenderer {
|
|||
|| !MimeTypes.AUDIO_FLAC.equalsIgnoreCase(format.sampleMimeType)) {
|
||||
return FORMAT_UNSUPPORTED_TYPE;
|
||||
}
|
||||
// Compute the PCM encoding that the FLAC decoder will output.
|
||||
@C.PcmEncoding int pcmEncoding;
|
||||
// Compute the format that the FLAC decoder will output.
|
||||
Format outputFormat;
|
||||
if (format.initializationData.isEmpty()) {
|
||||
// The initialization data might not be set if the format was obtained from a manifest (e.g.
|
||||
// for DASH playbacks) rather than directly from the media. In this case we assume
|
||||
// ENCODING_PCM_16BIT. If the actual encoding is different then playback will still succeed as
|
||||
// long as the AudioSink supports it, which will always be true when using DefaultAudioSink.
|
||||
pcmEncoding = C.ENCODING_PCM_16BIT;
|
||||
outputFormat =
|
||||
Util.getPcmFormat(C.ENCODING_PCM_16BIT, format.channelCount, format.sampleRate);
|
||||
} else {
|
||||
int streamMetadataOffset =
|
||||
FlacConstants.STREAM_MARKER_SIZE + FlacConstants.METADATA_BLOCK_HEADER_SIZE;
|
||||
FlacStreamMetadata streamMetadata =
|
||||
new FlacStreamMetadata(format.initializationData.get(0), streamMetadataOffset);
|
||||
pcmEncoding = Util.getPcmEncoding(streamMetadata.bitsPerSample);
|
||||
outputFormat = getOutputFormat(streamMetadata);
|
||||
}
|
||||
if (!supportsOutput(format.channelCount, format.sampleRate, pcmEncoding)) {
|
||||
if (!sinkSupportsFormat(outputFormat)) {
|
||||
return FORMAT_UNSUPPORTED_SUBTYPE;
|
||||
} else if (format.drmInitData != null && format.exoMediaCryptoType == null) {
|
||||
} else if (format.exoMediaCryptoType != null) {
|
||||
return FORMAT_UNSUPPORTED_DRM;
|
||||
} else {
|
||||
return FORMAT_HANDLED;
|
||||
|
|
@ -115,19 +116,19 @@ public final class LibflacAudioRenderer extends DecoderAudioRenderer {
|
|||
TraceUtil.beginSection("createFlacDecoder");
|
||||
FlacDecoder decoder =
|
||||
new FlacDecoder(NUM_BUFFERS, NUM_BUFFERS, format.maxInputSize, format.initializationData);
|
||||
streamMetadata = decoder.getStreamMetadata();
|
||||
TraceUtil.endSection();
|
||||
return decoder;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Format getOutputFormat() {
|
||||
Assertions.checkNotNull(streamMetadata);
|
||||
return new Format.Builder()
|
||||
.setSampleMimeType(MimeTypes.AUDIO_RAW)
|
||||
.setChannelCount(streamMetadata.channels)
|
||||
.setSampleRate(streamMetadata.sampleRate)
|
||||
.setPcmEncoding(Util.getPcmEncoding(streamMetadata.bitsPerSample))
|
||||
.build();
|
||||
protected Format getOutputFormat(FlacDecoder decoder) {
|
||||
return getOutputFormat(decoder.getStreamMetadata());
|
||||
}
|
||||
|
||||
private static Format getOutputFormat(FlacStreamMetadata streamMetadata) {
|
||||
return Util.getPcmFormat(
|
||||
Util.getPcmEncoding(streamMetadata.bitsPerSample),
|
||||
streamMetadata.channels,
|
||||
streamMetadata.sampleRate);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ import org.junit.runner.RunWith;
|
|||
public final class DefaultRenderersFactoryTest {
|
||||
|
||||
@Test
|
||||
public void createRenderers_instantiatesVpxRenderer() {
|
||||
public void createRenderers_instantiatesFlacRenderer() {
|
||||
DefaultRenderersFactoryAsserts.assertExtensionRendererCreated(
|
||||
LibflacAudioRenderer.class, C.TRACK_TYPE_AUDIO);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,24 +11,9 @@
|
|||
// 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.library'
|
||||
apply from: "$gradle.ext.exoplayerSettingsDir/common_library_config.gradle"
|
||||
|
||||
android {
|
||||
compileSdkVersion project.ext.compileSdkVersion
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
minSdkVersion 19
|
||||
targetSdkVersion project.ext.targetSdkVersion
|
||||
}
|
||||
|
||||
testOptions.unitTests.includeAndroidResources = true
|
||||
}
|
||||
android.defaultConfig.minSdkVersion 19
|
||||
|
||||
dependencies {
|
||||
implementation project(modulePrefix + 'library-core')
|
||||
|
|
|
|||
|
|
@ -26,35 +26,29 @@ locally. Instructions for doing this can be found in ExoPlayer's
|
|||
|
||||
## Using the extension ##
|
||||
|
||||
To play ads alongside a single-window content `MediaSource`, prepare the player
|
||||
with an `AdsMediaSource` constructed using an `ImaAdsLoader`, the content
|
||||
`MediaSource` and an overlay `ViewGroup` on top of the player. Pass an ad tag
|
||||
URI from your ad campaign when creating the `ImaAdsLoader`. The IMA
|
||||
documentation includes some [sample ad tags][] for testing. Note that the IMA
|
||||
To use the extension, follow the instructions on the
|
||||
[Ad insertion page](https://exoplayer.dev/ad-insertion.html#declarative-ad-support)
|
||||
of the developer guide. The `AdsLoaderProvider` passed to the player's
|
||||
`DefaultMediaSourceFactory` should return an `ImaAdsLoader`. Note that the IMA
|
||||
extension only supports players which are accessed on the application's main
|
||||
thread.
|
||||
|
||||
Resuming the player after entering the background requires some special handling
|
||||
when playing ads. The player and its media source are released on entering the
|
||||
background, and are recreated when the player returns to the foreground. When
|
||||
playing ads it is necessary to persist ad playback state while in the background
|
||||
by keeping a reference to the `ImaAdsLoader`. Reuse it when resuming playback of
|
||||
the same content/ads by passing it in when constructing the new
|
||||
`AdsMediaSource`. It is also important to persist the player position when
|
||||
background, and are recreated when returning to the foreground. When playing ads
|
||||
it is necessary to persist ad playback state while in the background by keeping
|
||||
a reference to the `ImaAdsLoader`. When re-entering the foreground, pass the
|
||||
same instance back when `AdsLoaderProvider.getAdsLoader(Uri adTagUri)` is called
|
||||
to restore the state. It is also important to persist the player position when
|
||||
entering the background by storing the value of `player.getContentPosition()`.
|
||||
On returning to the foreground, seek to that position before preparing the new
|
||||
player instance. Finally, it is important to call `ImaAdsLoader.release()` when
|
||||
playback of the content/ads has finished and will not be resumed.
|
||||
playback has finished and will not be resumed.
|
||||
|
||||
You can try the IMA extension in the ExoPlayer demo app. To do this you must
|
||||
select and build one of the `withExtensions` build variants of the demo app in
|
||||
Android Studio. You can find IMA test content in the "IMA sample ad tags"
|
||||
section of the app. The demo app's `PlayerActivity` also shows how to persist
|
||||
the `ImaAdsLoader` instance and the player position when backgrounded during ad
|
||||
playback.
|
||||
|
||||
[top level README]: https://github.com/google/ExoPlayer/blob/release-v2/README.md
|
||||
[sample ad tags]: https://developers.google.com/interactive-media-ads/docs/sdks/android/tags
|
||||
You can try the IMA extension in the ExoPlayer demo app, which has test content
|
||||
in the "IMA sample ad tags" section of the sample chooser. The demo app's
|
||||
`PlayerActivity` also shows how to persist the `ImaAdsLoader` instance and the
|
||||
player position when backgrounded during ad playback.
|
||||
|
||||
## Links ##
|
||||
|
||||
|
|
|
|||
|
|
@ -11,22 +11,10 @@
|
|||
// 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.library'
|
||||
apply from: "$gradle.ext.exoplayerSettingsDir/common_library_config.gradle"
|
||||
|
||||
android {
|
||||
compileSdkVersion project.ext.compileSdkVersion
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
minSdkVersion project.ext.minSdkVersion
|
||||
targetSdkVersion project.ext.targetSdkVersion
|
||||
consumerProguardFiles 'proguard-rules.txt'
|
||||
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
|
||||
// Enable multidex for androidTests.
|
||||
multiDexEnabled true
|
||||
}
|
||||
|
|
@ -34,22 +22,42 @@ android {
|
|||
sourceSets {
|
||||
androidTest.assets.srcDir '../../testdata/src/test/assets/'
|
||||
}
|
||||
|
||||
testOptions.unitTests.includeAndroidResources = true
|
||||
}
|
||||
|
||||
dependencies {
|
||||
api 'com.google.ads.interactivemedia.v3:interactivemedia:3.18.1'
|
||||
api 'com.google.ads.interactivemedia.v3:interactivemedia:3.19.4'
|
||||
implementation project(modulePrefix + 'library-core')
|
||||
implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion
|
||||
implementation 'com.google.android.gms:play-services-ads-identifier:17.0.0'
|
||||
implementation ('com.google.guava:guava:' + guavaVersion) {
|
||||
exclude group: 'com.google.code.findbugs', module: 'jsr305'
|
||||
exclude group: 'org.checkerframework', module: 'checker-compat-qual'
|
||||
exclude group: 'com.google.errorprone', module: 'error_prone_annotations'
|
||||
exclude group: 'com.google.j2objc', module: 'j2objc-annotations'
|
||||
exclude group: 'org.codehaus.mojo', module: 'animal-sniffer-annotations'
|
||||
}
|
||||
compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion
|
||||
compileOnly 'org.jetbrains.kotlin:kotlin-annotations-jvm:' + kotlinAnnotationsVersion
|
||||
androidTestImplementation project(modulePrefix + 'testutils')
|
||||
androidTestImplementation 'androidx.multidex:multidex:' + androidxMultidexVersion
|
||||
androidTestImplementation 'androidx.test:rules:' + androidxTestRulesVersion
|
||||
androidTestImplementation 'androidx.test:runner:' + androidxTestRunnerVersion
|
||||
androidTestImplementation 'com.android.support:multidex:1.0.3'
|
||||
androidTestImplementation ('com.google.guava:guava:' + guavaVersion) {
|
||||
exclude group: 'com.google.code.findbugs', module: 'jsr305'
|
||||
exclude group: 'org.checkerframework', module: 'checker-compat-qual'
|
||||
exclude group: 'com.google.errorprone', module: 'error_prone_annotations'
|
||||
exclude group: 'com.google.j2objc', module: 'j2objc-annotations'
|
||||
exclude group: 'org.codehaus.mojo', module: 'animal-sniffer-annotations'
|
||||
}
|
||||
androidTestCompileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion
|
||||
testImplementation project(modulePrefix + 'testutils')
|
||||
testImplementation ('com.google.guava:guava:' + guavaVersion) {
|
||||
exclude group: 'com.google.code.findbugs', module: 'jsr305'
|
||||
exclude group: 'org.checkerframework', module: 'checker-compat-qual'
|
||||
exclude group: 'com.google.errorprone', module: 'error_prone_annotations'
|
||||
exclude group: 'com.google.j2objc', module: 'j2objc-annotations'
|
||||
exclude group: 'org.codehaus.mojo', module: 'animal-sniffer-annotations'
|
||||
}
|
||||
testImplementation 'org.robolectric:robolectric:' + robolectricVersion
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -20,7 +20,6 @@ import static com.google.common.truth.Truth.assertThat;
|
|||
import android.content.Context;
|
||||
import android.net.Uri;
|
||||
import android.view.Surface;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.FrameLayout;
|
||||
import androidx.annotation.Nullable;
|
||||
|
|
@ -39,6 +38,7 @@ import com.google.android.exoplayer2.decoder.DecoderCounters;
|
|||
import com.google.android.exoplayer2.drm.DrmSessionManager;
|
||||
import com.google.android.exoplayer2.source.DefaultMediaSourceFactory;
|
||||
import com.google.android.exoplayer2.source.MediaSource;
|
||||
import com.google.android.exoplayer2.source.ads.AdsLoader;
|
||||
import com.google.android.exoplayer2.source.ads.AdsLoader.AdViewProvider;
|
||||
import com.google.android.exoplayer2.source.ads.AdsMediaSource;
|
||||
import com.google.android.exoplayer2.testutil.ActionSchedule;
|
||||
|
|
@ -49,7 +49,7 @@ import com.google.android.exoplayer2.trackselection.MappingTrackSelector;
|
|||
import com.google.android.exoplayer2.upstream.DataSource;
|
||||
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
|
||||
import com.google.android.exoplayer2.util.Assertions;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
|
@ -234,29 +234,26 @@ public final class ImaPlaybackTest {
|
|||
@Override
|
||||
protected MediaSource buildSource(
|
||||
HostActivity host,
|
||||
String userAgent,
|
||||
DrmSessionManager drmSessionManager,
|
||||
FrameLayout overlayFrameLayout) {
|
||||
Context context = host.getApplicationContext();
|
||||
DataSource.Factory dataSourceFactory =
|
||||
new DefaultDataSourceFactory(
|
||||
context, Util.getUserAgent(context, ImaPlaybackTest.class.getSimpleName()));
|
||||
DataSource.Factory dataSourceFactory = new DefaultDataSourceFactory(context);
|
||||
MediaSource contentMediaSource =
|
||||
DefaultMediaSourceFactory.newInstance(context)
|
||||
.createMediaSource(MediaItem.fromUri(contentUri));
|
||||
new DefaultMediaSourceFactory(context).createMediaSource(MediaItem.fromUri(contentUri));
|
||||
return new AdsMediaSource(
|
||||
contentMediaSource,
|
||||
dataSourceFactory,
|
||||
Assertions.checkNotNull(imaAdsLoader),
|
||||
new AdViewProvider() {
|
||||
|
||||
@Override
|
||||
public ViewGroup getAdViewGroup() {
|
||||
return overlayFrameLayout;
|
||||
}
|
||||
|
||||
@Override
|
||||
public View[] getAdOverlayViews() {
|
||||
return new View[0];
|
||||
public ImmutableList<AdsLoader.OverlayInfo> getAdOverlayInfos() {
|
||||
return ImmutableList.of();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,56 @@
|
|||
/*
|
||||
* Copyright (C) 2020 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.ima;
|
||||
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.source.ads.AdPlaybackState;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Static utility class for constructing {@link AdPlaybackState} instances from IMA-specific data.
|
||||
*/
|
||||
/* package */ final class AdPlaybackStateFactory {
|
||||
private AdPlaybackStateFactory() {}
|
||||
|
||||
/**
|
||||
* Construct an {@link AdPlaybackState} from the provided {@code cuePoints}.
|
||||
*
|
||||
* @param cuePoints The cue points of the ads in seconds.
|
||||
* @return The {@link AdPlaybackState}.
|
||||
*/
|
||||
public static AdPlaybackState fromCuePoints(List<Float> cuePoints) {
|
||||
if (cuePoints.isEmpty()) {
|
||||
// If no cue points are specified, there is a preroll ad.
|
||||
return new AdPlaybackState(/* adGroupTimesUs...= */ 0);
|
||||
}
|
||||
|
||||
int count = cuePoints.size();
|
||||
long[] adGroupTimesUs = new long[count];
|
||||
int adGroupIndex = 0;
|
||||
for (int i = 0; i < count; i++) {
|
||||
double cuePoint = cuePoints.get(i);
|
||||
if (cuePoint == -1.0) {
|
||||
adGroupTimesUs[count - 1] = C.TIME_END_OF_SOURCE;
|
||||
} else {
|
||||
adGroupTimesUs[adGroupIndex++] = Math.round(C.MICROS_PER_SECOND * cuePoint);
|
||||
}
|
||||
}
|
||||
// Cue points may be out of order, so sort them.
|
||||
Arrays.sort(adGroupTimesUs, 0, adGroupIndex);
|
||||
return new AdPlaybackState(adGroupTimesUs);
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -15,12 +15,16 @@
|
|||
*/
|
||||
package com.google.android.exoplayer2.ext.ima;
|
||||
|
||||
import static androidx.test.core.app.ApplicationProvider.getApplicationContext;
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyDouble;
|
||||
import static org.mockito.Mockito.atLeastOnce;
|
||||
import static org.mockito.Mockito.doAnswer;
|
||||
import static org.mockito.Mockito.doNothing;
|
||||
import static org.mockito.Mockito.inOrder;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
|
|
@ -29,7 +33,6 @@ import android.view.View;
|
|||
import android.view.ViewGroup;
|
||||
import android.widget.FrameLayout;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.test.core.app.ApplicationProvider;
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
import com.google.ads.interactivemedia.v3.api.Ad;
|
||||
import com.google.ads.interactivemedia.v3.api.AdDisplayContainer;
|
||||
|
|
@ -40,15 +43,19 @@ import com.google.ads.interactivemedia.v3.api.AdsManager;
|
|||
import com.google.ads.interactivemedia.v3.api.AdsManagerLoadedEvent;
|
||||
import com.google.ads.interactivemedia.v3.api.AdsRenderingSettings;
|
||||
import com.google.ads.interactivemedia.v3.api.AdsRequest;
|
||||
import com.google.ads.interactivemedia.v3.api.FriendlyObstruction;
|
||||
import com.google.ads.interactivemedia.v3.api.ImaSdkSettings;
|
||||
import com.google.ads.interactivemedia.v3.api.player.AdMediaInfo;
|
||||
import com.google.ads.interactivemedia.v3.api.player.ContentProgressProvider;
|
||||
import com.google.ads.interactivemedia.v3.api.player.VideoAdPlayer;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.ExoPlaybackException;
|
||||
import com.google.android.exoplayer2.MediaItem;
|
||||
import com.google.android.exoplayer2.Player;
|
||||
import com.google.android.exoplayer2.Timeline;
|
||||
import com.google.android.exoplayer2.Timeline.Period;
|
||||
import com.google.android.exoplayer2.ext.ima.ImaAdsLoader.ImaFactory;
|
||||
import com.google.android.exoplayer2.source.MaskingMediaSource.DummyTimeline;
|
||||
import com.google.android.exoplayer2.source.MaskingMediaSource.PlaceholderTimeline;
|
||||
import com.google.android.exoplayer2.source.ads.AdPlaybackState;
|
||||
import com.google.android.exoplayer2.source.ads.AdsLoader;
|
||||
import com.google.android.exoplayer2.source.ads.AdsMediaSource.AdLoadException;
|
||||
|
|
@ -56,7 +63,10 @@ import com.google.android.exoplayer2.source.ads.SinglePeriodAdTimeline;
|
|||
import com.google.android.exoplayer2.testutil.FakeTimeline;
|
||||
import com.google.android.exoplayer2.testutil.FakeTimeline.TimelineWindowDefinition;
|
||||
import com.google.android.exoplayer2.upstream.DataSpec;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import java.io.IOException;
|
||||
import java.time.Duration;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
|
|
@ -73,6 +83,7 @@ import org.mockito.Mock;
|
|||
import org.mockito.junit.MockitoJUnit;
|
||||
import org.mockito.junit.MockitoRule;
|
||||
import org.mockito.stubbing.Answer;
|
||||
import org.robolectric.shadows.ShadowSystemClock;
|
||||
|
||||
/** Tests for {@link ImaAdsLoader}. */
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
|
|
@ -88,8 +99,7 @@ public final class ImaAdsLoaderTest {
|
|||
private static final Uri TEST_URI = Uri.EMPTY;
|
||||
private static final AdMediaInfo TEST_AD_MEDIA_INFO = new AdMediaInfo(TEST_URI.toString());
|
||||
private static final long TEST_AD_DURATION_US = 5 * C.MICROS_PER_SECOND;
|
||||
private static final long[][] PREROLL_ADS_DURATIONS_US = new long[][] {{TEST_AD_DURATION_US}};
|
||||
private static final Float[] PREROLL_CUE_POINTS_SECONDS = new Float[] {0f};
|
||||
private static final ImmutableList<Float> PREROLL_CUE_POINTS_SECONDS = ImmutableList.of(0f);
|
||||
|
||||
@Rule public final MockitoRule mockito = MockitoJUnit.rule();
|
||||
|
||||
|
|
@ -100,13 +110,17 @@ public final class ImaAdsLoaderTest {
|
|||
@Mock private AdsRequest mockAdsRequest;
|
||||
@Mock private AdsManagerLoadedEvent mockAdsManagerLoadedEvent;
|
||||
@Mock private com.google.ads.interactivemedia.v3.api.AdsLoader mockAdsLoader;
|
||||
@Mock private FriendlyObstruction mockFriendlyObstruction;
|
||||
@Mock private ImaFactory mockImaFactory;
|
||||
@Mock private AdPodInfo mockAdPodInfo;
|
||||
@Mock private Ad mockPrerollSingleAd;
|
||||
|
||||
private ViewGroup adViewGroup;
|
||||
private View adOverlayView;
|
||||
private AdsLoader.AdViewProvider adViewProvider;
|
||||
private AdsLoader.AdViewProvider audioAdsAdViewProvider;
|
||||
private AdEvent.AdEventListener adEventListener;
|
||||
private ContentProgressProvider contentProgressProvider;
|
||||
private VideoAdPlayer videoAdPlayer;
|
||||
private TestAdsLoaderListener adsLoaderListener;
|
||||
private FakePlayer fakeExoPlayer;
|
||||
private ImaAdsLoader imaAdsLoader;
|
||||
|
|
@ -114,8 +128,8 @@ public final class ImaAdsLoaderTest {
|
|||
@Before
|
||||
public void setUp() {
|
||||
setupMocks();
|
||||
adViewGroup = new FrameLayout(ApplicationProvider.getApplicationContext());
|
||||
adOverlayView = new View(ApplicationProvider.getApplicationContext());
|
||||
adViewGroup = new FrameLayout(getApplicationContext());
|
||||
View adOverlayView = new View(getApplicationContext());
|
||||
adViewProvider =
|
||||
new AdsLoader.AdViewProvider() {
|
||||
@Override
|
||||
|
|
@ -124,8 +138,21 @@ public final class ImaAdsLoaderTest {
|
|||
}
|
||||
|
||||
@Override
|
||||
public View[] getAdOverlayViews() {
|
||||
return new View[] {adOverlayView};
|
||||
public ImmutableList<AdsLoader.OverlayInfo> getAdOverlayInfos() {
|
||||
return ImmutableList.of(
|
||||
new AdsLoader.OverlayInfo(adOverlayView, AdsLoader.OverlayInfo.PURPOSE_CLOSE_AD));
|
||||
}
|
||||
};
|
||||
audioAdsAdViewProvider =
|
||||
new AdsLoader.AdViewProvider() {
|
||||
@Override
|
||||
public ViewGroup getAdViewGroup() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ImmutableList<AdsLoader.OverlayInfo> getAdOverlayInfos() {
|
||||
return ImmutableList.of();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
@ -140,24 +167,36 @@ public final class ImaAdsLoaderTest {
|
|||
@Test
|
||||
public void builder_overridesPlayerType() {
|
||||
when(mockImaSdkSettings.getPlayerType()).thenReturn("test player type");
|
||||
setupPlayback(CONTENT_TIMELINE, PREROLL_ADS_DURATIONS_US, PREROLL_CUE_POINTS_SECONDS);
|
||||
setupPlayback(CONTENT_TIMELINE, PREROLL_CUE_POINTS_SECONDS);
|
||||
|
||||
verify(mockImaSdkSettings).setPlayerType("google/exo.ext.ima");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void start_setsAdUiViewGroup() {
|
||||
setupPlayback(CONTENT_TIMELINE, PREROLL_ADS_DURATIONS_US, PREROLL_CUE_POINTS_SECONDS);
|
||||
setupPlayback(CONTENT_TIMELINE, PREROLL_CUE_POINTS_SECONDS);
|
||||
imaAdsLoader.start(adsLoaderListener, adViewProvider);
|
||||
|
||||
verify(mockAdDisplayContainer, atLeastOnce()).setAdContainer(adViewGroup);
|
||||
verify(mockAdDisplayContainer, atLeastOnce()).registerVideoControlsOverlay(adOverlayView);
|
||||
verify(mockImaFactory, atLeastOnce()).createAdDisplayContainer(adViewGroup, videoAdPlayer);
|
||||
verify(mockImaFactory, never()).createAudioAdDisplayContainer(any(), any());
|
||||
verify(mockAdDisplayContainer).registerFriendlyObstruction(mockFriendlyObstruction);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void startForAudioOnlyAds_createsAudioOnlyAdDisplayContainer() {
|
||||
setupPlayback(CONTENT_TIMELINE, PREROLL_CUE_POINTS_SECONDS);
|
||||
imaAdsLoader.start(adsLoaderListener, audioAdsAdViewProvider);
|
||||
|
||||
verify(mockImaFactory, atLeastOnce())
|
||||
.createAudioAdDisplayContainer(getApplicationContext(), videoAdPlayer);
|
||||
verify(mockImaFactory, never()).createAdDisplayContainer(any(), any());
|
||||
verify(mockAdDisplayContainer, never()).registerFriendlyObstruction(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void start_withPlaceholderContent_initializedAdsLoader() {
|
||||
Timeline placeholderTimeline = new DummyTimeline(/* tag= */ null);
|
||||
setupPlayback(placeholderTimeline, PREROLL_ADS_DURATIONS_US, PREROLL_CUE_POINTS_SECONDS);
|
||||
Timeline placeholderTimeline = new PlaceholderTimeline(MediaItem.fromUri(Uri.EMPTY));
|
||||
setupPlayback(placeholderTimeline, PREROLL_CUE_POINTS_SECONDS);
|
||||
imaAdsLoader.start(adsLoaderListener, adViewProvider);
|
||||
|
||||
// We'll only create the rendering settings when initializing the ads loader.
|
||||
|
|
@ -166,26 +205,27 @@ public final class ImaAdsLoaderTest {
|
|||
|
||||
@Test
|
||||
public void start_updatesAdPlaybackState() {
|
||||
setupPlayback(CONTENT_TIMELINE, PREROLL_ADS_DURATIONS_US, PREROLL_CUE_POINTS_SECONDS);
|
||||
setupPlayback(CONTENT_TIMELINE, PREROLL_CUE_POINTS_SECONDS);
|
||||
imaAdsLoader.start(adsLoaderListener, adViewProvider);
|
||||
|
||||
assertThat(adsLoaderListener.adPlaybackState)
|
||||
.isEqualTo(
|
||||
new AdPlaybackState(/* adGroupTimesUs...= */ 0)
|
||||
.withAdDurationsUs(PREROLL_ADS_DURATIONS_US)
|
||||
.withContentDurationUs(CONTENT_PERIOD_DURATION_US));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void startAfterRelease() {
|
||||
setupPlayback(CONTENT_TIMELINE, PREROLL_ADS_DURATIONS_US, PREROLL_CUE_POINTS_SECONDS);
|
||||
setupPlayback(CONTENT_TIMELINE, PREROLL_CUE_POINTS_SECONDS);
|
||||
imaAdsLoader.release();
|
||||
imaAdsLoader.start(adsLoaderListener, adViewProvider);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void startAndCallbacksAfterRelease() {
|
||||
setupPlayback(CONTENT_TIMELINE, PREROLL_ADS_DURATIONS_US, PREROLL_CUE_POINTS_SECONDS);
|
||||
setupPlayback(CONTENT_TIMELINE, PREROLL_CUE_POINTS_SECONDS);
|
||||
// Request ads in order to get a reference to the ad event listener.
|
||||
imaAdsLoader.requestAds(adViewGroup);
|
||||
imaAdsLoader.release();
|
||||
imaAdsLoader.start(adsLoaderListener, adViewProvider);
|
||||
fakeExoPlayer.setPlayingContentPosition(/* position= */ 0);
|
||||
|
|
@ -196,47 +236,47 @@ public final class ImaAdsLoaderTest {
|
|||
// when using Robolectric and accessing VideoProgressUpdate.VIDEO_TIME_NOT_READY, due to the IMA
|
||||
// SDK being proguarded.
|
||||
imaAdsLoader.requestAds(adViewGroup);
|
||||
imaAdsLoader.onAdEvent(getAdEvent(AdEventType.LOADED, mockPrerollSingleAd));
|
||||
imaAdsLoader.loadAd(TEST_AD_MEDIA_INFO, mockAdPodInfo);
|
||||
imaAdsLoader.onAdEvent(getAdEvent(AdEventType.CONTENT_PAUSE_REQUESTED, mockPrerollSingleAd));
|
||||
imaAdsLoader.playAd(TEST_AD_MEDIA_INFO);
|
||||
imaAdsLoader.onAdEvent(getAdEvent(AdEventType.STARTED, mockPrerollSingleAd));
|
||||
imaAdsLoader.pauseAd(TEST_AD_MEDIA_INFO);
|
||||
imaAdsLoader.stopAd(TEST_AD_MEDIA_INFO);
|
||||
adEventListener.onAdEvent(getAdEvent(AdEventType.LOADED, mockPrerollSingleAd));
|
||||
videoAdPlayer.loadAd(TEST_AD_MEDIA_INFO, mockAdPodInfo);
|
||||
adEventListener.onAdEvent(getAdEvent(AdEventType.CONTENT_PAUSE_REQUESTED, mockPrerollSingleAd));
|
||||
videoAdPlayer.playAd(TEST_AD_MEDIA_INFO);
|
||||
adEventListener.onAdEvent(getAdEvent(AdEventType.STARTED, mockPrerollSingleAd));
|
||||
videoAdPlayer.pauseAd(TEST_AD_MEDIA_INFO);
|
||||
videoAdPlayer.stopAd(TEST_AD_MEDIA_INFO);
|
||||
imaAdsLoader.onPlayerError(ExoPlaybackException.createForSource(new IOException()));
|
||||
imaAdsLoader.onPositionDiscontinuity(Player.DISCONTINUITY_REASON_SEEK);
|
||||
imaAdsLoader.onAdEvent(getAdEvent(AdEventType.CONTENT_RESUME_REQUESTED, /* ad= */ null));
|
||||
adEventListener.onAdEvent(getAdEvent(AdEventType.CONTENT_RESUME_REQUESTED, /* ad= */ null));
|
||||
imaAdsLoader.handlePrepareError(
|
||||
/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0, new IOException());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void playback_withPrerollAd_marksAdAsPlayed() {
|
||||
setupPlayback(CONTENT_TIMELINE, PREROLL_ADS_DURATIONS_US, PREROLL_CUE_POINTS_SECONDS);
|
||||
setupPlayback(CONTENT_TIMELINE, PREROLL_CUE_POINTS_SECONDS);
|
||||
|
||||
// Load the preroll ad.
|
||||
imaAdsLoader.start(adsLoaderListener, adViewProvider);
|
||||
imaAdsLoader.onAdEvent(getAdEvent(AdEventType.LOADED, mockPrerollSingleAd));
|
||||
imaAdsLoader.loadAd(TEST_AD_MEDIA_INFO, mockAdPodInfo);
|
||||
imaAdsLoader.onAdEvent(getAdEvent(AdEventType.CONTENT_PAUSE_REQUESTED, mockPrerollSingleAd));
|
||||
adEventListener.onAdEvent(getAdEvent(AdEventType.LOADED, mockPrerollSingleAd));
|
||||
videoAdPlayer.loadAd(TEST_AD_MEDIA_INFO, mockAdPodInfo);
|
||||
adEventListener.onAdEvent(getAdEvent(AdEventType.CONTENT_PAUSE_REQUESTED, mockPrerollSingleAd));
|
||||
|
||||
// Play the preroll ad.
|
||||
imaAdsLoader.playAd(TEST_AD_MEDIA_INFO);
|
||||
videoAdPlayer.playAd(TEST_AD_MEDIA_INFO);
|
||||
fakeExoPlayer.setPlayingAdPosition(
|
||||
/* adGroupIndex= */ 0,
|
||||
/* adIndexInAdGroup= */ 0,
|
||||
/* position= */ 0,
|
||||
/* contentPosition= */ 0);
|
||||
fakeExoPlayer.setState(Player.STATE_READY, true);
|
||||
imaAdsLoader.onAdEvent(getAdEvent(AdEventType.STARTED, mockPrerollSingleAd));
|
||||
imaAdsLoader.onAdEvent(getAdEvent(AdEventType.FIRST_QUARTILE, mockPrerollSingleAd));
|
||||
imaAdsLoader.onAdEvent(getAdEvent(AdEventType.MIDPOINT, mockPrerollSingleAd));
|
||||
imaAdsLoader.onAdEvent(getAdEvent(AdEventType.THIRD_QUARTILE, mockPrerollSingleAd));
|
||||
adEventListener.onAdEvent(getAdEvent(AdEventType.STARTED, mockPrerollSingleAd));
|
||||
adEventListener.onAdEvent(getAdEvent(AdEventType.FIRST_QUARTILE, mockPrerollSingleAd));
|
||||
adEventListener.onAdEvent(getAdEvent(AdEventType.MIDPOINT, mockPrerollSingleAd));
|
||||
adEventListener.onAdEvent(getAdEvent(AdEventType.THIRD_QUARTILE, mockPrerollSingleAd));
|
||||
|
||||
// Play the content.
|
||||
fakeExoPlayer.setPlayingContentPosition(0);
|
||||
imaAdsLoader.stopAd(TEST_AD_MEDIA_INFO);
|
||||
imaAdsLoader.onAdEvent(getAdEvent(AdEventType.CONTENT_RESUME_REQUESTED, /* ad= */ null));
|
||||
videoAdPlayer.stopAd(TEST_AD_MEDIA_INFO);
|
||||
adEventListener.onAdEvent(getAdEvent(AdEventType.CONTENT_RESUME_REQUESTED, /* ad= */ null));
|
||||
|
||||
// Verify that the preroll ad has been marked as played.
|
||||
assertThat(adsLoaderListener.adPlaybackState)
|
||||
|
|
@ -245,32 +285,479 @@ public final class ImaAdsLoaderTest {
|
|||
.withContentDurationUs(CONTENT_PERIOD_DURATION_US)
|
||||
.withAdCount(/* adGroupIndex= */ 0, /* adCount= */ 1)
|
||||
.withAdUri(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0, /* uri= */ TEST_URI)
|
||||
.withAdDurationsUs(PREROLL_ADS_DURATIONS_US)
|
||||
.withAdDurationsUs(new long[][] {{TEST_AD_DURATION_US}})
|
||||
.withPlayedAd(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0)
|
||||
.withAdResumePositionUs(/* adResumePositionUs= */ 0));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void playback_withMidrollFetchError_marksAdAsInErrorState() {
|
||||
AdEvent mockMidrollFetchErrorAdEvent = mock(AdEvent.class);
|
||||
when(mockMidrollFetchErrorAdEvent.getType()).thenReturn(AdEventType.AD_BREAK_FETCH_ERROR);
|
||||
when(mockMidrollFetchErrorAdEvent.getAdData())
|
||||
.thenReturn(ImmutableMap.of("adBreakTime", "20.5"));
|
||||
setupPlayback(CONTENT_TIMELINE, ImmutableList.of(20.5f));
|
||||
|
||||
// Simulate loading an empty midroll ad.
|
||||
imaAdsLoader.start(adsLoaderListener, adViewProvider);
|
||||
adEventListener.onAdEvent(mockMidrollFetchErrorAdEvent);
|
||||
|
||||
assertThat(adsLoaderListener.adPlaybackState)
|
||||
.isEqualTo(
|
||||
new AdPlaybackState(/* adGroupTimesUs...= */ 20_500_000)
|
||||
.withContentDurationUs(CONTENT_PERIOD_DURATION_US)
|
||||
.withAdDurationsUs(new long[][] {{TEST_AD_DURATION_US}})
|
||||
.withAdCount(/* adGroupIndex= */ 0, /* adCount= */ 1)
|
||||
.withAdLoadError(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void playback_withPostrollFetchError_marksAdAsInErrorState() {
|
||||
AdEvent mockPostrollFetchErrorAdEvent = mock(AdEvent.class);
|
||||
when(mockPostrollFetchErrorAdEvent.getType()).thenReturn(AdEventType.AD_BREAK_FETCH_ERROR);
|
||||
when(mockPostrollFetchErrorAdEvent.getAdData())
|
||||
.thenReturn(ImmutableMap.of("adBreakTime", "-1"));
|
||||
setupPlayback(CONTENT_TIMELINE, ImmutableList.of(-1f));
|
||||
|
||||
// Simulate loading an empty postroll ad.
|
||||
imaAdsLoader.start(adsLoaderListener, adViewProvider);
|
||||
adEventListener.onAdEvent(mockPostrollFetchErrorAdEvent);
|
||||
|
||||
assertThat(adsLoaderListener.adPlaybackState)
|
||||
.isEqualTo(
|
||||
new AdPlaybackState(/* adGroupTimesUs...= */ C.TIME_END_OF_SOURCE)
|
||||
.withContentDurationUs(CONTENT_PERIOD_DURATION_US)
|
||||
.withAdDurationsUs(new long[][] {{TEST_AD_DURATION_US}})
|
||||
.withAdCount(/* adGroupIndex= */ 0, /* adCount= */ 1)
|
||||
.withAdLoadError(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void playback_withAdNotPreloadingBeforeTimeout_hasNoError() {
|
||||
// Simulate an ad at 2 seconds.
|
||||
long adGroupPositionInWindowUs = 2 * C.MICROS_PER_SECOND;
|
||||
long adGroupTimeUs =
|
||||
adGroupPositionInWindowUs
|
||||
+ TimelineWindowDefinition.DEFAULT_WINDOW_OFFSET_IN_FIRST_PERIOD_US;
|
||||
ImmutableList<Float> cuePoints = ImmutableList.of((float) adGroupTimeUs / C.MICROS_PER_SECOND);
|
||||
setupPlayback(CONTENT_TIMELINE, cuePoints);
|
||||
|
||||
// Advance playback to just before the midroll and simulate buffering.
|
||||
imaAdsLoader.start(adsLoaderListener, adViewProvider);
|
||||
fakeExoPlayer.setPlayingContentPosition(C.usToMs(adGroupPositionInWindowUs));
|
||||
fakeExoPlayer.setState(Player.STATE_BUFFERING, /* playWhenReady= */ true);
|
||||
// Advance before the timeout and simulating polling content progress.
|
||||
ShadowSystemClock.advanceBy(Duration.ofSeconds(1));
|
||||
contentProgressProvider.getContentProgress();
|
||||
|
||||
assertThat(adsLoaderListener.adPlaybackState)
|
||||
.isEqualTo(
|
||||
AdPlaybackStateFactory.fromCuePoints(cuePoints)
|
||||
.withContentDurationUs(CONTENT_PERIOD_DURATION_US));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void playback_withAdNotPreloadingAfterTimeout_hasErrorAdGroup() {
|
||||
// Simulate an ad at 2 seconds.
|
||||
long adGroupPositionInWindowUs = 2 * C.MICROS_PER_SECOND;
|
||||
long adGroupTimeUs =
|
||||
adGroupPositionInWindowUs
|
||||
+ TimelineWindowDefinition.DEFAULT_WINDOW_OFFSET_IN_FIRST_PERIOD_US;
|
||||
ImmutableList<Float> cuePoints = ImmutableList.of((float) adGroupTimeUs / C.MICROS_PER_SECOND);
|
||||
setupPlayback(CONTENT_TIMELINE, cuePoints);
|
||||
|
||||
// Advance playback to just before the midroll and simulate buffering.
|
||||
imaAdsLoader.start(adsLoaderListener, adViewProvider);
|
||||
fakeExoPlayer.setPlayingContentPosition(C.usToMs(adGroupPositionInWindowUs));
|
||||
fakeExoPlayer.setState(Player.STATE_BUFFERING, /* playWhenReady= */ true);
|
||||
// Advance past the timeout and simulate polling content progress.
|
||||
ShadowSystemClock.advanceBy(Duration.ofSeconds(5));
|
||||
contentProgressProvider.getContentProgress();
|
||||
|
||||
assertThat(adsLoaderListener.adPlaybackState)
|
||||
.isEqualTo(
|
||||
AdPlaybackStateFactory.fromCuePoints(cuePoints)
|
||||
.withContentDurationUs(CONTENT_PERIOD_DURATION_US)
|
||||
.withAdDurationsUs(new long[][] {{TEST_AD_DURATION_US}})
|
||||
.withAdCount(/* adGroupIndex= */ 0, /* adCount= */ 1)
|
||||
.withAdLoadError(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void resumePlaybackBeforeMidroll_playsPreroll() {
|
||||
long midrollWindowTimeUs = 2 * C.MICROS_PER_SECOND;
|
||||
long midrollPeriodTimeUs =
|
||||
midrollWindowTimeUs + TimelineWindowDefinition.DEFAULT_WINDOW_OFFSET_IN_FIRST_PERIOD_US;
|
||||
ImmutableList<Float> cuePoints =
|
||||
ImmutableList.of(0f, (float) midrollPeriodTimeUs / C.MICROS_PER_SECOND);
|
||||
setupPlayback(CONTENT_TIMELINE, cuePoints);
|
||||
|
||||
fakeExoPlayer.setPlayingContentPosition(C.usToMs(midrollWindowTimeUs) - 1_000);
|
||||
imaAdsLoader.start(adsLoaderListener, adViewProvider);
|
||||
|
||||
verify(mockAdsRenderingSettings, never()).setPlayAdsAfterTime(anyDouble());
|
||||
assertThat(adsLoaderListener.adPlaybackState)
|
||||
.isEqualTo(
|
||||
AdPlaybackStateFactory.fromCuePoints(cuePoints)
|
||||
.withContentDurationUs(CONTENT_PERIOD_DURATION_US));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void resumePlaybackAtMidroll_skipsPreroll() {
|
||||
long midrollWindowTimeUs = 2 * C.MICROS_PER_SECOND;
|
||||
long midrollPeriodTimeUs =
|
||||
midrollWindowTimeUs + TimelineWindowDefinition.DEFAULT_WINDOW_OFFSET_IN_FIRST_PERIOD_US;
|
||||
ImmutableList<Float> cuePoints =
|
||||
ImmutableList.of(0f, (float) midrollPeriodTimeUs / C.MICROS_PER_SECOND);
|
||||
setupPlayback(CONTENT_TIMELINE, cuePoints);
|
||||
|
||||
fakeExoPlayer.setPlayingContentPosition(C.usToMs(midrollWindowTimeUs));
|
||||
imaAdsLoader.start(adsLoaderListener, adViewProvider);
|
||||
|
||||
ArgumentCaptor<Double> playAdsAfterTimeCaptor = ArgumentCaptor.forClass(Double.class);
|
||||
verify(mockAdsRenderingSettings).setPlayAdsAfterTime(playAdsAfterTimeCaptor.capture());
|
||||
double expectedPlayAdsAfterTimeUs = midrollPeriodTimeUs / 2d;
|
||||
assertThat(playAdsAfterTimeCaptor.getValue())
|
||||
.isWithin(0.1)
|
||||
.of(expectedPlayAdsAfterTimeUs / C.MICROS_PER_SECOND);
|
||||
assertThat(adsLoaderListener.adPlaybackState)
|
||||
.isEqualTo(
|
||||
AdPlaybackStateFactory.fromCuePoints(cuePoints)
|
||||
.withContentDurationUs(CONTENT_PERIOD_DURATION_US)
|
||||
.withSkippedAdGroup(/* adGroupIndex= */ 0));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void resumePlaybackAfterMidroll_skipsPreroll() {
|
||||
long midrollWindowTimeUs = 2 * C.MICROS_PER_SECOND;
|
||||
long midrollPeriodTimeUs =
|
||||
midrollWindowTimeUs + TimelineWindowDefinition.DEFAULT_WINDOW_OFFSET_IN_FIRST_PERIOD_US;
|
||||
ImmutableList<Float> cuePoints =
|
||||
ImmutableList.of(0f, (float) midrollPeriodTimeUs / C.MICROS_PER_SECOND);
|
||||
setupPlayback(CONTENT_TIMELINE, cuePoints);
|
||||
|
||||
fakeExoPlayer.setPlayingContentPosition(C.usToMs(midrollWindowTimeUs) + 1_000);
|
||||
imaAdsLoader.start(adsLoaderListener, adViewProvider);
|
||||
|
||||
ArgumentCaptor<Double> playAdsAfterTimeCaptor = ArgumentCaptor.forClass(Double.class);
|
||||
verify(mockAdsRenderingSettings).setPlayAdsAfterTime(playAdsAfterTimeCaptor.capture());
|
||||
double expectedPlayAdsAfterTimeUs = midrollPeriodTimeUs / 2d;
|
||||
assertThat(playAdsAfterTimeCaptor.getValue())
|
||||
.isWithin(0.1)
|
||||
.of(expectedPlayAdsAfterTimeUs / C.MICROS_PER_SECOND);
|
||||
assertThat(adsLoaderListener.adPlaybackState)
|
||||
.isEqualTo(
|
||||
AdPlaybackStateFactory.fromCuePoints(cuePoints)
|
||||
.withContentDurationUs(CONTENT_PERIOD_DURATION_US)
|
||||
.withSkippedAdGroup(/* adGroupIndex= */ 0));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void resumePlaybackBeforeSecondMidroll_playsFirstMidroll() {
|
||||
long firstMidrollWindowTimeUs = 2 * C.MICROS_PER_SECOND;
|
||||
long firstMidrollPeriodTimeUs =
|
||||
firstMidrollWindowTimeUs
|
||||
+ TimelineWindowDefinition.DEFAULT_WINDOW_OFFSET_IN_FIRST_PERIOD_US;
|
||||
long secondMidrollWindowTimeUs = 4 * C.MICROS_PER_SECOND;
|
||||
long secondMidrollPeriodTimeUs =
|
||||
secondMidrollWindowTimeUs
|
||||
+ TimelineWindowDefinition.DEFAULT_WINDOW_OFFSET_IN_FIRST_PERIOD_US;
|
||||
ImmutableList<Float> cuePoints =
|
||||
ImmutableList.of(
|
||||
(float) firstMidrollPeriodTimeUs / C.MICROS_PER_SECOND,
|
||||
(float) secondMidrollPeriodTimeUs / C.MICROS_PER_SECOND);
|
||||
setupPlayback(CONTENT_TIMELINE, cuePoints);
|
||||
|
||||
fakeExoPlayer.setPlayingContentPosition(C.usToMs(secondMidrollWindowTimeUs) - 1_000);
|
||||
imaAdsLoader.start(adsLoaderListener, adViewProvider);
|
||||
|
||||
verify(mockAdsRenderingSettings, never()).setPlayAdsAfterTime(anyDouble());
|
||||
assertThat(adsLoaderListener.adPlaybackState)
|
||||
.isEqualTo(
|
||||
AdPlaybackStateFactory.fromCuePoints(cuePoints)
|
||||
.withContentDurationUs(CONTENT_PERIOD_DURATION_US));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void resumePlaybackAtSecondMidroll_skipsFirstMidroll() {
|
||||
long firstMidrollWindowTimeUs = 2 * C.MICROS_PER_SECOND;
|
||||
long firstMidrollPeriodTimeUs =
|
||||
firstMidrollWindowTimeUs
|
||||
+ TimelineWindowDefinition.DEFAULT_WINDOW_OFFSET_IN_FIRST_PERIOD_US;
|
||||
long secondMidrollWindowTimeUs = 4 * C.MICROS_PER_SECOND;
|
||||
long secondMidrollPeriodTimeUs =
|
||||
secondMidrollWindowTimeUs
|
||||
+ TimelineWindowDefinition.DEFAULT_WINDOW_OFFSET_IN_FIRST_PERIOD_US;
|
||||
ImmutableList<Float> cuePoints =
|
||||
ImmutableList.of(
|
||||
(float) firstMidrollPeriodTimeUs / C.MICROS_PER_SECOND,
|
||||
(float) secondMidrollPeriodTimeUs / C.MICROS_PER_SECOND);
|
||||
setupPlayback(CONTENT_TIMELINE, cuePoints);
|
||||
|
||||
fakeExoPlayer.setPlayingContentPosition(C.usToMs(secondMidrollWindowTimeUs));
|
||||
imaAdsLoader.start(adsLoaderListener, adViewProvider);
|
||||
|
||||
ArgumentCaptor<Double> playAdsAfterTimeCaptor = ArgumentCaptor.forClass(Double.class);
|
||||
verify(mockAdsRenderingSettings).setPlayAdsAfterTime(playAdsAfterTimeCaptor.capture());
|
||||
double expectedPlayAdsAfterTimeUs = (firstMidrollPeriodTimeUs + secondMidrollPeriodTimeUs) / 2d;
|
||||
assertThat(playAdsAfterTimeCaptor.getValue())
|
||||
.isWithin(0.1)
|
||||
.of(expectedPlayAdsAfterTimeUs / C.MICROS_PER_SECOND);
|
||||
assertThat(adsLoaderListener.adPlaybackState)
|
||||
.isEqualTo(
|
||||
AdPlaybackStateFactory.fromCuePoints(cuePoints)
|
||||
.withContentDurationUs(CONTENT_PERIOD_DURATION_US)
|
||||
.withSkippedAdGroup(/* adGroupIndex= */ 0));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void resumePlaybackBeforeMidroll_withoutPlayAdBeforeStartPosition_skipsPreroll() {
|
||||
long midrollWindowTimeUs = 2 * C.MICROS_PER_SECOND;
|
||||
long midrollPeriodTimeUs =
|
||||
midrollWindowTimeUs + TimelineWindowDefinition.DEFAULT_WINDOW_OFFSET_IN_FIRST_PERIOD_US;
|
||||
ImmutableList<Float> cuePoints =
|
||||
ImmutableList.of(0f, (float) midrollPeriodTimeUs / C.MICROS_PER_SECOND);
|
||||
setupPlayback(
|
||||
CONTENT_TIMELINE,
|
||||
cuePoints,
|
||||
new ImaAdsLoader.Builder(getApplicationContext())
|
||||
.setPlayAdBeforeStartPosition(false)
|
||||
.setImaFactory(mockImaFactory)
|
||||
.setImaSdkSettings(mockImaSdkSettings)
|
||||
.buildForAdTag(TEST_URI));
|
||||
|
||||
fakeExoPlayer.setPlayingContentPosition(C.usToMs(midrollWindowTimeUs) - 1_000);
|
||||
imaAdsLoader.start(adsLoaderListener, adViewProvider);
|
||||
|
||||
ArgumentCaptor<Double> playAdsAfterTimeCaptor = ArgumentCaptor.forClass(Double.class);
|
||||
verify(mockAdsRenderingSettings).setPlayAdsAfterTime(playAdsAfterTimeCaptor.capture());
|
||||
double expectedPlayAdsAfterTimeUs = midrollPeriodTimeUs / 2d;
|
||||
assertThat(playAdsAfterTimeCaptor.getValue())
|
||||
.isWithin(0.1d)
|
||||
.of(expectedPlayAdsAfterTimeUs / C.MICROS_PER_SECOND);
|
||||
assertThat(adsLoaderListener.adPlaybackState)
|
||||
.isEqualTo(
|
||||
AdPlaybackStateFactory.fromCuePoints(cuePoints)
|
||||
.withSkippedAdGroup(/* adGroupIndex= */ 0)
|
||||
.withContentDurationUs(CONTENT_PERIOD_DURATION_US));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void resumePlaybackAtMidroll_withoutPlayAdBeforeStartPosition_skipsPreroll() {
|
||||
long midrollWindowTimeUs = 2 * C.MICROS_PER_SECOND;
|
||||
long midrollPeriodTimeUs =
|
||||
midrollWindowTimeUs + TimelineWindowDefinition.DEFAULT_WINDOW_OFFSET_IN_FIRST_PERIOD_US;
|
||||
ImmutableList<Float> cuePoints =
|
||||
ImmutableList.of(0f, (float) midrollPeriodTimeUs / C.MICROS_PER_SECOND);
|
||||
setupPlayback(
|
||||
CONTENT_TIMELINE,
|
||||
cuePoints,
|
||||
new ImaAdsLoader.Builder(getApplicationContext())
|
||||
.setPlayAdBeforeStartPosition(false)
|
||||
.setImaFactory(mockImaFactory)
|
||||
.setImaSdkSettings(mockImaSdkSettings)
|
||||
.buildForAdTag(TEST_URI));
|
||||
|
||||
fakeExoPlayer.setPlayingContentPosition(C.usToMs(midrollWindowTimeUs));
|
||||
imaAdsLoader.start(adsLoaderListener, adViewProvider);
|
||||
|
||||
ArgumentCaptor<Double> playAdsAfterTimeCaptor = ArgumentCaptor.forClass(Double.class);
|
||||
verify(mockAdsRenderingSettings).setPlayAdsAfterTime(playAdsAfterTimeCaptor.capture());
|
||||
double expectedPlayAdsAfterTimeUs = midrollPeriodTimeUs / 2d;
|
||||
assertThat(playAdsAfterTimeCaptor.getValue())
|
||||
.isWithin(0.1d)
|
||||
.of(expectedPlayAdsAfterTimeUs / C.MICROS_PER_SECOND);
|
||||
assertThat(adsLoaderListener.adPlaybackState)
|
||||
.isEqualTo(
|
||||
AdPlaybackStateFactory.fromCuePoints(cuePoints)
|
||||
.withContentDurationUs(CONTENT_PERIOD_DURATION_US)
|
||||
.withSkippedAdGroup(/* adGroupIndex= */ 0));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void resumePlaybackAfterMidroll_withoutPlayAdBeforeStartPosition_skipsMidroll() {
|
||||
long midrollWindowTimeUs = 2 * C.MICROS_PER_SECOND;
|
||||
long midrollPeriodTimeUs =
|
||||
midrollWindowTimeUs + TimelineWindowDefinition.DEFAULT_WINDOW_OFFSET_IN_FIRST_PERIOD_US;
|
||||
ImmutableList<Float> cuePoints =
|
||||
ImmutableList.of(0f, (float) midrollPeriodTimeUs / C.MICROS_PER_SECOND);
|
||||
setupPlayback(
|
||||
CONTENT_TIMELINE,
|
||||
cuePoints,
|
||||
new ImaAdsLoader.Builder(getApplicationContext())
|
||||
.setPlayAdBeforeStartPosition(false)
|
||||
.setImaFactory(mockImaFactory)
|
||||
.setImaSdkSettings(mockImaSdkSettings)
|
||||
.buildForAdTag(TEST_URI));
|
||||
|
||||
fakeExoPlayer.setPlayingContentPosition(C.usToMs(midrollWindowTimeUs) + 1_000);
|
||||
imaAdsLoader.start(adsLoaderListener, adViewProvider);
|
||||
|
||||
verify(mockAdsManager).destroy();
|
||||
assertThat(adsLoaderListener.adPlaybackState)
|
||||
.isEqualTo(
|
||||
AdPlaybackStateFactory.fromCuePoints(cuePoints)
|
||||
.withContentDurationUs(CONTENT_PERIOD_DURATION_US)
|
||||
.withSkippedAdGroup(/* adGroupIndex= */ 0)
|
||||
.withSkippedAdGroup(/* adGroupIndex= */ 1));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void
|
||||
resumePlaybackBeforeSecondMidroll_withoutPlayAdBeforeStartPosition_skipsFirstMidroll() {
|
||||
long firstMidrollWindowTimeUs = 2 * C.MICROS_PER_SECOND;
|
||||
long firstMidrollPeriodTimeUs =
|
||||
firstMidrollWindowTimeUs
|
||||
+ TimelineWindowDefinition.DEFAULT_WINDOW_OFFSET_IN_FIRST_PERIOD_US;
|
||||
long secondMidrollWindowTimeUs = 4 * C.MICROS_PER_SECOND;
|
||||
long secondMidrollPeriodTimeUs =
|
||||
secondMidrollWindowTimeUs
|
||||
+ TimelineWindowDefinition.DEFAULT_WINDOW_OFFSET_IN_FIRST_PERIOD_US;
|
||||
ImmutableList<Float> cuePoints =
|
||||
ImmutableList.of(
|
||||
(float) firstMidrollPeriodTimeUs / C.MICROS_PER_SECOND,
|
||||
(float) secondMidrollPeriodTimeUs / C.MICROS_PER_SECOND);
|
||||
setupPlayback(
|
||||
CONTENT_TIMELINE,
|
||||
cuePoints,
|
||||
new ImaAdsLoader.Builder(getApplicationContext())
|
||||
.setPlayAdBeforeStartPosition(false)
|
||||
.setImaFactory(mockImaFactory)
|
||||
.setImaSdkSettings(mockImaSdkSettings)
|
||||
.buildForAdTag(TEST_URI));
|
||||
|
||||
fakeExoPlayer.setPlayingContentPosition(C.usToMs(secondMidrollWindowTimeUs) - 1_000);
|
||||
imaAdsLoader.start(adsLoaderListener, adViewProvider);
|
||||
|
||||
ArgumentCaptor<Double> playAdsAfterTimeCaptor = ArgumentCaptor.forClass(Double.class);
|
||||
verify(mockAdsRenderingSettings).setPlayAdsAfterTime(playAdsAfterTimeCaptor.capture());
|
||||
double expectedPlayAdsAfterTimeUs = (firstMidrollPeriodTimeUs + secondMidrollPeriodTimeUs) / 2d;
|
||||
assertThat(playAdsAfterTimeCaptor.getValue())
|
||||
.isWithin(0.1d)
|
||||
.of(expectedPlayAdsAfterTimeUs / C.MICROS_PER_SECOND);
|
||||
assertThat(adsLoaderListener.adPlaybackState)
|
||||
.isEqualTo(
|
||||
AdPlaybackStateFactory.fromCuePoints(cuePoints)
|
||||
.withSkippedAdGroup(/* adGroupIndex= */ 0)
|
||||
.withContentDurationUs(CONTENT_PERIOD_DURATION_US));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void resumePlaybackAtSecondMidroll_withoutPlayAdBeforeStartPosition_skipsFirstMidroll() {
|
||||
long firstMidrollWindowTimeUs = 2 * C.MICROS_PER_SECOND;
|
||||
long firstMidrollPeriodTimeUs =
|
||||
firstMidrollWindowTimeUs
|
||||
+ TimelineWindowDefinition.DEFAULT_WINDOW_OFFSET_IN_FIRST_PERIOD_US;
|
||||
long secondMidrollWindowTimeUs = 4 * C.MICROS_PER_SECOND;
|
||||
long secondMidrollPeriodTimeUs =
|
||||
secondMidrollWindowTimeUs
|
||||
+ TimelineWindowDefinition.DEFAULT_WINDOW_OFFSET_IN_FIRST_PERIOD_US;
|
||||
ImmutableList<Float> cuePoints =
|
||||
ImmutableList.of(
|
||||
(float) firstMidrollPeriodTimeUs / C.MICROS_PER_SECOND,
|
||||
(float) secondMidrollPeriodTimeUs / C.MICROS_PER_SECOND);
|
||||
setupPlayback(
|
||||
CONTENT_TIMELINE,
|
||||
cuePoints,
|
||||
new ImaAdsLoader.Builder(getApplicationContext())
|
||||
.setPlayAdBeforeStartPosition(false)
|
||||
.setImaFactory(mockImaFactory)
|
||||
.setImaSdkSettings(mockImaSdkSettings)
|
||||
.buildForAdTag(TEST_URI));
|
||||
|
||||
fakeExoPlayer.setPlayingContentPosition(C.usToMs(secondMidrollWindowTimeUs));
|
||||
imaAdsLoader.start(adsLoaderListener, adViewProvider);
|
||||
|
||||
ArgumentCaptor<Double> playAdsAfterTimeCaptor = ArgumentCaptor.forClass(Double.class);
|
||||
verify(mockAdsRenderingSettings).setPlayAdsAfterTime(playAdsAfterTimeCaptor.capture());
|
||||
double expectedPlayAdsAfterTimeUs = (firstMidrollPeriodTimeUs + secondMidrollPeriodTimeUs) / 2d;
|
||||
assertThat(playAdsAfterTimeCaptor.getValue())
|
||||
.isWithin(0.1d)
|
||||
.of(expectedPlayAdsAfterTimeUs / C.MICROS_PER_SECOND);
|
||||
assertThat(adsLoaderListener.adPlaybackState)
|
||||
.isEqualTo(
|
||||
AdPlaybackStateFactory.fromCuePoints(cuePoints)
|
||||
.withContentDurationUs(CONTENT_PERIOD_DURATION_US)
|
||||
.withSkippedAdGroup(/* adGroupIndex= */ 0));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void stop_unregistersAllVideoControlOverlays() {
|
||||
setupPlayback(CONTENT_TIMELINE, PREROLL_ADS_DURATIONS_US, PREROLL_CUE_POINTS_SECONDS);
|
||||
setupPlayback(CONTENT_TIMELINE, PREROLL_CUE_POINTS_SECONDS);
|
||||
imaAdsLoader.start(adsLoaderListener, adViewProvider);
|
||||
imaAdsLoader.requestAds(adViewGroup);
|
||||
imaAdsLoader.stop();
|
||||
|
||||
InOrder inOrder = inOrder(mockAdDisplayContainer);
|
||||
inOrder.verify(mockAdDisplayContainer).registerVideoControlsOverlay(adOverlayView);
|
||||
inOrder.verify(mockAdDisplayContainer).unregisterAllVideoControlsOverlays();
|
||||
inOrder.verify(mockAdDisplayContainer).registerFriendlyObstruction(mockFriendlyObstruction);
|
||||
inOrder.verify(mockAdDisplayContainer).unregisterAllFriendlyObstructions();
|
||||
}
|
||||
|
||||
private void setupPlayback(Timeline contentTimeline, long[][] adDurationsUs, Float[] cuePoints) {
|
||||
fakeExoPlayer = new FakePlayer();
|
||||
adsLoaderListener = new TestAdsLoaderListener(fakeExoPlayer, contentTimeline, adDurationsUs);
|
||||
when(mockAdsManager.getAdCuePoints()).thenReturn(Arrays.asList(cuePoints));
|
||||
imaAdsLoader =
|
||||
new ImaAdsLoader.Builder(ApplicationProvider.getApplicationContext())
|
||||
@Test
|
||||
public void loadAd_withLargeAdCuePoint_updatesAdPlaybackStateWithLoadedAd() {
|
||||
float midrollTimeSecs = 1_765f;
|
||||
ImmutableList<Float> cuePoints = ImmutableList.of(midrollTimeSecs);
|
||||
setupPlayback(CONTENT_TIMELINE, cuePoints);
|
||||
imaAdsLoader.start(adsLoaderListener, adViewProvider);
|
||||
videoAdPlayer.loadAd(
|
||||
TEST_AD_MEDIA_INFO,
|
||||
new AdPodInfo() {
|
||||
@Override
|
||||
public int getTotalAds() {
|
||||
return 1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getAdPosition() {
|
||||
return 1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isBumper() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public double getMaxDuration() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getPodIndex() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public double getTimeOffset() {
|
||||
return midrollTimeSecs;
|
||||
}
|
||||
});
|
||||
|
||||
assertThat(adsLoaderListener.adPlaybackState)
|
||||
.isEqualTo(
|
||||
AdPlaybackStateFactory.fromCuePoints(cuePoints)
|
||||
.withContentDurationUs(CONTENT_PERIOD_DURATION_US)
|
||||
.withAdCount(/* adGroupIndex= */ 0, /* adCount= */ 1)
|
||||
.withAdUri(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0, TEST_URI)
|
||||
.withAdDurationsUs(new long[][] {{TEST_AD_DURATION_US}}));
|
||||
}
|
||||
|
||||
private void setupPlayback(Timeline contentTimeline, List<Float> cuePoints) {
|
||||
setupPlayback(
|
||||
contentTimeline,
|
||||
cuePoints,
|
||||
new ImaAdsLoader.Builder(getApplicationContext())
|
||||
.setImaFactory(mockImaFactory)
|
||||
.setImaSdkSettings(mockImaSdkSettings)
|
||||
.buildForAdTag(TEST_URI);
|
||||
.buildForAdTag(TEST_URI));
|
||||
}
|
||||
|
||||
private void setupPlayback(
|
||||
Timeline contentTimeline, List<Float> cuePoints, ImaAdsLoader imaAdsLoader) {
|
||||
fakeExoPlayer = new FakePlayer();
|
||||
adsLoaderListener = new TestAdsLoaderListener(fakeExoPlayer, contentTimeline);
|
||||
when(mockAdsManager.getAdCuePoints()).thenReturn(cuePoints);
|
||||
this.imaAdsLoader = imaAdsLoader;
|
||||
imaAdsLoader.setPlayer(fakeExoPlayer);
|
||||
}
|
||||
|
||||
|
|
@ -278,9 +765,11 @@ public final class ImaAdsLoaderTest {
|
|||
ArgumentCaptor<Object> userRequestContextCaptor = ArgumentCaptor.forClass(Object.class);
|
||||
doNothing().when(mockAdsRequest).setUserRequestContext(userRequestContextCaptor.capture());
|
||||
when(mockAdsRequest.getUserRequestContext())
|
||||
.thenAnswer((Answer<Object>) invocation -> userRequestContextCaptor.getValue());
|
||||
.thenAnswer(invocation -> userRequestContextCaptor.getValue());
|
||||
List<com.google.ads.interactivemedia.v3.api.AdsLoader.AdsLoadedListener> adsLoadedListeners =
|
||||
new ArrayList<>();
|
||||
// Deliberately don't handle removeAdsLoadedListener to allow testing behavior if the IMA SDK
|
||||
// invokes callbacks after release.
|
||||
doAnswer(
|
||||
invocation -> {
|
||||
adsLoadedListeners.add(invocation.getArgument(0));
|
||||
|
|
@ -288,13 +777,6 @@ public final class ImaAdsLoaderTest {
|
|||
})
|
||||
.when(mockAdsLoader)
|
||||
.addAdsLoadedListener(any());
|
||||
doAnswer(
|
||||
invocation -> {
|
||||
adsLoadedListeners.remove(invocation.getArgument(0));
|
||||
return null;
|
||||
})
|
||||
.when(mockAdsLoader)
|
||||
.removeAdsLoadedListener(any());
|
||||
when(mockAdsManagerLoadedEvent.getAdsManager()).thenReturn(mockAdsManager);
|
||||
when(mockAdsManagerLoadedEvent.getUserRequestContext())
|
||||
.thenAnswer(invocation -> mockAdsRequest.getUserRequestContext());
|
||||
|
|
@ -310,10 +792,41 @@ public final class ImaAdsLoaderTest {
|
|||
.when(mockAdsLoader)
|
||||
.requestAds(mockAdsRequest);
|
||||
|
||||
when(mockImaFactory.createAdDisplayContainer()).thenReturn(mockAdDisplayContainer);
|
||||
doAnswer(
|
||||
invocation -> {
|
||||
adEventListener = invocation.getArgument(0);
|
||||
return null;
|
||||
})
|
||||
.when(mockAdsManager)
|
||||
.addAdEventListener(any());
|
||||
|
||||
doAnswer(
|
||||
invocation -> {
|
||||
contentProgressProvider = invocation.getArgument(0);
|
||||
return null;
|
||||
})
|
||||
.when(mockAdsRequest)
|
||||
.setContentProgressProvider(any());
|
||||
|
||||
doAnswer(
|
||||
invocation -> {
|
||||
videoAdPlayer = invocation.getArgument(1);
|
||||
return mockAdDisplayContainer;
|
||||
})
|
||||
.when(mockImaFactory)
|
||||
.createAdDisplayContainer(any(), any());
|
||||
doAnswer(
|
||||
invocation -> {
|
||||
videoAdPlayer = invocation.getArgument(1);
|
||||
return mockAdDisplayContainer;
|
||||
})
|
||||
.when(mockImaFactory)
|
||||
.createAudioAdDisplayContainer(any(), any());
|
||||
when(mockImaFactory.createAdsRenderingSettings()).thenReturn(mockAdsRenderingSettings);
|
||||
when(mockImaFactory.createAdsRequest()).thenReturn(mockAdsRequest);
|
||||
when(mockImaFactory.createAdsLoader(any(), any(), any())).thenReturn(mockAdsLoader);
|
||||
when(mockImaFactory.createFriendlyObstruction(any(), any(), any()))
|
||||
.thenReturn(mockFriendlyObstruction);
|
||||
|
||||
when(mockAdPodInfo.getPodIndex()).thenReturn(0);
|
||||
when(mockAdPodInfo.getTotalAds()).thenReturn(1);
|
||||
|
|
@ -347,19 +860,21 @@ public final class ImaAdsLoaderTest {
|
|||
|
||||
private final FakePlayer fakeExoPlayer;
|
||||
private final Timeline contentTimeline;
|
||||
private final long[][] adDurationsUs;
|
||||
|
||||
public AdPlaybackState adPlaybackState;
|
||||
|
||||
public TestAdsLoaderListener(
|
||||
FakePlayer fakeExoPlayer, Timeline contentTimeline, long[][] adDurationsUs) {
|
||||
public TestAdsLoaderListener(FakePlayer fakeExoPlayer, Timeline contentTimeline) {
|
||||
this.fakeExoPlayer = fakeExoPlayer;
|
||||
this.contentTimeline = contentTimeline;
|
||||
this.adDurationsUs = adDurationsUs;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAdPlaybackState(AdPlaybackState adPlaybackState) {
|
||||
long[][] adDurationsUs = new long[adPlaybackState.adGroupCount][];
|
||||
for (int adGroupIndex = 0; adGroupIndex < adPlaybackState.adGroupCount; adGroupIndex++) {
|
||||
adDurationsUs[adGroupIndex] = new long[adPlaybackState.adGroups[adGroupIndex].uris.length];
|
||||
Arrays.fill(adDurationsUs[adGroupIndex], TEST_AD_DURATION_US);
|
||||
}
|
||||
adPlaybackState = adPlaybackState.withAdDurationsUs(adDurationsUs);
|
||||
this.adPlaybackState = adPlaybackState;
|
||||
fakeExoPlayer.updateTimeline(
|
||||
|
|
|
|||
|
|
@ -1,12 +1,10 @@
|
|||
# ExoPlayer Firebase JobDispatcher extension #
|
||||
|
||||
**DEPRECATED - Please use [WorkManager extension][] or [PlatformScheduler][]
|
||||
instead.**
|
||||
**This extension is deprecated. Use the [WorkManager extension][] instead.**
|
||||
|
||||
This extension provides a Scheduler implementation which uses [Firebase JobDispatcher][].
|
||||
|
||||
[WorkManager extension]: https://github.com/google/ExoPlayer/blob/release-v2/extensions/workmanager/README.md
|
||||
[PlatformScheduler]: https://github.com/google/ExoPlayer/blob/release-v2/library/core/src/main/java/com/google/android/exoplayer2/scheduler/PlatformScheduler.java
|
||||
[Firebase JobDispatcher]: https://github.com/firebase/firebase-jobdispatcher-android
|
||||
|
||||
## Getting the extension ##
|
||||
|
|
|
|||
|
|
@ -13,24 +13,7 @@
|
|||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
apply from: '../../constants.gradle'
|
||||
apply plugin: 'com.android.library'
|
||||
|
||||
android {
|
||||
compileSdkVersion project.ext.compileSdkVersion
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
minSdkVersion project.ext.minSdkVersion
|
||||
targetSdkVersion project.ext.targetSdkVersion
|
||||
}
|
||||
|
||||
testOptions.unitTests.includeAndroidResources = true
|
||||
}
|
||||
apply from: "$gradle.ext.exoplayerSettingsDir/common_library_config.gradle"
|
||||
|
||||
dependencies {
|
||||
implementation project(modulePrefix + 'library-core')
|
||||
|
|
|
|||
|
|
@ -60,11 +60,15 @@ import com.google.android.exoplayer2.util.Util;
|
|||
@Deprecated
|
||||
public final class JobDispatcherScheduler implements Scheduler {
|
||||
|
||||
private static final boolean DEBUG = false;
|
||||
private static final String TAG = "JobDispatcherScheduler";
|
||||
private static final String KEY_SERVICE_ACTION = "service_action";
|
||||
private static final String KEY_SERVICE_PACKAGE = "service_package";
|
||||
private static final String KEY_REQUIREMENTS = "requirements";
|
||||
private static final int SUPPORTED_REQUIREMENTS =
|
||||
Requirements.NETWORK
|
||||
| Requirements.NETWORK_UNMETERED
|
||||
| Requirements.DEVICE_IDLE
|
||||
| Requirements.DEVICE_CHARGING;
|
||||
|
||||
private final String jobTag;
|
||||
private final FirebaseJobDispatcher jobDispatcher;
|
||||
|
|
@ -85,35 +89,44 @@ public final class JobDispatcherScheduler implements Scheduler {
|
|||
public boolean schedule(Requirements requirements, String servicePackage, String serviceAction) {
|
||||
Job job = buildJob(jobDispatcher, requirements, jobTag, servicePackage, serviceAction);
|
||||
int result = jobDispatcher.schedule(job);
|
||||
logd("Scheduling job: " + jobTag + " result: " + result);
|
||||
return result == FirebaseJobDispatcher.SCHEDULE_RESULT_SUCCESS;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean cancel() {
|
||||
int result = jobDispatcher.cancel(jobTag);
|
||||
logd("Canceling job: " + jobTag + " result: " + result);
|
||||
return result == FirebaseJobDispatcher.CANCEL_RESULT_SUCCESS;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Requirements getSupportedRequirements(Requirements requirements) {
|
||||
return requirements.filterRequirements(SUPPORTED_REQUIREMENTS);
|
||||
}
|
||||
|
||||
private static Job buildJob(
|
||||
FirebaseJobDispatcher dispatcher,
|
||||
Requirements requirements,
|
||||
String tag,
|
||||
String servicePackage,
|
||||
String serviceAction) {
|
||||
Requirements filteredRequirements = requirements.filterRequirements(SUPPORTED_REQUIREMENTS);
|
||||
if (!filteredRequirements.equals(requirements)) {
|
||||
Log.w(
|
||||
TAG,
|
||||
"Ignoring unsupported requirements: "
|
||||
+ (filteredRequirements.getRequirements() ^ requirements.getRequirements()));
|
||||
}
|
||||
|
||||
Job.Builder builder =
|
||||
dispatcher
|
||||
.newJobBuilder()
|
||||
.setService(JobDispatcherSchedulerService.class) // the JobService that will be called
|
||||
.setTag(tag);
|
||||
|
||||
if (requirements.isUnmeteredNetworkRequired()) {
|
||||
builder.addConstraint(Constraint.ON_UNMETERED_NETWORK);
|
||||
} else if (requirements.isNetworkRequired()) {
|
||||
builder.addConstraint(Constraint.ON_ANY_NETWORK);
|
||||
}
|
||||
|
||||
if (requirements.isIdleRequired()) {
|
||||
builder.addConstraint(Constraint.DEVICE_IDLE);
|
||||
}
|
||||
|
|
@ -131,31 +144,20 @@ public final class JobDispatcherScheduler implements Scheduler {
|
|||
return builder.build();
|
||||
}
|
||||
|
||||
private static void logd(String message) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, message);
|
||||
}
|
||||
}
|
||||
|
||||
/** A {@link JobService} that starts the target service if the requirements are met. */
|
||||
public static final class JobDispatcherSchedulerService extends JobService {
|
||||
@Override
|
||||
public boolean onStartJob(JobParameters params) {
|
||||
logd("JobDispatcherSchedulerService is started");
|
||||
Bundle extras = params.getExtras();
|
||||
Assertions.checkNotNull(extras, "Service started without extras.");
|
||||
Bundle extras = Assertions.checkNotNull(params.getExtras());
|
||||
Requirements requirements = new Requirements(extras.getInt(KEY_REQUIREMENTS));
|
||||
if (requirements.checkRequirements(this)) {
|
||||
logd("Requirements are met");
|
||||
String serviceAction = extras.getString(KEY_SERVICE_ACTION);
|
||||
String servicePackage = extras.getString(KEY_SERVICE_PACKAGE);
|
||||
Assertions.checkNotNull(serviceAction, "Service action missing.");
|
||||
Assertions.checkNotNull(servicePackage, "Service package missing.");
|
||||
int notMetRequirements = requirements.getNotMetRequirements(this);
|
||||
if (notMetRequirements == 0) {
|
||||
String serviceAction = Assertions.checkNotNull(extras.getString(KEY_SERVICE_ACTION));
|
||||
String servicePackage = Assertions.checkNotNull(extras.getString(KEY_SERVICE_PACKAGE));
|
||||
Intent intent = new Intent(serviceAction).setPackage(servicePackage);
|
||||
logd("Starting service action: " + serviceAction + " package: " + servicePackage);
|
||||
Util.startForegroundService(this, intent);
|
||||
} else {
|
||||
logd("Requirements are not met");
|
||||
Log.w(TAG, "Requirements not met: " + notMetRequirements);
|
||||
jobFinished(params, /* needsReschedule */ true);
|
||||
}
|
||||
return false;
|
||||
|
|
|
|||
|
|
@ -11,24 +11,9 @@
|
|||
// 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.library'
|
||||
apply from: "$gradle.ext.exoplayerSettingsDir/common_library_config.gradle"
|
||||
|
||||
android {
|
||||
compileSdkVersion project.ext.compileSdkVersion
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
minSdkVersion 17
|
||||
targetSdkVersion project.ext.targetSdkVersion
|
||||
}
|
||||
|
||||
testOptions.unitTests.includeAndroidResources = true
|
||||
}
|
||||
android.defaultConfig.minSdkVersion 17
|
||||
|
||||
dependencies {
|
||||
implementation project(modulePrefix + 'library-core')
|
||||
|
|
|
|||
|
|
@ -72,7 +72,7 @@ public final class LeanbackPlayerAdapter extends PlayerAdapter implements Runnab
|
|||
this.context = context;
|
||||
this.player = player;
|
||||
this.updatePeriodMs = updatePeriodMs;
|
||||
handler = Util.createHandler();
|
||||
handler = Util.createHandlerForCurrentOrMainLooper();
|
||||
componentListener = new ComponentListener();
|
||||
controlDispatcher = new DefaultControlDispatcher();
|
||||
}
|
||||
|
|
|
|||
53
extensions/media2/README.md
Normal file
53
extensions/media2/README.md
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
# ExoPlayer Media2 extension #
|
||||
|
||||
The Media2 extension provides builders for [SessionPlayer][] and [MediaSession.SessionCallback][] in
|
||||
the [Media2 library][].
|
||||
|
||||
Compared to [MediaSessionConnector][] that uses [MediaSessionCompat][], this provides finer grained
|
||||
control for incoming calls, so you can selectively allow/reject commands per controller.
|
||||
|
||||
## Getting the extension ##
|
||||
|
||||
The easiest way to use the extension is to add it as a gradle dependency:
|
||||
|
||||
```gradle
|
||||
implementation 'com.google.android.exoplayer:extension-media2:2.X.X'
|
||||
```
|
||||
|
||||
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
|
||||
locally. Instructions for doing this can be found in ExoPlayer's
|
||||
[top level README][].
|
||||
|
||||
[top level README]: https://github.com/google/ExoPlayer/blob/release-v2/README.md
|
||||
|
||||
## Using the extension ##
|
||||
|
||||
### Using `SessionPlayerConnector` ###
|
||||
|
||||
`SessionPlayerConnector` is a [SessionPlayer][] implementation wrapping a given `Player`.
|
||||
You can use a [SessionPlayer][] instance to build a [MediaSession][], or to set the player
|
||||
associated with a [VideoView][] or [MediaControlView][]
|
||||
|
||||
### Using `SessionCallbackBuilder` ###
|
||||
|
||||
`SessionCallbackBuilder` lets you build a [MediaSession.SessionCallback][] instance given its
|
||||
collaborators. You can use a [MediaSession.SessionCallback][] to build a [MediaSession][].
|
||||
|
||||
## Links ##
|
||||
|
||||
* [Javadoc][]: Classes matching
|
||||
`com.google.android.exoplayer2.ext.media2.*` belong to this module.
|
||||
|
||||
[Javadoc]: https://exoplayer.dev/doc/reference/index.html
|
||||
|
||||
[SessionPlayer]: https://developer.android.com/reference/androidx/media2/common/SessionPlayer
|
||||
[MediaSession]: https://developer.android.com/reference/androidx/media2/session/MediaSession
|
||||
[MediaSession.SessionCallback]: https://developer.android.com/reference/androidx/media2/session/MediaSession.SessionCallback
|
||||
[Media2 library]: https://developer.android.com/jetpack/androidx/releases/media2
|
||||
[MediaSessionCompat]: https://developer.android.com/reference/android/support/v4/media/session/MediaSessionCompat
|
||||
[MediaSessionConnector]: https://exoplayer.dev/doc/reference/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.html
|
||||
[VideoView]: https://developer.android.com/reference/androidx/media2/widget/VideoView
|
||||
[MediaControlView]: https://developer.android.com/reference/androidx/media2/widget/MediaControlView
|
||||
49
extensions/media2/build.gradle
Normal file
49
extensions/media2/build.gradle
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
// Copyright 2019 The Android Open Source Project
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
apply from: "$gradle.ext.exoplayerSettingsDir/common_library_config.gradle"
|
||||
|
||||
android.defaultConfig.minSdkVersion 19
|
||||
|
||||
dependencies {
|
||||
implementation project(modulePrefix + 'library-core')
|
||||
implementation 'androidx.collection:collection:' + androidxCollectionVersion
|
||||
implementation 'androidx.concurrent:concurrent-futures:1.1.0'
|
||||
implementation ('com.google.guava:guava:' + guavaVersion) {
|
||||
exclude group: 'com.google.code.findbugs', module: 'jsr305'
|
||||
exclude group: 'org.checkerframework', module: 'checker-compat-qual'
|
||||
exclude group: 'com.google.errorprone', module: 'error_prone_annotations'
|
||||
exclude group: 'com.google.j2objc', module: 'j2objc-annotations'
|
||||
exclude group: 'org.codehaus.mojo', module: 'animal-sniffer-annotations'
|
||||
}
|
||||
api 'androidx.media2:media2-session:1.0.3'
|
||||
compileOnly 'org.checkerframework:checker-compat-qual:' + checkerframeworkCompatVersion
|
||||
compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion
|
||||
compileOnly 'org.jetbrains.kotlin:kotlin-annotations-jvm:' + kotlinAnnotationsVersion
|
||||
androidTestImplementation 'androidx.test.ext:junit:' + androidxTestJUnitVersion
|
||||
androidTestImplementation 'androidx.test:core:' + androidxTestCoreVersion
|
||||
androidTestImplementation 'androidx.test:runner:' + androidxTestRunnerVersion
|
||||
androidTestImplementation 'androidx.test:rules:' + androidxTestRulesVersion
|
||||
androidTestImplementation 'com.google.truth:truth:' + truthVersion
|
||||
}
|
||||
|
||||
ext {
|
||||
javadocTitle = 'Media2 extension'
|
||||
}
|
||||
apply from: '../../javadoc_library.gradle'
|
||||
|
||||
ext {
|
||||
releaseArtifact = 'extension-media2'
|
||||
releaseDescription = 'Media2 extension for ExoPlayer.'
|
||||
}
|
||||
apply from: '../../publish.gradle'
|
||||
38
extensions/media2/src/androidTest/AndroidManifest.xml
Normal file
38
extensions/media2/src/androidTest/AndroidManifest.xml
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
<?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.exoplayer2.ext.media2.test">
|
||||
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
|
||||
<uses-sdk/>
|
||||
|
||||
<application
|
||||
android:allowBackup="false"
|
||||
tools:ignore="MissingApplicationIcon,HardcodedDebugMode">
|
||||
<activity android:name="com.google.android.exoplayer2.ext.media2.MediaStubActivity"
|
||||
android:configChanges="keyboardHidden|orientation|screenSize"
|
||||
android:exported="false"
|
||||
android:label="MediaStubActivity"/>
|
||||
</application>
|
||||
|
||||
<instrumentation
|
||||
android:targetPackage="com.google.android.exoplayer2.ext.media2.test"
|
||||
android:name="androidx.test.runner.AndroidJUnitRunner"/>
|
||||
|
||||
</manifest>
|
||||
|
|
@ -0,0 +1,88 @@
|
|||
/*
|
||||
* Copyright 2020 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.media2;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static java.util.concurrent.TimeUnit.MILLISECONDS;
|
||||
|
||||
import android.content.Context;
|
||||
import android.support.v4.media.session.MediaControllerCompat;
|
||||
import android.support.v4.media.session.MediaSessionCompat;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.media2.common.SessionPlayer;
|
||||
import androidx.media2.common.SessionPlayer.PlayerResult;
|
||||
import androidx.media2.session.MediaSession;
|
||||
import androidx.test.core.app.ApplicationProvider;
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
import androidx.test.platform.app.InstrumentationRegistry;
|
||||
import com.google.android.exoplayer2.ext.media2.test.R;
|
||||
import com.google.android.exoplayer2.util.Assertions;
|
||||
import com.google.common.util.concurrent.ListenableFuture;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
/** Unit test for {@link MediaSessionUtil} */
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public class MediaSessionUtilTest {
|
||||
private static final int PLAYER_STATE_CHANGE_WAIT_TIME_MS = 5_000;
|
||||
|
||||
@Rule public final PlayerTestRule playerTestRule = new PlayerTestRule();
|
||||
|
||||
@Test
|
||||
public void getSessionCompatToken_withMediaControllerCompat_returnsValidToken() throws Exception {
|
||||
Context context = ApplicationProvider.getApplicationContext();
|
||||
|
||||
SessionPlayerConnector sessionPlayerConnector = playerTestRule.getSessionPlayerConnector();
|
||||
MediaSession.SessionCallback sessionCallback =
|
||||
new SessionCallbackBuilder(context, sessionPlayerConnector).build();
|
||||
TestUtils.loadResource(R.raw.audio, sessionPlayerConnector);
|
||||
ListenableFuture<PlayerResult> prepareResult = sessionPlayerConnector.prepare();
|
||||
CountDownLatch latch = new CountDownLatch(1);
|
||||
sessionPlayerConnector.registerPlayerCallback(
|
||||
playerTestRule.getExecutor(),
|
||||
new SessionPlayer.PlayerCallback() {
|
||||
@Override
|
||||
public void onPlayerStateChanged(@NonNull SessionPlayer player, int playerState) {
|
||||
if (playerState == SessionPlayer.PLAYER_STATE_PLAYING) {
|
||||
latch.countDown();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
MediaSession session2 =
|
||||
new MediaSession.Builder(context, sessionPlayerConnector)
|
||||
.setSessionCallback(playerTestRule.getExecutor(), sessionCallback)
|
||||
.build();
|
||||
|
||||
InstrumentationRegistry.getInstrumentation()
|
||||
.runOnMainSync(
|
||||
() -> {
|
||||
try {
|
||||
MediaSessionCompat.Token token =
|
||||
Assertions.checkNotNull(MediaSessionUtil.getSessionCompatToken(session2));
|
||||
MediaControllerCompat controllerCompat = new MediaControllerCompat(context, token);
|
||||
controllerCompat.getTransportControls().play();
|
||||
} catch (Exception e) {
|
||||
throw new IllegalStateException(e);
|
||||
}
|
||||
});
|
||||
assertThat(prepareResult.get(PLAYER_STATE_CHANGE_WAIT_TIME_MS, MILLISECONDS).getResultCode())
|
||||
.isEqualTo(PlayerResult.RESULT_SUCCESS);
|
||||
assertThat(latch.await(PLAYER_STATE_CHANGE_WAIT_TIME_MS, MILLISECONDS)).isTrue();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,83 @@
|
|||
/*
|
||||
* Copyright 2019 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.google.android.exoplayer2.ext.media2;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.KeyguardManager;
|
||||
import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
import android.util.Log;
|
||||
import android.view.SurfaceHolder;
|
||||
import android.view.SurfaceView;
|
||||
import android.view.WindowManager;
|
||||
import com.google.android.exoplayer2.ext.media2.test.R;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
|
||||
/** Stub activity to play media contents on. */
|
||||
public final class MediaStubActivity extends Activity {
|
||||
|
||||
private static final String TAG = "MediaStubActivity";
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.mediaplayer);
|
||||
|
||||
// disable enter animation.
|
||||
overridePendingTransition(0, 0);
|
||||
|
||||
if (Util.SDK_INT >= 27) {
|
||||
getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
|
||||
setTurnScreenOn(true);
|
||||
setShowWhenLocked(true);
|
||||
KeyguardManager keyguardManager =
|
||||
(KeyguardManager) getSystemService(Context.KEYGUARD_SERVICE);
|
||||
keyguardManager.requestDismissKeyguard(this, null);
|
||||
} else {
|
||||
getWindow()
|
||||
.addFlags(
|
||||
WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
|
||||
| WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON
|
||||
| WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED
|
||||
| WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void finish() {
|
||||
super.finish();
|
||||
|
||||
// disable exit animation.
|
||||
overridePendingTransition(0, 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResume() {
|
||||
Log.i(TAG, "onResume");
|
||||
super.onResume();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPause() {
|
||||
Log.i(TAG, "onPause");
|
||||
super.onPause();
|
||||
}
|
||||
|
||||
public SurfaceHolder getSurfaceHolder() {
|
||||
SurfaceView surface = findViewById(R.id.surface);
|
||||
return surface.getHolder();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,185 @@
|
|||
/*
|
||||
* Copyright 2019 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.google.android.exoplayer2.ext.media2;
|
||||
|
||||
import android.content.Context;
|
||||
import android.net.Uri;
|
||||
import android.os.Looper;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.test.core.app.ApplicationProvider;
|
||||
import androidx.test.platform.app.InstrumentationRegistry;
|
||||
import com.google.android.exoplayer2.SimpleExoPlayer;
|
||||
import com.google.android.exoplayer2.source.DefaultMediaSourceFactory;
|
||||
import com.google.android.exoplayer2.upstream.DataSource;
|
||||
import com.google.android.exoplayer2.upstream.DataSpec;
|
||||
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
|
||||
import com.google.android.exoplayer2.upstream.TransferListener;
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import org.junit.rules.ExternalResource;
|
||||
|
||||
/** Rule for tests that use {@link SessionPlayerConnector}. */
|
||||
/* package */ final class PlayerTestRule extends ExternalResource {
|
||||
|
||||
/** Instrumentation to attach to {@link DataSource} instances used by the player. */
|
||||
public interface DataSourceInstrumentation {
|
||||
|
||||
/** Called at the start of {@link DataSource#open}. */
|
||||
void onPreOpen(DataSpec dataSpec);
|
||||
}
|
||||
|
||||
private Context context;
|
||||
private ExecutorService executor;
|
||||
|
||||
private SessionPlayerConnector sessionPlayerConnector;
|
||||
private SimpleExoPlayer exoPlayer;
|
||||
@Nullable private DataSourceInstrumentation dataSourceInstrumentation;
|
||||
|
||||
@Override
|
||||
protected void before() {
|
||||
// Workaround limitation in androidx.media2.session:1.0.3 which session can only be instantiated
|
||||
// on thread with prepared Looper.
|
||||
// TODO: Remove when androidx.media2.session:1.1.0 is released without the limitation
|
||||
// [Internal: b/146536708]
|
||||
if (Looper.myLooper() == null) {
|
||||
Looper.prepare();
|
||||
}
|
||||
|
||||
context = ApplicationProvider.getApplicationContext();
|
||||
executor = Executors.newFixedThreadPool(1);
|
||||
|
||||
InstrumentationRegistry.getInstrumentation()
|
||||
.runOnMainSync(
|
||||
() -> {
|
||||
// Initialize AudioManager on the main thread to workaround that
|
||||
// audio focus listener is called on the thread where the AudioManager was
|
||||
// originally initialized. [Internal: b/78617702]
|
||||
// Without posting this, audio focus listeners wouldn't be called because the
|
||||
// listeners would be posted to the test thread (here) where it waits until the
|
||||
// tests are finished.
|
||||
context.getSystemService(Context.AUDIO_SERVICE);
|
||||
|
||||
DataSource.Factory dataSourceFactory = new InstrumentingDataSourceFactory(context);
|
||||
exoPlayer =
|
||||
new SimpleExoPlayer.Builder(context)
|
||||
.setLooper(Looper.myLooper())
|
||||
.setMediaSourceFactory(new DefaultMediaSourceFactory(dataSourceFactory))
|
||||
.build();
|
||||
sessionPlayerConnector = new SessionPlayerConnector(exoPlayer);
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void after() {
|
||||
if (sessionPlayerConnector != null) {
|
||||
sessionPlayerConnector.close();
|
||||
sessionPlayerConnector = null;
|
||||
}
|
||||
if (exoPlayer != null) {
|
||||
InstrumentationRegistry.getInstrumentation()
|
||||
.runOnMainSync(
|
||||
() -> {
|
||||
exoPlayer.release();
|
||||
exoPlayer = null;
|
||||
});
|
||||
}
|
||||
if (executor != null) {
|
||||
executor.shutdown();
|
||||
executor = null;
|
||||
}
|
||||
}
|
||||
|
||||
public void setDataSourceInstrumentation(
|
||||
@Nullable DataSourceInstrumentation dataSourceInstrumentation) {
|
||||
this.dataSourceInstrumentation = dataSourceInstrumentation;
|
||||
}
|
||||
|
||||
public ExecutorService getExecutor() {
|
||||
return executor;
|
||||
}
|
||||
|
||||
public SessionPlayerConnector getSessionPlayerConnector() {
|
||||
return sessionPlayerConnector;
|
||||
}
|
||||
|
||||
public SimpleExoPlayer getSimpleExoPlayer() {
|
||||
return exoPlayer;
|
||||
}
|
||||
|
||||
private final class InstrumentingDataSourceFactory implements DataSource.Factory {
|
||||
|
||||
private final DefaultDataSourceFactory defaultDataSourceFactory;
|
||||
|
||||
public InstrumentingDataSourceFactory(Context context) {
|
||||
defaultDataSourceFactory = new DefaultDataSourceFactory(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public DataSource createDataSource() {
|
||||
DataSource dataSource = defaultDataSourceFactory.createDataSource();
|
||||
return dataSourceInstrumentation == null
|
||||
? dataSource
|
||||
: new InstrumentedDataSource(dataSource, dataSourceInstrumentation);
|
||||
}
|
||||
}
|
||||
|
||||
private static final class InstrumentedDataSource implements DataSource {
|
||||
|
||||
private final DataSource wrappedDataSource;
|
||||
private final DataSourceInstrumentation instrumentation;
|
||||
|
||||
public InstrumentedDataSource(
|
||||
DataSource wrappedDataSource, DataSourceInstrumentation instrumentation) {
|
||||
this.wrappedDataSource = wrappedDataSource;
|
||||
this.instrumentation = instrumentation;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addTransferListener(TransferListener transferListener) {
|
||||
wrappedDataSource.addTransferListener(transferListener);
|
||||
}
|
||||
|
||||
@Override
|
||||
public long open(DataSpec dataSpec) throws IOException {
|
||||
instrumentation.onPreOpen(dataSpec);
|
||||
return wrappedDataSource.open(dataSpec);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public Uri getUri() {
|
||||
return wrappedDataSource.getUri();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, List<String>> getResponseHeaders() {
|
||||
return wrappedDataSource.getResponseHeaders();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int read(byte[] target, int offset, int length) throws IOException {
|
||||
return wrappedDataSource.read(target, offset, length);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
wrappedDataSource.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,680 @@
|
|||
/*
|
||||
* Copyright 2019 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.google.android.exoplayer2.ext.media2;
|
||||
|
||||
import static com.google.android.exoplayer2.ext.media2.TestUtils.assertPlayerResultSuccess;
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static com.google.common.truth.Truth.assertWithMessage;
|
||||
import static java.util.concurrent.TimeUnit.MILLISECONDS;
|
||||
|
||||
import android.content.Context;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.text.TextUtils;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.media2.common.MediaItem;
|
||||
import androidx.media2.common.MediaMetadata;
|
||||
import androidx.media2.common.Rating;
|
||||
import androidx.media2.common.SessionPlayer;
|
||||
import androidx.media2.common.UriMediaItem;
|
||||
import androidx.media2.session.HeartRating;
|
||||
import androidx.media2.session.MediaController;
|
||||
import androidx.media2.session.MediaSession;
|
||||
import androidx.media2.session.SessionCommand;
|
||||
import androidx.media2.session.SessionCommandGroup;
|
||||
import androidx.media2.session.SessionResult;
|
||||
import androidx.test.core.app.ApplicationProvider;
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
import androidx.test.filters.LargeTest;
|
||||
import androidx.test.platform.app.InstrumentationRegistry;
|
||||
import androidx.test.rule.ActivityTestRule;
|
||||
import com.google.android.exoplayer2.SimpleExoPlayer;
|
||||
import com.google.android.exoplayer2.ext.media2.test.R;
|
||||
import com.google.android.exoplayer2.upstream.RawResourceDataSource;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.concurrent.Future;
|
||||
import org.junit.Before;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
/** Tests {@link SessionCallbackBuilder}. */
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public class SessionCallbackBuilderTest {
|
||||
@Rule
|
||||
public final ActivityTestRule<MediaStubActivity> activityRule =
|
||||
new ActivityTestRule<>(MediaStubActivity.class);
|
||||
|
||||
@Rule public final PlayerTestRule playerTestRule = new PlayerTestRule();
|
||||
|
||||
private static final String MEDIA_SESSION_ID = SessionCallbackBuilderTest.class.getSimpleName();
|
||||
private static final long CONTROLLER_COMMAND_WAIT_TIME_MS = 3_000;
|
||||
private static final long PLAYER_STATE_CHANGE_OVER_SESSION_WAIT_TIME_MS = 10_000;
|
||||
private static final long PLAYER_STATE_CHANGE_WAIT_TIME_MS = 5_000;
|
||||
|
||||
private Context context;
|
||||
private Executor executor;
|
||||
private SessionPlayerConnector sessionPlayerConnector;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
context = ApplicationProvider.getApplicationContext();
|
||||
executor = playerTestRule.getExecutor();
|
||||
sessionPlayerConnector = playerTestRule.getSessionPlayerConnector();
|
||||
|
||||
// Sets the surface to the player for manual check.
|
||||
InstrumentationRegistry.getInstrumentation()
|
||||
.runOnMainSync(
|
||||
() -> {
|
||||
SimpleExoPlayer exoPlayer = playerTestRule.getSimpleExoPlayer();
|
||||
exoPlayer
|
||||
.getVideoComponent()
|
||||
.setVideoSurfaceHolder(activityRule.getActivity().getSurfaceHolder());
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void constructor() throws Exception {
|
||||
try (MediaSession session =
|
||||
createMediaSession(
|
||||
sessionPlayerConnector,
|
||||
new SessionCallbackBuilder(context, sessionPlayerConnector).build())) {
|
||||
assertPlayerResultSuccess(sessionPlayerConnector.setMediaItem(TestUtils.createMediaItem()));
|
||||
assertPlayerResultSuccess(sessionPlayerConnector.prepare());
|
||||
|
||||
OnConnectedListener listener =
|
||||
(controller, allowedCommands) -> {
|
||||
List<Integer> disallowedCommandCodes =
|
||||
Arrays.asList(
|
||||
SessionCommand.COMMAND_CODE_SESSION_SET_RATING, // no rating callback
|
||||
SessionCommand.COMMAND_CODE_PLAYER_ADD_PLAYLIST_ITEM, // no media item provider
|
||||
SessionCommand
|
||||
.COMMAND_CODE_PLAYER_REPLACE_PLAYLIST_ITEM, // no media item provider
|
||||
SessionCommand.COMMAND_CODE_PLAYER_SET_MEDIA_ITEM, // no media item provider
|
||||
SessionCommand.COMMAND_CODE_PLAYER_SET_PLAYLIST, // no media item provider
|
||||
SessionCommand.COMMAND_CODE_SESSION_REWIND, // no current media item
|
||||
SessionCommand.COMMAND_CODE_SESSION_FAST_FORWARD // no current media item
|
||||
);
|
||||
assertDisallowedCommands(disallowedCommandCodes, allowedCommands);
|
||||
};
|
||||
try (MediaController controller = createConnectedController(session, listener, null)) {
|
||||
assertThat(controller.getPlayerState()).isEqualTo(SessionPlayer.PLAYER_STATE_PAUSED);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void allowedCommand_withoutPlaylist_disallowsSkipTo() throws Exception {
|
||||
int testRewindIncrementMs = 100;
|
||||
int testFastForwardIncrementMs = 100;
|
||||
|
||||
try (MediaSession session =
|
||||
createMediaSession(
|
||||
sessionPlayerConnector,
|
||||
new SessionCallbackBuilder(context, sessionPlayerConnector)
|
||||
.setRatingCallback(
|
||||
(mediaSession, controller, mediaId, rating) ->
|
||||
SessionResult.RESULT_ERROR_BAD_VALUE)
|
||||
.setRewindIncrementMs(testRewindIncrementMs)
|
||||
.setFastForwardIncrementMs(testFastForwardIncrementMs)
|
||||
.setMediaItemProvider(new SessionCallbackBuilder.MediaIdMediaItemProvider())
|
||||
.build())) {
|
||||
assertPlayerResultSuccess(sessionPlayerConnector.setMediaItem(TestUtils.createMediaItem()));
|
||||
assertPlayerResultSuccess(sessionPlayerConnector.prepare());
|
||||
|
||||
CountDownLatch latch = new CountDownLatch(1);
|
||||
OnConnectedListener listener =
|
||||
(controller, allowedCommands) -> {
|
||||
List<Integer> disallowedCommandCodes =
|
||||
Arrays.asList(
|
||||
SessionCommand.COMMAND_CODE_PLAYER_SKIP_TO_PLAYLIST_ITEM,
|
||||
SessionCommand.COMMAND_CODE_PLAYER_SKIP_TO_PREVIOUS_PLAYLIST_ITEM,
|
||||
SessionCommand.COMMAND_CODE_PLAYER_SKIP_TO_NEXT_PLAYLIST_ITEM);
|
||||
assertDisallowedCommands(disallowedCommandCodes, allowedCommands);
|
||||
latch.countDown();
|
||||
};
|
||||
try (MediaController controller = createConnectedController(session, listener, null)) {
|
||||
assertThat(latch.await(CONTROLLER_COMMAND_WAIT_TIME_MS, MILLISECONDS)).isTrue();
|
||||
|
||||
assertSessionResultFailure(controller.skipToNextPlaylistItem());
|
||||
assertSessionResultFailure(controller.skipToPreviousPlaylistItem());
|
||||
assertSessionResultFailure(controller.skipToPlaylistItem(0));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void allowedCommand_whenPlaylistSet_allowsSkipTo() throws Exception {
|
||||
List<MediaItem> testPlaylist = new ArrayList<>();
|
||||
testPlaylist.add(TestUtils.createMediaItem(R.raw.video_desks));
|
||||
testPlaylist.add(TestUtils.createMediaItem(R.raw.video_not_seekable));
|
||||
int testRewindIncrementMs = 100;
|
||||
int testFastForwardIncrementMs = 100;
|
||||
|
||||
try (MediaSession session =
|
||||
createMediaSession(
|
||||
sessionPlayerConnector,
|
||||
new SessionCallbackBuilder(context, sessionPlayerConnector)
|
||||
.setRatingCallback(
|
||||
(mediaSession, controller, mediaId, rating) ->
|
||||
SessionResult.RESULT_ERROR_BAD_VALUE)
|
||||
.setRewindIncrementMs(testRewindIncrementMs)
|
||||
.setFastForwardIncrementMs(testFastForwardIncrementMs)
|
||||
.setMediaItemProvider(new SessionCallbackBuilder.MediaIdMediaItemProvider())
|
||||
.build())) {
|
||||
|
||||
assertPlayerResultSuccess(sessionPlayerConnector.setPlaylist(testPlaylist, null));
|
||||
assertPlayerResultSuccess(sessionPlayerConnector.prepare());
|
||||
|
||||
OnConnectedListener connectedListener =
|
||||
(controller, allowedCommands) -> {
|
||||
List<Integer> allowedCommandCodes =
|
||||
Arrays.asList(
|
||||
SessionCommand.COMMAND_CODE_PLAYER_SKIP_TO_NEXT_PLAYLIST_ITEM,
|
||||
SessionCommand.COMMAND_CODE_PLAYER_SEEK_TO,
|
||||
SessionCommand.COMMAND_CODE_SESSION_REWIND,
|
||||
SessionCommand.COMMAND_CODE_SESSION_FAST_FORWARD);
|
||||
assertAllowedCommands(allowedCommandCodes, allowedCommands);
|
||||
|
||||
List<Integer> disallowedCommandCodes =
|
||||
Arrays.asList(SessionCommand.COMMAND_CODE_PLAYER_SKIP_TO_PREVIOUS_PLAYLIST_ITEM);
|
||||
assertDisallowedCommands(disallowedCommandCodes, allowedCommands);
|
||||
};
|
||||
|
||||
CountDownLatch allowedCommandChangedLatch = new CountDownLatch(1);
|
||||
OnAllowedCommandsChangedListener allowedCommandChangedListener =
|
||||
(controller, allowedCommands) -> {
|
||||
List<Integer> allowedCommandCodes =
|
||||
Arrays.asList(SessionCommand.COMMAND_CODE_PLAYER_SKIP_TO_PREVIOUS_PLAYLIST_ITEM);
|
||||
assertAllowedCommands(allowedCommandCodes, allowedCommands);
|
||||
|
||||
List<Integer> disallowedCommandCodes =
|
||||
Arrays.asList(
|
||||
SessionCommand.COMMAND_CODE_PLAYER_SKIP_TO_NEXT_PLAYLIST_ITEM,
|
||||
SessionCommand.COMMAND_CODE_PLAYER_SEEK_TO,
|
||||
SessionCommand.COMMAND_CODE_SESSION_REWIND,
|
||||
SessionCommand.COMMAND_CODE_SESSION_FAST_FORWARD);
|
||||
assertDisallowedCommands(disallowedCommandCodes, allowedCommands);
|
||||
allowedCommandChangedLatch.countDown();
|
||||
};
|
||||
try (MediaController controller =
|
||||
createConnectedController(session, connectedListener, allowedCommandChangedListener)) {
|
||||
assertPlayerResultSuccess(sessionPlayerConnector.skipToNextPlaylistItem());
|
||||
|
||||
assertThat(allowedCommandChangedLatch.await(CONTROLLER_COMMAND_WAIT_TIME_MS, MILLISECONDS))
|
||||
.isTrue();
|
||||
|
||||
// Also test whether the rewind fails as expected.
|
||||
assertSessionResultFailure(controller.rewind());
|
||||
assertThat(sessionPlayerConnector.getCurrentPosition()).isEqualTo(0);
|
||||
assertThat(controller.getCurrentPosition()).isEqualTo(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void allowedCommand_afterCurrentMediaItemPrepared_notifiesSeekToAvailable()
|
||||
throws Exception {
|
||||
List<MediaItem> testPlaylist = new ArrayList<>();
|
||||
testPlaylist.add(TestUtils.createMediaItem(R.raw.video_desks));
|
||||
UriMediaItem secondPlaylistItem = TestUtils.createMediaItem(R.raw.video_big_buck_bunny);
|
||||
testPlaylist.add(secondPlaylistItem);
|
||||
|
||||
CountDownLatch readAllowedLatch = new CountDownLatch(1);
|
||||
playerTestRule.setDataSourceInstrumentation(
|
||||
dataSpec -> {
|
||||
if (dataSpec.uri.equals(secondPlaylistItem.getUri())) {
|
||||
try {
|
||||
assertThat(readAllowedLatch.await(PLAYER_STATE_CHANGE_WAIT_TIME_MS, MILLISECONDS))
|
||||
.isTrue();
|
||||
} catch (Exception e) {
|
||||
assertWithMessage("Unexpected exception %s", e).fail();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
try (MediaSession session =
|
||||
createMediaSession(
|
||||
sessionPlayerConnector,
|
||||
new SessionCallbackBuilder(context, sessionPlayerConnector).build())) {
|
||||
|
||||
assertPlayerResultSuccess(sessionPlayerConnector.setPlaylist(testPlaylist, null));
|
||||
assertPlayerResultSuccess(sessionPlayerConnector.prepare());
|
||||
|
||||
CountDownLatch seekToAllowedForSecondMediaItem = new CountDownLatch(1);
|
||||
OnAllowedCommandsChangedListener allowedCommandsChangedListener =
|
||||
(controller, allowedCommands) -> {
|
||||
if (allowedCommands.hasCommand(SessionCommand.COMMAND_CODE_PLAYER_SEEK_TO)
|
||||
&& controller.getCurrentMediaItemIndex() == 1) {
|
||||
seekToAllowedForSecondMediaItem.countDown();
|
||||
}
|
||||
};
|
||||
try (MediaController controller =
|
||||
createConnectedController(
|
||||
session, /* onConnectedListener= */ null, allowedCommandsChangedListener)) {
|
||||
assertPlayerResultSuccess(sessionPlayerConnector.skipToNextPlaylistItem());
|
||||
|
||||
readAllowedLatch.countDown();
|
||||
assertThat(
|
||||
seekToAllowedForSecondMediaItem.await(
|
||||
CONTROLLER_COMMAND_WAIT_TIME_MS, MILLISECONDS))
|
||||
.isTrue();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void setRatingCallback_withRatingCallback_receivesRatingCallback() throws Exception {
|
||||
String testMediaId = "testRating";
|
||||
Rating testRating = new HeartRating(true);
|
||||
CountDownLatch latch = new CountDownLatch(1);
|
||||
|
||||
SessionCallbackBuilder.RatingCallback ratingCallback =
|
||||
(session, controller, mediaId, rating) -> {
|
||||
assertThat(mediaId).isEqualTo(testMediaId);
|
||||
assertThat(rating).isEqualTo(testRating);
|
||||
latch.countDown();
|
||||
return SessionResult.RESULT_SUCCESS;
|
||||
};
|
||||
|
||||
try (MediaSession session =
|
||||
createMediaSession(
|
||||
sessionPlayerConnector,
|
||||
new SessionCallbackBuilder(context, sessionPlayerConnector)
|
||||
.setRatingCallback(ratingCallback)
|
||||
.build())) {
|
||||
try (MediaController controller = createConnectedController(session)) {
|
||||
assertSessionResultSuccess(
|
||||
controller.setRating(testMediaId, testRating), CONTROLLER_COMMAND_WAIT_TIME_MS);
|
||||
assertThat(latch.await(0, MILLISECONDS)).isTrue();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void setCustomCommandProvider_withCustomCommandProvider_receivesCustomCommand()
|
||||
throws Exception {
|
||||
SessionCommand testCommand = new SessionCommand("exo.ext.media2.COMMAND", null);
|
||||
CountDownLatch latch = new CountDownLatch(1);
|
||||
|
||||
SessionCallbackBuilder.CustomCommandProvider provider =
|
||||
new SessionCallbackBuilder.CustomCommandProvider() {
|
||||
@Override
|
||||
public SessionResult onCustomCommand(
|
||||
MediaSession session,
|
||||
MediaSession.ControllerInfo controllerInfo,
|
||||
SessionCommand customCommand,
|
||||
@Nullable Bundle args) {
|
||||
assertThat(customCommand.getCustomAction()).isEqualTo(testCommand.getCustomAction());
|
||||
assertThat(args).isNull();
|
||||
latch.countDown();
|
||||
return new SessionResult(SessionResult.RESULT_SUCCESS, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public SessionCommandGroup getCustomCommands(
|
||||
MediaSession session, MediaSession.ControllerInfo controllerInfo) {
|
||||
return new SessionCommandGroup.Builder().addCommand(testCommand).build();
|
||||
}
|
||||
};
|
||||
|
||||
try (MediaSession session =
|
||||
createMediaSession(
|
||||
sessionPlayerConnector,
|
||||
new SessionCallbackBuilder(context, sessionPlayerConnector)
|
||||
.setCustomCommandProvider(provider)
|
||||
.build())) {
|
||||
OnAllowedCommandsChangedListener listener =
|
||||
(controller, allowedCommands) -> {
|
||||
boolean foundCustomCommand = false;
|
||||
for (SessionCommand command : allowedCommands.getCommands()) {
|
||||
if (TextUtils.equals(testCommand.getCustomAction(), command.getCustomAction())) {
|
||||
foundCustomCommand = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
assertThat(foundCustomCommand).isTrue();
|
||||
};
|
||||
try (MediaController controller = createConnectedController(session, null, listener)) {
|
||||
assertSessionResultSuccess(
|
||||
controller.sendCustomCommand(testCommand, null), CONTROLLER_COMMAND_WAIT_TIME_MS);
|
||||
assertThat(latch.await(0, MILLISECONDS)).isTrue();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@LargeTest
|
||||
@Test
|
||||
public void setRewindIncrementMs_withPositiveRewindIncrement_rewinds() throws Exception {
|
||||
int testResId = R.raw.video_big_buck_bunny;
|
||||
int testDuration = 10_000;
|
||||
int tolerance = 100;
|
||||
int testSeekPosition = 2_000;
|
||||
int testRewindIncrementMs = 500;
|
||||
|
||||
TestUtils.loadResource(testResId, sessionPlayerConnector);
|
||||
|
||||
// seekTo() sometimes takes couple of seconds. Disable default timeout behavior.
|
||||
try (MediaSession session =
|
||||
createMediaSession(
|
||||
sessionPlayerConnector,
|
||||
new SessionCallbackBuilder(context, sessionPlayerConnector)
|
||||
.setRewindIncrementMs(testRewindIncrementMs)
|
||||
.setSeekTimeoutMs(0)
|
||||
.build())) {
|
||||
try (MediaController controller = createConnectedController(session)) {
|
||||
// Prepare first to ensure that seek() works.
|
||||
assertSessionResultSuccess(
|
||||
controller.prepare(), PLAYER_STATE_CHANGE_OVER_SESSION_WAIT_TIME_MS);
|
||||
|
||||
assertThat((float) sessionPlayerConnector.getDuration())
|
||||
.isWithin(tolerance)
|
||||
.of(testDuration);
|
||||
assertSessionResultSuccess(
|
||||
controller.seekTo(testSeekPosition), PLAYER_STATE_CHANGE_OVER_SESSION_WAIT_TIME_MS);
|
||||
assertThat((float) sessionPlayerConnector.getCurrentPosition())
|
||||
.isWithin(tolerance)
|
||||
.of(testSeekPosition);
|
||||
|
||||
// Test rewind
|
||||
assertSessionResultSuccess(
|
||||
controller.rewind(), PLAYER_STATE_CHANGE_OVER_SESSION_WAIT_TIME_MS);
|
||||
assertThat((float) sessionPlayerConnector.getCurrentPosition())
|
||||
.isWithin(tolerance)
|
||||
.of(testSeekPosition - testRewindIncrementMs);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@LargeTest
|
||||
@Test
|
||||
public void setFastForwardIncrementMs_withPositiveFastForwardIncrement_fastsForward()
|
||||
throws Exception {
|
||||
int testResId = R.raw.video_big_buck_bunny;
|
||||
int testDuration = 10_000;
|
||||
int tolerance = 100;
|
||||
int testSeekPosition = 2_000;
|
||||
int testFastForwardIncrementMs = 300;
|
||||
|
||||
TestUtils.loadResource(testResId, sessionPlayerConnector);
|
||||
|
||||
// seekTo() sometimes takes couple of seconds. Disable default timeout behavior.
|
||||
try (MediaSession session =
|
||||
createMediaSession(
|
||||
sessionPlayerConnector,
|
||||
new SessionCallbackBuilder(context, sessionPlayerConnector)
|
||||
.setFastForwardIncrementMs(testFastForwardIncrementMs)
|
||||
.setSeekTimeoutMs(0)
|
||||
.build())) {
|
||||
try (MediaController controller = createConnectedController(session)) {
|
||||
// Prepare first to ensure that seek() works.
|
||||
assertSessionResultSuccess(
|
||||
controller.prepare(), PLAYER_STATE_CHANGE_OVER_SESSION_WAIT_TIME_MS);
|
||||
|
||||
assertThat((float) sessionPlayerConnector.getDuration())
|
||||
.isWithin(tolerance)
|
||||
.of(testDuration);
|
||||
assertSessionResultSuccess(
|
||||
controller.seekTo(testSeekPosition), PLAYER_STATE_CHANGE_OVER_SESSION_WAIT_TIME_MS);
|
||||
assertThat((float) sessionPlayerConnector.getCurrentPosition())
|
||||
.isWithin(tolerance)
|
||||
.of(testSeekPosition);
|
||||
|
||||
// Test fast-forward
|
||||
assertSessionResultSuccess(
|
||||
controller.fastForward(), PLAYER_STATE_CHANGE_OVER_SESSION_WAIT_TIME_MS);
|
||||
assertThat((float) sessionPlayerConnector.getCurrentPosition())
|
||||
.isWithin(tolerance)
|
||||
.of(testSeekPosition + testFastForwardIncrementMs);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void setMediaItemProvider_withMediaItemProvider_receivesOnCreateMediaItem()
|
||||
throws Exception {
|
||||
Uri testMediaUri = RawResourceDataSource.buildRawResourceUri(R.raw.audio);
|
||||
|
||||
CountDownLatch providerLatch = new CountDownLatch(1);
|
||||
SessionCallbackBuilder.MediaIdMediaItemProvider mediaIdMediaItemProvider =
|
||||
new SessionCallbackBuilder.MediaIdMediaItemProvider();
|
||||
SessionCallbackBuilder.MediaItemProvider provider =
|
||||
(session, controllerInfo, mediaId) -> {
|
||||
assertThat(mediaId).isEqualTo(testMediaUri.toString());
|
||||
providerLatch.countDown();
|
||||
return mediaIdMediaItemProvider.onCreateMediaItem(session, controllerInfo, mediaId);
|
||||
};
|
||||
|
||||
CountDownLatch currentMediaItemChangedLatch = new CountDownLatch(1);
|
||||
sessionPlayerConnector.registerPlayerCallback(
|
||||
executor,
|
||||
new SessionPlayer.PlayerCallback() {
|
||||
@Override
|
||||
public void onCurrentMediaItemChanged(
|
||||
@NonNull SessionPlayer player, @NonNull MediaItem item) {
|
||||
MediaMetadata metadata = item.getMetadata();
|
||||
assertThat(metadata.getString(MediaMetadata.METADATA_KEY_MEDIA_ID))
|
||||
.isEqualTo(testMediaUri.toString());
|
||||
currentMediaItemChangedLatch.countDown();
|
||||
}
|
||||
});
|
||||
|
||||
try (MediaSession session =
|
||||
createMediaSession(
|
||||
sessionPlayerConnector,
|
||||
new SessionCallbackBuilder(context, sessionPlayerConnector)
|
||||
.setMediaItemProvider(provider)
|
||||
.build())) {
|
||||
try (MediaController controller = createConnectedController(session)) {
|
||||
assertSessionResultSuccess(
|
||||
controller.setMediaItem(testMediaUri.toString()),
|
||||
PLAYER_STATE_CHANGE_OVER_SESSION_WAIT_TIME_MS);
|
||||
assertThat(providerLatch.await(0, MILLISECONDS)).isTrue();
|
||||
assertThat(
|
||||
currentMediaItemChangedLatch.await(CONTROLLER_COMMAND_WAIT_TIME_MS, MILLISECONDS))
|
||||
.isTrue();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void setSkipCallback_withSkipBackward_receivesOnSkipBackward() throws Exception {
|
||||
CountDownLatch skipBackwardCalledLatch = new CountDownLatch(1);
|
||||
SessionCallbackBuilder.SkipCallback skipCallback =
|
||||
new SessionCallbackBuilder.SkipCallback() {
|
||||
@Override
|
||||
public int onSkipBackward(
|
||||
MediaSession session, MediaSession.ControllerInfo controllerInfo) {
|
||||
skipBackwardCalledLatch.countDown();
|
||||
return SessionResult.RESULT_SUCCESS;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int onSkipForward(
|
||||
MediaSession session, MediaSession.ControllerInfo controllerInfo) {
|
||||
return SessionResult.RESULT_ERROR_NOT_SUPPORTED;
|
||||
}
|
||||
};
|
||||
try (MediaSession session =
|
||||
createMediaSession(
|
||||
sessionPlayerConnector,
|
||||
new SessionCallbackBuilder(context, sessionPlayerConnector)
|
||||
.setSkipCallback(skipCallback)
|
||||
.build())) {
|
||||
try (MediaController controller = createConnectedController(session)) {
|
||||
assertSessionResultSuccess(controller.skipBackward(), CONTROLLER_COMMAND_WAIT_TIME_MS);
|
||||
assertThat(skipBackwardCalledLatch.await(0, MILLISECONDS)).isTrue();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void setSkipCallback_withSkipForward_receivesOnSkipForward() throws Exception {
|
||||
CountDownLatch skipForwardCalledLatch = new CountDownLatch(1);
|
||||
SessionCallbackBuilder.SkipCallback skipCallback =
|
||||
new SessionCallbackBuilder.SkipCallback() {
|
||||
@Override
|
||||
public int onSkipBackward(
|
||||
MediaSession session, MediaSession.ControllerInfo controllerInfo) {
|
||||
return SessionResult.RESULT_ERROR_NOT_SUPPORTED;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int onSkipForward(
|
||||
MediaSession session, MediaSession.ControllerInfo controllerInfo) {
|
||||
skipForwardCalledLatch.countDown();
|
||||
return SessionResult.RESULT_SUCCESS;
|
||||
}
|
||||
};
|
||||
try (MediaSession session =
|
||||
createMediaSession(
|
||||
sessionPlayerConnector,
|
||||
new SessionCallbackBuilder(context, sessionPlayerConnector)
|
||||
.setSkipCallback(skipCallback)
|
||||
.build())) {
|
||||
try (MediaController controller = createConnectedController(session)) {
|
||||
assertSessionResultSuccess(controller.skipForward(), CONTROLLER_COMMAND_WAIT_TIME_MS);
|
||||
assertThat(skipForwardCalledLatch.await(0, MILLISECONDS)).isTrue();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void setPostConnectCallback_afterConnect_receivesOnPostConnect() throws Exception {
|
||||
CountDownLatch postConnectLatch = new CountDownLatch(1);
|
||||
SessionCallbackBuilder.PostConnectCallback postConnectCallback =
|
||||
(session, controllerInfo) -> postConnectLatch.countDown();
|
||||
try (MediaSession session =
|
||||
createMediaSession(
|
||||
sessionPlayerConnector,
|
||||
new SessionCallbackBuilder(context, sessionPlayerConnector)
|
||||
.setPostConnectCallback(postConnectCallback)
|
||||
.build())) {
|
||||
try (MediaController controller = createConnectedController(session)) {
|
||||
assertThat(postConnectLatch.await(CONTROLLER_COMMAND_WAIT_TIME_MS, MILLISECONDS)).isTrue();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void setDisconnectedCallback_afterDisconnect_receivesOnDisconnected() throws Exception {
|
||||
CountDownLatch disconnectedLatch = new CountDownLatch(1);
|
||||
SessionCallbackBuilder.DisconnectedCallback disconnectCallback =
|
||||
(session, controllerInfo) -> disconnectedLatch.countDown();
|
||||
try (MediaSession session =
|
||||
createMediaSession(
|
||||
sessionPlayerConnector,
|
||||
new SessionCallbackBuilder(context, sessionPlayerConnector)
|
||||
.setDisconnectedCallback(disconnectCallback)
|
||||
.build())) {
|
||||
try (MediaController controller = createConnectedController(session)) {}
|
||||
assertThat(disconnectedLatch.await(CONTROLLER_COMMAND_WAIT_TIME_MS, MILLISECONDS)).isTrue();
|
||||
}
|
||||
}
|
||||
|
||||
private MediaSession createMediaSession(
|
||||
SessionPlayer sessionPlayer, MediaSession.SessionCallback callback) {
|
||||
return new MediaSession.Builder(context, sessionPlayer)
|
||||
.setSessionCallback(executor, callback)
|
||||
.setId(MEDIA_SESSION_ID)
|
||||
.build();
|
||||
}
|
||||
|
||||
private MediaController createConnectedController(MediaSession session) throws Exception {
|
||||
return createConnectedController(session, null, null);
|
||||
}
|
||||
|
||||
private MediaController createConnectedController(
|
||||
MediaSession session,
|
||||
OnConnectedListener onConnectedListener,
|
||||
OnAllowedCommandsChangedListener onAllowedCommandsChangedListener)
|
||||
throws Exception {
|
||||
CountDownLatch latch = new CountDownLatch(1);
|
||||
MediaController.ControllerCallback callback =
|
||||
new MediaController.ControllerCallback() {
|
||||
@Override
|
||||
public void onAllowedCommandsChanged(
|
||||
@NonNull MediaController controller, @NonNull SessionCommandGroup commands) {
|
||||
if (onAllowedCommandsChangedListener != null) {
|
||||
onAllowedCommandsChangedListener.onAllowedCommandsChanged(controller, commands);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onConnected(
|
||||
@NonNull MediaController controller, @NonNull SessionCommandGroup allowedCommands) {
|
||||
if (onConnectedListener != null) {
|
||||
onConnectedListener.onConnected(controller, allowedCommands);
|
||||
}
|
||||
latch.countDown();
|
||||
}
|
||||
};
|
||||
MediaController controller =
|
||||
new MediaController.Builder(context)
|
||||
.setSessionToken(session.getToken())
|
||||
.setControllerCallback(ContextCompat.getMainExecutor(context), callback)
|
||||
.build();
|
||||
latch.await();
|
||||
return controller;
|
||||
}
|
||||
|
||||
private static void assertSessionResultSuccess(Future<SessionResult> future) throws Exception {
|
||||
assertSessionResultSuccess(future, CONTROLLER_COMMAND_WAIT_TIME_MS);
|
||||
}
|
||||
|
||||
private static void assertSessionResultSuccess(Future<SessionResult> future, long timeoutMs)
|
||||
throws Exception {
|
||||
SessionResult result = future.get(timeoutMs, MILLISECONDS);
|
||||
assertThat(result.getResultCode()).isEqualTo(SessionResult.RESULT_SUCCESS);
|
||||
}
|
||||
|
||||
private static void assertSessionResultFailure(Future<SessionResult> future) throws Exception {
|
||||
SessionResult result = future.get(PLAYER_STATE_CHANGE_OVER_SESSION_WAIT_TIME_MS, MILLISECONDS);
|
||||
assertThat(result.getResultCode()).isNotEqualTo(SessionResult.RESULT_SUCCESS);
|
||||
}
|
||||
|
||||
private static void assertAllowedCommands(
|
||||
List<Integer> expectedAllowedCommandsCode, SessionCommandGroup allowedCommands) {
|
||||
for (int commandCode : expectedAllowedCommandsCode) {
|
||||
assertWithMessage("Command should be allowed, code=" + commandCode)
|
||||
.that(allowedCommands.hasCommand(commandCode))
|
||||
.isTrue();
|
||||
}
|
||||
}
|
||||
|
||||
private static void assertDisallowedCommands(
|
||||
List<Integer> expectedDisallowedCommandsCode, SessionCommandGroup allowedCommands) {
|
||||
for (int commandCode : expectedDisallowedCommandsCode) {
|
||||
assertWithMessage("Command shouldn't be allowed, code=" + commandCode)
|
||||
.that(allowedCommands.hasCommand(commandCode))
|
||||
.isFalse();
|
||||
}
|
||||
}
|
||||
|
||||
private interface OnAllowedCommandsChangedListener {
|
||||
void onAllowedCommandsChanged(MediaController controller, SessionCommandGroup allowedCommands);
|
||||
}
|
||||
|
||||
private interface OnConnectedListener {
|
||||
void onConnected(MediaController controller, SessionCommandGroup allowedCommands);
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,80 @@
|
|||
/*
|
||||
* Copyright 2019 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.google.android.exoplayer2.ext.media2;
|
||||
|
||||
import static androidx.media2.common.SessionPlayer.PlayerResult.RESULT_SUCCESS;
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static java.util.concurrent.TimeUnit.MILLISECONDS;
|
||||
|
||||
import androidx.media2.common.MediaItem;
|
||||
import androidx.media2.common.MediaMetadata;
|
||||
import androidx.media2.common.SessionPlayer;
|
||||
import androidx.media2.common.SessionPlayer.PlayerResult;
|
||||
import androidx.media2.common.UriMediaItem;
|
||||
import com.google.android.exoplayer2.ext.media2.test.R;
|
||||
import com.google.android.exoplayer2.upstream.RawResourceDataSource;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.Future;
|
||||
|
||||
/** Utilities for tests. */
|
||||
/* package */ final class TestUtils {
|
||||
private static final long PLAYER_STATE_CHANGE_WAIT_TIME_MS = 5_000;
|
||||
|
||||
public static UriMediaItem createMediaItem() {
|
||||
return createMediaItem(R.raw.video_desks);
|
||||
}
|
||||
|
||||
public static UriMediaItem createMediaItem(int resId) {
|
||||
MediaMetadata metadata =
|
||||
new MediaMetadata.Builder()
|
||||
.putString(MediaMetadata.METADATA_KEY_MEDIA_ID, Integer.toString(resId))
|
||||
.build();
|
||||
return new UriMediaItem.Builder(RawResourceDataSource.buildRawResourceUri(resId))
|
||||
.setMetadata(metadata)
|
||||
.build();
|
||||
}
|
||||
|
||||
public static List<MediaItem> createPlaylist(int size) {
|
||||
List<MediaItem> items = new ArrayList<>();
|
||||
for (int i = 0; i < size; ++i) {
|
||||
items.add(createMediaItem());
|
||||
}
|
||||
return items;
|
||||
}
|
||||
|
||||
public static void loadResource(int resId, SessionPlayer sessionPlayer) throws Exception {
|
||||
MediaItem mediaItem = createMediaItem(resId);
|
||||
assertPlayerResultSuccess(sessionPlayer.setMediaItem(mediaItem));
|
||||
}
|
||||
|
||||
public static void assertPlayerResultSuccess(Future<PlayerResult> future) throws Exception {
|
||||
assertPlayerResult(future, RESULT_SUCCESS);
|
||||
}
|
||||
|
||||
public static void assertPlayerResult(
|
||||
Future<PlayerResult> future, /* @PlayerResult.ResultCode */ int playerResult)
|
||||
throws Exception {
|
||||
assertThat(future).isNotNull();
|
||||
PlayerResult result = future.get(PLAYER_STATE_CHANGE_WAIT_TIME_MS, MILLISECONDS);
|
||||
assertThat(result).isNotNull();
|
||||
assertThat(result.getResultCode()).isEqualTo(playerResult);
|
||||
}
|
||||
|
||||
private TestUtils() {
|
||||
// Prevent from instantiation.
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue