Merge remote-tracking branch 'upstream/dev-v2' into dev-v2

This commit is contained in:
Will 2020-09-19 02:23:33 +08:00
commit 8bed008934
1610 changed files with 55036 additions and 20120 deletions

View file

@ -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

View file

@ -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 Androids MediaPlayer API for playing audio and video both

View file

@ -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.

View file

@ -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

View 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
}

View file

@ -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'

View file

@ -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')

View file

@ -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.

View file

@ -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.

View file

@ -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'

View file

@ -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 {}

View file

@ -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);

View file

@ -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;

View file

@ -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

View file

@ -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
}

View file

@ -32,4 +32,3 @@ void main() {
gl_FragColor = videoColor * (1.0 - overlayColor.a)
+ overlayColor * overlayColor.a;
}

View file

@ -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;
}

View file

@ -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;

View file

@ -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();
}

View file

@ -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;

View file

@ -27,4 +27,3 @@
app:surface_type="none"/>
</FrameLayout>

View file

@ -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.

View file

@ -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'

View file

@ -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);
}

View file

@ -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"

View file

@ -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"
}
]
}

View file

@ -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);
}
}

View file

@ -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));

View file

@ -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() {}
}

View file

@ -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));
}
}
}
}

View file

@ -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]));
}
}

View file

@ -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;
}
}

View file

@ -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));
}

View file

@ -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

View file

@ -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;

View file

@ -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"

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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

View file

@ -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();
}

View file

@ -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;

View file

@ -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

View file

@ -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 {

View file

@ -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.

View file

@ -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);

View file

@ -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})

View file

@ -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

View file

@ -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();

View file

@ -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,

View file

@ -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();
}
}

View file

@ -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;
}

View file

@ -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')

View file

@ -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);

View file

@ -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;
}

View file

@ -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.

View file

@ -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 {

View file

@ -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;

View file

@ -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

View file

@ -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 {

View file

@ -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

View file

@ -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;
}
}
}

View file

@ -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);
}

View file

@ -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(

View file

@ -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)

View file

@ -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

View 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})

View file

@ -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 \

View file

@ -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;
}

View file

@ -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);
}
}

View file

@ -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 {

View file

@ -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 {

View file

@ -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");
}
}

View file

@ -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();

View file

@ -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);
}

View file

@ -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

View file

@ -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,

View file

@ -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);
}
}

View file

@ -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);
}

View file

@ -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')

View file

@ -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 ##

View file

@ -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
}

View file

@ -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();
}
});
}

View file

@ -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);
}
}

View file

@ -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(

View file

@ -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 ##

View file

@ -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')

View file

@ -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;

View file

@ -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')

View file

@ -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();
}

View 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

View 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'

View 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>

View file

@ -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();
}
}

View file

@ -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();
}
}

View file

@ -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();
}
}
}

View file

@ -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);
}
}

View file

@ -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