mirror of
https://github.com/samsonjs/media.git
synced 2026-04-24 14:37:45 +00:00
Merge remote-tracking branch 'origin/dev-v2' into vorbis-comments
Line up this branch with the dev branch instead of the release branch.
This commit is contained in:
commit
49967483f6
301 changed files with 13931 additions and 6276 deletions
203
RELEASENOTES.md
203
RELEASENOTES.md
|
|
@ -1,5 +1,107 @@
|
|||
# Release notes
|
||||
|
||||
### dev-v2 (not yet released)
|
||||
|
||||
* Core library:
|
||||
* Support preferred video role flags in track selection
|
||||
((#9402)[https://github.com/google/ExoPlayer/issues/9402]).
|
||||
* Prefer audio content preferences (for example, "default" audio track or
|
||||
track matching system Locale language) over technical track selection
|
||||
constraints (for example, preferred MIME type, or maximum channel
|
||||
count).
|
||||
* Prohibit duplicate `TrackGroup`s in a `TrackGroupArray`. `TrackGroup`s
|
||||
can always be made distinguishable by setting an `id` in the
|
||||
`TrackGroup` constructor. This fixes a crash when resuming playback
|
||||
after backgrounding the app with an active track override
|
||||
((#9718)[https://github.com/google/ExoPlayer/issues/9718]).
|
||||
* Sleep and retry when creating a `MediaCodec` instance fails. This works
|
||||
around an issue that occurs on some devices when switching a surface
|
||||
from a secure codec to another codec
|
||||
(#8696)[https://github.com/google/ExoPlayer/issues/8696].
|
||||
* Add `MediaCodecAdapter.getMetrics()` to allow users obtain metrics data
|
||||
from `MediaCodec`.
|
||||
([#9766](https://github.com/google/ExoPlayer/issues/9766)).
|
||||
* Amend logic in `AdaptiveTrackSelection` to allow a quality increase
|
||||
under sufficient network bandwidth even if playback is very close to the
|
||||
live edge ((#9784)[https://github.com/google/ExoPlayer/issues/9784]).
|
||||
* Fix Maven dependency resolution
|
||||
((#8353)[https://github.com/google/ExoPlayer/issues/8353]).
|
||||
* Android 12 compatibility:
|
||||
* Upgrade the Cast extension to depend on
|
||||
`com.google.android.gms:play-services-cast-framework:20.1.0`. Earlier
|
||||
versions of `play-services-cast-framework` are not compatible with apps
|
||||
targeting Android 12, and will crash with an `IllegalArgumentException`
|
||||
when creating `PendingIntent`s
|
||||
([#9528](https://github.com/google/ExoPlayer/issues/9528)).
|
||||
* Audio:
|
||||
* Add a `Builder` to `DefaultAudioSink` and deprecate the existing
|
||||
constructors.
|
||||
* Change `AudioCapabilities` APIs to require passing explicitly
|
||||
`AudioCapabilities.DEFAULT_AUDIO_CAPABILITIES` instead of `null`.
|
||||
* Extractors:
|
||||
* Fix inconsistency with spec in H.265 SPS nal units parsing
|
||||
((#9719)[https://github.com/google/ExoPlayer/issues/9719]).
|
||||
* Text:
|
||||
* Add a `MediaItem.SubtitleConfiguration#id` field which is propagated to
|
||||
the `Format#id` field of the subtitle track created from the
|
||||
configuration
|
||||
((#9673)[https://github.com/google/ExoPlayer/issues/9673]).
|
||||
* Rename `DecoderCounters#inputBufferCount` to `queuedInputBufferCount`.
|
||||
* DRM:
|
||||
* Remove `playbackLooper` from `DrmSessionManager.(pre)acquireSession`.
|
||||
When a `DrmSessionManager` is used by an app in a custom `MediaSource`,
|
||||
the `playbackLooper` needs to be passed to `DrmSessionManager.setPlayer`
|
||||
instead.
|
||||
* IMA:
|
||||
* Add a method to `AdPlaybackState` to allow resetting an ad group so that
|
||||
it can be played again
|
||||
([#9615](https://github.com/google/ExoPlayer/issues/9615)).
|
||||
* DASH:
|
||||
* Support the `forced-subtitle` track role
|
||||
([#9727](https://github.com/google/ExoPlayer/issues/9727)).
|
||||
* Stop interpreting the `main` track role as `C.SELECTION_FLAG_DEFAULT`.
|
||||
* HLS:
|
||||
* Use chunkless preparation by default to improve start up time. If your
|
||||
renditions contain muxed closed-caption tracks that are *not* declared
|
||||
in the master playlist, you should add them to the master playlist to be
|
||||
available for playback, or turn off chunkless preparation with
|
||||
`HlsMediaSource.Factory.setAllowChunklessPreparation(false)`.
|
||||
* Support key-frame accurate seeking in HLS
|
||||
([#2882](https://github.com/google/ExoPlayer/issues/2882)).
|
||||
* Correctly populate `Format.label` for audio only HLS streams
|
||||
([#9608](https://github.com/google/ExoPlayer/issues/9608)).
|
||||
* Timestamp adjuster initialization occurs after opening the `DataSource`
|
||||
([#9777](https://github.com/google/ExoPlayer/pull/9777)).
|
||||
* UI:
|
||||
* Fix the color of the numbers in `StyledPlayerView` rewind and
|
||||
fastforward buttons when using certain themes
|
||||
([#9765](https://github.com/google/ExoPlayer/issues/9765)).
|
||||
* Transformer:
|
||||
* Increase required min API version to 21.
|
||||
* `TransformationException` is now used to describe errors that occur
|
||||
during a transformation.
|
||||
* Add `TransformationRequest` for specifying the transformation options.
|
||||
* MediaSession extension:
|
||||
* Remove deprecated call to `onStop(/* reset= */ true)` and provide an
|
||||
opt-out flag for apps that don't want to clear the playlist on stop.
|
||||
* RTSP:
|
||||
* Provide a client API to override the `SocketFactory` used for any server
|
||||
connection ([#9606](https://github.com/google/ExoPlayer/pull/9606)).
|
||||
* Prefers DIGEST authentication method over BASIC if both are present.
|
||||
([#9800](https://github.com/google/ExoPlayer/issues/9800)).
|
||||
* Cast extension
|
||||
* Fix bug that prevented `CastPlayer` from calling `onIsPlayingChanged`
|
||||
correctly.
|
||||
* Remove deprecated symbols:
|
||||
* Remove `MediaSourceFactory#setDrmSessionManager`,
|
||||
`MediaSourceFactory#setDrmHttpDataSourceFactory`, and
|
||||
`MediaSourceFactory#setDrmUserAgent`. Use
|
||||
`MediaSourceFactory#setDrmSessionManagerProvider` instead.
|
||||
* Remove `MediaSourceFactory#setStreamKeys`. Use
|
||||
`MediaItem.Builder#setStreamKeys` instead.
|
||||
* Remove `MediaSourceFactory#createMediaSource(Uri)`. Use
|
||||
`MediaSourceFactory#createMediaSource(MediaItem)` instead.
|
||||
|
||||
### 2.16.1 (2021-11-18)
|
||||
|
||||
* Core Library:
|
||||
|
|
@ -522,8 +624,8 @@
|
|||
* The most used methods of `Player`'s audio, video, text and metadata
|
||||
components have been added directly to `Player`.
|
||||
* Add `Player.getAvailableCommands`, `Player.isCommandAvailable` and
|
||||
`Listener.onAvailableCommandsChanged` to query which commands
|
||||
that can be executed on the player.
|
||||
`Listener.onAvailableCommandsChanged` to query which commands that can
|
||||
be executed on the player.
|
||||
* Add a `Player.Listener` interface to receive all player events.
|
||||
Component listeners and `EventListener` have been deprecated.
|
||||
* Add `Player.getMediaMetadata`, which returns a combined and structured
|
||||
|
|
@ -532,8 +634,8 @@
|
|||
* `Player.setPlaybackParameters` no longer accepts null, use
|
||||
`PlaybackParameters.DEFAULT` instead.
|
||||
* Report information about the old and the new playback positions to
|
||||
`Listener.onPositionDiscontinuity`. Add `DISCONTINUITY_REASON_SKIP`
|
||||
and `DISCONTINUITY_REASON_REMOVE` as discontinuity reasons, and rename
|
||||
`Listener.onPositionDiscontinuity`. Add `DISCONTINUITY_REASON_SKIP` and
|
||||
`DISCONTINUITY_REASON_REMOVE` as discontinuity reasons, and rename
|
||||
`DISCONTINUITY_REASON_PERIOD_TRANSITION` to
|
||||
`DISCONTINUITY_REASON_AUTO_TRANSITION`. Remove
|
||||
`DISCONTINUITY_REASON_AD_INSERTION`, for which
|
||||
|
|
@ -588,8 +690,8 @@
|
|||
dispatched for each track in each period.
|
||||
* Include the session state in DRM session-acquired listener methods.
|
||||
* UI:
|
||||
* Add `PlayerNotificationManager.Builder`, with the ability to
|
||||
specify which group the notification should belong to.
|
||||
* Add `PlayerNotificationManager.Builder`, with the ability to specify
|
||||
which group the notification should belong to.
|
||||
* Remove `setUseSensorRotation` from `PlayerView` and `StyledPlayerView`.
|
||||
Instead, cast the view returned by `getVideoSurfaceView` to
|
||||
`SphericalGLSurfaceView`, and then call `setUseSensorRotation` on the
|
||||
|
|
@ -661,7 +763,8 @@
|
|||
|
||||
### 2.13.3 (2021-04-14)
|
||||
|
||||
* Published via the Google Maven repository (i.e., google()) rather than JCenter.
|
||||
* Published via the Google Maven repository (i.e., google()) rather than
|
||||
JCenter.
|
||||
* Core:
|
||||
* Reset playback speed when live playback speed control becomes unused
|
||||
([#8664](https://github.com/google/ExoPlayer/issues/8664)).
|
||||
|
|
@ -816,8 +919,8 @@
|
|||
* Remove `Player.setVideoDecoderOutputBufferRenderer` from Player API. Use
|
||||
`setVideoSurfaceView` and `clearVideoSurfaceView` instead.
|
||||
* Default `SingleSampleMediaSource.treatLoadErrorsAsEndOfStream` to `true`
|
||||
so that errors loading external subtitle files do not cause playback
|
||||
to fail ([#8430](https://github.com/google/ExoPlayer/issues/8430)). A
|
||||
so that errors loading external subtitle files do not cause playback to
|
||||
fail ([#8430](https://github.com/google/ExoPlayer/issues/8430)). A
|
||||
warning will be logged by `SingleSampleMediaPeriod` whenever a load
|
||||
error is treated as though the end of the stream has been reached.
|
||||
* Time out on release to prevent ANRs if an underlying platform call is
|
||||
|
|
@ -898,9 +1001,8 @@
|
|||
([#7847](https://github.com/google/ExoPlayer/issues/7847)).
|
||||
* Drop key and provision responses if `DefaultDrmSession` is released
|
||||
while waiting for the response. This prevents harmless log messages of
|
||||
the form:
|
||||
`IllegalStateException: sending message to a Handler on a dead thread`
|
||||
([#8328](https://github.com/google/ExoPlayer/issues/8328)).
|
||||
the form: `IllegalStateException: sending message to a Handler on a dead
|
||||
thread` ([#8328](https://github.com/google/ExoPlayer/issues/8328)).
|
||||
* Allow apps to fully customize DRM behaviour for each `MediaItem` by
|
||||
passing a `DrmSessionManagerProvider` to `MediaSourceFactory`
|
||||
([#8466](https://github.com/google/ExoPlayer/issues/8466)).
|
||||
|
|
@ -915,8 +1017,8 @@
|
|||
existing decoder instance for the new format, and if not then the
|
||||
reasons why.
|
||||
* Video:
|
||||
* Fall back to AVC/HEVC decoders for Dolby Vision streams with level 10
|
||||
to 13 ([#8530](https://github.com/google/ExoPlayer/issues/8530)).
|
||||
* Fall back to AVC/HEVC decoders for Dolby Vision streams with level 10 to
|
||||
13 ([#8530](https://github.com/google/ExoPlayer/issues/8530)).
|
||||
* Fix VP9 format capability checks on API level 23 and earlier. The
|
||||
platform does not correctly report the VP9 level supported by the
|
||||
decoder in this case, so we estimate it based on the decoder's maximum
|
||||
|
|
@ -998,8 +1100,8 @@
|
|||
* `ExtractorsMediaSource.Factory.setMinLoadableRetryCount(int)`. Use
|
||||
`ExtractorsMediaSource.Factory.setLoadErrorHandlingPolicy(LoadErrorHandlingPolicy)`
|
||||
instead.
|
||||
* `FixedTrackSelection.Factory`. If you need to disable adaptive
|
||||
selection in `DefaultTrackSelector`, enable the
|
||||
* `FixedTrackSelection.Factory`. If you need to disable adaptive selection
|
||||
in `DefaultTrackSelector`, enable the
|
||||
`DefaultTrackSelector.Parameters.forceHighestSupportedBitrate` flag.
|
||||
* `HlsMediaSource.Factory.setMinLoadableRetryCount(int)`. Use
|
||||
`HlsMediaSource.Factory.setLoadErrorHandlingPolicy(LoadErrorHandlingPolicy)`
|
||||
|
|
@ -1012,8 +1114,8 @@
|
|||
`MappedTrackInfo.getUnmappedTrackGroups()` instead.
|
||||
* `MappedTrackInfo.length`. Use `MappedTrackInfo.getRendererCount()`
|
||||
instead.
|
||||
* `Player.DefaultEventListener.onTimelineChanged(Timeline, Object)`.
|
||||
Use `Player.EventListener.onTimelineChanged(Timeline, int)` instead.
|
||||
* `Player.DefaultEventListener.onTimelineChanged(Timeline, Object)`. Use
|
||||
`Player.EventListener.onTimelineChanged(Timeline, int)` instead.
|
||||
* `Player.setAudioAttributes(AudioAttributes)`. Use
|
||||
`Player.AudioComponent.setAudioAttributes(AudioAttributes, boolean)`
|
||||
instead.
|
||||
|
|
@ -1029,8 +1131,8 @@
|
|||
`SimpleExoPlayer.removeVideoListener(VideoListener)` instead.
|
||||
* `SimpleExoPlayer.getAudioStreamType()`. Use
|
||||
`SimpleExoPlayer.getAudioAttributes()` instead.
|
||||
* `SimpleExoPlayer.setAudioDebugListener(AudioRendererEventListener)`.
|
||||
Use `SimpleExoPlayer.addAnalyticsListener(AnalyticsListener)` instead.
|
||||
* `SimpleExoPlayer.setAudioDebugListener(AudioRendererEventListener)`. Use
|
||||
`SimpleExoPlayer.addAnalyticsListener(AnalyticsListener)` instead.
|
||||
* `SimpleExoPlayer.setAudioStreamType(int)`. Use
|
||||
`SimpleExoPlayer.setAudioAttributes(AudioAttributes)` instead.
|
||||
* `SimpleExoPlayer.setMetadataOutput(MetadataOutput)`. Use
|
||||
|
|
@ -1041,12 +1143,11 @@
|
|||
* `SimpleExoPlayer.setPlaybackParams(PlaybackParams)`. Use
|
||||
`SimpleExoPlayer.setPlaybackParameters(PlaybackParameters)` instead.
|
||||
* `SimpleExoPlayer.setTextOutput(TextOutput)`. Use
|
||||
`SimpleExoPlayer.addTextOutput(TextOutput)` instead. If your
|
||||
application is calling `SimpleExoPlayer.setTextOutput(null)`, make sure
|
||||
to replace this call with a call to
|
||||
`SimpleExoPlayer.removeTextOutput(TextOutput)`.
|
||||
* `SimpleExoPlayer.setVideoDebugListener(VideoRendererEventListener)`.
|
||||
Use `SimpleExoPlayer.addAnalyticsListener(AnalyticsListener)` instead.
|
||||
`SimpleExoPlayer.addTextOutput(TextOutput)` instead. If your application
|
||||
is calling `SimpleExoPlayer.setTextOutput(null)`, make sure to replace
|
||||
this call with a call to `SimpleExoPlayer.removeTextOutput(TextOutput)`.
|
||||
* `SimpleExoPlayer.setVideoDebugListener(VideoRendererEventListener)`. Use
|
||||
`SimpleExoPlayer.addAnalyticsListener(AnalyticsListener)` instead.
|
||||
* `SimpleExoPlayer.setVideoListener(VideoListener)`. Use
|
||||
`SimpleExoPlayer.addVideoListener(VideoListener)` instead. If your
|
||||
application is calling `SimpleExoPlayer.setVideoListener(null)`, make
|
||||
|
|
@ -1070,7 +1171,7 @@
|
|||
`SsMediaSource.Factory.setLoadErrorHandlingPolicy(LoadErrorHandlingPolicy)`
|
||||
instead.
|
||||
|
||||
### 2.12.3 (2021-01-13) ###
|
||||
### 2.12.3 (2021-01-13)
|
||||
|
||||
* Core library:
|
||||
* Fix `MediaCodecRenderer` issue where empty streams would fail to play in
|
||||
|
|
@ -1107,7 +1208,7 @@
|
|||
fix a deadlock while creating PlaybackStateCompat internally.
|
||||
([#8011](https://github.com/google/ExoPlayer/issues/8011)).
|
||||
|
||||
### 2.12.2 (2020-12-01) ###
|
||||
### 2.12.2 (2020-12-01)
|
||||
|
||||
* Core library:
|
||||
* Suppress exceptions from registering and unregistering the stream volume
|
||||
|
|
@ -1168,7 +1269,7 @@
|
|||
* Allow to remove all playlist items that makes the player reset
|
||||
([#8047](https://github.com/google/ExoPlayer/issues/8047)).
|
||||
|
||||
### 2.12.1 (2020-10-23) ###
|
||||
### 2.12.1 (2020-10-23)
|
||||
|
||||
* Core library:
|
||||
* Fix issue where `Player.setMediaItems` would ignore its `resetPosition`
|
||||
|
|
@ -1207,7 +1308,7 @@
|
|||
([#8058](https://github.com/google/ExoPlayer/issues/8058)).
|
||||
* Extractors:
|
||||
* MP4:
|
||||
* Add support for `_mp2` boxes
|
||||
* Add support for `_mp2` boxes
|
||||
([#7967](https://github.com/google/ExoPlayer/issues/7967)).
|
||||
* Fix playback of files containing `pcm_alaw` or `pcm_mulaw` audio
|
||||
tracks, by enabling sample rechunking for such tracks.
|
||||
|
|
@ -1243,11 +1344,11 @@
|
|||
([#7961](https://github.com/google/ExoPlayer/issues/7961)).
|
||||
* Fix incorrect truncation of large cue point positions
|
||||
([#8067](https://github.com/google/ExoPlayer/issues/8067)).
|
||||
* Upgrade IMA SDK dependency to 3.20.1. This brings in a fix for
|
||||
companion ads rendering when targeting API 29
|
||||
* Upgrade IMA SDK dependency to 3.20.1. This brings in a fix for companion
|
||||
ads rendering when targeting API 29
|
||||
([#6432](https://github.com/google/ExoPlayer/issues/6432)).
|
||||
|
||||
### 2.12.0 (2020-09-11) ###
|
||||
### 2.12.0 (2020-09-11)
|
||||
|
||||
To learn more about what's new in 2.12, read the corresponding
|
||||
[blog post](https://medium.com/google-exoplayer/exoplayer-2-12-whats-new-e43ef8ff72e7).
|
||||
|
|
@ -1278,8 +1379,7 @@ To learn more about what's new in 2.12, read the corresponding
|
|||
* Remove `PlaybackParameters.skipSilence`, and replace it with
|
||||
`AudioComponent.setSkipSilenceEnabled`. This method is also
|
||||
available on `SimpleExoPlayer`. An
|
||||
`AudioListener.onSkipSilenceEnabledChanged` callback is also
|
||||
added.
|
||||
`AudioListener.onSkipSilenceEnabledChanged` callback is also added.
|
||||
* 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
|
||||
|
|
@ -1607,20 +1707,19 @@ To learn more about what's new in 2.12, read the corresponding
|
|||
* Add support for downloading DRM-protected content using offline Widevine
|
||||
licenses.
|
||||
|
||||
### 2.11.8 (2020-08-25) ###
|
||||
### 2.11.8 (2020-08-25)
|
||||
|
||||
* Fix distorted playback of floating point audio when samples exceed the
|
||||
`[-1, 1]` nominal range.
|
||||
* 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
|
||||
* 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.
|
||||
* 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.
|
||||
|
|
@ -1633,12 +1732,12 @@ To learn more about what's new in 2.12, read the corresponding
|
|||
* 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) ###
|
||||
### 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) ###
|
||||
### 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
|
||||
|
|
@ -1653,14 +1752,14 @@ To learn more about what's new in 2.12, read the corresponding
|
|||
([#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
|
||||
* 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) ###
|
||||
### 2.11.5 (2020-06-05)
|
||||
|
||||
* Improve the smoothness of video playback immediately after starting, seeking
|
||||
or resuming a playback
|
||||
|
|
@ -1668,8 +1767,8 @@ To learn more about what's new in 2.12, read the corresponding
|
|||
* 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
|
||||
* 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`
|
||||
|
|
@ -1701,13 +1800,11 @@ To learn more about what's new in 2.12, read the corresponding
|
|||
([#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.
|
||||
* 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
|
||||
* 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),
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ project.ext {
|
|||
junitVersion = '4.13.2'
|
||||
// Use the same Guava version as the Android repo:
|
||||
// https://cs.android.com/android/platform/superproject/+/master:external/guava/METADATA
|
||||
guavaVersion = '27.1-android'
|
||||
guavaVersion = '31.0.1-android'
|
||||
mockitoVersion = '3.12.4'
|
||||
robolectricVersion = '4.6.1'
|
||||
// Keep this in sync with Google's internal Checker Framework version.
|
||||
|
|
|
|||
|
|
@ -28,7 +28,6 @@ import com.google.android.exoplayer2.Timeline;
|
|||
import com.google.android.exoplayer2.TracksInfo;
|
||||
import com.google.android.exoplayer2.ext.cast.CastPlayer;
|
||||
import com.google.android.exoplayer2.ext.cast.SessionAvailabilityListener;
|
||||
import com.google.android.exoplayer2.ui.PlayerView;
|
||||
import com.google.android.exoplayer2.ui.StyledPlayerControlView;
|
||||
import com.google.android.exoplayer2.ui.StyledPlayerView;
|
||||
import com.google.android.gms.cast.framework.CastContext;
|
||||
|
|
@ -67,7 +66,7 @@ import java.util.ArrayList;
|
|||
*
|
||||
* @param context A {@link Context}.
|
||||
* @param listener A {@link Listener} for queue position changes.
|
||||
* @param playerView The {@link PlayerView} for playback.
|
||||
* @param playerView The {@link StyledPlayerView} for playback.
|
||||
* @param castContext The {@link CastContext}.
|
||||
*/
|
||||
public PlayerManager(
|
||||
|
|
|
|||
|
|
@ -26,7 +26,6 @@ import android.graphics.Paint;
|
|||
import android.graphics.drawable.BitmapDrawable;
|
||||
import android.opengl.GLES20;
|
||||
import android.opengl.GLUtils;
|
||||
import androidx.annotation.Nullable;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.util.GlUtil;
|
||||
import java.io.IOException;
|
||||
|
|
@ -52,8 +51,6 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
|||
private final Canvas overlayCanvas;
|
||||
|
||||
private GlUtil.@MonotonicNonNull Program program;
|
||||
@Nullable private GlUtil.Attribute[] attributes;
|
||||
@Nullable private GlUtil.Uniform[] uniforms;
|
||||
|
||||
private float bitmapScaleX;
|
||||
private float bitmapScaleY;
|
||||
|
|
@ -88,31 +85,24 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
|||
} catch (IOException e) {
|
||||
throw new IllegalStateException(e);
|
||||
}
|
||||
program.use();
|
||||
GlUtil.Attribute[] attributes = program.getAttributes();
|
||||
for (GlUtil.Attribute attribute : attributes) {
|
||||
if (attribute.name.equals("a_position")) {
|
||||
attribute.setBuffer(
|
||||
new float[] {
|
||||
-1, -1, 0, 1,
|
||||
1, -1, 0, 1,
|
||||
-1, 1, 0, 1,
|
||||
1, 1, 0, 1
|
||||
},
|
||||
4);
|
||||
} else if (attribute.name.equals("a_texcoord")) {
|
||||
attribute.setBuffer(
|
||||
new float[] {
|
||||
0, 0, 0, 1,
|
||||
1, 0, 0, 1,
|
||||
0, 1, 0, 1,
|
||||
1, 1, 0, 1
|
||||
},
|
||||
4);
|
||||
}
|
||||
}
|
||||
this.attributes = attributes;
|
||||
this.uniforms = program.getUniforms();
|
||||
program.setBufferAttribute(
|
||||
"a_position",
|
||||
new float[] {
|
||||
-1, -1, 0, 1,
|
||||
1, -1, 0, 1,
|
||||
-1, 1, 0, 1,
|
||||
1, 1, 0, 1
|
||||
},
|
||||
4);
|
||||
program.setBufferAttribute(
|
||||
"a_texcoord",
|
||||
new float[] {
|
||||
0, 0, 0, 1,
|
||||
1, 0, 0, 1,
|
||||
0, 1, 0, 1,
|
||||
1, 1, 0, 1
|
||||
},
|
||||
4);
|
||||
GLES20.glGenTextures(1, textures, 0);
|
||||
GLES20.glBindTexture(GL10.GL_TEXTURE_2D, textures[0]);
|
||||
GLES20.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_MIN_FILTER, GL10.GL_NEAREST);
|
||||
|
|
@ -141,36 +131,22 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
|||
GlUtil.checkGlError();
|
||||
|
||||
// Run the shader program.
|
||||
GlUtil.Uniform[] uniforms = checkNotNull(this.uniforms);
|
||||
GlUtil.Attribute[] attributes = checkNotNull(this.attributes);
|
||||
for (GlUtil.Uniform uniform : uniforms) {
|
||||
switch (uniform.name) {
|
||||
case "tex_sampler_0":
|
||||
uniform.setSamplerTexId(frameTexture, /* unit= */ 0);
|
||||
break;
|
||||
case "tex_sampler_1":
|
||||
uniform.setSamplerTexId(textures[0], /* unit= */ 1);
|
||||
break;
|
||||
case "scaleX":
|
||||
uniform.setFloat(bitmapScaleX);
|
||||
break;
|
||||
case "scaleY":
|
||||
uniform.setFloat(bitmapScaleY);
|
||||
break;
|
||||
case "tex_transform":
|
||||
uniform.setFloats(transformMatrix);
|
||||
break;
|
||||
default: // fall out
|
||||
}
|
||||
}
|
||||
for (GlUtil.Attribute copyExternalAttribute : attributes) {
|
||||
copyExternalAttribute.bind();
|
||||
}
|
||||
for (GlUtil.Uniform copyExternalUniform : uniforms) {
|
||||
copyExternalUniform.bind();
|
||||
}
|
||||
GlUtil.Program program = checkNotNull(this.program);
|
||||
program.setSamplerTexIdUniform("tex_sampler_0", frameTexture, /* unit= */ 0);
|
||||
program.setSamplerTexIdUniform("tex_sampler_1", textures[0], /* unit= */ 1);
|
||||
program.setFloatUniform("scaleX", bitmapScaleX);
|
||||
program.setFloatUniform("scaleY", bitmapScaleY);
|
||||
program.setFloatsUniform("tex_transform", transformMatrix);
|
||||
program.bindAttributesAndUniforms();
|
||||
GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);
|
||||
GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, /* first= */ 0, /* count= */ 4);
|
||||
GlUtil.checkGlError();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void release() {
|
||||
if (program != null) {
|
||||
program.delete();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ import com.google.android.exoplayer2.drm.HttpMediaDrmCallback;
|
|||
import com.google.android.exoplayer2.source.MediaSource;
|
||||
import com.google.android.exoplayer2.source.ProgressiveMediaSource;
|
||||
import com.google.android.exoplayer2.source.dash.DashMediaSource;
|
||||
import com.google.android.exoplayer2.ui.PlayerView;
|
||||
import com.google.android.exoplayer2.ui.StyledPlayerView;
|
||||
import com.google.android.exoplayer2.upstream.DataSource;
|
||||
import com.google.android.exoplayer2.upstream.DefaultDataSource;
|
||||
import com.google.android.exoplayer2.upstream.DefaultHttpDataSource;
|
||||
|
|
@ -61,7 +61,7 @@ public final class MainActivity extends Activity {
|
|||
private static final String DRM_SCHEME_EXTRA = "drm_scheme";
|
||||
private static final String DRM_LICENSE_URL_EXTRA = "drm_license_url";
|
||||
|
||||
@Nullable private PlayerView playerView;
|
||||
@Nullable private StyledPlayerView playerView;
|
||||
@Nullable private VideoProcessingGLSurfaceView videoProcessingGLSurfaceView;
|
||||
|
||||
@Nullable private ExoPlayer player;
|
||||
|
|
@ -161,12 +161,12 @@ public final class MainActivity extends Activity {
|
|||
if (type == C.TYPE_DASH) {
|
||||
mediaSource =
|
||||
new DashMediaSource.Factory(dataSourceFactory)
|
||||
.setDrmSessionManager(drmSessionManager)
|
||||
.setDrmSessionManagerProvider(unusedMediaItem -> drmSessionManager)
|
||||
.createMediaSource(MediaItem.fromUri(uri));
|
||||
} else if (type == C.TYPE_OTHER) {
|
||||
mediaSource =
|
||||
new ProgressiveMediaSource.Factory(dataSourceFactory)
|
||||
.setDrmSessionManager(drmSessionManager)
|
||||
.setDrmSessionManagerProvider(unusedMediaItem -> drmSessionManager)
|
||||
.createMediaSource(MediaItem.fromUri(uri));
|
||||
} else {
|
||||
throw new IllegalStateException();
|
||||
|
|
|
|||
|
|
@ -64,6 +64,9 @@ public final class VideoProcessingGLSurfaceView extends GLSurfaceView {
|
|||
* @param transformMatrix The 4 * 4 transform matrix to be applied to the texture.
|
||||
*/
|
||||
void draw(int frameTexture, long frameTimestampUs, float[] transformMatrix);
|
||||
|
||||
/** Releases any resources associated with this {@link VideoProcessor}. */
|
||||
void release();
|
||||
}
|
||||
|
||||
private static final int EGL_PROTECTED_CONTENT_EXT = 0x32C0;
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@
|
|||
android:layout_height="match_parent"
|
||||
android:keepScreenOn="true">
|
||||
|
||||
<com.google.android.exoplayer2.ui.PlayerView
|
||||
<com.google.android.exoplayer2.ui.StyledPlayerView
|
||||
android:id="@+id/player_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
|
|
|
|||
|
|
@ -237,11 +237,11 @@
|
|||
"uri": "https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_16x9/bipbop_16x9_variant.m3u8"
|
||||
},
|
||||
{
|
||||
"name": "Apple master playlist advanced (TS)",
|
||||
"name": "Apple multivariant playlist advanced (TS)",
|
||||
"uri": "https://devstreaming-cdn.apple.com/videos/streaming/examples/img_bipbop_adv_example_ts/master.m3u8"
|
||||
},
|
||||
{
|
||||
"name": "Apple master playlist advanced (FMP4)",
|
||||
"name": "Apple multivariant playlist advanced (FMP4)",
|
||||
"uri": "https://devstreaming-cdn.apple.com/videos/streaming/examples/img_bipbop_adv_example_fmp4/master.m3u8"
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@ import com.google.android.exoplayer2.mediacodec.MediaCodecRenderer.DecoderInitia
|
|||
import com.google.android.exoplayer2.mediacodec.MediaCodecUtil.DecoderQueryException;
|
||||
import com.google.android.exoplayer2.offline.DownloadRequest;
|
||||
import com.google.android.exoplayer2.source.DefaultMediaSourceFactory;
|
||||
import com.google.android.exoplayer2.source.MediaSourceFactory;
|
||||
import com.google.android.exoplayer2.source.MediaSource;
|
||||
import com.google.android.exoplayer2.source.ads.AdsLoader;
|
||||
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
|
||||
import com.google.android.exoplayer2.ui.StyledPlayerControlView;
|
||||
|
|
@ -234,7 +234,7 @@ public class PlayerActivity extends AppCompatActivity
|
|||
}
|
||||
}
|
||||
|
||||
// PlayerControlView.VisibilityListener implementation
|
||||
// StyledPlayerControlView.VisibilityListener implementation
|
||||
|
||||
@Override
|
||||
public void onVisibilityChange(int visibility) {
|
||||
|
|
@ -261,7 +261,7 @@ public class PlayerActivity extends AppCompatActivity
|
|||
intent.getBooleanExtra(IntentUtil.PREFER_EXTENSION_DECODERS_EXTRA, false);
|
||||
RenderersFactory renderersFactory =
|
||||
DemoUtil.buildRenderersFactory(/* context= */ this, preferExtensionDecoders);
|
||||
MediaSourceFactory mediaSourceFactory =
|
||||
MediaSource.Factory mediaSourceFactory =
|
||||
new DefaultMediaSourceFactory(dataSourceFactory)
|
||||
.setAdsLoaderProvider(this::getAdsLoader)
|
||||
.setAdViewProvider(playerView);
|
||||
|
|
|
|||
|
|
@ -206,12 +206,12 @@ public final class MainActivity extends Activity {
|
|||
if (type == C.TYPE_DASH) {
|
||||
mediaSource =
|
||||
new DashMediaSource.Factory(dataSourceFactory)
|
||||
.setDrmSessionManager(drmSessionManager)
|
||||
.setDrmSessionManagerProvider(unusedMediaItem -> drmSessionManager)
|
||||
.createMediaSource(MediaItem.fromUri(uri));
|
||||
} else if (type == C.TYPE_OTHER) {
|
||||
mediaSource =
|
||||
new ProgressiveMediaSource.Factory(dataSourceFactory)
|
||||
.setDrmSessionManager(drmSessionManager)
|
||||
.setDrmSessionManagerProvider(unusedMediaItem -> drmSessionManager)
|
||||
.createMediaSource(MediaItem.fromUri(uri));
|
||||
} else {
|
||||
throw new IllegalStateException();
|
||||
|
|
|
|||
|
|
@ -46,10 +46,10 @@ MediaItem mediaItem =
|
|||
|
||||
To enable player support for media items that specify ad tags, it's necessary to
|
||||
build and inject a `DefaultMediaSourceFactory` configured with an
|
||||
`AdsLoaderProvider` and an `AdViewProvider` when creating the player:
|
||||
`AdsLoader.Provider` and an `AdViewProvider` when creating the player:
|
||||
|
||||
~~~
|
||||
MediaSourceFactory mediaSourceFactory =
|
||||
MediaSource.Factory mediaSourceFactory =
|
||||
new DefaultMediaSourceFactory(context)
|
||||
.setAdsLoaderProvider(adsLoaderProvider)
|
||||
.setAdViewProvider(playerView);
|
||||
|
|
@ -61,7 +61,7 @@ ExoPlayer player = new ExoPlayer.Builder(context)
|
|||
|
||||
Internally, `DefaultMediaSourceFactory` will wrap the content media source in an
|
||||
`AdsMediaSource`. The `AdsMediaSource` will obtain an `AdsLoader` from the
|
||||
`AdsLoaderProvider` and use it to insert ads as defined by the media item's ad
|
||||
`AdsLoader.Provider` and use it to insert ads as defined by the media item's ad
|
||||
tag.
|
||||
|
||||
ExoPlayer's `StyledPlayerView` and `PlayerView` UI components both implement
|
||||
|
|
|
|||
|
|
@ -14,10 +14,10 @@ Components common to all `ExoPlayer` implementations are:
|
|||
|
||||
* `MediaSource` instances that define media to be played, load the media, and
|
||||
from which the loaded media can be read. `MediaSource` instances are created
|
||||
from `MediaItem`s by a `MediaSourceFactory` inside the player. They can also
|
||||
from `MediaItem`s by a `MediaSource.Factory` inside the player. They can also
|
||||
be passed directly to the player using the [media source based playlist API].
|
||||
* A `MediaSourceFactory` that converts `MediaItem`s to `MediaSource`s. The
|
||||
`MediaSourceFactory` is injected when the player is created.
|
||||
* A `MediaSource.Factory` that converts `MediaItem`s to `MediaSource`s. The
|
||||
`MediaSource.Factory` is injected when the player is created.
|
||||
* `Renderer`s that render individual components of the media. `Renderer`s are
|
||||
injected when the player is created.
|
||||
* A `TrackSelector` that selects tracks provided by the `MediaSource` to be
|
||||
|
|
@ -245,9 +245,9 @@ required. Some use cases for custom implementations are:
|
|||
appropriate if you wish to obtain media samples to feed to renderers in a
|
||||
custom way, or if you wish to implement custom `MediaSource` compositing
|
||||
behavior.
|
||||
* `MediaSourceFactory` – Implementing a custom `MediaSourceFactory` allows
|
||||
an application to customize the way in which `MediaSource`s are created from
|
||||
`MediaItem`s.
|
||||
* `MediaSource.Factory` – Implementing a custom `MediaSource.Factory`
|
||||
allows an application to customize the way in which `MediaSource`s are created
|
||||
from `MediaItem`s.
|
||||
* `DataSource` – ExoPlayer’s upstream package already contains a number of
|
||||
`DataSource` implementations for different use cases. You may want to
|
||||
implement you own `DataSource` class to load data in another way, such as over
|
||||
|
|
|
|||
|
|
@ -394,9 +394,7 @@ When building the `MediaItem`, `MediaItem.playbackProperties.streamKeys` must be
|
|||
set to match those in the `DownloadRequest` so that the player only tries to
|
||||
play the subset of tracks that have been downloaded. Using
|
||||
`Download.request.toMediaItem` and `DownloadRequest.toMediaItem` to build the
|
||||
`MediaItem` will take care of this for you. If building a `MediaSource` to pass
|
||||
directly to the player, it is similarly important to configure the stream keys
|
||||
by calling `MediaSourceFactory.setStreamKeys`.
|
||||
`MediaItem` will take care of this for you.
|
||||
|
||||
If you see data being requested from the network when trying to play downloaded
|
||||
adaptive content, the most likely cause is that the player is trying to adapt to
|
||||
|
|
|
|||
|
|
@ -85,7 +85,7 @@ building the media item.
|
|||
|
||||
If an app wants to customise the `DrmSessionManager` used for playback, they can
|
||||
implement a `DrmSessionManagerProvider` and pass this to the
|
||||
`MediaSourceFactory` which is [used when building the player]. The provider can
|
||||
`MediaSource.Factory` which is [used when building the player]. The provider can
|
||||
choose whether to instantiate a new manager instance each time or not. To always
|
||||
use the same instance:
|
||||
|
||||
|
|
@ -93,7 +93,7 @@ use the same instance:
|
|||
DrmSessionManager customDrmSessionManager =
|
||||
new CustomDrmSessionManager(/* ... */);
|
||||
// Pass a drm session manager provider to the media source factory.
|
||||
MediaSourceFactory mediaSourceFactory =
|
||||
MediaSource.Factory mediaSourceFactory =
|
||||
new DefaultMediaSourceFactory(dataSourceFactory)
|
||||
.setDrmSessionManagerProvider(mediaItem -> customDrmSessionManager);
|
||||
~~~
|
||||
|
|
|
|||
|
|
@ -90,7 +90,7 @@ For more information, see the
|
|||
|
||||
A file that defines the structure and location of media in
|
||||
[adaptive streaming](#adaptive-streaming) protocols. Examples include
|
||||
[DASH](#dash) [MPD](#mpd) files, [HLS](#hls) master playlist files and
|
||||
[DASH](#dash) [MPD](#mpd) files, [HLS](#hls) multivariant playlist files and
|
||||
[Smooth Streaming](#smooth-streaming) manifest files. Not to be confused with an
|
||||
AndroidManifest XML file.
|
||||
|
||||
|
|
|
|||
29
docs/hls.md
29
docs/hls.md
|
|
@ -30,8 +30,8 @@ If your URI doesn't end with `.m3u8`, you can pass `MimeTypes.APPLICATION_M3U8`
|
|||
to `setMimeType` of `MediaItem.Builder` to explicitly indicate the type of the
|
||||
content.
|
||||
|
||||
The URI of the media item may point to either a media playlist or a master
|
||||
playlist. If the URI points to a master playlist that declares multiple
|
||||
The URI of the media item may point to either a media playlist or a multivariant
|
||||
playlist. If the URI points to a multivariant playlist that declares multiple
|
||||
`#EXT-X-STREAM-INF` tags then ExoPlayer will automatically adapt between
|
||||
variants, taking into account both available bandwidth and device capabilities.
|
||||
|
||||
|
|
@ -86,23 +86,29 @@ player.addListener(
|
|||
ExoPlayer provides multiple ways for you to tailor playback experience to your
|
||||
app's needs. See the [Customization page][] for examples.
|
||||
|
||||
### Enabling faster start-up times ###
|
||||
### Disabling chunkless preparation ###
|
||||
|
||||
You can improve HLS start up times noticeably by enabling chunkless preparation.
|
||||
When you enable chunkless preparation and `#EXT-X-STREAM-INF` tags contain the
|
||||
`CODECS` attribute, ExoPlayer will avoid downloading media segments as part of
|
||||
preparation. The following snippet shows how to enable chunkless preparation.
|
||||
By default, ExoPlayer will use chunkless preparation. This means that ExoPlayer
|
||||
will only use the information in the multivariant playlist to prepare the
|
||||
stream, which works if the `#EXT-X-STREAM-INF` tags contain the `CODECS`
|
||||
attribute.
|
||||
|
||||
You may need to disable this feature if your media segments contain muxed
|
||||
closed-caption tracks that are not declared in the multivariant playlist with a
|
||||
`#EXT-X-MEDIA:TYPE=CLOSED-CAPTIONS` tag. Otherwise, these closed-caption tracks
|
||||
won't be detected and played. You can disable chunkless preparation in the
|
||||
`HlsMediaSource.Factory` as shown in the following snippet. Note that this
|
||||
will increase start up time as ExoPlayer needs to download a media segment to
|
||||
discover these additional tracks and it is preferable to declare the
|
||||
closed-caption tracks in the multivariant playlist instead.
|
||||
~~~
|
||||
HlsMediaSource hlsMediaSource =
|
||||
new HlsMediaSource.Factory(dataSourceFactory)
|
||||
.setAllowChunklessPreparation(true)
|
||||
.setAllowChunklessPreparation(false)
|
||||
.createMediaSource(MediaItem.fromUri(hlsUri));
|
||||
~~~
|
||||
{: .language-java}
|
||||
|
||||
You can find more details in our [Medium post about chunkless preparation][].
|
||||
|
||||
## Creating high quality HLS content ##
|
||||
|
||||
In order to get the most out of ExoPlayer, there are certain guidelines you can
|
||||
|
|
@ -114,7 +120,7 @@ ExoPlayer][] for a full explanation. The main points are:
|
|||
segments.
|
||||
* Use the `#EXT-X-INDEPENDENT-SEGMENTS` tag.
|
||||
* Prefer demuxed streams, as opposed to files that include both video and audio.
|
||||
* Include all information you can in the Master Playlist.
|
||||
* Include all information you can in the Multivariant Playlist.
|
||||
|
||||
The following guidelines apply specifically for live streams:
|
||||
|
||||
|
|
@ -127,5 +133,4 @@ The following guidelines apply specifically for live streams:
|
|||
[PlayerView]: {{ site.exo_sdk }}/ui/PlayerView.html
|
||||
[UI components]: {{ site.baseurl }}/ui-components.html
|
||||
[Customization page]: {{ site.baseurl }}/customization.html
|
||||
[Medium post about chunkless preparation]: https://medium.com/google-exoplayer/faster-hls-preparation-f6611aa15ea6
|
||||
[Medium post about HLS playback in ExoPlayer]: https://medium.com/google-exoplayer/hls-playback-in-exoplayer-a33959a47be7
|
||||
|
|
|
|||
|
|
@ -71,7 +71,7 @@ Errors that cause playback to fail can be received by implementing
|
|||
`onPlayerError(PlaybackException error)` in a registered
|
||||
`Player.Listener`. When a failure occurs, this method will be called
|
||||
immediately before the playback state transitions to `Player.STATE_IDLE`.
|
||||
Failed or stopped playbacks can be retried by calling `ExoPlayer.retry`.
|
||||
Failed or stopped playbacks can be retried by calling `ExoPlayer.prepare`.
|
||||
|
||||
Note that some [`Player`][] implementations pass instances of subclasses of
|
||||
`PlaybackException` to provide additional information about the failure. For
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ title: Media items
|
|||
|
||||
The [playlist API][] is based on `MediaItem`s, which can be conveniently built
|
||||
using `MediaItem.Builder`. Inside the player, media items are converted into
|
||||
playable `MediaSource`s by a `MediaSourceFactory`. Without
|
||||
playable `MediaSource`s by a `MediaSource.Factory`. Without
|
||||
[custom configuration]({{ site.baseurl }}/media-sources.html#customizing-media-source-creation),
|
||||
this conversion is carried out by a `DefaultMediaSourceFactory`, which is
|
||||
capable of building complex media sources corresponding to the properties of the
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ redirect_from:
|
|||
|
||||
In ExoPlayer every piece of media is represented by a `MediaItem`. However
|
||||
internally, the player needs `MediaSource` instances to play the content. The
|
||||
player creates these from media items using a `MediaSourceFactory`.
|
||||
player creates these from media items using a `MediaSource.Factory`.
|
||||
|
||||
By default the player uses a `DefaultMediaSourceFactory`, which can create
|
||||
instances of the following content `MediaSource` implementations:
|
||||
|
|
@ -27,13 +27,13 @@ customization.
|
|||
|
||||
## Customizing media source creation ##
|
||||
|
||||
When building the player, a `MediaSourceFactory` can be injected. For example, if
|
||||
an app wants to insert ads and use a `CacheDataSource.Factory` to support
|
||||
When building the player, a `MediaSource.Factory` can be injected. For example,
|
||||
if an app wants to insert ads and use a `CacheDataSource.Factory` to support
|
||||
caching, an instance of `DefaultMediaSourceFactory` can be configured to match
|
||||
these requirements and injected during player construction:
|
||||
|
||||
~~~
|
||||
MediaSourceFactory mediaSourceFactory =
|
||||
MediaSource.Factory mediaSourceFactory =
|
||||
new DefaultMediaSourceFactory(cacheDataSourceFactory)
|
||||
.setAdsLoaderProvider(adsLoaderProvider)
|
||||
.setAdViewProvider(playerView);
|
||||
|
|
@ -47,7 +47,7 @@ The
|
|||
[`DefaultMediaSourceFactory` JavaDoc]({{ site.baseurl }}/doc/reference/com/google/android/exoplayer2/source/DefaultMediaSourceFactory.html)
|
||||
describes the available options in more detail.
|
||||
|
||||
It's also possible to inject a custom `MediaSourceFactory` implementation, for
|
||||
It's also possible to inject a custom `MediaSource.Factory` implementation, for
|
||||
example to support creation of a custom media source type. The factory's
|
||||
`createMediaSource(MediaItem)` will be called to create a media source for each
|
||||
media item that is
|
||||
|
|
@ -57,7 +57,7 @@ media item that is
|
|||
|
||||
The [`ExoPlayer`] interface defines additional playlist methods that accept
|
||||
media sources rather than media items. This makes it possible to bypass the
|
||||
player's internal `MediaSourceFactory` and pass media source instances to the
|
||||
player's internal `MediaSource.Factory` and pass media source instances to the
|
||||
player directly:
|
||||
|
||||
~~~
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ title: Retrieving metadata
|
|||
|
||||
The metadata of the media can be retrieved during playback in multiple ways. The
|
||||
most straightforward is to listen for the
|
||||
`Player.EventListener#onMediaMetadataChanged` event; this will provide a
|
||||
`Player.Listener#onMediaMetadataChanged` event; this will provide a
|
||||
[`MediaMetadata`][] object for use, which has fields such as `title` and
|
||||
`albumArtist`. Alternatively, calling `Player#getMediaMetadata` returns the same
|
||||
object.
|
||||
|
|
|
|||
21
docs/rtsp.md
21
docs/rtsp.md
|
|
@ -75,3 +75,24 @@ end-of-stream signal under poor network conditions.
|
|||
RTP/TCP offers better compatibility under some network setups. You can configure
|
||||
ExoPlayer to use RTP/TCP by default with
|
||||
`RtspMediaSource.Factory.setForceUseRtpTcp()`.
|
||||
|
||||
### Passing a custom SocketFactory
|
||||
Custom `SocketFactory` instances can be useful when particular routing is
|
||||
required (e.g. when RTSP traffic needs to pass a specific interface, or the
|
||||
socket needs additional connectivity flags).
|
||||
|
||||
By default, `RtspMediaSource` will use Java's standard socket factory
|
||||
(`SocketFactory.getDefault()`) to create connections to the remote endpoints.
|
||||
This behavior can be overridden using
|
||||
`RtspMediaSource.Factory.setSocketFactory()`.
|
||||
|
||||
~~~
|
||||
// Create an RTSP media source pointing to an RTSP uri and override the socket
|
||||
// factory.
|
||||
MediaSource mediaSource =
|
||||
new RtspMediaSource.Factory()
|
||||
.setSocketFactory(...)
|
||||
.createMediaSource(MediaItem.fromUri(rtspUri));
|
||||
~~~
|
||||
{: .language-java}
|
||||
|
||||
|
|
|
|||
|
|
@ -106,9 +106,9 @@ ExoPlayer player =
|
|||
|
||||
## Custom `MediaSource` instantiation ##
|
||||
|
||||
If your app is using a custom `MediaSourceFactory` and you want
|
||||
If your app is using a custom `MediaSource.Factory` and you want
|
||||
`DefaultMediaSourceFactory` to be removed by code stripping, you should pass
|
||||
your `MediaSourceFactory` directly to the `ExoPlayer.Builder` constructor.
|
||||
your `MediaSource.Factory` directly to the `ExoPlayer.Builder` constructor.
|
||||
|
||||
~~~
|
||||
ExoPlayer player =
|
||||
|
|
@ -117,13 +117,13 @@ ExoPlayer player =
|
|||
{: .language-java}
|
||||
|
||||
If your app is using `MediaSource`s directly instead of `MediaItem`s you should
|
||||
pass `MediaSourceFactory.UNSUPPORTED` to the `ExoPlayer.Builder` constructor, to
|
||||
ensure `DefaultMediaSourceFactory` and `DefaultExtractorsFactory` can be
|
||||
pass `MediaSource.Factory.UNSUPPORTED` to the `ExoPlayer.Builder` constructor,
|
||||
to ensure `DefaultMediaSourceFactory` and `DefaultExtractorsFactory` can be
|
||||
stripped by code shrinking.
|
||||
|
||||
~~~
|
||||
ExoPlayer player =
|
||||
new ExoPlayer.Builder(context, MediaSourceFactory.UNSUPPORTED).build();
|
||||
new ExoPlayer.Builder(context, MediaSource.Factory.UNSUPPORTED).build();
|
||||
ProgressiveMediaSource mediaSource =
|
||||
new ProgressiveMediaSource.Factory(
|
||||
dataSourceFactory, customExtractorsFactory)
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ The [Transformer API][] can be used to convert media streams. It takes an input
|
|||
media stream, applies changes to it as configured by the app, and produces the
|
||||
corresponding output file. The available transformations are:
|
||||
|
||||
* Transmuxing between container formats.
|
||||
* Track removal.
|
||||
* Flattening of slow motion videos or, in other words, their conversion into
|
||||
normal videos that retain the desired slow motion effects, but can be played
|
||||
|
|
@ -16,7 +15,7 @@ corresponding output file. The available transformations are:
|
|||
|
||||
## Starting a transformation ##
|
||||
|
||||
To transform media, you need add the following dependency to your app’s
|
||||
To transform media, you need to add the following dependency to your app’s
|
||||
`build.gradle` file:
|
||||
|
||||
~~~
|
||||
|
|
@ -28,16 +27,13 @@ where `2.X.X` is your preferred ExoPlayer version.
|
|||
|
||||
You can then start a transformation by building a `Transformer` instance and
|
||||
calling `startTransformation` on it. The code sample below starts a
|
||||
transformation that removes the audio track from the input and sets the output
|
||||
container format to WebM:
|
||||
transformation that removes the audio track from the input:
|
||||
|
||||
~~~
|
||||
// Configure and create a Transformer instance.
|
||||
Transformer transformer =
|
||||
new Transformer.Builder()
|
||||
.setContext(context)
|
||||
new Transformer.Builder(context)
|
||||
.setRemoveAudio(true)
|
||||
.setOutputMimeType(MimeTypes.VIDEO_WEBM)
|
||||
.setListener(transformerListener)
|
||||
.build();
|
||||
// Start the transformation.
|
||||
|
|
@ -45,14 +41,16 @@ transformer.startTransformation(inputMediaItem, outputPath);
|
|||
~~~
|
||||
{: .language-java}
|
||||
|
||||
Other parameters, such as the `MediaSourceFactory`, can be passed to the
|
||||
Other parameters, such as the `MediaSource.Factory`, can be passed to the
|
||||
builder.
|
||||
|
||||
`startTransformation` receives a `MediaItem` describing the input, and a path or
|
||||
a `ParcelFileDescriptor` indicating where the output should be written. The
|
||||
input can be a progressive or an adaptive stream, but the output is always a
|
||||
progressive stream. For adaptive inputs, the highest resolution tracks are
|
||||
always selected for the transformation.
|
||||
always selected for the transformation. The input can be of any container format
|
||||
supported by ExoPlayer (see the [Supported formats page][] for details), but the
|
||||
output is always an MP4 file.
|
||||
|
||||
Multiple transformations can be executed sequentially with the same
|
||||
`Transformer` instance, but concurrent transformations with the same instance
|
||||
|
|
@ -72,7 +70,7 @@ Transformer.Listener transformerListener =
|
|||
}
|
||||
|
||||
@Override
|
||||
public void onTransformationError(MediaItem inputMediaItem, Exception e) {
|
||||
public void onTransformationError(MediaItem inputMediaItem, TransformationException e) {
|
||||
displayError(e);
|
||||
}
|
||||
};
|
||||
|
|
@ -121,8 +119,7 @@ method.
|
|||
|
||||
~~~
|
||||
Transformer transformer =
|
||||
new Transformer.Builder()
|
||||
.setContext(context)
|
||||
new Transformer.Builder(context)
|
||||
.setFlattenForSlowMotion(true)
|
||||
.setListener(transformerListener)
|
||||
.build();
|
||||
|
|
@ -137,4 +134,5 @@ flattened version of the video instead of the original one.
|
|||
Currently, Samsung's slow motion format is the only one supported.
|
||||
|
||||
[Transformer API]: {{ site.exo_sdk }}/transformer/Transformer.html
|
||||
[Supported formats page]: {{ site.baseurl }}/supported-formats.html
|
||||
|
||||
|
|
|
|||
|
|
@ -190,14 +190,15 @@ Note that overriding these drawables will also affect the appearance of
|
|||
All of the view components inflate their layouts from corresponding layout
|
||||
files, which are specified in their Javadoc. For example when a
|
||||
`PlayerControlView` is instantiated, it inflates its layout from
|
||||
`exo_player_control_view.xml`. To customize these layouts, an application can
|
||||
define layout files with the same names in its own `res/layout*` directories.
|
||||
These layout files will override the ones provided by the ExoPlayer library.
|
||||
`exo_player_control_view.xml`. To customize these layouts, an application
|
||||
can define layout files with the same names in its own `res/layout*`
|
||||
directories. These layout files will override the ones provided by the ExoPlayer
|
||||
library.
|
||||
|
||||
As an example, suppose we want our playback controls to consist of only a
|
||||
play/pause button positioned in the center of the view. We can achieve this by
|
||||
creating an `exo_player_control_view.xml` file in the application’s `res/layout`
|
||||
directory, containing:
|
||||
creating an `exo_player_control_view.xml` file in the application’s
|
||||
`res/layout` directory, containing:
|
||||
|
||||
~~~
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@
|
|||
apply from: "$gradle.ext.exoplayerSettingsDir/common_library_config.gradle"
|
||||
|
||||
dependencies {
|
||||
api 'com.google.android.gms:play-services-cast-framework:20.0.0'
|
||||
api 'com.google.android.gms:play-services-cast-framework:20.1.0'
|
||||
implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion
|
||||
implementation project(modulePrefix + 'library-common')
|
||||
compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion
|
||||
|
|
|
|||
|
|
@ -816,13 +816,7 @@ public final class CastPlayer extends BasePlayer {
|
|||
!getCurrentTimeline().isEmpty()
|
||||
? getCurrentTimeline().getPeriod(oldWindowIndex, period, /* setIds= */ true).uid
|
||||
: null;
|
||||
boolean wasPlaying = playbackState == Player.STATE_READY && playWhenReady.value;
|
||||
updatePlayerStateAndNotifyIfChanged(/* resultCallback= */ null);
|
||||
boolean isPlaying = playbackState == Player.STATE_READY && playWhenReady.value;
|
||||
if (wasPlaying != isPlaying) {
|
||||
listeners.queueEvent(
|
||||
Player.EVENT_IS_PLAYING_CHANGED, listener -> listener.onIsPlayingChanged(isPlaying));
|
||||
}
|
||||
updateRepeatModeAndNotifyIfChanged(/* resultCallback= */ null);
|
||||
updatePlaybackRateAndNotifyIfChanged(/* resultCallback= */ null);
|
||||
boolean playingPeriodChangedByTimelineChange = updateTimelineAndNotifyIfChanged();
|
||||
|
|
@ -1058,7 +1052,8 @@ public final class CastPlayer extends BasePlayer {
|
|||
new TracksInfo.TrackGroupInfo[castMediaTracks.size()];
|
||||
for (int i = 0; i < castMediaTracks.size(); i++) {
|
||||
MediaTrack mediaTrack = castMediaTracks.get(i);
|
||||
trackGroups[i] = new TrackGroup(CastUtils.mediaTrackToFormat(mediaTrack));
|
||||
trackGroups[i] =
|
||||
new TrackGroup(/* id= */ Integer.toString(i), CastUtils.mediaTrackToFormat(mediaTrack));
|
||||
|
||||
long id = mediaTrack.getId();
|
||||
@C.TrackType int trackType = MimeTypes.getTrackType(mediaTrack.getContentType());
|
||||
|
|
@ -1092,7 +1087,7 @@ public final class CastPlayer extends BasePlayer {
|
|||
|
||||
private void updateAvailableCommandsAndNotifyIfChanged() {
|
||||
Commands previousAvailableCommands = availableCommands;
|
||||
availableCommands = getAvailableCommands(PERMANENT_AVAILABLE_COMMANDS);
|
||||
availableCommands = Util.getAvailableCommands(/* player= */ this, PERMANENT_AVAILABLE_COMMANDS);
|
||||
if (!availableCommands.equals(previousAvailableCommands)) {
|
||||
listeners.queueEvent(
|
||||
Player.EVENT_AVAILABLE_COMMANDS_CHANGED,
|
||||
|
|
@ -1215,6 +1210,7 @@ public final class CastPlayer extends BasePlayer {
|
|||
boolean playWhenReady,
|
||||
@Player.PlayWhenReadyChangeReason int playWhenReadyChangeReason,
|
||||
@Player.State int playbackState) {
|
||||
boolean wasPlaying = this.playbackState == Player.STATE_READY && this.playWhenReady.value;
|
||||
boolean playWhenReadyChanged = this.playWhenReady.value != playWhenReady;
|
||||
boolean playbackStateChanged = this.playbackState != playbackState;
|
||||
if (playWhenReadyChanged || playbackStateChanged) {
|
||||
|
|
@ -1233,6 +1229,11 @@ public final class CastPlayer extends BasePlayer {
|
|||
Player.EVENT_PLAY_WHEN_READY_CHANGED,
|
||||
listener -> listener.onPlayWhenReadyChanged(playWhenReady, playWhenReadyChangeReason));
|
||||
}
|
||||
boolean isPlaying = playbackState == Player.STATE_READY && playWhenReady;
|
||||
if (wasPlaying != isPlaying) {
|
||||
listeners.queueEvent(
|
||||
Player.EVENT_IS_PLAYING_CHANGED, listener -> listener.onIsPlayingChanged(isPlaying));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ public final class DefaultMediaItemConverter implements MediaItemConverter {
|
|||
|
||||
private static final String KEY_MEDIA_ITEM = "mediaItem";
|
||||
private static final String KEY_PLAYER_CONFIG = "exoPlayerConfig";
|
||||
private static final String KEY_MEDIA_ID = "mediaId";
|
||||
private static final String KEY_URI = "uri";
|
||||
private static final String KEY_TITLE = "title";
|
||||
private static final String KEY_MIME_TYPE = "mimeType";
|
||||
|
|
@ -77,6 +78,7 @@ public final class DefaultMediaItemConverter implements MediaItemConverter {
|
|||
JSONObject mediaItemJson = customData.getJSONObject(KEY_MEDIA_ITEM);
|
||||
MediaItem.Builder builder = new MediaItem.Builder();
|
||||
builder.setUri(Uri.parse(mediaItemJson.getString(KEY_URI)));
|
||||
builder.setMediaId(mediaItemJson.getString(KEY_MEDIA_ID));
|
||||
if (mediaItemJson.has(KEY_TITLE)) {
|
||||
com.google.android.exoplayer2.MediaMetadata mediaMetadata =
|
||||
new com.google.android.exoplayer2.MediaMetadata.Builder()
|
||||
|
|
@ -130,6 +132,7 @@ public final class DefaultMediaItemConverter implements MediaItemConverter {
|
|||
private static JSONObject getMediaItemJson(MediaItem mediaItem) throws JSONException {
|
||||
Assertions.checkNotNull(mediaItem.localConfiguration);
|
||||
JSONObject json = new JSONObject();
|
||||
json.put(KEY_MEDIA_ID, mediaItem.mediaId);
|
||||
json.put(KEY_TITLE, mediaItem.mediaMetadata.title);
|
||||
json.put(KEY_URI, mediaItem.localConfiguration.uri.toString());
|
||||
json.put(KEY_MIME_TYPE, mediaItem.localConfiguration.mimeType);
|
||||
|
|
|
|||
|
|
@ -52,6 +52,7 @@ import static org.mockito.ArgumentMatchers.anyLong;
|
|||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.reset;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.verifyNoMoreInteractions;
|
||||
|
|
@ -138,13 +139,18 @@ public class CastPlayerTest {
|
|||
@SuppressWarnings("deprecation")
|
||||
@Test
|
||||
public void setPlayWhenReady_masksRemoteState() {
|
||||
when(mockRemoteMediaClient.getPlayerState()).thenReturn(MediaStatus.PLAYER_STATE_PLAYING);
|
||||
// Trigger initial update to get out of STATE_IDLE to make onIsPlaying() be called.
|
||||
remoteMediaClientCallback.onStatusUpdated();
|
||||
reset(mockListener);
|
||||
when(mockRemoteMediaClient.play()).thenReturn(mockPendingResult);
|
||||
assertThat(castPlayer.getPlayWhenReady()).isFalse();
|
||||
|
||||
castPlayer.play();
|
||||
verify(mockPendingResult).setResultCallback(setResultCallbackArgumentCaptor.capture());
|
||||
assertThat(castPlayer.getPlayWhenReady()).isTrue();
|
||||
verify(mockListener).onPlayerStateChanged(true, Player.STATE_IDLE);
|
||||
verify(mockListener).onPlayerStateChanged(true, Player.STATE_READY);
|
||||
verify(mockListener).onIsPlayingChanged(true);
|
||||
verify(mockListener)
|
||||
.onPlayWhenReadyChanged(true, Player.PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST);
|
||||
|
||||
|
|
@ -163,13 +169,18 @@ public class CastPlayerTest {
|
|||
@SuppressWarnings("deprecation")
|
||||
@Test
|
||||
public void setPlayWhenReadyMasking_updatesUponResultChange() {
|
||||
when(mockRemoteMediaClient.getPlayerState()).thenReturn(MediaStatus.PLAYER_STATE_PLAYING);
|
||||
// Trigger initial update to get out of STATE_IDLE to make onIsPlaying() be called.
|
||||
remoteMediaClientCallback.onStatusUpdated();
|
||||
reset(mockListener);
|
||||
when(mockRemoteMediaClient.play()).thenReturn(mockPendingResult);
|
||||
assertThat(castPlayer.getPlayWhenReady()).isFalse();
|
||||
|
||||
castPlayer.play();
|
||||
verify(mockPendingResult).setResultCallback(setResultCallbackArgumentCaptor.capture());
|
||||
assertThat(castPlayer.getPlayWhenReady()).isTrue();
|
||||
verify(mockListener).onPlayerStateChanged(true, Player.STATE_IDLE);
|
||||
verify(mockListener).onIsPlayingChanged(true);
|
||||
verify(mockListener).onPlayerStateChanged(true, Player.STATE_READY);
|
||||
verify(mockListener)
|
||||
.onPlayWhenReadyChanged(true, Player.PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST);
|
||||
|
||||
|
|
@ -177,38 +188,52 @@ public class CastPlayerTest {
|
|||
setResultCallbackArgumentCaptor
|
||||
.getValue()
|
||||
.onResult(mock(RemoteMediaClient.MediaChannelResult.class));
|
||||
verify(mockListener).onPlayerStateChanged(false, Player.STATE_IDLE);
|
||||
verify(mockListener).onPlayerStateChanged(false, Player.STATE_READY);
|
||||
verify(mockListener).onIsPlayingChanged(false);
|
||||
verify(mockListener).onPlayWhenReadyChanged(false, Player.PLAY_WHEN_READY_CHANGE_REASON_REMOTE);
|
||||
assertThat(castPlayer.getPlayWhenReady()).isFalse();
|
||||
verifyNoMoreInteractions(mockListener);
|
||||
}
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
@Test
|
||||
public void setPlayWhenReady_correctChangeReasonOnPause() {
|
||||
when(mockRemoteMediaClient.getPlayerState()).thenReturn(MediaStatus.PLAYER_STATE_PLAYING);
|
||||
// Trigger initial update to get out of STATE_IDLE to make onIsPlaying() be called.
|
||||
remoteMediaClientCallback.onStatusUpdated();
|
||||
reset(mockListener);
|
||||
when(mockRemoteMediaClient.play()).thenReturn(mockPendingResult);
|
||||
when(mockRemoteMediaClient.pause()).thenReturn(mockPendingResult);
|
||||
|
||||
castPlayer.play();
|
||||
assertThat(castPlayer.getPlayWhenReady()).isTrue();
|
||||
verify(mockListener).onPlayerStateChanged(true, Player.STATE_IDLE);
|
||||
verify(mockListener).onIsPlayingChanged(true);
|
||||
verify(mockListener).onPlayerStateChanged(true, Player.STATE_READY);
|
||||
verify(mockListener)
|
||||
.onPlayWhenReadyChanged(true, Player.PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST);
|
||||
|
||||
castPlayer.pause();
|
||||
assertThat(castPlayer.getPlayWhenReady()).isFalse();
|
||||
verify(mockListener).onPlayerStateChanged(false, Player.STATE_IDLE);
|
||||
verify(mockListener).onIsPlayingChanged(false);
|
||||
verify(mockListener)
|
||||
.onPlayWhenReadyChanged(false, Player.PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST);
|
||||
verify(mockListener).onPlayerStateChanged(false, Player.STATE_READY);
|
||||
verifyNoMoreInteractions(mockListener);
|
||||
}
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
@Test
|
||||
public void playWhenReady_changesOnStatusUpdates() {
|
||||
when(mockRemoteMediaClient.getPlayerState()).thenReturn(MediaStatus.PLAYER_STATE_PLAYING);
|
||||
assertThat(castPlayer.getPlayWhenReady()).isFalse();
|
||||
when(mockRemoteMediaClient.isPaused()).thenReturn(false);
|
||||
remoteMediaClientCallback.onStatusUpdated();
|
||||
verify(mockListener).onPlayerStateChanged(true, Player.STATE_IDLE);
|
||||
verify(mockListener).onPlayerStateChanged(true, Player.STATE_READY);
|
||||
verify(mockListener).onPlaybackStateChanged(Player.STATE_READY);
|
||||
verify(mockListener).onPlayWhenReadyChanged(true, Player.PLAY_WHEN_READY_CHANGE_REASON_REMOTE);
|
||||
assertThat(castPlayer.getPlayWhenReady()).isTrue();
|
||||
verify(mockListener).onIsPlayingChanged(true);
|
||||
verifyNoMoreInteractions(mockListener);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
|
|||
|
|
@ -64,7 +64,7 @@ public final class FfmpegAudioRenderer extends DecoderAudioRenderer<FfmpegAudioD
|
|||
this(
|
||||
eventHandler,
|
||||
eventListener,
|
||||
new DefaultAudioSink(/* audioCapabilities= */ null, audioProcessors));
|
||||
new DefaultAudioSink.Builder().setAudioProcessors(audioProcessors).build());
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ public final class FfmpegLibrary {
|
|||
/**
|
||||
* Override the names of the FFmpeg native libraries. If an application wishes to call this
|
||||
* method, it must do so before calling any other method defined by this class, and before
|
||||
* instantiating a {@link FfmpegAudioRenderer} instance.
|
||||
* instantiating a {@link FfmpegAudioRenderer} or {@link FfmpegVideoRenderer} instance.
|
||||
*
|
||||
* @param libraries The names of the FFmpeg native libraries.
|
||||
*/
|
||||
|
|
@ -140,6 +140,10 @@ public final class FfmpegLibrary {
|
|||
return "pcm_mulaw";
|
||||
case MimeTypes.AUDIO_ALAW:
|
||||
return "pcm_alaw";
|
||||
case MimeTypes.VIDEO_H264:
|
||||
return "h264";
|
||||
case MimeTypes.VIDEO_H265:
|
||||
return "hevc";
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,135 @@
|
|||
/*
|
||||
* 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.ffmpeg;
|
||||
|
||||
import static com.google.android.exoplayer2.decoder.DecoderReuseEvaluation.DISCARD_REASON_MIME_TYPE_CHANGED;
|
||||
import static com.google.android.exoplayer2.decoder.DecoderReuseEvaluation.REUSE_RESULT_NO;
|
||||
import static com.google.android.exoplayer2.decoder.DecoderReuseEvaluation.REUSE_RESULT_YES_WITHOUT_RECONFIGURATION;
|
||||
|
||||
import android.os.Handler;
|
||||
import android.view.Surface;
|
||||
import androidx.annotation.Nullable;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.Format;
|
||||
import com.google.android.exoplayer2.RendererCapabilities;
|
||||
import com.google.android.exoplayer2.decoder.CryptoConfig;
|
||||
import com.google.android.exoplayer2.decoder.Decoder;
|
||||
import com.google.android.exoplayer2.decoder.DecoderInputBuffer;
|
||||
import com.google.android.exoplayer2.decoder.DecoderReuseEvaluation;
|
||||
import com.google.android.exoplayer2.decoder.VideoDecoderOutputBuffer;
|
||||
import com.google.android.exoplayer2.util.TraceUtil;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
import com.google.android.exoplayer2.video.DecoderVideoRenderer;
|
||||
import com.google.android.exoplayer2.video.VideoRendererEventListener;
|
||||
|
||||
// TODO: Remove the NOTE below.
|
||||
/**
|
||||
* <b>NOTE: This class if under development and is not yet functional.</b>
|
||||
*
|
||||
* <p>Decodes and renders video using FFmpeg.
|
||||
*/
|
||||
public final class FfmpegVideoRenderer extends DecoderVideoRenderer {
|
||||
|
||||
private static final String TAG = "FfmpegVideoRenderer";
|
||||
|
||||
/**
|
||||
* Creates a new instance.
|
||||
*
|
||||
* @param allowedJoiningTimeMs The maximum duration in milliseconds for which this video renderer
|
||||
* can attempt to seamlessly join an ongoing playback.
|
||||
* @param eventHandler A handler to use when delivering events to {@code eventListener}. May be
|
||||
* null if delivery of events is not required.
|
||||
* @param eventListener A listener of events. May be null if delivery of events is not required.
|
||||
* @param maxDroppedFramesToNotify The maximum number of frames that can be dropped between
|
||||
* invocations of {@link VideoRendererEventListener#onDroppedFrames(int, long)}.
|
||||
*/
|
||||
public FfmpegVideoRenderer(
|
||||
long allowedJoiningTimeMs,
|
||||
@Nullable Handler eventHandler,
|
||||
@Nullable VideoRendererEventListener eventListener,
|
||||
int maxDroppedFramesToNotify) {
|
||||
super(allowedJoiningTimeMs, eventHandler, eventListener, maxDroppedFramesToNotify);
|
||||
// TODO: Implement.
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return TAG;
|
||||
}
|
||||
|
||||
@Override
|
||||
@RendererCapabilities.Capabilities
|
||||
public final int supportsFormat(Format format) {
|
||||
// TODO: Remove this line and uncomment the implementation below.
|
||||
return C.FORMAT_UNSUPPORTED_TYPE;
|
||||
/*
|
||||
String mimeType = Assertions.checkNotNull(format.sampleMimeType);
|
||||
if (!FfmpegLibrary.isAvailable() || !MimeTypes.isVideo(mimeType)) {
|
||||
return FORMAT_UNSUPPORTED_TYPE;
|
||||
} else if (!FfmpegLibrary.supportsFormat(format.sampleMimeType)) {
|
||||
return RendererCapabilities.create(FORMAT_UNSUPPORTED_SUBTYPE);
|
||||
} else if (format.exoMediaCryptoType != null) {
|
||||
return RendererCapabilities.create(FORMAT_UNSUPPORTED_DRM);
|
||||
} else {
|
||||
return RendererCapabilities.create(
|
||||
FORMAT_HANDLED,
|
||||
ADAPTIVE_SEAMLESS,
|
||||
TUNNELING_NOT_SUPPORTED);
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
||||
@SuppressWarnings("nullness:return")
|
||||
@Override
|
||||
protected Decoder<DecoderInputBuffer, VideoDecoderOutputBuffer, FfmpegDecoderException>
|
||||
createDecoder(Format format, @Nullable CryptoConfig cryptoConfig)
|
||||
throws FfmpegDecoderException {
|
||||
TraceUtil.beginSection("createFfmpegVideoDecoder");
|
||||
// TODO: Implement, remove the SuppressWarnings annotation, and update the return type to use
|
||||
// the concrete type of the decoder (probably FfmepgVideoDecoder).
|
||||
TraceUtil.endSection();
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void renderOutputBufferToSurface(VideoDecoderOutputBuffer outputBuffer, Surface surface)
|
||||
throws FfmpegDecoderException {
|
||||
// TODO: Implement.
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void setDecoderOutputMode(@C.VideoOutputMode int outputMode) {
|
||||
// TODO: Uncomment the implementation below.
|
||||
/*
|
||||
if (decoder != null) {
|
||||
decoder.setOutputMode(outputMode);
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
||||
@Override
|
||||
protected DecoderReuseEvaluation canReuseDecoder(
|
||||
String decoderName, Format oldFormat, Format newFormat) {
|
||||
boolean sameMimeType = Util.areEqual(oldFormat.sampleMimeType, newFormat.sampleMimeType);
|
||||
// TODO: Ability to reuse the decoder may be MIME type dependent.
|
||||
return new DecoderReuseEvaluation(
|
||||
decoderName,
|
||||
oldFormat,
|
||||
newFormat,
|
||||
sameMimeType ? REUSE_RESULT_YES_WITHOUT_RECONFIGURATION : REUSE_RESULT_NO,
|
||||
sameMimeType ? 0 : DISCARD_REASON_MIME_TYPE_CHANGED);
|
||||
}
|
||||
}
|
||||
|
|
@ -22,7 +22,8 @@ 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 {
|
||||
|
|
@ -32,4 +33,10 @@ public final class DefaultRenderersFactoryTest {
|
|||
DefaultRenderersFactoryAsserts.assertExtensionRendererCreated(
|
||||
FfmpegAudioRenderer.class, C.TRACK_TYPE_AUDIO);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void createRenderers_instantiatesFfmpegVideoRenderer() {
|
||||
DefaultRenderersFactoryAsserts.assertExtensionRendererCreated(
|
||||
FfmpegVideoRenderer.class, C.TRACK_TYPE_VIDEO);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,7 +29,6 @@ import com.google.android.exoplayer2.PlaybackException;
|
|||
import com.google.android.exoplayer2.Player;
|
||||
import com.google.android.exoplayer2.Renderer;
|
||||
import com.google.android.exoplayer2.RenderersFactory;
|
||||
import com.google.android.exoplayer2.audio.AudioProcessor;
|
||||
import com.google.android.exoplayer2.audio.AudioSink;
|
||||
import com.google.android.exoplayer2.audio.DefaultAudioSink;
|
||||
import com.google.android.exoplayer2.extractor.mkv.MatroskaExtractor;
|
||||
|
|
@ -67,9 +66,7 @@ public class FlacPlaybackTest {
|
|||
}
|
||||
|
||||
private static void playAndAssertAudioSinkInput(String fileName) throws Exception {
|
||||
CapturingAudioSink audioSink =
|
||||
new CapturingAudioSink(
|
||||
new DefaultAudioSink(/* audioCapabilities= */ null, new AudioProcessor[0]));
|
||||
CapturingAudioSink audioSink = new CapturingAudioSink(new DefaultAudioSink.Builder().build());
|
||||
|
||||
TestPlaybackRunnable testPlaybackRunnable =
|
||||
new TestPlaybackRunnable(
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@ import com.google.android.exoplayer2.C;
|
|||
import com.google.android.exoplayer2.ExoPlayerLibraryInfo;
|
||||
import com.google.android.exoplayer2.Player;
|
||||
import com.google.android.exoplayer2.Timeline;
|
||||
import com.google.android.exoplayer2.source.MediaSourceFactory;
|
||||
import com.google.android.exoplayer2.source.MediaSource;
|
||||
import com.google.android.exoplayer2.source.ads.AdsLoader;
|
||||
import com.google.android.exoplayer2.source.ads.AdsMediaSource;
|
||||
import com.google.android.exoplayer2.ui.AdViewProvider;
|
||||
|
|
@ -86,7 +86,7 @@ import java.util.Set;
|
|||
* href="https://developers.google.com/interactive-media-ads/docs/sdks/android/client-side/omsdk">IMA
|
||||
* SDK Open Measurement documentation</a> for more information.
|
||||
*/
|
||||
public final class ImaAdsLoader implements Player.Listener, AdsLoader {
|
||||
public final class ImaAdsLoader implements AdsLoader {
|
||||
|
||||
static {
|
||||
ExoPlayerLibraryInfo.registerModule("goog.exo.ima");
|
||||
|
|
@ -222,7 +222,7 @@ public final class ImaAdsLoader implements Player.Listener, AdsLoader {
|
|||
|
||||
/**
|
||||
* Sets the MIME types to prioritize for linear ad media. If not specified, MIME types supported
|
||||
* by the {@link MediaSourceFactory adMediaSourceFactory} used to construct the {@link
|
||||
* by the {@link MediaSource.Factory adMediaSourceFactory} used to construct the {@link
|
||||
* AdsMediaSource} will be used.
|
||||
*
|
||||
* @param adMediaMimeTypes The MIME types to prioritize for linear ad media. May contain {@link
|
||||
|
|
@ -386,6 +386,7 @@ public final class ImaAdsLoader implements Player.Listener, AdsLoader {
|
|||
private final ImaUtil.Configuration configuration;
|
||||
private final Context context;
|
||||
private final ImaUtil.ImaFactory imaFactory;
|
||||
private final PlayerListenerImpl playerListener;
|
||||
private final HashMap<Object, AdTagLoader> adTagLoaderByAdsId;
|
||||
private final HashMap<AdsMediaSource, AdTagLoader> adTagLoaderByAdsMediaSource;
|
||||
private final Timeline.Period period;
|
||||
|
|
@ -402,6 +403,7 @@ public final class ImaAdsLoader implements Player.Listener, AdsLoader {
|
|||
this.context = context.getApplicationContext();
|
||||
this.configuration = configuration;
|
||||
this.imaFactory = imaFactory;
|
||||
playerListener = new PlayerListenerImpl();
|
||||
supportedMimeTypes = ImmutableList.of();
|
||||
adTagLoaderByAdsId = new HashMap<>();
|
||||
adTagLoaderByAdsMediaSource = new HashMap<>();
|
||||
|
|
@ -532,7 +534,7 @@ public final class ImaAdsLoader implements Player.Listener, AdsLoader {
|
|||
if (player == null) {
|
||||
return;
|
||||
}
|
||||
player.addListener(this);
|
||||
player.addListener(playerListener);
|
||||
}
|
||||
|
||||
@Nullable AdTagLoader adTagLoader = adTagLoaderByAdsId.get(adsId);
|
||||
|
|
@ -554,7 +556,7 @@ public final class ImaAdsLoader implements Player.Listener, AdsLoader {
|
|||
}
|
||||
|
||||
if (player != null && adTagLoaderByAdsMediaSource.isEmpty()) {
|
||||
player.removeListener(this);
|
||||
player.removeListener(playerListener);
|
||||
player = null;
|
||||
}
|
||||
}
|
||||
|
|
@ -562,7 +564,7 @@ public final class ImaAdsLoader implements Player.Listener, AdsLoader {
|
|||
@Override
|
||||
public void release() {
|
||||
if (player != null) {
|
||||
player.removeListener(this);
|
||||
player.removeListener(playerListener);
|
||||
player = null;
|
||||
maybeUpdateCurrentAdTagLoader();
|
||||
}
|
||||
|
|
@ -602,37 +604,6 @@ public final class ImaAdsLoader implements Player.Listener, AdsLoader {
|
|||
.handlePrepareError(adGroupIndex, adIndexInAdGroup, exception);
|
||||
}
|
||||
|
||||
// Player.Listener implementation.
|
||||
|
||||
@Override
|
||||
public void onTimelineChanged(Timeline timeline, @Player.TimelineChangeReason int reason) {
|
||||
if (timeline.isEmpty()) {
|
||||
// The player is being reset or contains no media.
|
||||
return;
|
||||
}
|
||||
maybeUpdateCurrentAdTagLoader();
|
||||
maybePreloadNextPeriodAds();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPositionDiscontinuity(
|
||||
Player.PositionInfo oldPosition,
|
||||
Player.PositionInfo newPosition,
|
||||
@Player.DiscontinuityReason int reason) {
|
||||
maybeUpdateCurrentAdTagLoader();
|
||||
maybePreloadNextPeriodAds();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onShuffleModeEnabledChanged(boolean shuffleModeEnabled) {
|
||||
maybePreloadNextPeriodAds();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRepeatModeChanged(@Player.RepeatMode int repeatMode) {
|
||||
maybePreloadNextPeriodAds();
|
||||
}
|
||||
|
||||
// Internal methods.
|
||||
|
||||
private void maybeUpdateCurrentAdTagLoader() {
|
||||
|
|
@ -672,7 +643,7 @@ public final class ImaAdsLoader implements Player.Listener, AdsLoader {
|
|||
}
|
||||
|
||||
private void maybePreloadNextPeriodAds() {
|
||||
@Nullable Player player = this.player;
|
||||
@Nullable Player player = ImaAdsLoader.this.player;
|
||||
if (player == null) {
|
||||
return;
|
||||
}
|
||||
|
|
@ -700,12 +671,44 @@ public final class ImaAdsLoader implements Player.Listener, AdsLoader {
|
|||
return;
|
||||
}
|
||||
long periodPositionUs =
|
||||
timeline.getPeriodPosition(
|
||||
timeline.getPeriodPositionUs(
|
||||
window, period, period.windowIndex, /* windowPositionUs= */ C.TIME_UNSET)
|
||||
.second;
|
||||
nextAdTagLoader.maybePreloadAds(Util.usToMs(periodPositionUs), Util.usToMs(period.durationUs));
|
||||
}
|
||||
|
||||
private final class PlayerListenerImpl implements Player.Listener {
|
||||
|
||||
@Override
|
||||
public void onTimelineChanged(Timeline timeline, @Player.TimelineChangeReason int reason) {
|
||||
if (timeline.isEmpty()) {
|
||||
// The player is being reset or contains no media.
|
||||
return;
|
||||
}
|
||||
maybeUpdateCurrentAdTagLoader();
|
||||
maybePreloadNextPeriodAds();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPositionDiscontinuity(
|
||||
Player.PositionInfo oldPosition,
|
||||
Player.PositionInfo newPosition,
|
||||
@Player.DiscontinuityReason int reason) {
|
||||
maybeUpdateCurrentAdTagLoader();
|
||||
maybePreloadNextPeriodAds();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onShuffleModeEnabledChanged(boolean shuffleModeEnabled) {
|
||||
maybePreloadNextPeriodAds();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRepeatModeChanged(@Player.RepeatMode int repeatMode) {
|
||||
maybePreloadNextPeriodAds();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Default {@link ImaUtil.ImaFactory} for non-test usage, which delegates to {@link
|
||||
* ImaSdkFactory}.
|
||||
|
|
|
|||
|
|
@ -15,6 +15,9 @@
|
|||
*/
|
||||
package com.google.android.exoplayer2.ext.ima;
|
||||
|
||||
import static com.google.android.exoplayer2.util.Assertions.checkNotNull;
|
||||
import static com.google.android.exoplayer2.util.Assertions.checkState;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Looper;
|
||||
import android.view.View;
|
||||
|
|
@ -36,15 +39,22 @@ import com.google.ads.interactivemedia.v3.api.UiElement;
|
|||
import com.google.ads.interactivemedia.v3.api.player.VideoAdPlayer;
|
||||
import com.google.ads.interactivemedia.v3.api.player.VideoProgressUpdate;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.Timeline;
|
||||
import com.google.android.exoplayer2.source.ads.AdPlaybackState;
|
||||
import com.google.android.exoplayer2.ui.AdOverlayInfo;
|
||||
import com.google.android.exoplayer2.ui.AdViewProvider;
|
||||
import com.google.android.exoplayer2.upstream.DataSchemeDataSource;
|
||||
import com.google.android.exoplayer2.upstream.DataSourceUtil;
|
||||
import com.google.android.exoplayer2.upstream.DataSpec;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
/** Utilities for working with IMA SDK and IMA extension data types. */
|
||||
|
|
@ -134,6 +144,32 @@ import java.util.Set;
|
|||
}
|
||||
}
|
||||
|
||||
/** Stores configuration for playing server side ad insertion content. */
|
||||
public static final class ServerSideAdInsertionConfiguration {
|
||||
|
||||
public final AdViewProvider adViewProvider;
|
||||
public final ImaSdkSettings imaSdkSettings;
|
||||
@Nullable public final AdEvent.AdEventListener applicationAdEventListener;
|
||||
@Nullable public final AdErrorEvent.AdErrorListener applicationAdErrorListener;
|
||||
public final ImmutableList<CompanionAdSlot> companionAdSlots;
|
||||
public final boolean debugModeEnabled;
|
||||
|
||||
public ServerSideAdInsertionConfiguration(
|
||||
AdViewProvider adViewProvider,
|
||||
ImaSdkSettings imaSdkSettings,
|
||||
@Nullable AdEvent.AdEventListener applicationAdEventListener,
|
||||
@Nullable AdErrorEvent.AdErrorListener applicationAdErrorListener,
|
||||
List<CompanionAdSlot> companionAdSlots,
|
||||
boolean debugModeEnabled) {
|
||||
this.imaSdkSettings = imaSdkSettings;
|
||||
this.adViewProvider = adViewProvider;
|
||||
this.applicationAdEventListener = applicationAdEventListener;
|
||||
this.applicationAdErrorListener = applicationAdErrorListener;
|
||||
this.companionAdSlots = ImmutableList.copyOf(companionAdSlots);
|
||||
this.debugModeEnabled = debugModeEnabled;
|
||||
}
|
||||
}
|
||||
|
||||
public static final int TIMEOUT_UNSET = -1;
|
||||
public static final int BITRATE_UNSET = -1;
|
||||
|
||||
|
|
@ -227,5 +263,125 @@ import java.util.Set;
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Splits an {@link AdPlaybackState} into a separate {@link AdPlaybackState} for each period of a
|
||||
* content timeline. Ad group times are expected to not take previous ad duration into account and
|
||||
* needs to be translated to the actual position in the {@code contentTimeline} by adding prior ad
|
||||
* durations.
|
||||
*
|
||||
* <p>If a period is enclosed by an ad group, the period is considered an ad period and gets an ad
|
||||
* playback state assigned with a single ad in a single ad group. The duration of the ad is set to
|
||||
* the duration of the period. All other periods are considered content periods with an empty ad
|
||||
* playback state without any ads.
|
||||
*
|
||||
* @param adPlaybackState The ad playback state to be split.
|
||||
* @param contentTimeline The content timeline for each period of which to create an {@link
|
||||
* AdPlaybackState}.
|
||||
* @return A map of ad playback states for each period UID in the content timeline.
|
||||
*/
|
||||
public static ImmutableMap<Object, AdPlaybackState> splitAdPlaybackStateForPeriods(
|
||||
AdPlaybackState adPlaybackState, Timeline contentTimeline) {
|
||||
Timeline.Period period = new Timeline.Period();
|
||||
if (contentTimeline.getPeriodCount() == 1) {
|
||||
// A single period gets the entire ad playback state that may contain multiple ad groups.
|
||||
return ImmutableMap.of(
|
||||
checkNotNull(
|
||||
contentTimeline.getPeriod(/* periodIndex= */ 0, period, /* setIds= */ true).uid),
|
||||
adPlaybackState);
|
||||
}
|
||||
|
||||
int periodIndex = 0;
|
||||
long totalElapsedContentDurationUs = 0;
|
||||
Object adsId = checkNotNull(adPlaybackState.adsId);
|
||||
AdPlaybackState contentOnlyAdPlaybackState = new AdPlaybackState(adsId);
|
||||
Map<Object, AdPlaybackState> adPlaybackStates = new HashMap<>();
|
||||
for (int i = adPlaybackState.removedAdGroupCount; i < adPlaybackState.adGroupCount; i++) {
|
||||
AdPlaybackState.AdGroup adGroup = adPlaybackState.getAdGroup(/* adGroupIndex= */ i);
|
||||
if (adGroup.timeUs == C.TIME_END_OF_SOURCE) {
|
||||
checkState(i == adPlaybackState.adGroupCount - 1);
|
||||
// The last ad group is a placeholder for a potential post roll. We can just stop here.
|
||||
break;
|
||||
}
|
||||
// The ad group start timeUs is in content position. We need to add the ad
|
||||
// duration before the ad group to translate the start time to the position in the period.
|
||||
long adGroupDurationUs = getTotalDurationUs(adGroup.durationsUs);
|
||||
long elapsedAdGroupAdDurationUs = 0;
|
||||
for (int j = periodIndex; j < contentTimeline.getPeriodCount(); j++) {
|
||||
contentTimeline.getPeriod(j, period, /* setIds= */ true);
|
||||
if (totalElapsedContentDurationUs < adGroup.timeUs) {
|
||||
// Period starts before the ad group, so it is a content period.
|
||||
adPlaybackStates.put(checkNotNull(period.uid), contentOnlyAdPlaybackState);
|
||||
totalElapsedContentDurationUs += period.durationUs;
|
||||
} else {
|
||||
long periodStartUs = totalElapsedContentDurationUs + elapsedAdGroupAdDurationUs;
|
||||
if (periodStartUs + period.durationUs <= adGroup.timeUs + adGroupDurationUs) {
|
||||
// The period ends before the end of the ad group, so it is an ad period (Note: An ad
|
||||
// reported by the IMA SDK may span multiple periods).
|
||||
adPlaybackStates.put(
|
||||
checkNotNull(period.uid),
|
||||
splitAdGroupForPeriod(adsId, adGroup, periodStartUs, period.durationUs));
|
||||
elapsedAdGroupAdDurationUs += period.durationUs;
|
||||
} else {
|
||||
// Period is after the current ad group. Continue with next ad group.
|
||||
break;
|
||||
}
|
||||
}
|
||||
// Increment the period index to the next unclassified period.
|
||||
periodIndex++;
|
||||
}
|
||||
}
|
||||
// The remaining periods end after the last ad group, so these are content periods.
|
||||
for (int i = periodIndex; i < contentTimeline.getPeriodCount(); i++) {
|
||||
contentTimeline.getPeriod(i, period, /* setIds= */ true);
|
||||
adPlaybackStates.put(checkNotNull(period.uid), contentOnlyAdPlaybackState);
|
||||
}
|
||||
return ImmutableMap.copyOf(adPlaybackStates);
|
||||
}
|
||||
|
||||
private static AdPlaybackState splitAdGroupForPeriod(
|
||||
Object adsId, AdPlaybackState.AdGroup adGroup, long periodStartUs, long periodDurationUs) {
|
||||
checkState(adGroup.timeUs <= periodStartUs);
|
||||
AdPlaybackState adPlaybackState =
|
||||
new AdPlaybackState(checkNotNull(adsId), /* adGroupTimesUs...= */ 0)
|
||||
.withAdCount(/* adGroupIndex= */ 0, /* adCount= */ 1)
|
||||
.withAdDurationsUs(/* adGroupIndex= */ 0, periodDurationUs)
|
||||
.withIsServerSideInserted(/* adGroupIndex= */ 0, true);
|
||||
long periodEndUs = periodStartUs + periodDurationUs;
|
||||
long adDurationsUs = 0;
|
||||
for (int i = 0; i < adGroup.count; i++) {
|
||||
adDurationsUs += adGroup.durationsUs[i];
|
||||
if (periodEndUs == adGroup.timeUs + adDurationsUs) {
|
||||
// Map the state of the global ad state to the period specific ad state.
|
||||
switch (adGroup.states[i]) {
|
||||
case AdPlaybackState.AD_STATE_PLAYED:
|
||||
adPlaybackState =
|
||||
adPlaybackState.withPlayedAd(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0);
|
||||
break;
|
||||
case AdPlaybackState.AD_STATE_SKIPPED:
|
||||
adPlaybackState =
|
||||
adPlaybackState.withSkippedAd(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0);
|
||||
break;
|
||||
case AdPlaybackState.AD_STATE_ERROR:
|
||||
adPlaybackState =
|
||||
adPlaybackState.withAdLoadError(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0);
|
||||
break;
|
||||
default:
|
||||
// Do nothing.
|
||||
break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
return adPlaybackState;
|
||||
}
|
||||
|
||||
private static long getTotalDurationUs(long[] durationsUs) {
|
||||
long totalDurationUs = 0;
|
||||
for (long adDurationUs : durationsUs) {
|
||||
totalDurationUs += adDurationUs;
|
||||
}
|
||||
return totalDurationUs;
|
||||
}
|
||||
|
||||
private ImaUtil() {}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,10 +15,13 @@
|
|||
*/
|
||||
package com.google.android.exoplayer2.ext.ima;
|
||||
|
||||
import static com.google.android.exoplayer2.util.Assertions.checkState;
|
||||
|
||||
import android.os.Looper;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.ExoPlayer;
|
||||
import com.google.android.exoplayer2.MediaItem;
|
||||
import com.google.android.exoplayer2.PlaybackException;
|
||||
import com.google.android.exoplayer2.Player;
|
||||
import com.google.android.exoplayer2.Timeline;
|
||||
import com.google.android.exoplayer2.TracksInfo;
|
||||
|
|
@ -182,6 +185,29 @@ import com.google.android.exoplayer2.util.Util;
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets an error on this player.
|
||||
*
|
||||
* <p>This will propagate the error to {@link Player.Listener#onPlayerError(PlaybackException)}
|
||||
* and {@link Player.Listener#onPlayerErrorChanged(PlaybackException)} and will also update the
|
||||
* state to {@link Player#STATE_IDLE}.
|
||||
*
|
||||
* <p>The player must be in {@link #STATE_BUFFERING} or {@link #STATE_READY}.
|
||||
*/
|
||||
@SuppressWarnings("deprecation") // Calling deprecated listener.onPlayerStateChanged()
|
||||
public void setPlayerError(PlaybackException error) {
|
||||
checkState(state == STATE_BUFFERING || state == STATE_READY);
|
||||
this.state = Player.STATE_IDLE;
|
||||
listeners.sendEvent(
|
||||
Player.EVENT_PLAYBACK_STATE_CHANGED,
|
||||
listener -> {
|
||||
listener.onPlayerError(error);
|
||||
listener.onPlayerErrorChanged(error);
|
||||
listener.onPlayerStateChanged(playWhenReady, state);
|
||||
listener.onPlaybackStateChanged(state);
|
||||
});
|
||||
}
|
||||
|
||||
// ExoPlayer methods. Other methods are unsupported.
|
||||
|
||||
@Override
|
||||
|
|
|
|||
|
|
@ -56,7 +56,6 @@ import com.google.ads.interactivemedia.v3.api.player.VideoAdPlayer;
|
|||
import com.google.ads.interactivemedia.v3.api.player.VideoProgressUpdate;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.ExoPlaybackException;
|
||||
import com.google.android.exoplayer2.MediaItem;
|
||||
import com.google.android.exoplayer2.PlaybackException;
|
||||
import com.google.android.exoplayer2.Player;
|
||||
import com.google.android.exoplayer2.Timeline;
|
||||
|
|
@ -276,30 +275,7 @@ public final class ImaAdsLoaderTest {
|
|||
ExoPlaybackException anException =
|
||||
ExoPlaybackException.createForSource(
|
||||
new IOException(), PlaybackException.ERROR_CODE_IO_UNSPECIFIED);
|
||||
imaAdsLoader.onPlayerErrorChanged(anException);
|
||||
imaAdsLoader.onPlayerError(anException);
|
||||
imaAdsLoader.onPositionDiscontinuity(
|
||||
new Player.PositionInfo(
|
||||
/* windowUid= */ new Object(),
|
||||
/* windowIndex= */ 0,
|
||||
/* mediaItem= */ MediaItem.fromUri("http://google.com/0"),
|
||||
/* periodUid= */ new Object(),
|
||||
/* periodIndex= */ 0,
|
||||
/* positionMs= */ 10_000,
|
||||
/* contentPositionMs= */ 0,
|
||||
/* adGroupIndex= */ -1,
|
||||
/* adIndexInAdGroup= */ -1),
|
||||
new Player.PositionInfo(
|
||||
/* windowUid= */ new Object(),
|
||||
/* windowIndex= */ 1,
|
||||
/* mediaItem= */ MediaItem.fromUri("http://google.com/1"),
|
||||
/* periodUid= */ new Object(),
|
||||
/* periodIndex= */ 0,
|
||||
/* positionMs= */ 20_000,
|
||||
/* contentPositionMs= */ 0,
|
||||
/* adGroupIndex= */ -1,
|
||||
/* adIndexInAdGroup= */ -1),
|
||||
Player.DISCONTINUITY_REASON_SEEK);
|
||||
fakePlayer.setPlayerError(anException);
|
||||
adEventListener.onAdEvent(getAdEvent(AdEventType.CONTENT_RESUME_REQUESTED, /* ad= */ null));
|
||||
imaAdsLoader.handlePrepareError(
|
||||
adsMediaSource, /* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0, new IOException());
|
||||
|
|
|
|||
|
|
@ -0,0 +1,506 @@
|
|||
/*
|
||||
* Copyright (C) 2021 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 static com.google.android.exoplayer2.testutil.FakeTimeline.TimelineWindowDefinition.DEFAULT_WINDOW_DURATION_US;
|
||||
import static com.google.android.exoplayer2.testutil.FakeTimeline.TimelineWindowDefinition.DEFAULT_WINDOW_OFFSET_IN_FIRST_PERIOD_US;
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
|
||||
import android.util.Pair;
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.Timeline;
|
||||
import com.google.android.exoplayer2.source.ads.AdPlaybackState;
|
||||
import com.google.android.exoplayer2.testutil.FakeTimeline;
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
/** Tests for {@link ImaUtil}. */
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public class ImaUtilTest {
|
||||
|
||||
@Test
|
||||
public void splitAdPlaybackStateForPeriods_emptyTimeline_emptyMapOfAdPlaybackStates() {
|
||||
AdPlaybackState adPlaybackState =
|
||||
new AdPlaybackState(/* adsId= */ "", 0, 20_000, C.TIME_END_OF_SOURCE);
|
||||
|
||||
ImmutableMap<Object, AdPlaybackState> adPlaybackStates =
|
||||
ImaUtil.splitAdPlaybackStateForPeriods(adPlaybackState, Timeline.EMPTY);
|
||||
|
||||
assertThat(adPlaybackStates).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void splitAdPlaybackStateForPeriods_singlePeriod_doesNotSplit() {
|
||||
AdPlaybackState adPlaybackState =
|
||||
new AdPlaybackState(/* adsId= */ "", 0, 20_000, C.TIME_END_OF_SOURCE);
|
||||
FakeTimeline singlePeriodTimeline =
|
||||
new FakeTimeline(
|
||||
new FakeTimeline.TimelineWindowDefinition(/* periodCount= */ 1, /* id= */ 0L));
|
||||
|
||||
ImmutableMap<Object, AdPlaybackState> adPlaybackStates =
|
||||
ImaUtil.splitAdPlaybackStateForPeriods(adPlaybackState, singlePeriodTimeline);
|
||||
|
||||
assertThat(adPlaybackStates).hasSize(1);
|
||||
assertThat(adPlaybackStates).containsEntry(new Pair<>(0L, 0), adPlaybackState);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void splitAdPlaybackStateForPeriods_livePlaceholder_isIgnored() {
|
||||
AdPlaybackState adPlaybackState =
|
||||
new AdPlaybackState(/* adsId= */ "", C.TIME_END_OF_SOURCE)
|
||||
.withIsServerSideInserted(/* adGroupIndex= */ 0, true);
|
||||
FakeTimeline singlePeriodTimeline =
|
||||
new FakeTimeline(
|
||||
new FakeTimeline.TimelineWindowDefinition(/* periodCount= */ 3, /* id= */ 0L));
|
||||
|
||||
ImmutableMap<Object, AdPlaybackState> adPlaybackStates =
|
||||
ImaUtil.splitAdPlaybackStateForPeriods(adPlaybackState, singlePeriodTimeline);
|
||||
|
||||
assertThat(adPlaybackStates).hasSize(3);
|
||||
assertThat(adPlaybackStates.get(new Pair<>(0L, 0)).adGroupCount).isEqualTo(0);
|
||||
assertThat(adPlaybackStates.get(new Pair<>(0L, 1)).adGroupCount).isEqualTo(0);
|
||||
assertThat(adPlaybackStates.get(new Pair<>(0L, 2)).adGroupCount).isEqualTo(0);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void splitAdPlaybackStateForPeriods_noAds_splitToEmptyAdPlaybackStates() {
|
||||
AdPlaybackState adPlaybackState = new AdPlaybackState(/* adsId= */ "adsId");
|
||||
FakeTimeline timeline =
|
||||
new FakeTimeline(
|
||||
new FakeTimeline.TimelineWindowDefinition(/* periodCount= */ 11, /* id= */ 0L));
|
||||
|
||||
ImmutableMap<Object, AdPlaybackState> adPlaybackStates =
|
||||
ImaUtil.splitAdPlaybackStateForPeriods(adPlaybackState, timeline);
|
||||
|
||||
assertThat(adPlaybackStates).hasSize(11);
|
||||
for (AdPlaybackState periodAdPlaybackState : adPlaybackStates.values()) {
|
||||
assertThat(periodAdPlaybackState.adsId).isEqualTo("adsId");
|
||||
assertThat(periodAdPlaybackState.adGroupCount).isEqualTo(0);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void splitAdPlaybackStateForPeriods_twoPrerollAds_splitToFirstTwoPeriods() {
|
||||
int periodCount = 4;
|
||||
long periodDurationUs = DEFAULT_WINDOW_DURATION_US / periodCount;
|
||||
AdPlaybackState adPlaybackState =
|
||||
new AdPlaybackState(/* adsId= */ "adsId", /* adGroupTimesUs... */ 0)
|
||||
.withAdCount(/* adGroupIndex= */ 0, 2)
|
||||
.withAdDurationsUs(
|
||||
/* adGroupIndex= */ 0,
|
||||
DEFAULT_WINDOW_OFFSET_IN_FIRST_PERIOD_US + periodDurationUs,
|
||||
periodDurationUs)
|
||||
.withIsServerSideInserted(/* adGroupIndex= */ 0, true);
|
||||
FakeTimeline timeline =
|
||||
new FakeTimeline(
|
||||
new FakeTimeline.TimelineWindowDefinition(
|
||||
/* periodCount= */ periodCount, /* id= */ 0L));
|
||||
|
||||
ImmutableMap<Object, AdPlaybackState> adPlaybackStates =
|
||||
ImaUtil.splitAdPlaybackStateForPeriods(adPlaybackState, timeline);
|
||||
|
||||
assertThat(adPlaybackStates).hasSize(periodCount);
|
||||
for (int i = 0; i < 2; i++) {
|
||||
Pair<Long, Integer> periodUid = new Pair<>(0L, i);
|
||||
AdPlaybackState periodAdPlaybackState = adPlaybackStates.get(periodUid);
|
||||
assertThat(periodAdPlaybackState.adGroupCount).isEqualTo(1);
|
||||
assertThat(periodAdPlaybackState.adsId).isEqualTo("adsId");
|
||||
assertThat(periodAdPlaybackState.getAdGroup(0).timeUs).isEqualTo(0);
|
||||
assertThat(periodAdPlaybackState.getAdGroup(0).isServerSideInserted).isTrue();
|
||||
assertThat(periodAdPlaybackState.getAdGroup(0).durationsUs).hasLength(1);
|
||||
int adDurationUs = i == 0 ? 125_500_000 : 2_500_000;
|
||||
assertThat(periodAdPlaybackState.getAdGroup(0).durationsUs[0]).isEqualTo(adDurationUs);
|
||||
}
|
||||
assertThat(adPlaybackStates.get(new Pair<>(0L, 2)).adGroupCount).isEqualTo(0);
|
||||
assertThat(adPlaybackStates.get(new Pair<>(0L, 3)).adGroupCount).isEqualTo(0);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void splitAdPlaybackStateForPeriods_onePrerollAdGroup_splitToFirstThreePeriods() {
|
||||
int periodCount = 4;
|
||||
long periodDurationUs = DEFAULT_WINDOW_DURATION_US / periodCount;
|
||||
AdPlaybackState adPlaybackState =
|
||||
new AdPlaybackState(/* adsId= */ "adsId", /* adGroupTimesUs... */ 0)
|
||||
.withAdCount(/* adGroupIndex= */ 0, 1)
|
||||
.withAdDurationsUs(
|
||||
/* adGroupIndex= */ 0,
|
||||
DEFAULT_WINDOW_OFFSET_IN_FIRST_PERIOD_US + 3 * periodDurationUs)
|
||||
.withIsServerSideInserted(/* adGroupIndex= */ 0, true);
|
||||
FakeTimeline timeline =
|
||||
new FakeTimeline(
|
||||
new FakeTimeline.TimelineWindowDefinition(
|
||||
/* periodCount= */ periodCount, /* id= */ 0L));
|
||||
|
||||
ImmutableMap<Object, AdPlaybackState> adPlaybackStates =
|
||||
ImaUtil.splitAdPlaybackStateForPeriods(adPlaybackState, timeline);
|
||||
|
||||
assertThat(adPlaybackStates).hasSize(periodCount);
|
||||
for (int i = 0; i < 3; i++) {
|
||||
Pair<Long, Integer> periodUid = new Pair<>(0L, i);
|
||||
AdPlaybackState periodAdPlaybackState = adPlaybackStates.get(periodUid);
|
||||
assertThat(periodAdPlaybackState.adGroupCount).isEqualTo(1);
|
||||
assertThat(periodAdPlaybackState.getAdGroup(0).durationsUs).hasLength(1);
|
||||
int adDurationUs = i == 0 ? 125_500_000 : 2_500_000;
|
||||
assertThat(periodAdPlaybackState.getAdGroup(0).durationsUs[0]).isEqualTo(adDurationUs);
|
||||
}
|
||||
assertThat(adPlaybackStates.get(new Pair<>(0L, 3)).adGroupCount).isEqualTo(0);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void splitAdPlaybackStateForPeriods_twoMidrollAds_splitToMiddleTwoPeriods() {
|
||||
int periodCount = 4;
|
||||
long periodDurationUs = DEFAULT_WINDOW_DURATION_US / periodCount;
|
||||
AdPlaybackState adPlaybackState =
|
||||
new AdPlaybackState(
|
||||
/* adsId= */ "adsId", DEFAULT_WINDOW_OFFSET_IN_FIRST_PERIOD_US + periodDurationUs)
|
||||
.withAdCount(/* adGroupIndex= */ 0, 2)
|
||||
.withAdDurationsUs(/* adGroupIndex= */ 0, periodDurationUs, periodDurationUs)
|
||||
.withIsServerSideInserted(/* adGroupIndex= */ 0, true);
|
||||
FakeTimeline timeline =
|
||||
new FakeTimeline(
|
||||
new FakeTimeline.TimelineWindowDefinition(
|
||||
/* periodCount= */ periodCount, /* id= */ 0L));
|
||||
|
||||
ImmutableMap<Object, AdPlaybackState> adPlaybackStates =
|
||||
ImaUtil.splitAdPlaybackStateForPeriods(adPlaybackState, timeline);
|
||||
|
||||
assertThat(adPlaybackStates).hasSize(periodCount);
|
||||
assertThat(adPlaybackStates.get(new Pair<>(0L, 0)).adGroupCount).isEqualTo(0);
|
||||
for (int i = 1; i < 3; i++) {
|
||||
Pair<Long, Integer> periodUid = new Pair<>(0L, i);
|
||||
AdPlaybackState periodAdPlaybackState = adPlaybackStates.get(periodUid);
|
||||
assertThat(periodAdPlaybackState.adGroupCount).isEqualTo(1);
|
||||
assertThat(periodAdPlaybackState.getAdGroup(0).timeUs).isEqualTo(0);
|
||||
assertThat(periodAdPlaybackState.getAdGroup(0).durationsUs).hasLength(1);
|
||||
assertThat(periodAdPlaybackState.getAdGroup(0).durationsUs[0]).isEqualTo(2_500_000);
|
||||
}
|
||||
assertThat(adPlaybackStates.get(new Pair<>(0L, 3)).adGroupCount).isEqualTo(0);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void splitAdPlaybackStateForPeriods_oneMidrollAdGroupOneAd_adSpansTwoPeriods() {
|
||||
int periodCount = 5;
|
||||
long periodDurationUs = DEFAULT_WINDOW_DURATION_US / periodCount;
|
||||
AdPlaybackState adPlaybackState =
|
||||
new AdPlaybackState(
|
||||
/* adsId= */ "adsId", DEFAULT_WINDOW_OFFSET_IN_FIRST_PERIOD_US + periodDurationUs)
|
||||
.withAdCount(/* adGroupIndex= */ 0, 1)
|
||||
.withAdDurationsUs(/* adGroupIndex= */ 0, 2 * periodDurationUs)
|
||||
.withIsServerSideInserted(/* adGroupIndex= */ 0, true);
|
||||
FakeTimeline timeline =
|
||||
new FakeTimeline(
|
||||
new FakeTimeline.TimelineWindowDefinition(
|
||||
/* periodCount= */ periodCount, /* id= */ 0L));
|
||||
|
||||
ImmutableMap<Object, AdPlaybackState> adPlaybackStates =
|
||||
ImaUtil.splitAdPlaybackStateForPeriods(adPlaybackState, timeline);
|
||||
|
||||
assertThat(adPlaybackStates).hasSize(periodCount);
|
||||
assertThat(adPlaybackStates.get(new Pair<>(0L, 0)).adGroupCount).isEqualTo(0);
|
||||
for (int i = 1; i < 3; i++) {
|
||||
Pair<Long, Integer> periodUid = new Pair<>(0L, i);
|
||||
AdPlaybackState periodAdPlaybackState = adPlaybackStates.get(periodUid);
|
||||
assertThat(periodAdPlaybackState.adGroupCount).isEqualTo(1);
|
||||
assertThat(periodAdPlaybackState.getAdGroup(0).durationsUs).hasLength(1);
|
||||
assertThat(periodAdPlaybackState.getAdGroup(0).durationsUs[0]).isEqualTo(2_000_000);
|
||||
}
|
||||
assertThat(adPlaybackStates.get(new Pair<>(0L, 3)).adGroupCount).isEqualTo(0);
|
||||
assertThat(adPlaybackStates.get(new Pair<>(0L, 4)).adGroupCount).isEqualTo(0);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void splitAdPlaybackStateForPeriods_oneMidrollAdGroupTwoAds_eachAdSplitsToOnePeriod() {
|
||||
int periodCount = 5;
|
||||
long periodDurationUs = DEFAULT_WINDOW_DURATION_US / periodCount;
|
||||
AdPlaybackState adPlaybackState =
|
||||
new AdPlaybackState(
|
||||
/* adsId= */ "adsId", DEFAULT_WINDOW_OFFSET_IN_FIRST_PERIOD_US + periodDurationUs)
|
||||
.withAdCount(/* adGroupIndex= */ 0, 2)
|
||||
.withAdDurationsUs(/* adGroupIndex= */ 0, periodDurationUs, periodDurationUs)
|
||||
.withIsServerSideInserted(/* adGroupIndex= */ 0, true);
|
||||
FakeTimeline timeline =
|
||||
new FakeTimeline(
|
||||
new FakeTimeline.TimelineWindowDefinition(
|
||||
/* periodCount= */ periodCount, /* id= */ 0L));
|
||||
|
||||
ImmutableMap<Object, AdPlaybackState> adPlaybackStates =
|
||||
ImaUtil.splitAdPlaybackStateForPeriods(adPlaybackState, timeline);
|
||||
|
||||
assertThat(adPlaybackStates).hasSize(periodCount);
|
||||
assertThat(adPlaybackStates.get(new Pair<>(0L, 0)).adGroupCount).isEqualTo(0);
|
||||
for (int i = 1; i < 3; i++) {
|
||||
Pair<Long, Integer> periodUid = new Pair<>(0L, i);
|
||||
AdPlaybackState periodAdPlaybackState = adPlaybackStates.get(periodUid);
|
||||
assertThat(periodAdPlaybackState.adGroupCount).isEqualTo(1);
|
||||
assertThat(periodAdPlaybackState.getAdGroup(0).durationsUs).hasLength(1);
|
||||
assertThat(periodAdPlaybackState.getAdGroup(0).durationsUs[0]).isEqualTo(2_000_000);
|
||||
}
|
||||
assertThat(adPlaybackStates.get(new Pair<>(0L, 3)).adGroupCount).isEqualTo(0);
|
||||
assertThat(adPlaybackStates.get(new Pair<>(0L, 4)).adGroupCount).isEqualTo(0);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void splitAdPlaybackStateForPeriods_twoPostrollAds_splitToLastTwoPeriods() {
|
||||
int periodCount = 4;
|
||||
long periodDurationUs = DEFAULT_WINDOW_DURATION_US / periodCount;
|
||||
AdPlaybackState adPlaybackState =
|
||||
new AdPlaybackState(
|
||||
/* adsId= */ "adsId",
|
||||
DEFAULT_WINDOW_OFFSET_IN_FIRST_PERIOD_US + 2 * periodDurationUs)
|
||||
.withAdCount(/* adGroupIndex= */ 0, 2)
|
||||
.withAdDurationsUs(/* adGroupIndex= */ 0, periodDurationUs, periodDurationUs)
|
||||
.withIsServerSideInserted(/* adGroupIndex= */ 0, true);
|
||||
FakeTimeline timeline =
|
||||
new FakeTimeline(
|
||||
new FakeTimeline.TimelineWindowDefinition(
|
||||
/* periodCount= */ periodCount, /* id= */ 0L));
|
||||
|
||||
ImmutableMap<Object, AdPlaybackState> adPlaybackStates =
|
||||
ImaUtil.splitAdPlaybackStateForPeriods(adPlaybackState, timeline);
|
||||
|
||||
assertThat(adPlaybackStates).hasSize(periodCount);
|
||||
assertThat(adPlaybackStates.get(new Pair<>(0L, 0)).adGroupCount).isEqualTo(0);
|
||||
assertThat(adPlaybackStates.get(new Pair<>(0L, 1)).adGroupCount).isEqualTo(0);
|
||||
for (int i = 2; i < periodCount; i++) {
|
||||
Pair<Long, Integer> periodUid = new Pair<>(0L, i);
|
||||
AdPlaybackState periodAdPlaybackState = adPlaybackStates.get(periodUid);
|
||||
assertThat(periodAdPlaybackState.adGroupCount).isEqualTo(1);
|
||||
assertThat(periodAdPlaybackState.getAdGroup(0).durationsUs).hasLength(1);
|
||||
assertThat(periodAdPlaybackState.getAdGroup(0).durationsUs[0]).isEqualTo(2_500_000);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void splitAdPlaybackStateForPeriods_onePostrollAdGroup_splitToLastThreePeriods() {
|
||||
int periodCount = 7;
|
||||
long periodDurationUs = DEFAULT_WINDOW_DURATION_US / periodCount;
|
||||
AdPlaybackState adPlaybackState =
|
||||
new AdPlaybackState(
|
||||
/* adsId= */ "adsId",
|
||||
DEFAULT_WINDOW_OFFSET_IN_FIRST_PERIOD_US + 4 * periodDurationUs)
|
||||
.withAdCount(/* adGroupIndex= */ 0, 1)
|
||||
.withAdDurationsUs(/* adGroupIndex= */ 0, 3 * periodDurationUs)
|
||||
.withIsServerSideInserted(/* adGroupIndex= */ 0, true);
|
||||
FakeTimeline timeline =
|
||||
new FakeTimeline(
|
||||
new FakeTimeline.TimelineWindowDefinition(
|
||||
/* periodCount= */ periodCount, /* id= */ 0L));
|
||||
|
||||
ImmutableMap<Object, AdPlaybackState> adPlaybackStates =
|
||||
ImaUtil.splitAdPlaybackStateForPeriods(adPlaybackState, timeline);
|
||||
|
||||
assertThat(adPlaybackStates).hasSize(periodCount);
|
||||
assertThat(adPlaybackStates.get(new Pair<>(0L, 0)).adGroupCount).isEqualTo(0);
|
||||
assertThat(adPlaybackStates.get(new Pair<>(0L, 1)).adGroupCount).isEqualTo(0);
|
||||
assertThat(adPlaybackStates.get(new Pair<>(0L, 2)).adGroupCount).isEqualTo(0);
|
||||
assertThat(adPlaybackStates.get(new Pair<>(0L, 3)).adGroupCount).isEqualTo(0);
|
||||
for (int i = 4; i < adPlaybackStates.size(); i++) {
|
||||
Pair<Long, Integer> periodUid = new Pair<>(0L, i);
|
||||
AdPlaybackState periodAdPlaybackState = adPlaybackStates.get(periodUid);
|
||||
assertThat(periodAdPlaybackState.adGroupCount).isEqualTo(1);
|
||||
assertThat(periodAdPlaybackState.getAdGroup(0).durationsUs).hasLength(1);
|
||||
assertThat(periodAdPlaybackState.getAdGroup(0).durationsUs[0]).isEqualTo(periodDurationUs);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void splitAdPlaybackStateForPeriods_preMidAndPostrollAdGroup_splitCorrectly() {
|
||||
int periodCount = 11;
|
||||
long periodDurationUs = DEFAULT_WINDOW_DURATION_US / periodCount;
|
||||
AdPlaybackState adPlaybackState =
|
||||
new AdPlaybackState(/* adsId= */ "adsId", 0, (2 * periodDurationUs), (5 * periodDurationUs))
|
||||
.withAdCount(/* adGroupIndex= */ 0, 2)
|
||||
.withAdDurationsUs(
|
||||
/* adGroupIndex= */ 0,
|
||||
DEFAULT_WINDOW_OFFSET_IN_FIRST_PERIOD_US + periodDurationUs,
|
||||
periodDurationUs)
|
||||
.withIsServerSideInserted(/* adGroupIndex= */ 0, true)
|
||||
.withAdCount(/* adGroupIndex= */ 1, 2)
|
||||
.withAdDurationsUs(/* adGroupIndex= */ 1, periodDurationUs, periodDurationUs)
|
||||
.withIsServerSideInserted(/* adGroupIndex= */ 1, true)
|
||||
.withAdCount(/* adGroupIndex= */ 2, 2)
|
||||
.withAdDurationsUs(/* adGroupIndex= */ 2, periodDurationUs, periodDurationUs)
|
||||
.withIsServerSideInserted(/* adGroupIndex= */ 2, true);
|
||||
FakeTimeline timeline =
|
||||
new FakeTimeline(
|
||||
new FakeTimeline.TimelineWindowDefinition(
|
||||
/* periodCount= */ periodCount, /* id= */ 0L));
|
||||
|
||||
ImmutableMap<Object, AdPlaybackState> adPlaybackStates =
|
||||
ImaUtil.splitAdPlaybackStateForPeriods(adPlaybackState, timeline);
|
||||
|
||||
assertThat(adPlaybackStates).hasSize(periodCount);
|
||||
assertThat(adPlaybackStates.get(new Pair<>(0L, 0)).adGroupCount).isEqualTo(1);
|
||||
assertThat(adPlaybackStates.get(new Pair<>(0L, 1)).adGroupCount).isEqualTo(1);
|
||||
assertThat(adPlaybackStates.get(new Pair<>(0L, 2)).adGroupCount).isEqualTo(0);
|
||||
assertThat(adPlaybackStates.get(new Pair<>(0L, 3)).adGroupCount).isEqualTo(0);
|
||||
assertThat(adPlaybackStates.get(new Pair<>(0L, 4)).adGroupCount).isEqualTo(1);
|
||||
assertThat(adPlaybackStates.get(new Pair<>(0L, 5)).adGroupCount).isEqualTo(1);
|
||||
assertThat(adPlaybackStates.get(new Pair<>(0L, 6)).adGroupCount).isEqualTo(0);
|
||||
assertThat(adPlaybackStates.get(new Pair<>(0L, 7)).adGroupCount).isEqualTo(0);
|
||||
assertThat(adPlaybackStates.get(new Pair<>(0L, 8)).adGroupCount).isEqualTo(0);
|
||||
assertThat(adPlaybackStates.get(new Pair<>(0L, 9)).adGroupCount).isEqualTo(1);
|
||||
assertThat(adPlaybackStates.get(new Pair<>(0L, 10)).adGroupCount).isEqualTo(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void splitAdPlaybackStateForPeriods_midAndPostrollAdGroup_splitCorrectly() {
|
||||
int periodCount = 9;
|
||||
long periodDurationUs = DEFAULT_WINDOW_DURATION_US / periodCount;
|
||||
AdPlaybackState adPlaybackState =
|
||||
new AdPlaybackState(
|
||||
/* adsId= */ "adsId",
|
||||
DEFAULT_WINDOW_OFFSET_IN_FIRST_PERIOD_US + (2 * periodDurationUs),
|
||||
DEFAULT_WINDOW_OFFSET_IN_FIRST_PERIOD_US + (5 * periodDurationUs))
|
||||
.withAdCount(/* adGroupIndex= */ 0, 2)
|
||||
.withAdDurationsUs(/* adGroupIndex= */ 0, periodDurationUs, periodDurationUs)
|
||||
.withIsServerSideInserted(/* adGroupIndex= */ 0, true)
|
||||
.withAdCount(/* adGroupIndex= */ 1, 2)
|
||||
.withAdDurationsUs(/* adGroupIndex= */ 1, periodDurationUs, periodDurationUs)
|
||||
.withIsServerSideInserted(/* adGroupIndex= */ 1, true);
|
||||
FakeTimeline timeline =
|
||||
new FakeTimeline(
|
||||
new FakeTimeline.TimelineWindowDefinition(
|
||||
/* periodCount= */ periodCount, /* id= */ 0L));
|
||||
|
||||
ImmutableMap<Object, AdPlaybackState> adPlaybackStates =
|
||||
ImaUtil.splitAdPlaybackStateForPeriods(adPlaybackState, timeline);
|
||||
|
||||
assertThat(adPlaybackStates).hasSize(periodCount);
|
||||
assertThat(adPlaybackStates.get(new Pair<>(0L, 0)).adGroupCount).isEqualTo(0);
|
||||
assertThat(adPlaybackStates.get(new Pair<>(0L, 1)).adGroupCount).isEqualTo(0);
|
||||
assertThat(adPlaybackStates.get(new Pair<>(0L, 2)).adGroupCount).isEqualTo(1);
|
||||
assertThat(adPlaybackStates.get(new Pair<>(0L, 3)).adGroupCount).isEqualTo(1);
|
||||
assertThat(adPlaybackStates.get(new Pair<>(0L, 4)).adGroupCount).isEqualTo(0);
|
||||
assertThat(adPlaybackStates.get(new Pair<>(0L, 5)).adGroupCount).isEqualTo(0);
|
||||
assertThat(adPlaybackStates.get(new Pair<>(0L, 6)).adGroupCount).isEqualTo(0);
|
||||
assertThat(adPlaybackStates.get(new Pair<>(0L, 7)).adGroupCount).isEqualTo(1);
|
||||
assertThat(adPlaybackStates.get(new Pair<>(0L, 8)).adGroupCount).isEqualTo(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void splitAdPlaybackStateForPeriods_correctAdsIdInSplitPlaybackStates() {
|
||||
int periodCount = 4;
|
||||
long periodDurationUs = DEFAULT_WINDOW_DURATION_US / periodCount;
|
||||
AdPlaybackState adPlaybackState =
|
||||
new AdPlaybackState(
|
||||
/* adsId= */ "adsId",
|
||||
DEFAULT_WINDOW_OFFSET_IN_FIRST_PERIOD_US + 2 * periodDurationUs)
|
||||
.withAdCount(/* adGroupIndex= */ 0, 1)
|
||||
.withAdDurationsUs(/* adGroupIndex= */ 0, 2 * periodDurationUs)
|
||||
.withIsServerSideInserted(/* adGroupIndex= */ 0, true);
|
||||
FakeTimeline timeline =
|
||||
new FakeTimeline(
|
||||
new FakeTimeline.TimelineWindowDefinition(
|
||||
/* periodCount= */ periodCount, /* id= */ 0L));
|
||||
|
||||
ImmutableMap<Object, AdPlaybackState> adPlaybackStates =
|
||||
ImaUtil.splitAdPlaybackStateForPeriods(adPlaybackState, timeline);
|
||||
|
||||
for (int i = 0; i < adPlaybackStates.size(); i++) {
|
||||
assertThat(adPlaybackStates.get(new Pair<>(0L, i)).adsId).isEqualTo("adsId");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void splitAdPlaybackStateForPeriods_correctAdPlaybackStates() {
|
||||
int periodCount = 7;
|
||||
long periodDurationUs = DEFAULT_WINDOW_DURATION_US / periodCount;
|
||||
AdPlaybackState adPlaybackState =
|
||||
new AdPlaybackState(/* adsId= */ "adsId", 0)
|
||||
.withAdCount(/* adGroupIndex= */ 0, periodCount)
|
||||
.withAdDurationsUs(
|
||||
/* adGroupIndex= */ 0, /* adDurationsUs...= */
|
||||
DEFAULT_WINDOW_OFFSET_IN_FIRST_PERIOD_US + periodDurationUs,
|
||||
periodDurationUs,
|
||||
periodDurationUs,
|
||||
periodDurationUs,
|
||||
periodDurationUs,
|
||||
periodDurationUs,
|
||||
periodDurationUs)
|
||||
.withPlayedAd(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0)
|
||||
.withSkippedAd(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 1)
|
||||
.withAdLoadError(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 2)
|
||||
.withIsServerSideInserted(/* adGroupIndex= */ 0, true);
|
||||
FakeTimeline timeline =
|
||||
new FakeTimeline(
|
||||
new FakeTimeline.TimelineWindowDefinition(
|
||||
/* periodCount= */ periodCount, /* id= */ 0L));
|
||||
|
||||
ImmutableMap<Object, AdPlaybackState> adPlaybackStates =
|
||||
ImaUtil.splitAdPlaybackStateForPeriods(adPlaybackState, timeline);
|
||||
|
||||
assertThat(adPlaybackStates.get(new Pair<>(0L, 0)).getAdGroup(/* adGroupIndex= */ 0).states[0])
|
||||
.isEqualTo(AdPlaybackState.AD_STATE_PLAYED);
|
||||
assertThat(adPlaybackStates.get(new Pair<>(0L, 1)).getAdGroup(/* adGroupIndex= */ 0).states[0])
|
||||
.isEqualTo(AdPlaybackState.AD_STATE_SKIPPED);
|
||||
assertThat(adPlaybackStates.get(new Pair<>(0L, 2)).getAdGroup(/* adGroupIndex= */ 0).states[0])
|
||||
.isEqualTo(AdPlaybackState.AD_STATE_ERROR);
|
||||
assertThat(adPlaybackStates.get(new Pair<>(0L, 3)).getAdGroup(/* adGroupIndex= */ 0).states[0])
|
||||
.isEqualTo(AdPlaybackState.AD_STATE_UNAVAILABLE);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void splitAdPlaybackStateForPeriods_lateMidrollAdGroupStartTimeUs_adGroupIgnored() {
|
||||
int periodCount = 4;
|
||||
long periodDurationUs = DEFAULT_WINDOW_DURATION_US / periodCount;
|
||||
AdPlaybackState adPlaybackState =
|
||||
new AdPlaybackState(
|
||||
/* adsId= */ "adsId",
|
||||
DEFAULT_WINDOW_OFFSET_IN_FIRST_PERIOD_US + periodDurationUs + 1)
|
||||
.withAdCount(/* adGroupIndex= */ 0, 1)
|
||||
.withAdDurationsUs(/* adGroupIndex= */ 0, /* adDurationsUs...= */ periodDurationUs)
|
||||
.withIsServerSideInserted(/* adGroupIndex= */ 0, true);
|
||||
FakeTimeline timeline =
|
||||
new FakeTimeline(
|
||||
new FakeTimeline.TimelineWindowDefinition(
|
||||
/* periodCount= */ periodCount, /* id= */ 0L));
|
||||
|
||||
ImmutableMap<Object, AdPlaybackState> adPlaybackStates =
|
||||
ImaUtil.splitAdPlaybackStateForPeriods(adPlaybackState, timeline);
|
||||
|
||||
assertThat(adPlaybackStates).hasSize(periodCount);
|
||||
for (AdPlaybackState periodAdPlaybackState : adPlaybackStates.values()) {
|
||||
assertThat(periodAdPlaybackState.adGroupCount).isEqualTo(0);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void splitAdPlaybackStateForPeriods_earlyMidrollAdGroupStartTimeUs_adGroupIgnored() {
|
||||
int periodCount = 4;
|
||||
long periodDurationUs = DEFAULT_WINDOW_DURATION_US / periodCount;
|
||||
AdPlaybackState adPlaybackState =
|
||||
new AdPlaybackState(/* adsId= */ "adsId", periodDurationUs - 1)
|
||||
.withAdCount(/* adGroupIndex= */ 0, 1)
|
||||
.withAdDurationsUs(/* adGroupIndex= */ 0, /* adDurationsUs...= */ periodDurationUs)
|
||||
.withIsServerSideInserted(/* adGroupIndex= */ 0, true);
|
||||
FakeTimeline timeline =
|
||||
new FakeTimeline(
|
||||
new FakeTimeline.TimelineWindowDefinition(
|
||||
/* periodCount= */ periodCount, /* id= */ 0L));
|
||||
|
||||
ImmutableMap<Object, AdPlaybackState> adPlaybackStates =
|
||||
ImaUtil.splitAdPlaybackStateForPeriods(adPlaybackState, timeline);
|
||||
|
||||
assertThat(adPlaybackStates).hasSize(periodCount);
|
||||
for (AdPlaybackState periodAdPlaybackState : adPlaybackStates.values()) {
|
||||
assertThat(periodAdPlaybackState.adGroupCount).isEqualTo(0);
|
||||
assertThat(periodAdPlaybackState.adsId).isEqualTo("adsId");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -466,6 +466,8 @@ public final class MediaSessionConnector {
|
|||
private long enabledPlaybackActions;
|
||||
private boolean metadataDeduplicationEnabled;
|
||||
private boolean dispatchUnsupportedActionsEnabled;
|
||||
private boolean clearMediaItemsOnStop;
|
||||
private boolean mapIdleToStopped;
|
||||
|
||||
/**
|
||||
* Creates an instance.
|
||||
|
|
@ -486,6 +488,7 @@ public final class MediaSessionConnector {
|
|||
enabledPlaybackActions = DEFAULT_PLAYBACK_ACTIONS;
|
||||
mediaSession.setFlags(BASE_MEDIA_SESSION_FLAGS);
|
||||
mediaSession.setCallback(componentListener, new Handler(looper));
|
||||
clearMediaItemsOnStop = true;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -699,6 +702,23 @@ public final class MediaSessionConnector {
|
|||
this.dispatchUnsupportedActionsEnabled = dispatchUnsupportedActionsEnabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets whether media items are cleared from the playlist when a client sends a {@link
|
||||
* MediaControllerCompat.TransportControls#stop()} command.
|
||||
*/
|
||||
public void setClearMediaItemsOnStop(boolean clearMediaItemsOnStop) {
|
||||
this.clearMediaItemsOnStop = clearMediaItemsOnStop;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets whether {@link Player#STATE_IDLE} should be mapped to {@link
|
||||
* PlaybackStateCompat#STATE_STOPPED}. The default is false {@link Player#STATE_IDLE} which maps
|
||||
* to {@link PlaybackStateCompat#STATE_NONE}.
|
||||
*/
|
||||
public void setMapStateIdleToSessionStateStopped(boolean mapIdleToStopped) {
|
||||
this.mapIdleToStopped = mapIdleToStopped;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets whether {@link MediaMetadataProvider#sameAs(MediaMetadataCompat, MediaMetadataCompat)}
|
||||
* should be consulted before calling {@link MediaSessionCompat#setMetadata(MediaMetadataCompat)}.
|
||||
|
|
@ -974,7 +994,7 @@ public final class MediaSessionConnector {
|
|||
player.seekTo(mediaItemIndex, positionMs);
|
||||
}
|
||||
|
||||
private static int getMediaSessionPlaybackState(
|
||||
private int getMediaSessionPlaybackState(
|
||||
@Player.State int exoPlayerPlaybackState, boolean playWhenReady) {
|
||||
switch (exoPlayerPlaybackState) {
|
||||
case Player.STATE_BUFFERING:
|
||||
|
|
@ -987,7 +1007,9 @@ public final class MediaSessionConnector {
|
|||
return PlaybackStateCompat.STATE_STOPPED;
|
||||
case Player.STATE_IDLE:
|
||||
default:
|
||||
return PlaybackStateCompat.STATE_NONE;
|
||||
return mapIdleToStopped
|
||||
? PlaybackStateCompat.STATE_STOPPED
|
||||
: PlaybackStateCompat.STATE_NONE;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1209,7 +1231,9 @@ public final class MediaSessionConnector {
|
|||
public void onStop() {
|
||||
if (canDispatchPlaybackAction(PlaybackStateCompat.ACTION_STOP)) {
|
||||
player.stop();
|
||||
player.clearMediaItems();
|
||||
if (clearMediaItemsOnStop) {
|
||||
player.clearMediaItems();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ public final class OpusLibrary {
|
|||
}
|
||||
|
||||
private static final LibraryLoader LOADER = new LibraryLoader("opusV2JNI");
|
||||
@C.CryptoType private static int cryptoType = C.CRYPTO_TYPE_UNSUPPORTED;
|
||||
private static @C.CryptoType int cryptoType = C.CRYPTO_TYPE_UNSUPPORTED;
|
||||
|
||||
private OpusLibrary() {}
|
||||
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ class CombinedJavadocPlugin implements Plugin<Project> {
|
|||
|
||||
// Dackka snapshots are listed at https://androidx.dev/dackka/builds.
|
||||
static final String DACKKA_JAR_URL =
|
||||
"https://androidx.dev/dackka/builds/7758117/artifacts/dackka-0.0.10.jar"
|
||||
"https://androidx.dev/dackka/builds/8003564/artifacts/dackka-0.0.14.jar"
|
||||
|
||||
@Override
|
||||
void apply(Project project) {
|
||||
|
|
@ -67,12 +67,12 @@ class CombinedJavadocPlugin implements Plugin<Project> {
|
|||
}
|
||||
}
|
||||
|
||||
def dackkaOutputDir = project.file("$project.buildDir/docs/dackka")
|
||||
project.task(DACKKA_TASK_NAME, type: JavaExec) {
|
||||
doFirst {
|
||||
// Recreate the output directory to remove any leftover files from a previous run.
|
||||
def outputDir = project.file("$project.buildDir/docs/dackka")
|
||||
project.delete outputDir
|
||||
project.mkdir outputDir
|
||||
project.delete dackkaOutputDir
|
||||
project.mkdir dackkaOutputDir
|
||||
|
||||
// Download the Dackka JAR.
|
||||
new URL(DACKKA_JAR_URL).withInputStream {
|
||||
|
|
@ -104,15 +104,27 @@ class CombinedJavadocPlugin implements Plugin<Project> {
|
|||
.filter({ f -> project.file(f).exists() }).join(";")
|
||||
def dependenciesString = project.files(dependencies).asPath.replace(':', ';')
|
||||
args("-moduleName", "",
|
||||
"-outputDir", "$outputDir",
|
||||
"-outputDir", "$dackkaOutputDir",
|
||||
"-globalLinks", "$globalLinksString",
|
||||
"-loggingLevel", "WARN",
|
||||
"-sourceSet", "-src $sourcesString -classpath $dependenciesString",
|
||||
"-offlineMode")
|
||||
environment("DEVSITE_TENANT", "androidx")
|
||||
environment("DEVSITE_TENANT", "androidx/media3")
|
||||
}
|
||||
description = "Generates combined javadoc for developer.android.com."
|
||||
classpath = project.files(new File(getTemporaryDir(), "dackka.jar"))
|
||||
doLast {
|
||||
libraryModules.each { libraryModule ->
|
||||
project.copy {
|
||||
from "${libraryModule.projectDir}/src/main/javadoc"
|
||||
into "${dackkaOutputDir}/reference/"
|
||||
}
|
||||
project.copy {
|
||||
from "${libraryModule.projectDir}/src/main/javadoc"
|
||||
into "${dackkaOutputDir}/reference/kotlin/"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ dependencies {
|
|||
// (but declared as runtime deps) [internal b/168188131].
|
||||
exclude group: 'com.google.code.findbugs', module: 'jsr305'
|
||||
exclude group: 'org.checkerframework', module: 'checker-compat-qual'
|
||||
exclude group: 'org.checkerframework', module: 'checker-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'
|
||||
|
|
|
|||
|
|
@ -382,37 +382,6 @@ public abstract class BasePlayer implements Player {
|
|||
: timeline.getWindow(getCurrentMediaItemIndex(), window).getDurationMs();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the {@link Commands} available in the player.
|
||||
*
|
||||
* @param permanentAvailableCommands The commands permanently available in the player.
|
||||
* @return The available {@link Commands}.
|
||||
*/
|
||||
protected Commands getAvailableCommands(Commands permanentAvailableCommands) {
|
||||
return new Commands.Builder()
|
||||
.addAll(permanentAvailableCommands)
|
||||
.addIf(COMMAND_SEEK_TO_DEFAULT_POSITION, !isPlayingAd())
|
||||
.addIf(COMMAND_SEEK_IN_CURRENT_MEDIA_ITEM, isCurrentMediaItemSeekable() && !isPlayingAd())
|
||||
.addIf(COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM, hasPreviousMediaItem() && !isPlayingAd())
|
||||
.addIf(
|
||||
COMMAND_SEEK_TO_PREVIOUS,
|
||||
!getCurrentTimeline().isEmpty()
|
||||
&& (hasPreviousMediaItem()
|
||||
|| !isCurrentMediaItemLive()
|
||||
|| isCurrentMediaItemSeekable())
|
||||
&& !isPlayingAd())
|
||||
.addIf(COMMAND_SEEK_TO_NEXT_MEDIA_ITEM, hasNextMediaItem() && !isPlayingAd())
|
||||
.addIf(
|
||||
COMMAND_SEEK_TO_NEXT,
|
||||
!getCurrentTimeline().isEmpty()
|
||||
&& (hasNextMediaItem() || (isCurrentMediaItemLive() && isCurrentMediaItemDynamic()))
|
||||
&& !isPlayingAd())
|
||||
.addIf(COMMAND_SEEK_TO_MEDIA_ITEM, !isPlayingAd())
|
||||
.addIf(COMMAND_SEEK_BACK, isCurrentMediaItemSeekable() && !isPlayingAd())
|
||||
.addIf(COMMAND_SEEK_FORWARD, isCurrentMediaItemSeekable() && !isPlayingAd())
|
||||
.build();
|
||||
}
|
||||
|
||||
@RepeatMode
|
||||
private int getRepeatModeForNavigation() {
|
||||
@RepeatMode int repeatMode = getRepeatMode();
|
||||
|
|
|
|||
|
|
@ -272,6 +272,20 @@ public final class C {
|
|||
/** @see AudioFormat#ENCODING_DOLBY_TRUEHD */
|
||||
public static final int ENCODING_DOLBY_TRUEHD = AudioFormat.ENCODING_DOLBY_TRUEHD;
|
||||
|
||||
/** Represents the behavior affecting whether spatialization will be used. */
|
||||
@Documented
|
||||
@Retention(RetentionPolicy.SOURCE)
|
||||
@Target({FIELD, METHOD, PARAMETER, LOCAL_VARIABLE, TYPE_USE})
|
||||
@IntDef({SPATIALIZATION_BEHAVIOR_AUTO, SPATIALIZATION_BEHAVIOR_NEVER})
|
||||
public @interface SpatializationBehavior {}
|
||||
|
||||
// TODO[b/190759307]: Update constant values and javadoc to use SDK once compile SDK target is set
|
||||
// to 32.
|
||||
/** See AudioAttributes#SPATIALIZATION_BEHAVIOR_AUTO */
|
||||
public static final int SPATIALIZATION_BEHAVIOR_AUTO = 0;
|
||||
/** See AudioAttributes#SPATIALIZATION_BEHAVIOR_NEVER */
|
||||
public static final int SPATIALIZATION_BEHAVIOR_NEVER = 1;
|
||||
|
||||
/**
|
||||
* Stream types for an {@link android.media.AudioTrack}. One of {@link #STREAM_TYPE_ALARM}, {@link
|
||||
* #STREAM_TYPE_DTMF}, {@link #STREAM_TYPE_MUSIC}, {@link #STREAM_TYPE_NOTIFICATION}, {@link
|
||||
|
|
@ -590,6 +604,7 @@ public final class C {
|
|||
flag = true,
|
||||
value = {SELECTION_FLAG_DEFAULT, SELECTION_FLAG_FORCED, SELECTION_FLAG_AUTOSELECT})
|
||||
public @interface SelectionFlags {}
|
||||
// LINT.IfChange(selection_flags)
|
||||
/** Indicates that the track should be selected if user preferences do not state otherwise. */
|
||||
public static final int SELECTION_FLAG_DEFAULT = 1;
|
||||
/**
|
||||
|
|
@ -1055,6 +1070,7 @@ public final class C {
|
|||
ROLE_FLAG_TRICK_PLAY
|
||||
})
|
||||
public @interface RoleFlags {}
|
||||
// LINT.IfChange(role_flags)
|
||||
/** Indicates a main track. */
|
||||
public static final int ROLE_FLAG_MAIN = 1;
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -631,7 +631,8 @@ public final class Format implements Bundleable {
|
|||
* <ul>
|
||||
* <li>DASH representations: Always {@link Format#NO_VALUE}.
|
||||
* <li>HLS variants: The {@code AVERAGE-BANDWIDTH} attribute defined on the corresponding {@code
|
||||
* EXT-X-STREAM-INF} tag in the master playlist, or {@link Format#NO_VALUE} if not present.
|
||||
* EXT-X-STREAM-INF} tag in the multivariant playlist, or {@link Format#NO_VALUE} if not
|
||||
* present.
|
||||
* <li>SmoothStreaming track elements: The {@code Bitrate} attribute defined on the
|
||||
* corresponding {@code TrackElement} in the manifest, or {@link Format#NO_VALUE} if not
|
||||
* present.
|
||||
|
|
@ -1300,7 +1301,9 @@ public final class Format implements Bundleable {
|
|||
schemes.add("unknown (" + schemeUuid + ")");
|
||||
}
|
||||
}
|
||||
builder.append(", drm=[").append(Joiner.on(',').join(schemes)).append(']');
|
||||
builder.append(", drm=[");
|
||||
Joiner.on(',').appendTo(builder, schemes);
|
||||
builder.append(']');
|
||||
}
|
||||
if (format.width != NO_VALUE && format.height != NO_VALUE) {
|
||||
builder.append(", res=").append(format.width).append("x").append(format.height);
|
||||
|
|
@ -1320,8 +1323,73 @@ public final class Format implements Bundleable {
|
|||
if (format.label != null) {
|
||||
builder.append(", label=").append(format.label);
|
||||
}
|
||||
if ((format.roleFlags & C.ROLE_FLAG_TRICK_PLAY) != 0) {
|
||||
builder.append(", trick-play-track");
|
||||
if (format.selectionFlags != 0) {
|
||||
List<String> selectionFlags = new ArrayList<>();
|
||||
// LINT.IfChange(selection_flags)
|
||||
if ((format.selectionFlags & C.SELECTION_FLAG_AUTOSELECT) != 0) {
|
||||
selectionFlags.add("auto");
|
||||
}
|
||||
if ((format.selectionFlags & C.SELECTION_FLAG_DEFAULT) != 0) {
|
||||
selectionFlags.add("default");
|
||||
}
|
||||
if ((format.selectionFlags & C.SELECTION_FLAG_FORCED) != 0) {
|
||||
selectionFlags.add("forced");
|
||||
}
|
||||
builder.append(", selectionFlags=[");
|
||||
Joiner.on(',').appendTo(builder, selectionFlags);
|
||||
builder.append("]");
|
||||
}
|
||||
if (format.roleFlags != 0) {
|
||||
// LINT.IfChange(role_flags)
|
||||
List<String> roleFlags = new ArrayList<>();
|
||||
if ((format.roleFlags & C.ROLE_FLAG_MAIN) != 0) {
|
||||
roleFlags.add("main");
|
||||
}
|
||||
if ((format.roleFlags & C.ROLE_FLAG_ALTERNATE) != 0) {
|
||||
roleFlags.add("alt");
|
||||
}
|
||||
if ((format.roleFlags & C.ROLE_FLAG_SUPPLEMENTARY) != 0) {
|
||||
roleFlags.add("supplementary");
|
||||
}
|
||||
if ((format.roleFlags & C.ROLE_FLAG_COMMENTARY) != 0) {
|
||||
roleFlags.add("commentary");
|
||||
}
|
||||
if ((format.roleFlags & C.ROLE_FLAG_DUB) != 0) {
|
||||
roleFlags.add("dub");
|
||||
}
|
||||
if ((format.roleFlags & C.ROLE_FLAG_EMERGENCY) != 0) {
|
||||
roleFlags.add("emergency");
|
||||
}
|
||||
if ((format.roleFlags & C.ROLE_FLAG_CAPTION) != 0) {
|
||||
roleFlags.add("caption");
|
||||
}
|
||||
if ((format.roleFlags & C.ROLE_FLAG_SUBTITLE) != 0) {
|
||||
roleFlags.add("subtitle");
|
||||
}
|
||||
if ((format.roleFlags & C.ROLE_FLAG_SIGN) != 0) {
|
||||
roleFlags.add("sign");
|
||||
}
|
||||
if ((format.roleFlags & C.ROLE_FLAG_DESCRIBES_VIDEO) != 0) {
|
||||
roleFlags.add("describes-video");
|
||||
}
|
||||
if ((format.roleFlags & C.ROLE_FLAG_DESCRIBES_MUSIC_AND_SOUND) != 0) {
|
||||
roleFlags.add("describes-music");
|
||||
}
|
||||
if ((format.roleFlags & C.ROLE_FLAG_ENHANCED_DIALOG_INTELLIGIBILITY) != 0) {
|
||||
roleFlags.add("enhanced-intelligibility");
|
||||
}
|
||||
if ((format.roleFlags & C.ROLE_FLAG_TRANSCRIBES_DIALOG) != 0) {
|
||||
roleFlags.add("transcribes-dialog");
|
||||
}
|
||||
if ((format.roleFlags & C.ROLE_FLAG_EASY_TO_READ) != 0) {
|
||||
roleFlags.add("easy-read");
|
||||
}
|
||||
if ((format.roleFlags & C.ROLE_FLAG_TRICK_PLAY) != 0) {
|
||||
roleFlags.add("trick-play");
|
||||
}
|
||||
builder.append(", roleFlags=[");
|
||||
Joiner.on(',').appendTo(builder, roleFlags);
|
||||
builder.append("]");
|
||||
}
|
||||
return builder.toString();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1238,6 +1238,7 @@ public final class MediaItem implements Bundleable {
|
|||
private @C.SelectionFlags int selectionFlags;
|
||||
private @C.RoleFlags int roleFlags;
|
||||
@Nullable private String label;
|
||||
@Nullable private String id;
|
||||
|
||||
/**
|
||||
* Constructs an instance.
|
||||
|
|
@ -1255,6 +1256,7 @@ public final class MediaItem implements Bundleable {
|
|||
this.selectionFlags = subtitleConfiguration.selectionFlags;
|
||||
this.roleFlags = subtitleConfiguration.roleFlags;
|
||||
this.label = subtitleConfiguration.label;
|
||||
this.id = subtitleConfiguration.id;
|
||||
}
|
||||
|
||||
/** Sets the {@link Uri} to the subtitle file. */
|
||||
|
|
@ -1293,6 +1295,12 @@ public final class MediaItem implements Bundleable {
|
|||
return this;
|
||||
}
|
||||
|
||||
/** Sets the optional ID for this subtitle track. */
|
||||
public Builder setId(@Nullable String id) {
|
||||
this.id = id;
|
||||
return this;
|
||||
}
|
||||
|
||||
/** Creates a {@link SubtitleConfiguration} from the values of this builder. */
|
||||
public SubtitleConfiguration build() {
|
||||
return new SubtitleConfiguration(this);
|
||||
|
|
@ -1315,20 +1323,27 @@ public final class MediaItem implements Bundleable {
|
|||
public final @C.RoleFlags int roleFlags;
|
||||
/** The label. */
|
||||
@Nullable public final String label;
|
||||
/**
|
||||
* The ID of the subtitles. This will be propagated to the {@link Format#id} of the subtitle
|
||||
* track created from this configuration.
|
||||
*/
|
||||
@Nullable public final String id;
|
||||
|
||||
private SubtitleConfiguration(
|
||||
Uri uri,
|
||||
String mimeType,
|
||||
@Nullable String language,
|
||||
@C.SelectionFlags int selectionFlags,
|
||||
@C.RoleFlags int roleFlags,
|
||||
@Nullable String label) {
|
||||
int selectionFlags,
|
||||
int roleFlags,
|
||||
@Nullable String label,
|
||||
@Nullable String id) {
|
||||
this.uri = uri;
|
||||
this.mimeType = mimeType;
|
||||
this.language = language;
|
||||
this.selectionFlags = selectionFlags;
|
||||
this.roleFlags = roleFlags;
|
||||
this.label = label;
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
private SubtitleConfiguration(Builder builder) {
|
||||
|
|
@ -1338,6 +1353,7 @@ public final class MediaItem implements Bundleable {
|
|||
this.selectionFlags = builder.selectionFlags;
|
||||
this.roleFlags = builder.roleFlags;
|
||||
this.label = builder.label;
|
||||
this.id = builder.id;
|
||||
}
|
||||
|
||||
/** Returns a {@link Builder} initialized with the values of this instance. */
|
||||
|
|
@ -1361,7 +1377,8 @@ public final class MediaItem implements Bundleable {
|
|||
&& Util.areEqual(language, other.language)
|
||||
&& selectionFlags == other.selectionFlags
|
||||
&& roleFlags == other.roleFlags
|
||||
&& Util.areEqual(label, other.label);
|
||||
&& Util.areEqual(label, other.label)
|
||||
&& Util.areEqual(id, other.id);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -1372,6 +1389,7 @@ public final class MediaItem implements Bundleable {
|
|||
result = 31 * result + selectionFlags;
|
||||
result = 31 * result + roleFlags;
|
||||
result = 31 * result + (label == null ? 0 : label.hashCode());
|
||||
result = 31 * result + (id == null ? 0 : id.hashCode());
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
|
@ -1402,7 +1420,7 @@ public final class MediaItem implements Bundleable {
|
|||
@C.SelectionFlags int selectionFlags,
|
||||
@C.RoleFlags int roleFlags,
|
||||
@Nullable String label) {
|
||||
super(uri, mimeType, language, selectionFlags, roleFlags, label);
|
||||
super(uri, mimeType, language, selectionFlags, roleFlags, label, /* id= */ null);
|
||||
}
|
||||
|
||||
private Subtitle(Builder builder) {
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@
|
|||
*/
|
||||
package com.google.android.exoplayer2;
|
||||
|
||||
import static com.google.android.exoplayer2.source.ads.AdPlaybackState.AD_STATE_UNAVAILABLE;
|
||||
import static com.google.android.exoplayer2.util.Assertions.checkArgument;
|
||||
import static com.google.android.exoplayer2.util.Assertions.checkState;
|
||||
import static java.lang.Math.max;
|
||||
|
|
@ -815,6 +816,21 @@ public abstract class Timeline implements Bundleable {
|
|||
return adGroup.count != C.LENGTH_UNSET ? adGroup.durationsUs[adIndexInAdGroup] : C.TIME_UNSET;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the state of the ad at index {@code adIndexInAdGroup} in the ad group at {@code
|
||||
* adGroupIndex}, or {@link AdPlaybackState#AD_STATE_UNAVAILABLE} if not yet known.
|
||||
*
|
||||
* @param adGroupIndex The ad group index.
|
||||
* @return The state of the ad, or {@link AdPlaybackState#AD_STATE_UNAVAILABLE} if not yet
|
||||
* known.
|
||||
*/
|
||||
public int getAdState(int adGroupIndex, int adIndexInAdGroup) {
|
||||
AdPlaybackState.AdGroup adGroup = adPlaybackState.getAdGroup(adGroupIndex);
|
||||
return adGroup.count != C.LENGTH_UNSET
|
||||
? adGroup.states[adIndexInAdGroup]
|
||||
: AD_STATE_UNAVAILABLE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the position offset in the first unplayed ad at which to begin playback, in
|
||||
* microseconds.
|
||||
|
|
@ -1174,13 +1190,13 @@ public abstract class Timeline implements Bundleable {
|
|||
}
|
||||
|
||||
/**
|
||||
* Calls {@link #getPeriodPosition(Window, Period, int, long, long)} with a zero default position
|
||||
* Calls {@link #getPeriodPositionUs(Window, Period, int, long)} with a zero default position
|
||||
* projection.
|
||||
*/
|
||||
public final Pair<Object, Long> getPeriodPositionUs(
|
||||
Window window, Period period, int windowIndex, long windowPositionUs) {
|
||||
return Assertions.checkNotNull(
|
||||
getPeriodPosition(
|
||||
getPeriodPositionUs(
|
||||
window, period, windowIndex, windowPositionUs, /* defaultPositionProjectionUs= */ 0));
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@
|
|||
package com.google.android.exoplayer2.audio;
|
||||
|
||||
import android.os.Bundle;
|
||||
import androidx.annotation.DoNotInline;
|
||||
import androidx.annotation.IntDef;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.RequiresApi;
|
||||
|
|
@ -25,6 +26,7 @@ import com.google.android.exoplayer2.util.Util;
|
|||
import java.lang.annotation.Documented;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
/**
|
||||
* Attributes for audio playback, which configure the underlying platform {@link
|
||||
|
|
@ -48,6 +50,7 @@ public final class AudioAttributes implements Bundleable {
|
|||
private @C.AudioFlags int flags;
|
||||
private @C.AudioUsage int usage;
|
||||
private @C.AudioAllowedCapturePolicy int allowedCapturePolicy;
|
||||
private @C.SpatializationBehavior int spatializationBehavior;
|
||||
|
||||
/**
|
||||
* Creates a new builder for {@link AudioAttributes}.
|
||||
|
|
@ -60,6 +63,7 @@ public final class AudioAttributes implements Bundleable {
|
|||
flags = 0;
|
||||
usage = C.USAGE_MEDIA;
|
||||
allowedCapturePolicy = C.ALLOW_CAPTURE_BY_ALL;
|
||||
spatializationBehavior = C.SPATIALIZATION_BEHAVIOR_AUTO;
|
||||
}
|
||||
|
||||
/** @see android.media.AudioAttributes.Builder#setContentType(int) */
|
||||
|
|
@ -86,9 +90,18 @@ public final class AudioAttributes implements Bundleable {
|
|||
return this;
|
||||
}
|
||||
|
||||
// TODO[b/190759307] Update javadoc to link to AudioAttributes.Builder#setSpatializationBehavior
|
||||
// once compile SDK target is set to 32.
|
||||
/** See AudioAttributes.Builder#setSpatializationBehavior(int). */
|
||||
public Builder setSpatializationBehavior(@C.SpatializationBehavior int spatializationBehavior) {
|
||||
this.spatializationBehavior = spatializationBehavior;
|
||||
return this;
|
||||
}
|
||||
|
||||
/** Creates an {@link AudioAttributes} instance from this builder. */
|
||||
public AudioAttributes build() {
|
||||
return new AudioAttributes(contentType, flags, usage, allowedCapturePolicy);
|
||||
return new AudioAttributes(
|
||||
contentType, flags, usage, allowedCapturePolicy, spatializationBehavior);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -96,6 +109,7 @@ public final class AudioAttributes implements Bundleable {
|
|||
public final @C.AudioFlags int flags;
|
||||
public final @C.AudioUsage int usage;
|
||||
public final @C.AudioAllowedCapturePolicy int allowedCapturePolicy;
|
||||
public final @C.SpatializationBehavior int spatializationBehavior;
|
||||
|
||||
@Nullable private android.media.AudioAttributes audioAttributesV21;
|
||||
|
||||
|
|
@ -103,11 +117,13 @@ public final class AudioAttributes implements Bundleable {
|
|||
@C.AudioContentType int contentType,
|
||||
@C.AudioFlags int flags,
|
||||
@C.AudioUsage int usage,
|
||||
@C.AudioAllowedCapturePolicy int allowedCapturePolicy) {
|
||||
@C.AudioAllowedCapturePolicy int allowedCapturePolicy,
|
||||
@C.SpatializationBehavior int spatializationBehavior) {
|
||||
this.contentType = contentType;
|
||||
this.flags = flags;
|
||||
this.usage = usage;
|
||||
this.allowedCapturePolicy = allowedCapturePolicy;
|
||||
this.spatializationBehavior = spatializationBehavior;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -124,7 +140,10 @@ public final class AudioAttributes implements Bundleable {
|
|||
.setFlags(flags)
|
||||
.setUsage(usage);
|
||||
if (Util.SDK_INT >= 29) {
|
||||
builder.setAllowedCapturePolicy(allowedCapturePolicy);
|
||||
Api29.setAllowedCapturePolicy(builder, allowedCapturePolicy);
|
||||
}
|
||||
if (Util.SDK_INT >= 32) {
|
||||
Api32.setSpatializationBehavior(builder, spatializationBehavior);
|
||||
}
|
||||
audioAttributesV21 = builder.build();
|
||||
}
|
||||
|
|
@ -143,7 +162,8 @@ public final class AudioAttributes implements Bundleable {
|
|||
return this.contentType == other.contentType
|
||||
&& this.flags == other.flags
|
||||
&& this.usage == other.usage
|
||||
&& this.allowedCapturePolicy == other.allowedCapturePolicy;
|
||||
&& this.allowedCapturePolicy == other.allowedCapturePolicy
|
||||
&& this.spatializationBehavior == other.spatializationBehavior;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -153,6 +173,7 @@ public final class AudioAttributes implements Bundleable {
|
|||
result = 31 * result + flags;
|
||||
result = 31 * result + usage;
|
||||
result = 31 * result + allowedCapturePolicy;
|
||||
result = 31 * result + spatializationBehavior;
|
||||
return result;
|
||||
}
|
||||
|
||||
|
|
@ -160,13 +181,20 @@ public final class AudioAttributes implements Bundleable {
|
|||
|
||||
@Documented
|
||||
@Retention(RetentionPolicy.SOURCE)
|
||||
@IntDef({FIELD_CONTENT_TYPE, FIELD_FLAGS, FIELD_USAGE, FIELD_ALLOWED_CAPTURE_POLICY})
|
||||
@IntDef({
|
||||
FIELD_CONTENT_TYPE,
|
||||
FIELD_FLAGS,
|
||||
FIELD_USAGE,
|
||||
FIELD_ALLOWED_CAPTURE_POLICY,
|
||||
FIELD_SPATIALIZATION_BEHAVIOR
|
||||
})
|
||||
private @interface FieldNumber {}
|
||||
|
||||
private static final int FIELD_CONTENT_TYPE = 0;
|
||||
private static final int FIELD_FLAGS = 1;
|
||||
private static final int FIELD_USAGE = 2;
|
||||
private static final int FIELD_ALLOWED_CAPTURE_POLICY = 3;
|
||||
private static final int FIELD_SPATIALIZATION_BEHAVIOR = 4;
|
||||
|
||||
@Override
|
||||
public Bundle toBundle() {
|
||||
|
|
@ -175,6 +203,7 @@ public final class AudioAttributes implements Bundleable {
|
|||
bundle.putInt(keyForField(FIELD_FLAGS), flags);
|
||||
bundle.putInt(keyForField(FIELD_USAGE), usage);
|
||||
bundle.putInt(keyForField(FIELD_ALLOWED_CAPTURE_POLICY), allowedCapturePolicy);
|
||||
bundle.putInt(keyForField(FIELD_SPATIALIZATION_BEHAVIOR), spatializationBehavior);
|
||||
return bundle;
|
||||
}
|
||||
|
||||
|
|
@ -194,10 +223,45 @@ public final class AudioAttributes implements Bundleable {
|
|||
if (bundle.containsKey(keyForField(FIELD_ALLOWED_CAPTURE_POLICY))) {
|
||||
builder.setAllowedCapturePolicy(bundle.getInt(keyForField(FIELD_ALLOWED_CAPTURE_POLICY)));
|
||||
}
|
||||
if (bundle.containsKey(keyForField(FIELD_SPATIALIZATION_BEHAVIOR))) {
|
||||
builder.setSpatializationBehavior(
|
||||
bundle.getInt(keyForField(FIELD_SPATIALIZATION_BEHAVIOR)));
|
||||
}
|
||||
return builder.build();
|
||||
};
|
||||
|
||||
private static String keyForField(@FieldNumber int field) {
|
||||
return Integer.toString(field, Character.MAX_RADIX);
|
||||
}
|
||||
|
||||
@RequiresApi(29)
|
||||
private static final class Api29 {
|
||||
private Api29() {}
|
||||
|
||||
@DoNotInline
|
||||
public static void setAllowedCapturePolicy(
|
||||
android.media.AudioAttributes.Builder builder,
|
||||
@C.AudioAllowedCapturePolicy int allowedCapturePolicy) {
|
||||
builder.setAllowedCapturePolicy(allowedCapturePolicy);
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(32)
|
||||
private static final class Api32 {
|
||||
private Api32() {}
|
||||
|
||||
@DoNotInline
|
||||
public static void setSpatializationBehavior(
|
||||
android.media.AudioAttributes.Builder builder,
|
||||
@C.SpatializationBehavior int spatializationBehavior) {
|
||||
try {
|
||||
// TODO[b/190759307]: Remove reflection once compile SDK target is set to 32.
|
||||
Method setSpatializationBehavior =
|
||||
builder.getClass().getMethod("setSpatializationBehavior", Integer.TYPE);
|
||||
setSpatializationBehavior.invoke(builder, spatializationBehavior);
|
||||
} catch (Exception e) {
|
||||
// Do nothing if reflection fails.
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ package com.google.android.exoplayer2.source;
|
|||
import static com.google.android.exoplayer2.util.Assertions.checkArgument;
|
||||
|
||||
import android.os.Bundle;
|
||||
import androidx.annotation.CheckResult;
|
||||
import androidx.annotation.IntDef;
|
||||
import androidx.annotation.Nullable;
|
||||
import com.google.android.exoplayer2.Bundleable;
|
||||
|
|
@ -40,6 +41,8 @@ public final class TrackGroup implements Bundleable {
|
|||
|
||||
/** The number of tracks in the group. */
|
||||
public final int length;
|
||||
/** An identifier for the track group. */
|
||||
public final String id;
|
||||
|
||||
private final Format[] formats;
|
||||
|
||||
|
|
@ -47,17 +50,39 @@ public final class TrackGroup implements Bundleable {
|
|||
private int hashCode;
|
||||
|
||||
/**
|
||||
* Constructs an instance {@code TrackGroup} containing the provided {@code formats}.
|
||||
* Constructs a track group containing the provided {@code formats}.
|
||||
*
|
||||
* @param formats Non empty array of format.
|
||||
* @param formats The list of {@link Format Formats}. Must not be empty.
|
||||
*/
|
||||
public TrackGroup(Format... formats) {
|
||||
this(/* id= */ "", formats);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a track group with the provided {@code id} and {@code formats}.
|
||||
*
|
||||
* @param id The identifier of the track group. May be an empty string.
|
||||
* @param formats The list of {@link Format Formats}. Must not be empty.
|
||||
*/
|
||||
public TrackGroup(String id, Format... formats) {
|
||||
checkArgument(formats.length > 0);
|
||||
this.id = id;
|
||||
this.formats = formats;
|
||||
this.length = formats.length;
|
||||
verifyCorrectness();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a copy of this track group with the specified {@code id}.
|
||||
*
|
||||
* @param id The identifier for the copy of the track group.
|
||||
* @return The copied track group.
|
||||
*/
|
||||
@CheckResult
|
||||
public TrackGroup copyWithId(String id) {
|
||||
return new TrackGroup(id, formats);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the format of the track at a given index.
|
||||
*
|
||||
|
|
@ -90,6 +115,7 @@ public final class TrackGroup implements Bundleable {
|
|||
public int hashCode() {
|
||||
if (hashCode == 0) {
|
||||
int result = 17;
|
||||
result = 31 * result + id.hashCode();
|
||||
result = 31 * result + Arrays.hashCode(formats);
|
||||
hashCode = result;
|
||||
}
|
||||
|
|
@ -105,25 +131,25 @@ public final class TrackGroup implements Bundleable {
|
|||
return false;
|
||||
}
|
||||
TrackGroup other = (TrackGroup) obj;
|
||||
return length == other.length && Arrays.equals(formats, other.formats);
|
||||
return length == other.length && id.equals(other.id) && Arrays.equals(formats, other.formats);
|
||||
}
|
||||
|
||||
// Bundleable implementation.
|
||||
|
||||
@Documented
|
||||
@Retention(RetentionPolicy.SOURCE)
|
||||
@IntDef({
|
||||
FIELD_FORMATS,
|
||||
})
|
||||
@IntDef({FIELD_FORMATS, FIELD_ID})
|
||||
private @interface FieldNumber {}
|
||||
|
||||
private static final int FIELD_FORMATS = 0;
|
||||
private static final int FIELD_ID = 1;
|
||||
|
||||
@Override
|
||||
public Bundle toBundle() {
|
||||
Bundle bundle = new Bundle();
|
||||
bundle.putParcelableArrayList(
|
||||
keyForField(FIELD_FORMATS), BundleableUtil.toBundleArrayList(Lists.newArrayList(formats)));
|
||||
bundle.putString(keyForField(FIELD_ID), id);
|
||||
return bundle;
|
||||
}
|
||||
|
||||
|
|
@ -135,7 +161,8 @@ public final class TrackGroup implements Bundleable {
|
|||
Format.CREATOR,
|
||||
bundle.getParcelableArrayList(keyForField(FIELD_FORMATS)),
|
||||
ImmutableList.of());
|
||||
return new TrackGroup(formats.toArray(new Format[0]));
|
||||
String id = bundle.getString(keyForField(FIELD_ID), /* defaultValue= */ "");
|
||||
return new TrackGroup(id, formats.toArray(new Format[0]));
|
||||
};
|
||||
|
||||
private static String keyForField(@FieldNumber int field) {
|
||||
|
|
|
|||
|
|
@ -21,32 +21,38 @@ import androidx.annotation.Nullable;
|
|||
import com.google.android.exoplayer2.Bundleable;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.util.BundleableUtil;
|
||||
import com.google.android.exoplayer2.util.Log;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.collect.Lists;
|
||||
import java.lang.annotation.Documented;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
/** An immutable array of {@link TrackGroup}s. */
|
||||
public final class TrackGroupArray implements Bundleable {
|
||||
|
||||
private static final String TAG = "TrackGroupArray";
|
||||
|
||||
/** The empty array. */
|
||||
public static final TrackGroupArray EMPTY = new TrackGroupArray();
|
||||
|
||||
/** The number of groups in the array. Greater than or equal to zero. */
|
||||
public final int length;
|
||||
|
||||
private final TrackGroup[] trackGroups;
|
||||
private final ImmutableList<TrackGroup> trackGroups;
|
||||
|
||||
// Lazily initialized hashcode.
|
||||
private int hashCode;
|
||||
|
||||
/** Construct a {@code TrackGroupArray} from an array of (possibly empty) trackGroups. */
|
||||
/**
|
||||
* Construct a {@code TrackGroupArray} from an array of {@link TrackGroup TrackGroups}.
|
||||
*
|
||||
* <p>The groups must not contain duplicates.
|
||||
*/
|
||||
public TrackGroupArray(TrackGroup... trackGroups) {
|
||||
this.trackGroups = trackGroups;
|
||||
this.trackGroups = ImmutableList.copyOf(trackGroups);
|
||||
this.length = trackGroups.length;
|
||||
verifyCorrectness();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -56,7 +62,7 @@ public final class TrackGroupArray implements Bundleable {
|
|||
* @return The group.
|
||||
*/
|
||||
public TrackGroup get(int index) {
|
||||
return trackGroups[index];
|
||||
return trackGroups.get(index);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -65,16 +71,9 @@ public final class TrackGroupArray implements Bundleable {
|
|||
* @param group The group.
|
||||
* @return The index of the group, or {@link C#INDEX_UNSET} if no such group exists.
|
||||
*/
|
||||
@SuppressWarnings("ReferenceEquality")
|
||||
public int indexOf(TrackGroup group) {
|
||||
for (int i = 0; i < length; i++) {
|
||||
// Suppressed reference equality warning because this is looking for the index of a specific
|
||||
// TrackGroup object, not the index of a potential equal TrackGroup.
|
||||
if (trackGroups[i] == group) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return C.INDEX_UNSET;
|
||||
int index = trackGroups.indexOf(group);
|
||||
return index >= 0 ? index : C.INDEX_UNSET;
|
||||
}
|
||||
|
||||
/** Returns whether this track group array is empty. */
|
||||
|
|
@ -85,7 +84,7 @@ public final class TrackGroupArray implements Bundleable {
|
|||
@Override
|
||||
public int hashCode() {
|
||||
if (hashCode == 0) {
|
||||
hashCode = Arrays.hashCode(trackGroups);
|
||||
hashCode = trackGroups.hashCode();
|
||||
}
|
||||
return hashCode;
|
||||
}
|
||||
|
|
@ -99,7 +98,7 @@ public final class TrackGroupArray implements Bundleable {
|
|||
return false;
|
||||
}
|
||||
TrackGroupArray other = (TrackGroupArray) obj;
|
||||
return length == other.length && Arrays.equals(trackGroups, other.trackGroups);
|
||||
return length == other.length && trackGroups.equals(other.trackGroups);
|
||||
}
|
||||
|
||||
// Bundleable implementation.
|
||||
|
|
@ -117,8 +116,7 @@ public final class TrackGroupArray implements Bundleable {
|
|||
public Bundle toBundle() {
|
||||
Bundle bundle = new Bundle();
|
||||
bundle.putParcelableArrayList(
|
||||
keyForField(FIELD_TRACK_GROUPS),
|
||||
BundleableUtil.toBundleArrayList(Lists.newArrayList(trackGroups)));
|
||||
keyForField(FIELD_TRACK_GROUPS), BundleableUtil.toBundleArrayList(trackGroups));
|
||||
return bundle;
|
||||
}
|
||||
|
||||
|
|
@ -133,6 +131,20 @@ public final class TrackGroupArray implements Bundleable {
|
|||
return new TrackGroupArray(trackGroups.toArray(new TrackGroup[0]));
|
||||
};
|
||||
|
||||
private void verifyCorrectness() {
|
||||
for (int i = 0; i < trackGroups.size(); i++) {
|
||||
for (int j = i + 1; j < trackGroups.size(); j++) {
|
||||
if (trackGroups.get(i).equals(trackGroups.get(j))) {
|
||||
Log.e(
|
||||
TAG,
|
||||
"",
|
||||
new IllegalArgumentException(
|
||||
"Multiple identical TrackGroups added to one TrackGroupArray."));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static String keyForField(@FieldNumber int field) {
|
||||
return Integer.toString(field, Character.MAX_RADIX);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -300,6 +300,28 @@ public final class AdPlaybackState implements Bundleable {
|
|||
timeUs, count, states, uris, durationsUs, contentResumeOffsetUs, isServerSideInserted);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an instance with all ads in final states (played, skipped, error) reset to either
|
||||
* available or unavailable, which allows to play them again.
|
||||
*/
|
||||
@CheckResult
|
||||
public AdGroup withAllAdsReset() {
|
||||
if (count == C.LENGTH_UNSET) {
|
||||
return this;
|
||||
}
|
||||
int count = this.states.length;
|
||||
@AdState int[] states = Arrays.copyOf(this.states, count);
|
||||
for (int i = 0; i < count; i++) {
|
||||
if (states[i] == AD_STATE_PLAYED
|
||||
|| states[i] == AD_STATE_SKIPPED
|
||||
|| states[i] == AD_STATE_ERROR) {
|
||||
states[i] = uris[i] == null ? AD_STATE_UNAVAILABLE : AD_STATE_AVAILABLE;
|
||||
}
|
||||
}
|
||||
return new AdGroup(
|
||||
timeUs, count, states, uris, durationsUs, contentResumeOffsetUs, isServerSideInserted);
|
||||
}
|
||||
|
||||
@CheckResult
|
||||
private static @AdState int[] copyStatesWithSpaceForAdCount(@AdState int[] states, int count) {
|
||||
int oldStateCount = states.length;
|
||||
|
|
@ -783,6 +805,19 @@ public final class AdPlaybackState implements Bundleable {
|
|||
adsId, adGroups, adResumePositionUs, contentDurationUs, removedAdGroupCount);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an instance with all ads in the specified ad group reset from final states (played,
|
||||
* skipped, error) to either available or unavailable, which allows to play them again.
|
||||
*/
|
||||
@CheckResult
|
||||
public AdPlaybackState withResetAdGroup(@IntRange(from = 0) int adGroupIndex) {
|
||||
int adjustedIndex = adGroupIndex - removedAdGroupCount;
|
||||
AdGroup[] adGroups = Util.nullSafeArrayCopy(this.adGroups, this.adGroups.length);
|
||||
adGroups[adjustedIndex] = adGroups[adjustedIndex].withAllAdsReset();
|
||||
return new AdPlaybackState(
|
||||
adsId, adGroups, adResumePositionUs, contentDurationUs, removedAdGroupCount);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(@Nullable Object o) {
|
||||
if (this == o) {
|
||||
|
|
|
|||
|
|
@ -80,6 +80,7 @@ public class TrackSelectionParameters implements Bundleable {
|
|||
private int viewportHeight;
|
||||
private boolean viewportOrientationMayChange;
|
||||
private ImmutableList<String> preferredVideoMimeTypes;
|
||||
private @C.RoleFlags int preferredVideoRoleFlags;
|
||||
// Audio
|
||||
private ImmutableList<String> preferredAudioLanguages;
|
||||
private @C.RoleFlags int preferredAudioRoleFlags;
|
||||
|
|
@ -111,6 +112,7 @@ public class TrackSelectionParameters implements Bundleable {
|
|||
viewportHeight = Integer.MAX_VALUE;
|
||||
viewportOrientationMayChange = true;
|
||||
preferredVideoMimeTypes = ImmutableList.of();
|
||||
preferredVideoRoleFlags = 0;
|
||||
// Audio
|
||||
preferredAudioLanguages = ImmutableList.of();
|
||||
preferredAudioRoleFlags = 0;
|
||||
|
|
@ -183,6 +185,10 @@ public class TrackSelectionParameters implements Bundleable {
|
|||
firstNonNull(
|
||||
bundle.getStringArray(keyForField(FIELD_PREFERRED_VIDEO_MIMETYPES)),
|
||||
new String[0]));
|
||||
preferredVideoRoleFlags =
|
||||
bundle.getInt(
|
||||
keyForField(FIELD_PREFERRED_VIDEO_ROLE_FLAGS),
|
||||
DEFAULT_WITHOUT_CONTEXT.preferredVideoRoleFlags);
|
||||
// Audio
|
||||
String[] preferredAudioLanguages1 =
|
||||
firstNonNull(
|
||||
|
|
@ -261,6 +267,7 @@ public class TrackSelectionParameters implements Bundleable {
|
|||
viewportHeight = parameters.viewportHeight;
|
||||
viewportOrientationMayChange = parameters.viewportOrientationMayChange;
|
||||
preferredVideoMimeTypes = parameters.preferredVideoMimeTypes;
|
||||
preferredVideoRoleFlags = parameters.preferredVideoRoleFlags;
|
||||
// Audio
|
||||
preferredAudioLanguages = parameters.preferredAudioLanguages;
|
||||
preferredAudioRoleFlags = parameters.preferredAudioRoleFlags;
|
||||
|
|
@ -441,6 +448,17 @@ public class TrackSelectionParameters implements Bundleable {
|
|||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the preferred {@link C.RoleFlags} for video tracks.
|
||||
*
|
||||
* @param preferredVideoRoleFlags Preferred video role flags.
|
||||
* @return This builder.
|
||||
*/
|
||||
public Builder setPreferredVideoRoleFlags(@C.RoleFlags int preferredVideoRoleFlags) {
|
||||
this.preferredVideoRoleFlags = preferredVideoRoleFlags;
|
||||
return this;
|
||||
}
|
||||
|
||||
// Audio
|
||||
|
||||
/**
|
||||
|
|
@ -770,6 +788,11 @@ public class TrackSelectionParameters implements Bundleable {
|
|||
* no preference. The default is an empty list.
|
||||
*/
|
||||
public final ImmutableList<String> preferredVideoMimeTypes;
|
||||
/**
|
||||
* The preferred {@link C.RoleFlags} for video tracks. {@code 0} selects the default track if
|
||||
* there is one, or the first track if there's no default. The default value is {@code 0}.
|
||||
*/
|
||||
public final @C.RoleFlags int preferredVideoRoleFlags;
|
||||
// Audio
|
||||
/**
|
||||
* The preferred languages for audio and forced text tracks as IETF BCP 47 conformant tags in
|
||||
|
|
@ -853,6 +876,7 @@ public class TrackSelectionParameters implements Bundleable {
|
|||
this.viewportHeight = builder.viewportHeight;
|
||||
this.viewportOrientationMayChange = builder.viewportOrientationMayChange;
|
||||
this.preferredVideoMimeTypes = builder.preferredVideoMimeTypes;
|
||||
this.preferredVideoRoleFlags = builder.preferredVideoRoleFlags;
|
||||
// Audio
|
||||
this.preferredAudioLanguages = builder.preferredAudioLanguages;
|
||||
this.preferredAudioRoleFlags = builder.preferredAudioRoleFlags;
|
||||
|
|
@ -898,6 +922,7 @@ public class TrackSelectionParameters implements Bundleable {
|
|||
&& viewportWidth == other.viewportWidth
|
||||
&& viewportHeight == other.viewportHeight
|
||||
&& preferredVideoMimeTypes.equals(other.preferredVideoMimeTypes)
|
||||
&& preferredVideoRoleFlags == other.preferredVideoRoleFlags
|
||||
// Audio
|
||||
&& preferredAudioLanguages.equals(other.preferredAudioLanguages)
|
||||
&& preferredAudioRoleFlags == other.preferredAudioRoleFlags
|
||||
|
|
@ -930,6 +955,7 @@ public class TrackSelectionParameters implements Bundleable {
|
|||
result = 31 * result + viewportWidth;
|
||||
result = 31 * result + viewportHeight;
|
||||
result = 31 * result + preferredVideoMimeTypes.hashCode();
|
||||
result = 31 * result + preferredVideoRoleFlags;
|
||||
// Audio
|
||||
result = 31 * result + preferredAudioLanguages.hashCode();
|
||||
result = 31 * result + preferredAudioRoleFlags;
|
||||
|
|
@ -978,6 +1004,7 @@ public class TrackSelectionParameters implements Bundleable {
|
|||
FIELD_SELECTION_OVERRIDE_KEYS,
|
||||
FIELD_SELECTION_OVERRIDE_VALUES,
|
||||
FIELD_DISABLED_TRACK_TYPE,
|
||||
FIELD_PREFERRED_VIDEO_ROLE_FLAGS
|
||||
})
|
||||
private @interface FieldNumber {}
|
||||
|
||||
|
|
@ -1006,6 +1033,7 @@ public class TrackSelectionParameters implements Bundleable {
|
|||
private static final int FIELD_SELECTION_OVERRIDE_KEYS = 23;
|
||||
private static final int FIELD_SELECTION_OVERRIDE_VALUES = 24;
|
||||
private static final int FIELD_DISABLED_TRACK_TYPE = 25;
|
||||
private static final int FIELD_PREFERRED_VIDEO_ROLE_FLAGS = 26;
|
||||
|
||||
@Override
|
||||
public Bundle toBundle() {
|
||||
|
|
@ -1027,6 +1055,7 @@ public class TrackSelectionParameters implements Bundleable {
|
|||
bundle.putStringArray(
|
||||
keyForField(FIELD_PREFERRED_VIDEO_MIMETYPES),
|
||||
preferredVideoMimeTypes.toArray(new String[0]));
|
||||
bundle.putInt(keyForField(FIELD_PREFERRED_VIDEO_ROLE_FLAGS), preferredVideoRoleFlags);
|
||||
// Audio
|
||||
bundle.putStringArray(
|
||||
keyForField(FIELD_PREFERRED_AUDIO_LANGUAGES),
|
||||
|
|
|
|||
|
|
@ -15,12 +15,19 @@
|
|||
*/
|
||||
package com.google.android.exoplayer2.ui;
|
||||
|
||||
import static java.lang.annotation.ElementType.FIELD;
|
||||
import static java.lang.annotation.ElementType.LOCAL_VARIABLE;
|
||||
import static java.lang.annotation.ElementType.METHOD;
|
||||
import static java.lang.annotation.ElementType.PARAMETER;
|
||||
import static java.lang.annotation.ElementType.TYPE_USE;
|
||||
|
||||
import android.view.View;
|
||||
import androidx.annotation.IntDef;
|
||||
import androidx.annotation.Nullable;
|
||||
import java.lang.annotation.Documented;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
/** Provides information about an overlay view shown on top of an ad view group. */
|
||||
public final class AdOverlayInfo {
|
||||
|
|
@ -31,41 +38,70 @@ public final class AdOverlayInfo {
|
|||
*/
|
||||
@Documented
|
||||
@Retention(RetentionPolicy.SOURCE)
|
||||
@Target({FIELD, METHOD, PARAMETER, LOCAL_VARIABLE, TYPE_USE})
|
||||
@IntDef({PURPOSE_CONTROLS, PURPOSE_CLOSE_AD, PURPOSE_OTHER, PURPOSE_NOT_VISIBLE})
|
||||
public @interface Purpose {}
|
||||
/** Purpose for playback controls overlaying the player. */
|
||||
public static final int PURPOSE_CONTROLS = 0;
|
||||
public static final int PURPOSE_CONTROLS = 1;
|
||||
/** Purpose for ad close buttons overlaying the player. */
|
||||
public static final int PURPOSE_CLOSE_AD = 1;
|
||||
public static final int PURPOSE_CLOSE_AD = 2;
|
||||
/** Purpose for other overlays. */
|
||||
public static final int PURPOSE_OTHER = 2;
|
||||
public static final int PURPOSE_OTHER = 3;
|
||||
/** Purpose for overlays that are not visible. */
|
||||
public static final int PURPOSE_NOT_VISIBLE = 3;
|
||||
public static final int PURPOSE_NOT_VISIBLE = 4;
|
||||
|
||||
/** A builder for {@link AdOverlayInfo} instances. */
|
||||
public static final class Builder {
|
||||
|
||||
private final View view;
|
||||
private final @Purpose int purpose;
|
||||
|
||||
@Nullable private String detailedReason;
|
||||
|
||||
/**
|
||||
* Creates a new builder.
|
||||
*
|
||||
* @param view The view that is overlaying the player.
|
||||
* @param purpose The purpose of the view.
|
||||
*/
|
||||
public Builder(View view, @Purpose int purpose) {
|
||||
this.view = view;
|
||||
this.purpose = purpose;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets an optional, detailed reason that the view is on top of the player.
|
||||
*
|
||||
* @return This builder, for convenience.
|
||||
*/
|
||||
public Builder setDetailedReason(@Nullable String detailedReason) {
|
||||
this.detailedReason = detailedReason;
|
||||
return this;
|
||||
}
|
||||
|
||||
/** Returns a new {@link AdOverlayInfo} instance with the current builder values. */
|
||||
// Using deprecated constructor while it still exists.
|
||||
@SuppressWarnings("deprecation")
|
||||
public AdOverlayInfo build() {
|
||||
return new AdOverlayInfo(view, purpose, detailedReason);
|
||||
}
|
||||
}
|
||||
|
||||
/** The overlay view. */
|
||||
public final View view;
|
||||
/** The purpose of the overlay view. */
|
||||
@Purpose public final int purpose;
|
||||
public final @Purpose int purpose;
|
||||
/** An optional, detailed reason that the overlay view is needed. */
|
||||
@Nullable public final String reasonDetail;
|
||||
|
||||
/**
|
||||
* Creates a new overlay info.
|
||||
*
|
||||
* @param view The view that is overlaying the player.
|
||||
* @param purpose The purpose of the view.
|
||||
*/
|
||||
/** @deprecated Use {@link Builder} instead. */
|
||||
@Deprecated
|
||||
public AdOverlayInfo(View view, @Purpose int purpose) {
|
||||
this(view, purpose, /* detailedReason= */ null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new overlay info.
|
||||
*
|
||||
* @param view The view that is overlaying the player.
|
||||
* @param purpose The purpose of the view.
|
||||
* @param detailedReason An optional, detailed reason that the view is on top of the player.
|
||||
*/
|
||||
/** @deprecated Use {@link Builder} instead. */
|
||||
@Deprecated
|
||||
public AdOverlayInfo(View view, @Purpose int purpose, @Nullable String detailedReason) {
|
||||
this.view = view;
|
||||
this.purpose = purpose;
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@
|
|||
package com.google.android.exoplayer2.util;
|
||||
|
||||
import static android.opengl.GLU.gluErrorString;
|
||||
import static com.google.android.exoplayer2.util.Assertions.checkNotNull;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.pm.PackageManager;
|
||||
|
|
@ -26,7 +27,6 @@ import android.opengl.EGLDisplay;
|
|||
import android.opengl.EGLSurface;
|
||||
import android.opengl.GLES11Ext;
|
||||
import android.opengl.GLES20;
|
||||
import android.text.TextUtils;
|
||||
import androidx.annotation.DoNotInline;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.RequiresApi;
|
||||
|
|
@ -38,9 +38,11 @@ import java.nio.ByteBuffer;
|
|||
import java.nio.ByteOrder;
|
||||
import java.nio.FloatBuffer;
|
||||
import java.nio.IntBuffer;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import javax.microedition.khronos.egl.EGL10;
|
||||
|
||||
/** GL utilities. */
|
||||
/** OpenGL ES 2.0 utilities. */
|
||||
public final class GlUtil {
|
||||
|
||||
/** Thrown when an OpenGL error occurs and {@link #glAssertionsEnabled} is {@code true}. */
|
||||
|
|
@ -51,28 +53,20 @@ public final class GlUtil {
|
|||
}
|
||||
}
|
||||
|
||||
/** Thrown when the required EGL version is not supported by the device. */
|
||||
public static final class UnsupportedEglVersionException extends Exception {}
|
||||
|
||||
/** GL program. */
|
||||
/**
|
||||
* Represents a GLSL shader program.
|
||||
*
|
||||
* <p>After constructing a program, keep a reference for its lifetime and call {@link #delete()}
|
||||
* (or release the current GL context) when it's no longer needed.
|
||||
*/
|
||||
public static final class Program {
|
||||
/** The identifier of a compiled and linked GLSL shader program. */
|
||||
private final int programId;
|
||||
|
||||
/**
|
||||
* Compiles a GL shader program from vertex and fragment shader GLSL GLES20 code.
|
||||
*
|
||||
* @param vertexShaderGlsl The vertex shader program.
|
||||
* @param fragmentShaderGlsl The fragment shader program.
|
||||
*/
|
||||
public Program(String vertexShaderGlsl, String fragmentShaderGlsl) {
|
||||
programId = GLES20.glCreateProgram();
|
||||
checkGlError();
|
||||
|
||||
// Add the vertex and fragment shaders.
|
||||
addShader(GLES20.GL_VERTEX_SHADER, vertexShaderGlsl);
|
||||
addShader(GLES20.GL_FRAGMENT_SHADER, fragmentShaderGlsl);
|
||||
}
|
||||
private final Attribute[] attributes;
|
||||
private final Uniform[] uniforms;
|
||||
private final Map<String, Attribute> attributeByName;
|
||||
private final Map<String, Uniform> uniformByName;
|
||||
|
||||
/**
|
||||
* Compiles a GL shader program from vertex and fragment shader GLSL GLES20 code.
|
||||
|
|
@ -88,267 +82,123 @@ public final class GlUtil {
|
|||
}
|
||||
|
||||
/**
|
||||
* Compiles a GL shader program from vertex and fragment shader GLSL GLES20 code.
|
||||
* Creates a GL shader program from vertex and fragment shader GLSL GLES20 code.
|
||||
*
|
||||
* @param vertexShaderGlsl The vertex shader program as arrays of strings. Strings are joined by
|
||||
* adding a new line character in between each of them.
|
||||
* @param fragmentShaderGlsl The fragment shader program as arrays of strings. Strings are
|
||||
* joined by adding a new line character in between each of them.
|
||||
* <p>This involves slow steps, like compiling, linking, and switching the GL program, so do not
|
||||
* call this in fast rendering loops.
|
||||
*
|
||||
* @param vertexShaderGlsl The vertex shader program.
|
||||
* @param fragmentShaderGlsl The fragment shader program.
|
||||
*/
|
||||
public Program(String[] vertexShaderGlsl, String[] fragmentShaderGlsl) {
|
||||
this(TextUtils.join("\n", vertexShaderGlsl), TextUtils.join("\n", fragmentShaderGlsl));
|
||||
}
|
||||
public Program(String vertexShaderGlsl, String fragmentShaderGlsl) {
|
||||
programId = GLES20.glCreateProgram();
|
||||
checkGlError();
|
||||
|
||||
/** Uses the program. */
|
||||
public void use() {
|
||||
// Link and check for errors.
|
||||
// Add the vertex and fragment shaders.
|
||||
addShader(programId, GLES20.GL_VERTEX_SHADER, vertexShaderGlsl);
|
||||
addShader(programId, GLES20.GL_FRAGMENT_SHADER, fragmentShaderGlsl);
|
||||
|
||||
// Link and use the program, and enumerate attributes/uniforms.
|
||||
GLES20.glLinkProgram(programId);
|
||||
int[] linkStatus = new int[] {GLES20.GL_FALSE};
|
||||
GLES20.glGetProgramiv(programId, GLES20.GL_LINK_STATUS, linkStatus, 0);
|
||||
GLES20.glGetProgramiv(programId, GLES20.GL_LINK_STATUS, linkStatus, /* offset= */ 0);
|
||||
if (linkStatus[0] != GLES20.GL_TRUE) {
|
||||
throwGlException(
|
||||
"Unable to link shader program: \n" + GLES20.glGetProgramInfoLog(programId));
|
||||
}
|
||||
checkGlError();
|
||||
|
||||
GLES20.glUseProgram(programId);
|
||||
attributeByName = new HashMap<>();
|
||||
int[] attributeCount = new int[1];
|
||||
GLES20.glGetProgramiv(
|
||||
programId, GLES20.GL_ACTIVE_ATTRIBUTES, attributeCount, /* offset= */ 0);
|
||||
attributes = new Attribute[attributeCount[0]];
|
||||
for (int i = 0; i < attributeCount[0]; i++) {
|
||||
Attribute attribute = Attribute.create(programId, i);
|
||||
attributes[i] = attribute;
|
||||
attributeByName.put(attribute.name, attribute);
|
||||
}
|
||||
uniformByName = new HashMap<>();
|
||||
int[] uniformCount = new int[1];
|
||||
GLES20.glGetProgramiv(programId, GLES20.GL_ACTIVE_UNIFORMS, uniformCount, /* offset= */ 0);
|
||||
uniforms = new Uniform[uniformCount[0]];
|
||||
for (int i = 0; i < uniformCount[0]; i++) {
|
||||
Uniform uniform = Uniform.create(programId, i);
|
||||
uniforms[i] = uniform;
|
||||
uniformByName.put(uniform.name, uniform);
|
||||
}
|
||||
checkGlError();
|
||||
}
|
||||
|
||||
/**
|
||||
* Uses the program.
|
||||
*
|
||||
* <p>Call this in the rendering loop to switch between different programs.
|
||||
*/
|
||||
public void use() {
|
||||
// TODO(http://b/205002913): When multiple GL programs are supported by Transformer, make sure
|
||||
// to call use() to switch between programs.
|
||||
GLES20.glUseProgram(programId);
|
||||
checkGlError();
|
||||
}
|
||||
|
||||
/** Deletes the program. Deleted programs cannot be used again. */
|
||||
public void delete() {
|
||||
GLES20.glDeleteProgram(programId);
|
||||
checkGlError();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the location of an {@link Attribute}, which has been enabled as a vertex attribute
|
||||
* array.
|
||||
*/
|
||||
public int getAttributeArrayLocationAndEnable(String attributeName) {
|
||||
int location = getAttributeLocation(attributeName);
|
||||
GLES20.glEnableVertexAttribArray(location);
|
||||
checkGlError();
|
||||
return location;
|
||||
}
|
||||
|
||||
/** Returns the location of an {@link Attribute}. */
|
||||
public int getAttribLocation(String attributeName) {
|
||||
return GLES20.glGetAttribLocation(programId, attributeName);
|
||||
private int getAttributeLocation(String attributeName) {
|
||||
return GlUtil.getAttributeLocation(programId, attributeName);
|
||||
}
|
||||
|
||||
/** Returns the location of a {@link Uniform}. */
|
||||
public int getUniformLocation(String uniformName) {
|
||||
return GLES20.glGetUniformLocation(programId, uniformName);
|
||||
return GlUtil.getUniformLocation(programId, uniformName);
|
||||
}
|
||||
|
||||
/** Returns the program's {@link Attribute}s. */
|
||||
public Attribute[] getAttributes() {
|
||||
int[] attributeCount = new int[1];
|
||||
GLES20.glGetProgramiv(programId, GLES20.GL_ACTIVE_ATTRIBUTES, attributeCount, 0);
|
||||
if (attributeCount[0] != 2) {
|
||||
throw new IllegalStateException("Expected two attributes.");
|
||||
/** Sets a float buffer type attribute. */
|
||||
public void setBufferAttribute(String name, float[] values, int size) {
|
||||
checkNotNull(attributeByName.get(name)).setBuffer(values, size);
|
||||
}
|
||||
|
||||
/** Sets a texture sampler type uniform. */
|
||||
public void setSamplerTexIdUniform(String name, int texId, int unit) {
|
||||
checkNotNull(uniformByName.get(name)).setSamplerTexId(texId, unit);
|
||||
}
|
||||
|
||||
/** Sets a float type uniform. */
|
||||
public void setFloatUniform(String name, float value) {
|
||||
checkNotNull(uniformByName.get(name)).setFloat(value);
|
||||
}
|
||||
|
||||
/** Sets a float array type uniform. */
|
||||
public void setFloatsUniform(String name, float[] value) {
|
||||
checkNotNull(uniformByName.get(name)).setFloats(value);
|
||||
}
|
||||
|
||||
/** Binds all attributes and uniforms in the program. */
|
||||
public void bindAttributesAndUniforms() {
|
||||
for (Attribute attribute : attributes) {
|
||||
attribute.bind();
|
||||
}
|
||||
|
||||
Attribute[] attributes = new Attribute[attributeCount[0]];
|
||||
for (int i = 0; i < attributeCount[0]; i++) {
|
||||
attributes[i] = createAttribute(i);
|
||||
for (Uniform uniform : uniforms) {
|
||||
uniform.bind();
|
||||
}
|
||||
return attributes;
|
||||
}
|
||||
|
||||
/** Returns the program's {@link Uniform}s. */
|
||||
public Uniform[] getUniforms() {
|
||||
int[] uniformCount = new int[1];
|
||||
GLES20.glGetProgramiv(programId, GLES20.GL_ACTIVE_UNIFORMS, uniformCount, 0);
|
||||
|
||||
Uniform[] uniforms = new Uniform[uniformCount[0]];
|
||||
for (int i = 0; i < uniformCount[0]; i++) {
|
||||
uniforms[i] = createUniform(i);
|
||||
}
|
||||
|
||||
return uniforms;
|
||||
}
|
||||
|
||||
private Attribute createAttribute(int index) {
|
||||
int[] length = new int[1];
|
||||
GLES20.glGetProgramiv(programId, GLES20.GL_ACTIVE_ATTRIBUTE_MAX_LENGTH, length, 0);
|
||||
|
||||
int[] type = new int[1];
|
||||
int[] size = new int[1];
|
||||
byte[] nameBytes = new byte[length[0]];
|
||||
int[] ignore = new int[1];
|
||||
|
||||
GLES20.glGetActiveAttrib(
|
||||
programId, index, length[0], ignore, 0, size, 0, type, 0, nameBytes, 0);
|
||||
String name = new String(nameBytes, 0, strlen(nameBytes));
|
||||
int location = getAttribLocation(name);
|
||||
|
||||
return new Attribute(name, index, location);
|
||||
}
|
||||
|
||||
private Uniform createUniform(int index) {
|
||||
int[] length = new int[1];
|
||||
GLES20.glGetProgramiv(programId, GLES20.GL_ACTIVE_UNIFORM_MAX_LENGTH, length, 0);
|
||||
|
||||
int[] type = new int[1];
|
||||
int[] size = new int[1];
|
||||
byte[] nameBytes = new byte[length[0]];
|
||||
int[] ignore = new int[1];
|
||||
|
||||
GLES20.glGetActiveUniform(
|
||||
programId, index, length[0], ignore, 0, size, 0, type, 0, nameBytes, 0);
|
||||
String name = new String(nameBytes, 0, strlen(nameBytes));
|
||||
int location = getUniformLocation(name);
|
||||
|
||||
return new Uniform(name, location, type[0]);
|
||||
}
|
||||
|
||||
private void addShader(int type, String glsl) {
|
||||
int shader = GLES20.glCreateShader(type);
|
||||
GLES20.glShaderSource(shader, glsl);
|
||||
GLES20.glCompileShader(shader);
|
||||
|
||||
int[] result = new int[] {GLES20.GL_FALSE};
|
||||
GLES20.glGetShaderiv(shader, GLES20.GL_COMPILE_STATUS, result, 0);
|
||||
if (result[0] != GLES20.GL_TRUE) {
|
||||
throwGlException(GLES20.glGetShaderInfoLog(shader) + ", source: " + glsl);
|
||||
}
|
||||
|
||||
GLES20.glAttachShader(programId, shader);
|
||||
GLES20.glDeleteShader(shader);
|
||||
checkGlError();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GL attribute, which can be attached to a buffer with {@link Attribute#setBuffer(float[], int)}.
|
||||
*/
|
||||
public static final class Attribute {
|
||||
|
||||
/** The name of the attribute in the GLSL sources. */
|
||||
public final String name;
|
||||
|
||||
private final int index;
|
||||
private final int location;
|
||||
|
||||
@Nullable private Buffer buffer;
|
||||
private int size;
|
||||
|
||||
/* Creates a new Attribute. */
|
||||
public Attribute(String name, int index, int location) {
|
||||
this.name = name;
|
||||
this.index = index;
|
||||
this.location = location;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures {@link #bind()} to attach vertices in {@code buffer} (each of size {@code size}
|
||||
* elements) to this {@link Attribute}.
|
||||
*
|
||||
* @param buffer Buffer to bind to this attribute.
|
||||
* @param size Number of elements per vertex.
|
||||
*/
|
||||
public void setBuffer(float[] buffer, int size) {
|
||||
this.buffer = createBuffer(buffer);
|
||||
this.size = size;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the vertex attribute to whatever was attached via {@link #setBuffer(float[], int)}.
|
||||
*
|
||||
* <p>Should be called before each drawing call.
|
||||
*/
|
||||
public void bind() {
|
||||
Buffer buffer = Assertions.checkNotNull(this.buffer, "call setBuffer before bind");
|
||||
GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, 0);
|
||||
GLES20.glVertexAttribPointer(
|
||||
location,
|
||||
size, // count
|
||||
GLES20.GL_FLOAT, // type
|
||||
false, // normalize
|
||||
0, // stride
|
||||
buffer);
|
||||
GLES20.glEnableVertexAttribArray(index);
|
||||
checkGlError();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GL uniform, which can be attached to a sampler using {@link Uniform#setSamplerTexId(int, int)}.
|
||||
*/
|
||||
public static final class Uniform {
|
||||
|
||||
/** The name of the uniform in the GLSL sources. */
|
||||
public final String name;
|
||||
|
||||
private final int location;
|
||||
private final int type;
|
||||
private final float[] value;
|
||||
|
||||
private int texId;
|
||||
private int unit;
|
||||
|
||||
/** Creates a new uniform. */
|
||||
public Uniform(String name, int location, int type) {
|
||||
this.name = name;
|
||||
this.location = location;
|
||||
this.type = type;
|
||||
this.value = new float[16];
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures {@link #bind()} to use the specified {@code texId} for this sampler uniform.
|
||||
*
|
||||
* @param texId The GL texture identifier from which to sample.
|
||||
* @param unit The GL texture unit index.
|
||||
*/
|
||||
public void setSamplerTexId(int texId, int unit) {
|
||||
this.texId = texId;
|
||||
this.unit = unit;
|
||||
}
|
||||
|
||||
/** Configures {@link #bind()} to use the specified float {@code value} for this uniform. */
|
||||
public void setFloat(float value) {
|
||||
this.value[0] = value;
|
||||
}
|
||||
|
||||
/** Configures {@link #bind()} to use the specified float[] {@code value} for this uniform. */
|
||||
public void setFloats(float[] value) {
|
||||
System.arraycopy(value, 0, this.value, 0, value.length);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the uniform to whatever value was passed via {@link #setSamplerTexId(int, int)}, {@link
|
||||
* #setFloat(float)} or {@link #setFloats(float[])}.
|
||||
*
|
||||
* <p>Should be called before each drawing call.
|
||||
*/
|
||||
public void bind() {
|
||||
if (type == GLES20.GL_FLOAT) {
|
||||
GLES20.glUniform1fv(location, 1, value, 0);
|
||||
checkGlError();
|
||||
return;
|
||||
}
|
||||
|
||||
if (type == GLES20.GL_FLOAT_MAT4) {
|
||||
GLES20.glUniformMatrix4fv(location, 1, false, value, 0);
|
||||
checkGlError();
|
||||
return;
|
||||
}
|
||||
|
||||
if (texId == 0) {
|
||||
throw new IllegalStateException("Call setSamplerTexId before bind.");
|
||||
}
|
||||
GLES20.glActiveTexture(GLES20.GL_TEXTURE0 + unit);
|
||||
if (type == GLES11Ext.GL_SAMPLER_EXTERNAL_OES) {
|
||||
GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, texId);
|
||||
} else if (type == GLES20.GL_SAMPLER_2D) {
|
||||
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, texId);
|
||||
} else {
|
||||
throw new IllegalStateException("Unexpected uniform type: " + type);
|
||||
}
|
||||
GLES20.glUniform1i(location, unit);
|
||||
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR);
|
||||
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR);
|
||||
GLES20.glTexParameteri(
|
||||
GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE);
|
||||
GLES20.glTexParameteri(
|
||||
GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE);
|
||||
checkGlError();
|
||||
}
|
||||
}
|
||||
|
||||
/** Represents an unset texture ID. */
|
||||
public static final int TEXTURE_ID_UNSET = -1;
|
||||
|
||||
/** Whether to throw a {@link GlException} in case of an OpenGL error. */
|
||||
public static boolean glAssertionsEnabled = false;
|
||||
|
||||
|
|
@ -406,15 +256,9 @@ public final class GlUtil {
|
|||
return Api17.createEglDisplay();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new {@link EGLContext} for the specified {@link EGLDisplay}.
|
||||
*
|
||||
* @throws UnsupportedEglVersionException If the device does not support EGL version 2. {@code
|
||||
* eglDisplay} is terminated before the exception is thrown in this case.
|
||||
*/
|
||||
/** Returns a new {@link EGLContext} for the specified {@link EGLDisplay}. */
|
||||
@RequiresApi(17)
|
||||
public static EGLContext createEglContext(EGLDisplay eglDisplay)
|
||||
throws UnsupportedEglVersionException {
|
||||
public static EGLContext createEglContext(EGLDisplay eglDisplay) {
|
||||
return Api17.createEglContext(eglDisplay);
|
||||
}
|
||||
|
||||
|
|
@ -437,11 +281,11 @@ public final class GlUtil {
|
|||
int lastError = GLES20.GL_NO_ERROR;
|
||||
int error;
|
||||
while ((error = GLES20.glGetError()) != GLES20.GL_NO_ERROR) {
|
||||
Log.e(TAG, "glError " + gluErrorString(error));
|
||||
Log.e(TAG, "glError: " + gluErrorString(error));
|
||||
lastError = error;
|
||||
}
|
||||
if (lastError != GLES20.GL_NO_ERROR) {
|
||||
throwGlException("glError " + gluErrorString(lastError));
|
||||
throwGlException("glError: " + gluErrorString(lastError));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -461,8 +305,7 @@ public final class GlUtil {
|
|||
* @param textureId The ID of the texture to delete.
|
||||
*/
|
||||
public static void deleteTexture(int textureId) {
|
||||
int[] textures = new int[] {textureId};
|
||||
GLES20.glDeleteTextures(1, textures, 0);
|
||||
GLES20.glDeleteTextures(/* n= */ 1, new int[] {textureId}, /* offset= */ 0);
|
||||
checkGlError();
|
||||
}
|
||||
|
||||
|
|
@ -519,7 +362,7 @@ public final class GlUtil {
|
|||
*/
|
||||
public static int createExternalTexture() {
|
||||
int[] texId = new int[1];
|
||||
GLES20.glGenTextures(1, IntBuffer.wrap(texId));
|
||||
GLES20.glGenTextures(/* n= */ 1, IntBuffer.wrap(texId));
|
||||
GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, texId[0]);
|
||||
GLES20.glTexParameteri(
|
||||
GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR);
|
||||
|
|
@ -533,6 +376,30 @@ public final class GlUtil {
|
|||
return texId[0];
|
||||
}
|
||||
|
||||
private static void addShader(int programId, int type, String glsl) {
|
||||
int shader = GLES20.glCreateShader(type);
|
||||
GLES20.glShaderSource(shader, glsl);
|
||||
GLES20.glCompileShader(shader);
|
||||
|
||||
int[] result = new int[] {GLES20.GL_FALSE};
|
||||
GLES20.glGetShaderiv(shader, GLES20.GL_COMPILE_STATUS, result, /* offset= */ 0);
|
||||
if (result[0] != GLES20.GL_TRUE) {
|
||||
throwGlException(GLES20.glGetShaderInfoLog(shader) + ", source: " + glsl);
|
||||
}
|
||||
|
||||
GLES20.glAttachShader(programId, shader);
|
||||
GLES20.glDeleteShader(shader);
|
||||
checkGlError();
|
||||
}
|
||||
|
||||
private static int getAttributeLocation(int programId, String attributeName) {
|
||||
return GLES20.glGetAttribLocation(programId, attributeName);
|
||||
}
|
||||
|
||||
private static int getUniformLocation(int programId, String uniformName) {
|
||||
return GLES20.glGetUniformLocation(programId, uniformName);
|
||||
}
|
||||
|
||||
private static void throwGlException(String errorMsg) {
|
||||
Log.e(TAG, errorMsg);
|
||||
if (glAssertionsEnabled) {
|
||||
|
|
@ -556,6 +423,190 @@ public final class GlUtil {
|
|||
return strVal.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* GL attribute, which can be attached to a buffer with {@link Attribute#setBuffer(float[], int)}.
|
||||
*/
|
||||
private static final class Attribute {
|
||||
|
||||
/* Returns the attribute at the given index in the program. */
|
||||
public static Attribute create(int programId, int index) {
|
||||
int[] length = new int[1];
|
||||
GLES20.glGetProgramiv(
|
||||
programId, GLES20.GL_ACTIVE_ATTRIBUTE_MAX_LENGTH, length, /* offset= */ 0);
|
||||
byte[] nameBytes = new byte[length[0]];
|
||||
|
||||
GLES20.glGetActiveAttrib(
|
||||
programId,
|
||||
index,
|
||||
length[0],
|
||||
/* unusedLength */ new int[1],
|
||||
/* lengthOffset= */ 0,
|
||||
/* unusedSize */ new int[1],
|
||||
/* sizeOffset= */ 0,
|
||||
/* unusedType */ new int[1],
|
||||
/* typeOffset= */ 0,
|
||||
nameBytes,
|
||||
/* nameOffset= */ 0);
|
||||
String name = new String(nameBytes, /* offset= */ 0, strlen(nameBytes));
|
||||
int location = getAttributeLocation(programId, name);
|
||||
|
||||
return new Attribute(name, index, location);
|
||||
}
|
||||
|
||||
/** The name of the attribute in the GLSL sources. */
|
||||
public final String name;
|
||||
|
||||
private final int index;
|
||||
private final int location;
|
||||
|
||||
@Nullable private Buffer buffer;
|
||||
private int size;
|
||||
|
||||
private Attribute(String name, int index, int location) {
|
||||
this.name = name;
|
||||
this.index = index;
|
||||
this.location = location;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures {@link #bind()} to attach vertices in {@code buffer} (each of size {@code size}
|
||||
* elements) to this {@link Attribute}.
|
||||
*
|
||||
* @param buffer Buffer to bind to this attribute.
|
||||
* @param size Number of elements per vertex.
|
||||
*/
|
||||
public void setBuffer(float[] buffer, int size) {
|
||||
this.buffer = createBuffer(buffer);
|
||||
this.size = size;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the vertex attribute to whatever was attached via {@link #setBuffer(float[], int)}.
|
||||
*
|
||||
* <p>Should be called before each drawing call.
|
||||
*/
|
||||
public void bind() {
|
||||
Buffer buffer = checkNotNull(this.buffer, "call setBuffer before bind");
|
||||
GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, /* buffer= */ 0);
|
||||
GLES20.glVertexAttribPointer(
|
||||
location, size, GLES20.GL_FLOAT, /* normalized= */ false, /* stride= */ 0, buffer);
|
||||
GLES20.glEnableVertexAttribArray(index);
|
||||
checkGlError();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GL uniform, which can be attached to a sampler using {@link Uniform#setSamplerTexId(int, int)}.
|
||||
*/
|
||||
private static final class Uniform {
|
||||
|
||||
/** Returns the uniform at the given index in the program. */
|
||||
public static Uniform create(int programId, int index) {
|
||||
int[] length = new int[1];
|
||||
GLES20.glGetProgramiv(
|
||||
programId, GLES20.GL_ACTIVE_UNIFORM_MAX_LENGTH, length, /* offset= */ 0);
|
||||
|
||||
int[] type = new int[1];
|
||||
byte[] nameBytes = new byte[length[0]];
|
||||
|
||||
GLES20.glGetActiveUniform(
|
||||
programId,
|
||||
index,
|
||||
length[0],
|
||||
/* unusedLength */ new int[1],
|
||||
/* lengthOffset= */ 0,
|
||||
/* unusedSize */ new int[1],
|
||||
/*sizeOffset= */ 0,
|
||||
type,
|
||||
/* typeOffset= */ 0,
|
||||
nameBytes,
|
||||
/* nameOffset= */ 0);
|
||||
String name = new String(nameBytes, /* offset= */ 0, strlen(nameBytes));
|
||||
int location = getUniformLocation(programId, name);
|
||||
|
||||
return new Uniform(name, location, type[0]);
|
||||
}
|
||||
|
||||
/** The name of the uniform in the GLSL sources. */
|
||||
public final String name;
|
||||
|
||||
private final int location;
|
||||
private final int type;
|
||||
private final float[] value;
|
||||
|
||||
private int texId;
|
||||
private int unit;
|
||||
|
||||
private Uniform(String name, int location, int type) {
|
||||
this.name = name;
|
||||
this.location = location;
|
||||
this.type = type;
|
||||
this.value = new float[16];
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures {@link #bind()} to use the specified {@code texId} for this sampler uniform.
|
||||
*
|
||||
* @param texId The GL texture identifier from which to sample.
|
||||
* @param unit The GL texture unit index.
|
||||
*/
|
||||
public void setSamplerTexId(int texId, int unit) {
|
||||
this.texId = texId;
|
||||
this.unit = unit;
|
||||
}
|
||||
|
||||
/** Configures {@link #bind()} to use the specified float {@code value} for this uniform. */
|
||||
public void setFloat(float value) {
|
||||
this.value[0] = value;
|
||||
}
|
||||
|
||||
/** Configures {@link #bind()} to use the specified float[] {@code value} for this uniform. */
|
||||
public void setFloats(float[] value) {
|
||||
System.arraycopy(value, /* srcPos= */ 0, this.value, /* destPos= */ 0, value.length);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the uniform to whatever value was passed via {@link #setSamplerTexId(int, int)}, {@link
|
||||
* #setFloat(float)} or {@link #setFloats(float[])}.
|
||||
*
|
||||
* <p>Should be called before each drawing call.
|
||||
*/
|
||||
public void bind() {
|
||||
if (type == GLES20.GL_FLOAT) {
|
||||
GLES20.glUniform1fv(location, /* count= */ 1, value, /* offset= */ 0);
|
||||
checkGlError();
|
||||
return;
|
||||
}
|
||||
|
||||
if (type == GLES20.GL_FLOAT_MAT4) {
|
||||
GLES20.glUniformMatrix4fv(
|
||||
location, /* count= */ 1, /* transpose= */ false, value, /* offset= */ 0);
|
||||
checkGlError();
|
||||
return;
|
||||
}
|
||||
|
||||
if (texId == 0) {
|
||||
throw new IllegalStateException("No call to setSamplerTexId() before bind.");
|
||||
}
|
||||
GLES20.glActiveTexture(GLES20.GL_TEXTURE0 + unit);
|
||||
if (type == GLES11Ext.GL_SAMPLER_EXTERNAL_OES) {
|
||||
GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, texId);
|
||||
} else if (type == GLES20.GL_SAMPLER_2D) {
|
||||
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, texId);
|
||||
} else {
|
||||
throw new IllegalStateException("Unexpected uniform type: " + type);
|
||||
}
|
||||
GLES20.glUniform1i(location, unit);
|
||||
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR);
|
||||
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR);
|
||||
GLES20.glTexParameteri(
|
||||
GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE);
|
||||
GLES20.glTexParameteri(
|
||||
GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE);
|
||||
checkGlError();
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(17)
|
||||
private static final class Api17 {
|
||||
private Api17() {}
|
||||
|
|
@ -564,9 +615,12 @@ public final class GlUtil {
|
|||
public static EGLDisplay createEglDisplay() {
|
||||
EGLDisplay eglDisplay = EGL14.eglGetDisplay(EGL14.EGL_DEFAULT_DISPLAY);
|
||||
checkEglException(!eglDisplay.equals(EGL14.EGL_NO_DISPLAY), "No EGL display.");
|
||||
int[] major = new int[1];
|
||||
int[] minor = new int[1];
|
||||
if (!EGL14.eglInitialize(eglDisplay, major, 0, minor, 0)) {
|
||||
if (!EGL14.eglInitialize(
|
||||
eglDisplay,
|
||||
/* unusedMajor */ new int[1],
|
||||
/* majorOffset= */ 0,
|
||||
/* unusedMinor */ new int[1],
|
||||
/* minorOffset= */ 0)) {
|
||||
throwGlException("Error in eglInitialize.");
|
||||
}
|
||||
checkGlError();
|
||||
|
|
@ -574,15 +628,20 @@ public final class GlUtil {
|
|||
}
|
||||
|
||||
@DoNotInline
|
||||
public static EGLContext createEglContext(EGLDisplay eglDisplay)
|
||||
throws UnsupportedEglVersionException {
|
||||
public static EGLContext createEglContext(EGLDisplay eglDisplay) {
|
||||
int[] contextAttributes = {EGL14.EGL_CONTEXT_CLIENT_VERSION, 2, EGL14.EGL_NONE};
|
||||
EGLContext eglContext =
|
||||
EGL14.eglCreateContext(
|
||||
eglDisplay, getEglConfig(eglDisplay), EGL14.EGL_NO_CONTEXT, contextAttributes, 0);
|
||||
eglDisplay,
|
||||
getEglConfig(eglDisplay),
|
||||
EGL14.EGL_NO_CONTEXT,
|
||||
contextAttributes,
|
||||
/* offset= */ 0);
|
||||
if (eglContext == null) {
|
||||
EGL14.eglTerminate(eglDisplay);
|
||||
throw new UnsupportedEglVersionException();
|
||||
throwGlException(
|
||||
"eglCreateContext() failed to create a valid context. The device may not support EGL"
|
||||
+ " version 2");
|
||||
}
|
||||
checkGlError();
|
||||
return eglContext;
|
||||
|
|
@ -591,20 +650,24 @@ public final class GlUtil {
|
|||
@DoNotInline
|
||||
public static EGLSurface getEglSurface(EGLDisplay eglDisplay, Object surface) {
|
||||
return EGL14.eglCreateWindowSurface(
|
||||
eglDisplay, getEglConfig(eglDisplay), surface, new int[] {EGL14.EGL_NONE}, 0);
|
||||
eglDisplay,
|
||||
getEglConfig(eglDisplay),
|
||||
surface,
|
||||
new int[] {EGL14.EGL_NONE},
|
||||
/* offset= */ 0);
|
||||
}
|
||||
|
||||
@DoNotInline
|
||||
public static void focusSurface(
|
||||
EGLDisplay eglDisplay, EGLContext eglContext, EGLSurface surface, int width, int height) {
|
||||
int[] fbos = new int[1];
|
||||
GLES20.glGetIntegerv(GLES20.GL_FRAMEBUFFER_BINDING, fbos, 0);
|
||||
int noFbo = 0;
|
||||
if (fbos[0] != noFbo) {
|
||||
GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, noFbo);
|
||||
int[] boundFrameBuffer = new int[1];
|
||||
GLES20.glGetIntegerv(GLES20.GL_FRAMEBUFFER_BINDING, boundFrameBuffer, /* offset= */ 0);
|
||||
int defaultFrameBuffer = 0;
|
||||
if (boundFrameBuffer[0] != defaultFrameBuffer) {
|
||||
GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, defaultFrameBuffer);
|
||||
}
|
||||
EGL14.eglMakeCurrent(eglDisplay, surface, surface, eglContext);
|
||||
GLES20.glViewport(0, 0, width, height);
|
||||
GLES20.glViewport(/* x= */ 0, /* y= */ 0, width, height);
|
||||
}
|
||||
|
||||
@DoNotInline
|
||||
|
|
@ -632,27 +695,27 @@ public final class GlUtil {
|
|||
|
||||
@DoNotInline
|
||||
private static EGLConfig getEglConfig(EGLDisplay eglDisplay) {
|
||||
int redSize = 8;
|
||||
int greenSize = 8;
|
||||
int blueSize = 8;
|
||||
int alphaSize = 8;
|
||||
int depthSize = 0;
|
||||
int stencilSize = 0;
|
||||
int[] defaultConfiguration =
|
||||
new int[] {
|
||||
EGL14.EGL_RENDERABLE_TYPE, EGL14.EGL_OPENGL_ES2_BIT,
|
||||
EGL14.EGL_RED_SIZE, redSize,
|
||||
EGL14.EGL_GREEN_SIZE, greenSize,
|
||||
EGL14.EGL_BLUE_SIZE, blueSize,
|
||||
EGL14.EGL_ALPHA_SIZE, alphaSize,
|
||||
EGL14.EGL_DEPTH_SIZE, depthSize,
|
||||
EGL14.EGL_STENCIL_SIZE, stencilSize,
|
||||
EGL14.EGL_RED_SIZE, /* redSize= */ 8,
|
||||
EGL14.EGL_GREEN_SIZE, /* greenSize= */ 8,
|
||||
EGL14.EGL_BLUE_SIZE, /* blueSize= */ 8,
|
||||
EGL14.EGL_ALPHA_SIZE, /* alphaSize= */ 8,
|
||||
EGL14.EGL_DEPTH_SIZE, /* depthSize= */ 0,
|
||||
EGL14.EGL_STENCIL_SIZE, /* stencilSize= */ 0,
|
||||
EGL14.EGL_NONE
|
||||
};
|
||||
int[] configsCount = new int[1];
|
||||
EGLConfig[] eglConfigs = new EGLConfig[1];
|
||||
if (!EGL14.eglChooseConfig(
|
||||
eglDisplay, defaultConfiguration, 0, eglConfigs, 0, 1, configsCount, 0)) {
|
||||
eglDisplay,
|
||||
defaultConfiguration,
|
||||
/* attrib_listOffset= */ 0,
|
||||
eglConfigs,
|
||||
/* configsOffset= */ 0,
|
||||
/* config_size= */ 1,
|
||||
/* unusedNumConfig */ new int[1],
|
||||
/* num_configOffset= */ 0)) {
|
||||
throwGlException("eglChooseConfig failed.");
|
||||
}
|
||||
return eglConfigs[0];
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ package com.google.android.exoplayer2.util;
|
|||
import android.text.TextUtils;
|
||||
import androidx.annotation.IntDef;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.Size;
|
||||
import java.lang.annotation.Documented;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
|
|
@ -78,7 +79,7 @@ public final class Log {
|
|||
|
||||
/** @see android.util.Log#d(String, String) */
|
||||
@Pure
|
||||
public static void d(String tag, String message) {
|
||||
public static void d(@Size(max = 23) String tag, String message) {
|
||||
if (logLevel == LOG_LEVEL_ALL) {
|
||||
android.util.Log.d(tag, message);
|
||||
}
|
||||
|
|
@ -86,13 +87,13 @@ public final class Log {
|
|||
|
||||
/** @see android.util.Log#d(String, String, Throwable) */
|
||||
@Pure
|
||||
public static void d(String tag, String message, @Nullable Throwable throwable) {
|
||||
public static void d(@Size(max = 23) String tag, String message, @Nullable Throwable throwable) {
|
||||
d(tag, appendThrowableString(message, throwable));
|
||||
}
|
||||
|
||||
/** @see android.util.Log#i(String, String) */
|
||||
@Pure
|
||||
public static void i(String tag, String message) {
|
||||
public static void i(@Size(max = 23) String tag, String message) {
|
||||
if (logLevel <= LOG_LEVEL_INFO) {
|
||||
android.util.Log.i(tag, message);
|
||||
}
|
||||
|
|
@ -100,13 +101,13 @@ public final class Log {
|
|||
|
||||
/** @see android.util.Log#i(String, String, Throwable) */
|
||||
@Pure
|
||||
public static void i(String tag, String message, @Nullable Throwable throwable) {
|
||||
public static void i(@Size(max = 23) String tag, String message, @Nullable Throwable throwable) {
|
||||
i(tag, appendThrowableString(message, throwable));
|
||||
}
|
||||
|
||||
/** @see android.util.Log#w(String, String) */
|
||||
@Pure
|
||||
public static void w(String tag, String message) {
|
||||
public static void w(@Size(max = 23) String tag, String message) {
|
||||
if (logLevel <= LOG_LEVEL_WARNING) {
|
||||
android.util.Log.w(tag, message);
|
||||
}
|
||||
|
|
@ -114,13 +115,13 @@ public final class Log {
|
|||
|
||||
/** @see android.util.Log#w(String, String, Throwable) */
|
||||
@Pure
|
||||
public static void w(String tag, String message, @Nullable Throwable throwable) {
|
||||
public static void w(@Size(max = 23) String tag, String message, @Nullable Throwable throwable) {
|
||||
w(tag, appendThrowableString(message, throwable));
|
||||
}
|
||||
|
||||
/** @see android.util.Log#e(String, String) */
|
||||
@Pure
|
||||
public static void e(String tag, String message) {
|
||||
public static void e(@Size(max = 23) String tag, String message) {
|
||||
if (logLevel <= LOG_LEVEL_ERROR) {
|
||||
android.util.Log.e(tag, message);
|
||||
}
|
||||
|
|
@ -128,7 +129,7 @@ public final class Log {
|
|||
|
||||
/** @see android.util.Log#e(String, String, Throwable) */
|
||||
@Pure
|
||||
public static void e(String tag, String message, @Nullable Throwable throwable) {
|
||||
public static void e(@Size(max = 23) String tag, String message, @Nullable Throwable throwable) {
|
||||
e(tag, appendThrowableString(message, throwable));
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -16,6 +16,15 @@
|
|||
package com.google.android.exoplayer2.util;
|
||||
|
||||
import static android.content.Context.UI_MODE_SERVICE;
|
||||
import static com.google.android.exoplayer2.Player.COMMAND_SEEK_BACK;
|
||||
import static com.google.android.exoplayer2.Player.COMMAND_SEEK_FORWARD;
|
||||
import static com.google.android.exoplayer2.Player.COMMAND_SEEK_IN_CURRENT_MEDIA_ITEM;
|
||||
import static com.google.android.exoplayer2.Player.COMMAND_SEEK_TO_DEFAULT_POSITION;
|
||||
import static com.google.android.exoplayer2.Player.COMMAND_SEEK_TO_MEDIA_ITEM;
|
||||
import static com.google.android.exoplayer2.Player.COMMAND_SEEK_TO_NEXT;
|
||||
import static com.google.android.exoplayer2.Player.COMMAND_SEEK_TO_NEXT_MEDIA_ITEM;
|
||||
import static com.google.android.exoplayer2.Player.COMMAND_SEEK_TO_PREVIOUS;
|
||||
import static com.google.android.exoplayer2.Player.COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM;
|
||||
import static com.google.android.exoplayer2.util.Assertions.checkNotNull;
|
||||
import static java.lang.Math.abs;
|
||||
import static java.lang.Math.max;
|
||||
|
|
@ -63,6 +72,8 @@ import com.google.android.exoplayer2.Format;
|
|||
import com.google.android.exoplayer2.MediaItem;
|
||||
import com.google.android.exoplayer2.ParserException;
|
||||
import com.google.android.exoplayer2.PlaybackException;
|
||||
import com.google.android.exoplayer2.Player;
|
||||
import com.google.android.exoplayer2.Player.Commands;
|
||||
import com.google.common.base.Ascii;
|
||||
import com.google.common.base.Charsets;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
|
|
@ -1130,6 +1141,18 @@ public final class Util {
|
|||
return (timeMs == C.TIME_UNSET || timeMs == C.TIME_END_OF_SOURCE) ? timeMs : (timeMs * 1000);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a time in seconds to the corresponding time in microseconds.
|
||||
*
|
||||
* @param timeSec The time in seconds.
|
||||
* @return The corresponding time in microseconds.
|
||||
*/
|
||||
public static long secToUs(double timeSec) {
|
||||
return BigDecimal.valueOf(timeSec)
|
||||
.multiply(BigDecimal.valueOf(C.MICROS_PER_SECOND))
|
||||
.longValue();
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses an xs:duration attribute value, returning the parsed duration in milliseconds.
|
||||
*
|
||||
|
|
@ -2477,6 +2500,43 @@ public final class Util {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the {@link Commands} available in the {@link Player}.
|
||||
*
|
||||
* @param player The {@link Player}.
|
||||
* @param permanentAvailableCommands The commands permanently available in the player.
|
||||
* @return The available {@link Commands}.
|
||||
*/
|
||||
public static Commands getAvailableCommands(Player player, Commands permanentAvailableCommands) {
|
||||
boolean isPlayingAd = player.isPlayingAd();
|
||||
boolean isCurrentMediaItemSeekable = player.isCurrentMediaItemSeekable();
|
||||
boolean hasPreviousMediaItem = player.hasPreviousMediaItem();
|
||||
boolean hasNextMediaItem = player.hasNextMediaItem();
|
||||
boolean isCurrentMediaItemLive = player.isCurrentMediaItemLive();
|
||||
boolean isCurrentMediaItemDynamic = player.isCurrentMediaItemDynamic();
|
||||
boolean isTimelineEmpty = player.getCurrentTimeline().isEmpty();
|
||||
return new Commands.Builder()
|
||||
.addAll(permanentAvailableCommands)
|
||||
.addIf(COMMAND_SEEK_TO_DEFAULT_POSITION, !isPlayingAd)
|
||||
.addIf(COMMAND_SEEK_IN_CURRENT_MEDIA_ITEM, isCurrentMediaItemSeekable && !isPlayingAd)
|
||||
.addIf(COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM, hasPreviousMediaItem && !isPlayingAd)
|
||||
.addIf(
|
||||
COMMAND_SEEK_TO_PREVIOUS,
|
||||
!isTimelineEmpty
|
||||
&& (hasPreviousMediaItem || !isCurrentMediaItemLive || isCurrentMediaItemSeekable)
|
||||
&& !isPlayingAd)
|
||||
.addIf(COMMAND_SEEK_TO_NEXT_MEDIA_ITEM, hasNextMediaItem && !isPlayingAd)
|
||||
.addIf(
|
||||
COMMAND_SEEK_TO_NEXT,
|
||||
!isTimelineEmpty
|
||||
&& (hasNextMediaItem || (isCurrentMediaItemLive && isCurrentMediaItemDynamic))
|
||||
&& !isPlayingAd)
|
||||
.addIf(COMMAND_SEEK_TO_MEDIA_ITEM, !isPlayingAd)
|
||||
.addIf(COMMAND_SEEK_BACK, isCurrentMediaItemSeekable && !isPlayingAd)
|
||||
.addIf(COMMAND_SEEK_FORWARD, isCurrentMediaItemSeekable && !isPlayingAd)
|
||||
.build();
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private static String getSystemProperty(String name) {
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -278,6 +278,7 @@ public class MediaItemTest {
|
|||
.setSelectionFlags(C.SELECTION_FLAG_FORCED)
|
||||
.setRoleFlags(C.ROLE_FLAG_ALTERNATE)
|
||||
.setLabel("label")
|
||||
.setId("id")
|
||||
.build());
|
||||
|
||||
MediaItem mediaItem =
|
||||
|
|
@ -619,6 +620,7 @@ public class MediaItemTest {
|
|||
.setSelectionFlags(C.SELECTION_FLAG_FORCED)
|
||||
.setRoleFlags(C.ROLE_FLAG_ALTERNATE)
|
||||
.setLabel("label")
|
||||
.setId("id")
|
||||
.build()))
|
||||
.setTag(new Object())
|
||||
.build();
|
||||
|
|
@ -675,6 +677,7 @@ public class MediaItemTest {
|
|||
.setSelectionFlags(C.SELECTION_FLAG_FORCED)
|
||||
.setRoleFlags(C.ROLE_FLAG_ALTERNATE)
|
||||
.setLabel("label")
|
||||
.setId("id")
|
||||
.build()))
|
||||
.setTag(new Object())
|
||||
.build();
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ public class AudioAttributesTest {
|
|||
.setFlags(C.FLAG_AUDIBILITY_ENFORCED)
|
||||
.setUsage(C.USAGE_ALARM)
|
||||
.setAllowedCapturePolicy(C.ALLOW_CAPTURE_BY_SYSTEM)
|
||||
.setSpatializationBehavior(C.SPATIALIZATION_BEHAVIOR_NEVER)
|
||||
.build();
|
||||
|
||||
assertThat(AudioAttributes.CREATOR.fromBundle(audioAttributes.toBundle()))
|
||||
|
|
|
|||
|
|
@ -32,8 +32,9 @@ public final class TrackGroupTest {
|
|||
Format.Builder formatBuilder = new Format.Builder();
|
||||
Format format1 = formatBuilder.setSampleMimeType(MimeTypes.VIDEO_H264).build();
|
||||
Format format2 = formatBuilder.setSampleMimeType(MimeTypes.AUDIO_AAC).build();
|
||||
String id = "abc";
|
||||
|
||||
TrackGroup trackGroupToBundle = new TrackGroup(format1, format2);
|
||||
TrackGroup trackGroupToBundle = new TrackGroup(id, format1, format2);
|
||||
|
||||
TrackGroup trackGroupFromBundle = TrackGroup.CREATOR.fromBundle(trackGroupToBundle.toBundle());
|
||||
|
||||
|
|
|
|||
|
|
@ -16,7 +16,10 @@
|
|||
package com.google.android.exoplayer2.source.ads;
|
||||
|
||||
import static com.google.android.exoplayer2.source.ads.AdPlaybackState.AD_STATE_AVAILABLE;
|
||||
import static com.google.android.exoplayer2.source.ads.AdPlaybackState.AD_STATE_ERROR;
|
||||
import static com.google.android.exoplayer2.source.ads.AdPlaybackState.AD_STATE_PLAYED;
|
||||
import static com.google.android.exoplayer2.source.ads.AdPlaybackState.AD_STATE_SKIPPED;
|
||||
import static com.google.android.exoplayer2.source.ads.AdPlaybackState.AD_STATE_UNAVAILABLE;
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static org.junit.Assert.fail;
|
||||
|
||||
|
|
@ -254,6 +257,60 @@ public class AdPlaybackStateTest {
|
|||
assertThat(state.getAdGroup(1).count).isEqualTo(0);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void withResetAdGroup_beforeSetAdCount_doesNothing() {
|
||||
AdPlaybackState state = new AdPlaybackState(TEST_ADS_ID, TEST_AD_GROUP_TIMES_US);
|
||||
|
||||
state = state.withResetAdGroup(/* adGroupIndex= */ 1);
|
||||
|
||||
assertThat(state.getAdGroup(1).count).isEqualTo(C.LENGTH_UNSET);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void withResetAdGroup_resetsAdsInFinalStates() {
|
||||
AdPlaybackState state = new AdPlaybackState(TEST_ADS_ID, TEST_AD_GROUP_TIMES_US);
|
||||
state = state.withAdCount(/* adGroupIndex= */ 1, /* adCount= */ 5);
|
||||
state =
|
||||
state.withAdDurationsUs(
|
||||
/* adGroupIndex= */ 1, /* adDurationsUs...= */ 1_000L, 2_000L, 3_000L, 4_000L, 5_000L);
|
||||
state = state.withAdUri(/* adGroupIndex= */ 1, /* adIndexInAdGroup= */ 1, Uri.EMPTY);
|
||||
state = state.withAdUri(/* adGroupIndex= */ 1, /* adIndexInAdGroup= */ 2, Uri.EMPTY);
|
||||
state = state.withAdUri(/* adGroupIndex= */ 1, /* adIndexInAdGroup= */ 3, Uri.EMPTY);
|
||||
state = state.withAdUri(/* adGroupIndex= */ 1, /* adIndexInAdGroup= */ 4, Uri.EMPTY);
|
||||
state = state.withPlayedAd(/* adGroupIndex= */ 1, /* adIndexInAdGroup= */ 2);
|
||||
state = state.withSkippedAd(/* adGroupIndex= */ 1, /* adIndexInAdGroup= */ 3);
|
||||
state = state.withAdLoadError(/* adGroupIndex= */ 1, /* adIndexInAdGroup= */ 4);
|
||||
// Verify setup.
|
||||
assertThat(state.getAdGroup(/* adGroupIndex= */ 1).states)
|
||||
.asList()
|
||||
.containsExactly(
|
||||
AD_STATE_UNAVAILABLE,
|
||||
AD_STATE_AVAILABLE,
|
||||
AD_STATE_PLAYED,
|
||||
AD_STATE_SKIPPED,
|
||||
AD_STATE_ERROR)
|
||||
.inOrder();
|
||||
|
||||
state = state.withResetAdGroup(/* adGroupIndex= */ 1);
|
||||
|
||||
assertThat(state.getAdGroup(/* adGroupIndex= */ 1).states)
|
||||
.asList()
|
||||
.containsExactly(
|
||||
AD_STATE_UNAVAILABLE,
|
||||
AD_STATE_AVAILABLE,
|
||||
AD_STATE_AVAILABLE,
|
||||
AD_STATE_AVAILABLE,
|
||||
AD_STATE_AVAILABLE)
|
||||
.inOrder();
|
||||
assertThat(state.getAdGroup(/* adGroupIndex= */ 1).uris)
|
||||
.asList()
|
||||
.containsExactly(null, Uri.EMPTY, Uri.EMPTY, Uri.EMPTY, Uri.EMPTY)
|
||||
.inOrder();
|
||||
assertThat(state.getAdGroup(/* adGroupIndex= */ 1).durationsUs)
|
||||
.asList()
|
||||
.containsExactly(1_000L, 2_000L, 3_000L, 4_000L, 5_000L);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void roundTripViaBundle_yieldsEqualFieldsExceptAdsId() {
|
||||
AdPlaybackState originalState =
|
||||
|
|
|
|||
|
|
@ -47,6 +47,7 @@ dependencies {
|
|||
compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion
|
||||
compileOnly 'org.checkerframework:checker-compat-qual:' + checkerframeworkCompatVersion
|
||||
compileOnly 'org.jetbrains.kotlin:kotlin-annotations-jvm:' + kotlinAnnotationsVersion
|
||||
compileOnly 'com.google.errorprone:error_prone_annotations:' + errorProneVersion
|
||||
androidTestImplementation 'androidx.test:runner:' + androidxTestRunnerVersion
|
||||
androidTestImplementation 'com.linkedin.dexmaker:dexmaker:' + dexmakerVersion
|
||||
androidTestImplementation 'com.linkedin.dexmaker:dexmaker-mockito:' + dexmakerVersion
|
||||
|
|
|
|||
|
|
@ -9,6 +9,10 @@
|
|||
-keepclassmembers class com.google.android.exoplayer2.ext.av1.Libgav1VideoRenderer {
|
||||
<init>(long, android.os.Handler, com.google.android.exoplayer2.video.VideoRendererEventListener, int);
|
||||
}
|
||||
-dontnote com.google.android.exoplayer2.ext.ffmpeg.FfmpegVideoRenderer
|
||||
-keepclassmembers class com.google.android.exoplayer2.ext.ffmpeg.FfmpegVideoRenderer {
|
||||
<init>(long, android.os.Handler, com.google.android.exoplayer2.video.VideoRendererEventListener, int);
|
||||
}
|
||||
-dontnote com.google.android.exoplayer2.ext.opus.LibopusAudioRenderer
|
||||
-keepclassmembers class com.google.android.exoplayer2.ext.opus.LibopusAudioRenderer {
|
||||
<init>(android.os.Handler, com.google.android.exoplayer2.audio.AudioRendererEventListener, com.google.android.exoplayer2.audio.AudioSink);
|
||||
|
|
|
|||
|
|
@ -15,9 +15,11 @@
|
|||
*/
|
||||
package com.google.android.exoplayer2;
|
||||
|
||||
import static com.google.android.exoplayer2.util.Assertions.checkNotNull;
|
||||
import static java.lang.Math.max;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import com.google.android.exoplayer2.analytics.PlayerId;
|
||||
import com.google.android.exoplayer2.decoder.DecoderInputBuffer;
|
||||
import com.google.android.exoplayer2.decoder.DecoderInputBuffer.InsufficientCapacityException;
|
||||
import com.google.android.exoplayer2.source.SampleStream;
|
||||
|
|
@ -26,6 +28,7 @@ import com.google.android.exoplayer2.source.SampleStream.ReadFlags;
|
|||
import com.google.android.exoplayer2.util.Assertions;
|
||||
import com.google.android.exoplayer2.util.MediaClock;
|
||||
import java.io.IOException;
|
||||
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
||||
|
||||
/** An abstract base class suitable for most {@link Renderer} implementations. */
|
||||
public abstract class BaseRenderer implements Renderer, RendererCapabilities {
|
||||
|
|
@ -35,6 +38,7 @@ public abstract class BaseRenderer implements Renderer, RendererCapabilities {
|
|||
|
||||
@Nullable private RendererConfiguration configuration;
|
||||
private int index;
|
||||
private @MonotonicNonNull PlayerId playerId;
|
||||
private int state;
|
||||
@Nullable private SampleStream stream;
|
||||
@Nullable private Format[] streamFormats;
|
||||
|
|
@ -65,8 +69,9 @@ public abstract class BaseRenderer implements Renderer, RendererCapabilities {
|
|||
}
|
||||
|
||||
@Override
|
||||
public final void setIndex(int index) {
|
||||
public final void init(int index, PlayerId playerId) {
|
||||
this.index = index;
|
||||
this.playerId = playerId;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -328,11 +333,24 @@ public abstract class BaseRenderer implements Renderer, RendererCapabilities {
|
|||
return Assertions.checkNotNull(configuration);
|
||||
}
|
||||
|
||||
/** Returns the index of the renderer within the player. */
|
||||
/**
|
||||
* Returns the index of the renderer within the player.
|
||||
*
|
||||
* <p>Must only be used after the renderer has been initialized by the player.
|
||||
*/
|
||||
protected final int getIndex() {
|
||||
return index;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the {@link PlayerId} of the player using this renderer.
|
||||
*
|
||||
* <p>Must only be used after the renderer has been initialized by the player.
|
||||
*/
|
||||
protected final PlayerId getPlayerId() {
|
||||
return checkNotNull(playerId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an {@link ExoPlaybackException} of type {@link ExoPlaybackException#TYPE_RENDERER} for
|
||||
* this renderer.
|
||||
|
|
|
|||
|
|
@ -26,7 +26,6 @@ import com.google.android.exoplayer2.audio.AudioCapabilities;
|
|||
import com.google.android.exoplayer2.audio.AudioRendererEventListener;
|
||||
import com.google.android.exoplayer2.audio.AudioSink;
|
||||
import com.google.android.exoplayer2.audio.DefaultAudioSink;
|
||||
import com.google.android.exoplayer2.audio.DefaultAudioSink.DefaultAudioProcessorChain;
|
||||
import com.google.android.exoplayer2.audio.MediaCodecAudioRenderer;
|
||||
import com.google.android.exoplayer2.mediacodec.DefaultMediaCodecAdapterFactory;
|
||||
import com.google.android.exoplayer2.mediacodec.MediaCodecAdapter;
|
||||
|
|
@ -454,6 +453,32 @@ public class DefaultRenderersFactory implements RenderersFactory {
|
|||
// The extension is present, but instantiation failed.
|
||||
throw new RuntimeException("Error instantiating AV1 extension", e);
|
||||
}
|
||||
|
||||
try {
|
||||
// Full class names used for constructor args so the LINT rule triggers if any of them move.
|
||||
Class<?> clazz =
|
||||
Class.forName("com.google.android.exoplayer2.ext.ffmpeg.FfmpegVideoRenderer");
|
||||
Constructor<?> constructor =
|
||||
clazz.getConstructor(
|
||||
long.class,
|
||||
android.os.Handler.class,
|
||||
com.google.android.exoplayer2.video.VideoRendererEventListener.class,
|
||||
int.class);
|
||||
Renderer renderer =
|
||||
(Renderer)
|
||||
constructor.newInstance(
|
||||
allowedVideoJoiningTimeMs,
|
||||
eventHandler,
|
||||
eventListener,
|
||||
MAX_DROPPED_VIDEO_FRAME_COUNT_TO_NOTIFY);
|
||||
out.add(extensionRendererIndex++, renderer);
|
||||
Log.i(TAG, "Loaded FfmpegVideoRenderer.");
|
||||
} catch (ClassNotFoundException e) {
|
||||
// Expected if the app was built without the extension.
|
||||
} catch (Exception e) {
|
||||
// The extension is present, but instantiation failed.
|
||||
throw new RuntimeException("Error instantiating FFmpeg extension", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -640,14 +665,15 @@ public class DefaultRenderersFactory implements RenderersFactory {
|
|||
boolean enableFloatOutput,
|
||||
boolean enableAudioTrackPlaybackParams,
|
||||
boolean enableOffload) {
|
||||
return new DefaultAudioSink(
|
||||
AudioCapabilities.getCapabilities(context),
|
||||
new DefaultAudioProcessorChain(),
|
||||
enableFloatOutput,
|
||||
enableAudioTrackPlaybackParams,
|
||||
enableOffload
|
||||
? DefaultAudioSink.OFFLOAD_MODE_ENABLED_GAPLESS_REQUIRED
|
||||
: DefaultAudioSink.OFFLOAD_MODE_DISABLED);
|
||||
return new DefaultAudioSink.Builder()
|
||||
.setAudioCapabilities(AudioCapabilities.getCapabilities(context))
|
||||
.setEnableFloatOutput(enableFloatOutput)
|
||||
.setEnableAudioTrackPlaybackParams(enableAudioTrackPlaybackParams)
|
||||
.setOffloadMode(
|
||||
enableOffload
|
||||
? DefaultAudioSink.OFFLOAD_MODE_ENABLED_GAPLESS_REQUIRED
|
||||
: DefaultAudioSink.OFFLOAD_MODE_DISABLED)
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -33,7 +33,6 @@ import androidx.annotation.VisibleForTesting;
|
|||
import com.google.android.exoplayer2.analytics.AnalyticsCollector;
|
||||
import com.google.android.exoplayer2.analytics.AnalyticsListener;
|
||||
import com.google.android.exoplayer2.audio.AudioAttributes;
|
||||
import com.google.android.exoplayer2.audio.AudioCapabilities;
|
||||
import com.google.android.exoplayer2.audio.AudioSink;
|
||||
import com.google.android.exoplayer2.audio.AuxEffectInfo;
|
||||
import com.google.android.exoplayer2.audio.DefaultAudioSink;
|
||||
|
|
@ -44,7 +43,6 @@ import com.google.android.exoplayer2.extractor.ExtractorsFactory;
|
|||
import com.google.android.exoplayer2.metadata.MetadataRenderer;
|
||||
import com.google.android.exoplayer2.source.DefaultMediaSourceFactory;
|
||||
import com.google.android.exoplayer2.source.MediaSource;
|
||||
import com.google.android.exoplayer2.source.MediaSourceFactory;
|
||||
import com.google.android.exoplayer2.source.ShuffleOrder;
|
||||
import com.google.android.exoplayer2.text.Cue;
|
||||
import com.google.android.exoplayer2.text.TextRenderer;
|
||||
|
|
@ -78,7 +76,7 @@ import java.util.List;
|
|||
* <ul>
|
||||
* <li><b>{@link MediaSource MediaSources}</b> that define the media to be played, load the media,
|
||||
* and from which the loaded media can be read. MediaSources are created from {@link MediaItem
|
||||
* MediaItems} by the {@link MediaSourceFactory} injected into the player {@link
|
||||
* MediaItems} by the {@link MediaSource.Factory} injected into the player {@link
|
||||
* Builder#setMediaSourceFactory Builder}, or can be added directly by methods like {@link
|
||||
* #setMediaSource(MediaSource)}. The library provides a {@link DefaultMediaSourceFactory} for
|
||||
* progressive media files, DASH, SmoothStreaming and HLS, which also includes functionality
|
||||
|
|
@ -369,7 +367,7 @@ public interface ExoPlayer extends Player {
|
|||
/* package */ Clock clock;
|
||||
/* package */ long foregroundModeTimeoutMs;
|
||||
/* package */ Supplier<RenderersFactory> renderersFactorySupplier;
|
||||
/* package */ Supplier<MediaSourceFactory> mediaSourceFactorySupplier;
|
||||
/* package */ Supplier<MediaSource.Factory> mediaSourceFactorySupplier;
|
||||
/* package */ Supplier<TrackSelector> trackSelectorSupplier;
|
||||
/* package */ Supplier<LoadControl> loadControlSupplier;
|
||||
/* package */ Supplier<BandwidthMeter> bandwidthMeterSupplier;
|
||||
|
|
@ -397,7 +395,7 @@ public interface ExoPlayer extends Player {
|
|||
* Creates a builder.
|
||||
*
|
||||
* <p>Use {@link #Builder(Context, RenderersFactory)}, {@link #Builder(Context,
|
||||
* MediaSourceFactory)} or {@link #Builder(Context, RenderersFactory, MediaSourceFactory)}
|
||||
* MediaSource.Factory)} or {@link #Builder(Context, RenderersFactory, MediaSource.Factory)}
|
||||
* instead, if you intend to provide a custom {@link RenderersFactory}, {@link
|
||||
* ExtractorsFactory} or {@link DefaultMediaSourceFactory}. This is to ensure that ProGuard or
|
||||
* R8 can remove ExoPlayer's {@link DefaultRenderersFactory}, {@link DefaultExtractorsFactory}
|
||||
|
|
@ -408,7 +406,7 @@ public interface ExoPlayer extends Player {
|
|||
* <ul>
|
||||
* <li>{@link RenderersFactory}: {@link DefaultRenderersFactory}
|
||||
* <li>{@link TrackSelector}: {@link DefaultTrackSelector}
|
||||
* <li>{@link MediaSourceFactory}: {@link DefaultMediaSourceFactory}
|
||||
* <li>{@link MediaSource.Factory}: {@link DefaultMediaSourceFactory}
|
||||
* <li>{@link LoadControl}: {@link DefaultLoadControl}
|
||||
* <li>{@link BandwidthMeter}: {@link DefaultBandwidthMeter#getSingletonInstance(Context)}
|
||||
* <li>{@link LivePlaybackSpeedControl}: {@link DefaultLivePlaybackSpeedControl}
|
||||
|
|
@ -463,7 +461,7 @@ public interface ExoPlayer extends Player {
|
|||
}
|
||||
|
||||
/**
|
||||
* Creates a builder with a custom {@link MediaSourceFactory}.
|
||||
* Creates a builder with a custom {@link MediaSource.Factory}.
|
||||
*
|
||||
* <p>See {@link #Builder(Context)} for a list of default values.
|
||||
*
|
||||
|
|
@ -475,12 +473,12 @@ public interface ExoPlayer extends Player {
|
|||
* @param mediaSourceFactory A factory for creating a {@link MediaSource} from a {@link
|
||||
* MediaItem}.
|
||||
*/
|
||||
public Builder(Context context, MediaSourceFactory mediaSourceFactory) {
|
||||
public Builder(Context context, MediaSource.Factory mediaSourceFactory) {
|
||||
this(context, () -> new DefaultRenderersFactory(context), () -> mediaSourceFactory);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a builder with a custom {@link RenderersFactory} and {@link MediaSourceFactory}.
|
||||
* Creates a builder with a custom {@link RenderersFactory} and {@link MediaSource.Factory}.
|
||||
*
|
||||
* <p>See {@link #Builder(Context)} for a list of default values.
|
||||
*
|
||||
|
|
@ -495,7 +493,9 @@ public interface ExoPlayer extends Player {
|
|||
* MediaItem}.
|
||||
*/
|
||||
public Builder(
|
||||
Context context, RenderersFactory renderersFactory, MediaSourceFactory mediaSourceFactory) {
|
||||
Context context,
|
||||
RenderersFactory renderersFactory,
|
||||
MediaSource.Factory mediaSourceFactory) {
|
||||
this(context, () -> renderersFactory, () -> mediaSourceFactory);
|
||||
}
|
||||
|
||||
|
|
@ -508,7 +508,7 @@ public interface ExoPlayer extends Player {
|
|||
* @param context A {@link Context}.
|
||||
* @param renderersFactory A factory for creating {@link Renderer Renderers} to be used by the
|
||||
* player.
|
||||
* @param mediaSourceFactory A {@link MediaSourceFactory}.
|
||||
* @param mediaSourceFactory A {@link MediaSource.Factory}.
|
||||
* @param trackSelector A {@link TrackSelector}.
|
||||
* @param loadControl A {@link LoadControl}.
|
||||
* @param bandwidthMeter A {@link BandwidthMeter}.
|
||||
|
|
@ -517,7 +517,7 @@ public interface ExoPlayer extends Player {
|
|||
public Builder(
|
||||
Context context,
|
||||
RenderersFactory renderersFactory,
|
||||
MediaSourceFactory mediaSourceFactory,
|
||||
MediaSource.Factory mediaSourceFactory,
|
||||
TrackSelector trackSelector,
|
||||
LoadControl loadControl,
|
||||
BandwidthMeter bandwidthMeter,
|
||||
|
|
@ -535,7 +535,7 @@ public interface ExoPlayer extends Player {
|
|||
private Builder(
|
||||
Context context,
|
||||
Supplier<RenderersFactory> renderersFactorySupplier,
|
||||
Supplier<MediaSourceFactory> mediaSourceFactorySupplier) {
|
||||
Supplier<MediaSource.Factory> mediaSourceFactorySupplier) {
|
||||
this(
|
||||
context,
|
||||
renderersFactorySupplier,
|
||||
|
|
@ -549,7 +549,7 @@ public interface ExoPlayer extends Player {
|
|||
private Builder(
|
||||
Context context,
|
||||
Supplier<RenderersFactory> renderersFactorySupplier,
|
||||
Supplier<MediaSourceFactory> mediaSourceFactorySupplier,
|
||||
Supplier<MediaSource.Factory> mediaSourceFactorySupplier,
|
||||
Supplier<TrackSelector> trackSelectorSupplier,
|
||||
Supplier<LoadControl> loadControlSupplier,
|
||||
Supplier<BandwidthMeter> bandwidthMeterSupplier,
|
||||
|
|
@ -608,13 +608,13 @@ public interface ExoPlayer extends Player {
|
|||
}
|
||||
|
||||
/**
|
||||
* Sets the {@link MediaSourceFactory} that will be used by the player.
|
||||
* Sets the {@link MediaSource.Factory} that will be used by the player.
|
||||
*
|
||||
* @param mediaSourceFactory A {@link MediaSourceFactory}.
|
||||
* @param mediaSourceFactory A {@link MediaSource.Factory}.
|
||||
* @return This builder.
|
||||
* @throws IllegalStateException If {@link #build()} has already been called.
|
||||
*/
|
||||
public Builder setMediaSourceFactory(MediaSourceFactory mediaSourceFactory) {
|
||||
public Builder setMediaSourceFactory(MediaSource.Factory mediaSourceFactory) {
|
||||
checkState(!buildCalled);
|
||||
this.mediaSourceFactorySupplier = () -> mediaSourceFactory;
|
||||
return this;
|
||||
|
|
@ -1492,8 +1492,7 @@ public interface ExoPlayer extends Player {
|
|||
* <ul>
|
||||
* <li>Audio offload rendering is enabled in {@link
|
||||
* DefaultRenderersFactory#setEnableAudioOffload} or the equivalent option passed to {@link
|
||||
* DefaultAudioSink#DefaultAudioSink(AudioCapabilities,
|
||||
* DefaultAudioSink.AudioProcessorChain, boolean, boolean, int)}.
|
||||
* DefaultAudioSink.Builder#setOffloadMode}.
|
||||
* <li>An audio track is playing in a format that the device supports offloading (for example,
|
||||
* MP3 or AAC).
|
||||
* <li>The {@link AudioSink} is playing with an offload {@link AudioTrack}.
|
||||
|
|
|
|||
|
|
@ -15,6 +15,39 @@
|
|||
*/
|
||||
package com.google.android.exoplayer2;
|
||||
|
||||
import static com.google.android.exoplayer2.Player.COMMAND_CHANGE_MEDIA_ITEMS;
|
||||
import static com.google.android.exoplayer2.Player.COMMAND_GET_CURRENT_MEDIA_ITEM;
|
||||
import static com.google.android.exoplayer2.Player.COMMAND_GET_MEDIA_ITEMS_METADATA;
|
||||
import static com.google.android.exoplayer2.Player.COMMAND_GET_TIMELINE;
|
||||
import static com.google.android.exoplayer2.Player.COMMAND_GET_TRACK_INFOS;
|
||||
import static com.google.android.exoplayer2.Player.COMMAND_PLAY_PAUSE;
|
||||
import static com.google.android.exoplayer2.Player.COMMAND_PREPARE;
|
||||
import static com.google.android.exoplayer2.Player.COMMAND_SEEK_TO_DEFAULT_POSITION;
|
||||
import static com.google.android.exoplayer2.Player.COMMAND_SEEK_TO_MEDIA_ITEM;
|
||||
import static com.google.android.exoplayer2.Player.COMMAND_SET_MEDIA_ITEMS_METADATA;
|
||||
import static com.google.android.exoplayer2.Player.COMMAND_SET_REPEAT_MODE;
|
||||
import static com.google.android.exoplayer2.Player.COMMAND_SET_SHUFFLE_MODE;
|
||||
import static com.google.android.exoplayer2.Player.COMMAND_SET_SPEED_AND_PITCH;
|
||||
import static com.google.android.exoplayer2.Player.COMMAND_SET_TRACK_SELECTION_PARAMETERS;
|
||||
import static com.google.android.exoplayer2.Player.COMMAND_STOP;
|
||||
import static com.google.android.exoplayer2.Player.DISCONTINUITY_REASON_AUTO_TRANSITION;
|
||||
import static com.google.android.exoplayer2.Player.DISCONTINUITY_REASON_INTERNAL;
|
||||
import static com.google.android.exoplayer2.Player.DISCONTINUITY_REASON_REMOVE;
|
||||
import static com.google.android.exoplayer2.Player.DISCONTINUITY_REASON_SEEK;
|
||||
import static com.google.android.exoplayer2.Player.EVENT_MEDIA_METADATA_CHANGED;
|
||||
import static com.google.android.exoplayer2.Player.EVENT_PLAYLIST_METADATA_CHANGED;
|
||||
import static com.google.android.exoplayer2.Player.EVENT_TRACK_SELECTION_PARAMETERS_CHANGED;
|
||||
import static com.google.android.exoplayer2.Player.MEDIA_ITEM_TRANSITION_REASON_AUTO;
|
||||
import static com.google.android.exoplayer2.Player.MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED;
|
||||
import static com.google.android.exoplayer2.Player.MEDIA_ITEM_TRANSITION_REASON_REPEAT;
|
||||
import static com.google.android.exoplayer2.Player.MEDIA_ITEM_TRANSITION_REASON_SEEK;
|
||||
import static com.google.android.exoplayer2.Player.PLAYBACK_SUPPRESSION_REASON_NONE;
|
||||
import static com.google.android.exoplayer2.Player.PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST;
|
||||
import static com.google.android.exoplayer2.Player.STATE_BUFFERING;
|
||||
import static com.google.android.exoplayer2.Player.STATE_ENDED;
|
||||
import static com.google.android.exoplayer2.Player.STATE_IDLE;
|
||||
import static com.google.android.exoplayer2.Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED;
|
||||
import static com.google.android.exoplayer2.Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE;
|
||||
import static com.google.android.exoplayer2.util.Assertions.checkNotNull;
|
||||
import static com.google.android.exoplayer2.util.Assertions.checkState;
|
||||
import static com.google.android.exoplayer2.util.Util.castNonNull;
|
||||
|
|
@ -22,26 +55,34 @@ import static java.lang.Math.max;
|
|||
import static java.lang.Math.min;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.media.metrics.LogSessionId;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.util.Pair;
|
||||
import android.view.Surface;
|
||||
import android.view.SurfaceHolder;
|
||||
import android.view.SurfaceView;
|
||||
import android.view.TextureView;
|
||||
import androidx.annotation.DoNotInline;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.RequiresApi;
|
||||
import com.google.android.exoplayer2.ExoPlayer.AudioOffloadListener;
|
||||
import com.google.android.exoplayer2.Player.Commands;
|
||||
import com.google.android.exoplayer2.Player.DiscontinuityReason;
|
||||
import com.google.android.exoplayer2.Player.EventListener;
|
||||
import com.google.android.exoplayer2.Player.Events;
|
||||
import com.google.android.exoplayer2.Player.Listener;
|
||||
import com.google.android.exoplayer2.Player.PlayWhenReadyChangeReason;
|
||||
import com.google.android.exoplayer2.Player.PlaybackSuppressionReason;
|
||||
import com.google.android.exoplayer2.Player.PositionInfo;
|
||||
import com.google.android.exoplayer2.Player.RepeatMode;
|
||||
import com.google.android.exoplayer2.Player.State;
|
||||
import com.google.android.exoplayer2.Player.TimelineChangeReason;
|
||||
import com.google.android.exoplayer2.PlayerMessage.Target;
|
||||
import com.google.android.exoplayer2.analytics.AnalyticsCollector;
|
||||
import com.google.android.exoplayer2.audio.AudioAttributes;
|
||||
import com.google.android.exoplayer2.analytics.PlayerId;
|
||||
import com.google.android.exoplayer2.metadata.Metadata;
|
||||
import com.google.android.exoplayer2.source.MediaSource;
|
||||
import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId;
|
||||
import com.google.android.exoplayer2.source.MediaSourceFactory;
|
||||
import com.google.android.exoplayer2.source.ShuffleOrder;
|
||||
import com.google.android.exoplayer2.source.TrackGroup;
|
||||
import com.google.android.exoplayer2.source.TrackGroupArray;
|
||||
import com.google.android.exoplayer2.text.Cue;
|
||||
import com.google.android.exoplayer2.trackselection.ExoTrackSelection;
|
||||
import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
|
||||
import com.google.android.exoplayer2.trackselection.TrackSelectionParameters;
|
||||
|
|
@ -54,15 +95,14 @@ import com.google.android.exoplayer2.util.HandlerWrapper;
|
|||
import com.google.android.exoplayer2.util.ListenerSet;
|
||||
import com.google.android.exoplayer2.util.Log;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
import com.google.android.exoplayer2.video.VideoSize;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CopyOnWriteArraySet;
|
||||
|
||||
/** An {@link ExoPlayer} implementation. */
|
||||
/* package */ final class ExoPlayerImpl extends BasePlayer {
|
||||
/** A helper class for the {@link SimpleExoPlayer} implementation of {@link ExoPlayer}. */
|
||||
/* package */ final class ExoPlayerImpl {
|
||||
|
||||
static {
|
||||
ExoPlayerLibraryInfo.registerModule("goog.exo.exoplayer");
|
||||
|
|
@ -80,6 +120,7 @@ import java.util.concurrent.CopyOnWriteArraySet;
|
|||
/* package */ final TrackSelectorResult emptyTrackSelectorResult;
|
||||
/* package */ final Commands permanentAvailableCommands;
|
||||
|
||||
private final Player wrappingPlayer;
|
||||
private final Renderer[] renderers;
|
||||
private final TrackSelector trackSelector;
|
||||
private final HandlerWrapper playbackInfoUpdateHandler;
|
||||
|
|
@ -88,10 +129,11 @@ import java.util.concurrent.CopyOnWriteArraySet;
|
|||
private final ListenerSet<Player.EventListener> listeners;
|
||||
private final CopyOnWriteArraySet<AudioOffloadListener> audioOffloadListeners;
|
||||
private final Timeline.Period period;
|
||||
private final Timeline.Window window;
|
||||
private final List<MediaSourceHolderSnapshot> mediaSourceHolderSnapshots;
|
||||
private final boolean useLazyPreparation;
|
||||
private final MediaSourceFactory mediaSourceFactory;
|
||||
@Nullable private final AnalyticsCollector analyticsCollector;
|
||||
private final MediaSource.Factory mediaSourceFactory;
|
||||
private final AnalyticsCollector analyticsCollector;
|
||||
private final Looper applicationLooper;
|
||||
private final BandwidthMeter bandwidthMeter;
|
||||
private final long seekBackIncrementMs;
|
||||
|
|
@ -129,7 +171,7 @@ import java.util.concurrent.CopyOnWriteArraySet;
|
|||
*
|
||||
* @param renderers The {@link Renderer}s.
|
||||
* @param trackSelector The {@link TrackSelector}.
|
||||
* @param mediaSourceFactory The {@link MediaSourceFactory}.
|
||||
* @param mediaSourceFactory The {@link MediaSource.Factory}.
|
||||
* @param loadControl The {@link LoadControl}.
|
||||
* @param bandwidthMeter The {@link BandwidthMeter}.
|
||||
* @param analyticsCollector The {@link AnalyticsCollector}.
|
||||
|
|
@ -137,16 +179,15 @@ import java.util.concurrent.CopyOnWriteArraySet;
|
|||
* loads and other initial preparation steps happen immediately. If true, these initial
|
||||
* preparations are triggered only when the player starts buffering the media.
|
||||
* @param seekParameters The {@link SeekParameters}.
|
||||
* @param seekBackIncrementMs The {@link #seekBack()} increment in milliseconds.
|
||||
* @param seekForwardIncrementMs The {@link #seekForward()} increment in milliseconds.
|
||||
* @param seekBackIncrementMs The seek back increment in milliseconds.
|
||||
* @param seekForwardIncrementMs The seek forward increment in milliseconds.
|
||||
* @param livePlaybackSpeedControl The {@link LivePlaybackSpeedControl}.
|
||||
* @param releaseTimeoutMs The timeout for calls to {@link #release()} in milliseconds.
|
||||
* @param pauseAtEndOfMediaItems Whether to pause playback at the end of each media item.
|
||||
* @param clock The {@link Clock}.
|
||||
* @param applicationLooper The {@link Looper} that must be used for all calls to the player and
|
||||
* which is used to call listeners on.
|
||||
* @param wrappingPlayer The {@link Player} wrapping this one if applicable. This player instance
|
||||
* should be used for all externally visible callbacks.
|
||||
* @param wrappingPlayer The {@link Player} using this class.
|
||||
* @param additionalPermanentAvailableCommands The {@link Commands} that are permanently available
|
||||
* in the wrapping player but that are not in this player.
|
||||
*/
|
||||
|
|
@ -154,10 +195,10 @@ import java.util.concurrent.CopyOnWriteArraySet;
|
|||
public ExoPlayerImpl(
|
||||
Renderer[] renderers,
|
||||
TrackSelector trackSelector,
|
||||
MediaSourceFactory mediaSourceFactory,
|
||||
MediaSource.Factory mediaSourceFactory,
|
||||
LoadControl loadControl,
|
||||
BandwidthMeter bandwidthMeter,
|
||||
@Nullable AnalyticsCollector analyticsCollector,
|
||||
AnalyticsCollector analyticsCollector,
|
||||
boolean useLazyPreparation,
|
||||
SeekParameters seekParameters,
|
||||
long seekBackIncrementMs,
|
||||
|
|
@ -167,7 +208,7 @@ import java.util.concurrent.CopyOnWriteArraySet;
|
|||
boolean pauseAtEndOfMediaItems,
|
||||
Clock clock,
|
||||
Looper applicationLooper,
|
||||
@Nullable Player wrappingPlayer,
|
||||
Player wrappingPlayer,
|
||||
Commands additionalPermanentAvailableCommands) {
|
||||
Log.i(
|
||||
TAG,
|
||||
|
|
@ -191,13 +232,13 @@ import java.util.concurrent.CopyOnWriteArraySet;
|
|||
this.pauseAtEndOfMediaItems = pauseAtEndOfMediaItems;
|
||||
this.applicationLooper = applicationLooper;
|
||||
this.clock = clock;
|
||||
this.wrappingPlayer = wrappingPlayer;
|
||||
repeatMode = Player.REPEAT_MODE_OFF;
|
||||
Player playerForListeners = wrappingPlayer != null ? wrappingPlayer : this;
|
||||
listeners =
|
||||
new ListenerSet<>(
|
||||
applicationLooper,
|
||||
clock,
|
||||
(listener, flags) -> listener.onEvents(playerForListeners, new Events(flags)));
|
||||
(listener, flags) -> listener.onEvents(wrappingPlayer, new Events(flags)));
|
||||
audioOffloadListeners = new CopyOnWriteArraySet<>();
|
||||
mediaSourceHolderSnapshots = new ArrayList<>();
|
||||
shuffleOrder = new ShuffleOrder.DefaultShuffleOrder(/* length= */ 0);
|
||||
|
|
@ -208,6 +249,7 @@ import java.util.concurrent.CopyOnWriteArraySet;
|
|||
TracksInfo.EMPTY,
|
||||
/* info= */ null);
|
||||
period = new Timeline.Period();
|
||||
window = new Timeline.Window();
|
||||
permanentAvailableCommands =
|
||||
new Commands.Builder()
|
||||
.addAll(
|
||||
|
|
@ -241,11 +283,10 @@ import java.util.concurrent.CopyOnWriteArraySet;
|
|||
playbackInfoUpdate ->
|
||||
playbackInfoUpdateHandler.post(() -> handlePlaybackInfo(playbackInfoUpdate));
|
||||
playbackInfo = PlaybackInfo.createDummy(emptyTrackSelectorResult);
|
||||
if (analyticsCollector != null) {
|
||||
analyticsCollector.setPlayer(playerForListeners, applicationLooper);
|
||||
addListener(analyticsCollector);
|
||||
bandwidthMeter.addEventListener(new Handler(applicationLooper), analyticsCollector);
|
||||
}
|
||||
analyticsCollector.setPlayer(wrappingPlayer, applicationLooper);
|
||||
addListener(analyticsCollector);
|
||||
bandwidthMeter.addEventListener(new Handler(applicationLooper), analyticsCollector);
|
||||
PlayerId playerId = Util.SDK_INT < 31 ? new PlayerId() : Api31.createPlayerId();
|
||||
internalPlayer =
|
||||
new ExoPlayerImplInternal(
|
||||
renderers,
|
||||
|
|
@ -262,7 +303,8 @@ import java.util.concurrent.CopyOnWriteArraySet;
|
|||
pauseAtEndOfMediaItems,
|
||||
applicationLooper,
|
||||
clock,
|
||||
playbackInfoUpdateListener);
|
||||
playbackInfoUpdateListener,
|
||||
playerId);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -291,7 +333,6 @@ import java.util.concurrent.CopyOnWriteArraySet;
|
|||
return internalPlayer.getPlaybackLooper();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Looper getApplicationLooper() {
|
||||
return applicationLooper;
|
||||
}
|
||||
|
|
@ -300,16 +341,10 @@ import java.util.concurrent.CopyOnWriteArraySet;
|
|||
return clock;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addListener(Listener listener) {
|
||||
addEventListener(listener);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeListener(Listener listener) {
|
||||
removeEventListener(listener);
|
||||
}
|
||||
|
||||
@SuppressWarnings("deprecation") // Register deprecated EventListener.
|
||||
public void addEventListener(Player.EventListener eventListener) {
|
||||
listeners.add(eventListener);
|
||||
|
|
@ -328,24 +363,20 @@ import java.util.concurrent.CopyOnWriteArraySet;
|
|||
audioOffloadListeners.remove(listener);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Commands getAvailableCommands() {
|
||||
return availableCommands;
|
||||
}
|
||||
|
||||
@Override
|
||||
@State
|
||||
public int getPlaybackState() {
|
||||
return playbackInfo.playbackState;
|
||||
}
|
||||
|
||||
@Override
|
||||
@PlaybackSuppressionReason
|
||||
public int getPlaybackSuppressionReason() {
|
||||
return playbackInfo.playbackSuppressionReason;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public ExoPlaybackException getPlayerError() {
|
||||
return playbackInfo.playbackError;
|
||||
|
|
@ -357,7 +388,6 @@ import java.util.concurrent.CopyOnWriteArraySet;
|
|||
prepare();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void prepare() {
|
||||
if (playbackInfo.playbackState != Player.STATE_IDLE) {
|
||||
return;
|
||||
|
|
@ -365,7 +395,7 @@ import java.util.concurrent.CopyOnWriteArraySet;
|
|||
PlaybackInfo playbackInfo = this.playbackInfo.copyWithPlaybackError(null);
|
||||
playbackInfo =
|
||||
playbackInfo.copyWithPlaybackState(
|
||||
playbackInfo.timeline.isEmpty() ? Player.STATE_ENDED : Player.STATE_BUFFERING);
|
||||
playbackInfo.timeline.isEmpty() ? STATE_ENDED : STATE_BUFFERING);
|
||||
// Trigger internal prepare first before updating the playback info and notifying external
|
||||
// listeners to ensure that new operations issued in the listener notifications reach the
|
||||
// player after this prepare. The internal player can't change the playback info immediately
|
||||
|
|
@ -402,12 +432,10 @@ import java.util.concurrent.CopyOnWriteArraySet;
|
|||
prepare();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setMediaItems(List<MediaItem> mediaItems, boolean resetPosition) {
|
||||
setMediaSources(createMediaSources(mediaItems), resetPosition);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setMediaItems(List<MediaItem> mediaItems, int startIndex, long startPositionMs) {
|
||||
setMediaSources(createMediaSources(mediaItems), startIndex, startPositionMs);
|
||||
}
|
||||
|
|
@ -443,7 +471,6 @@ import java.util.concurrent.CopyOnWriteArraySet;
|
|||
mediaSources, startWindowIndex, startPositionMs, /* resetToDefaultPosition= */ false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addMediaItems(int index, List<MediaItem> mediaItems) {
|
||||
index = min(index, mediaSourceHolderSnapshots.size());
|
||||
addMediaSources(index, createMediaSources(mediaItems));
|
||||
|
|
@ -471,7 +498,7 @@ import java.util.concurrent.CopyOnWriteArraySet;
|
|||
maskTimelineAndPosition(
|
||||
playbackInfo,
|
||||
newTimeline,
|
||||
getPeriodPositionAfterTimelineChanged(oldTimeline, newTimeline));
|
||||
getPeriodPositionUsAfterTimelineChanged(oldTimeline, newTimeline));
|
||||
internalPlayer.addMediaSources(index, holders, shuffleOrder);
|
||||
updatePlaybackInfo(
|
||||
newPlaybackInfo,
|
||||
|
|
@ -484,7 +511,6 @@ import java.util.concurrent.CopyOnWriteArraySet;
|
|||
/* ignored */ C.INDEX_UNSET);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeMediaItems(int fromIndex, int toIndex) {
|
||||
toIndex = min(toIndex, mediaSourceHolderSnapshots.size());
|
||||
PlaybackInfo newPlaybackInfo = removeMediaItemsInternal(fromIndex, toIndex);
|
||||
|
|
@ -496,12 +522,11 @@ import java.util.concurrent.CopyOnWriteArraySet;
|
|||
/* ignored */ PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST,
|
||||
/* seekProcessed= */ false,
|
||||
positionDiscontinuity,
|
||||
Player.DISCONTINUITY_REASON_REMOVE,
|
||||
DISCONTINUITY_REASON_REMOVE,
|
||||
/* discontinuityWindowStartPositionUs= */ getCurrentPositionUsInternal(newPlaybackInfo),
|
||||
/* ignored */ C.INDEX_UNSET);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void moveMediaItems(int fromIndex, int toIndex, int newFromIndex) {
|
||||
Assertions.checkArgument(
|
||||
fromIndex >= 0
|
||||
|
|
@ -517,7 +542,7 @@ import java.util.concurrent.CopyOnWriteArraySet;
|
|||
maskTimelineAndPosition(
|
||||
playbackInfo,
|
||||
newTimeline,
|
||||
getPeriodPositionAfterTimelineChanged(oldTimeline, newTimeline));
|
||||
getPeriodPositionUsAfterTimelineChanged(oldTimeline, newTimeline));
|
||||
internalPlayer.moveMediaSources(fromIndex, toIndex, newFromIndex, shuffleOrder);
|
||||
updatePlaybackInfo(
|
||||
newPlaybackInfo,
|
||||
|
|
@ -536,7 +561,7 @@ import java.util.concurrent.CopyOnWriteArraySet;
|
|||
maskTimelineAndPosition(
|
||||
playbackInfo,
|
||||
timeline,
|
||||
getPeriodPositionOrMaskWindowPosition(
|
||||
maskWindowPositionMsOrGetPeriodPositionUs(
|
||||
timeline, getCurrentMediaItemIndex(), getCurrentPosition()));
|
||||
pendingOperationAcks++;
|
||||
this.shuffleOrder = shuffleOrder;
|
||||
|
|
@ -552,14 +577,6 @@ import java.util.concurrent.CopyOnWriteArraySet;
|
|||
/* ignored */ C.INDEX_UNSET);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setPlayWhenReady(boolean playWhenReady) {
|
||||
setPlayWhenReady(
|
||||
playWhenReady,
|
||||
PLAYBACK_SUPPRESSION_REASON_NONE,
|
||||
PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST);
|
||||
}
|
||||
|
||||
public void setPauseAtEndOfMediaItems(boolean pauseAtEndOfMediaItems) {
|
||||
if (this.pauseAtEndOfMediaItems == pauseAtEndOfMediaItems) {
|
||||
return;
|
||||
|
|
@ -595,12 +612,10 @@ import java.util.concurrent.CopyOnWriteArraySet;
|
|||
/* ignored */ C.INDEX_UNSET);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean getPlayWhenReady() {
|
||||
return playbackInfo.playWhenReady;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setRepeatMode(@RepeatMode int repeatMode) {
|
||||
if (this.repeatMode != repeatMode) {
|
||||
this.repeatMode = repeatMode;
|
||||
|
|
@ -612,12 +627,11 @@ import java.util.concurrent.CopyOnWriteArraySet;
|
|||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public @RepeatMode int getRepeatMode() {
|
||||
@RepeatMode
|
||||
public int getRepeatMode() {
|
||||
return repeatMode;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setShuffleModeEnabled(boolean shuffleModeEnabled) {
|
||||
if (this.shuffleModeEnabled != shuffleModeEnabled) {
|
||||
this.shuffleModeEnabled = shuffleModeEnabled;
|
||||
|
|
@ -630,17 +644,14 @@ import java.util.concurrent.CopyOnWriteArraySet;
|
|||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean getShuffleModeEnabled() {
|
||||
return shuffleModeEnabled;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isLoading() {
|
||||
return playbackInfo.isLoading;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void seekTo(int mediaItemIndex, long positionMs) {
|
||||
Timeline timeline = playbackInfo.timeline;
|
||||
if (mediaItemIndex < 0
|
||||
|
|
@ -661,14 +672,14 @@ import java.util.concurrent.CopyOnWriteArraySet;
|
|||
}
|
||||
@Player.State
|
||||
int newPlaybackState =
|
||||
getPlaybackState() == Player.STATE_IDLE ? Player.STATE_IDLE : Player.STATE_BUFFERING;
|
||||
getPlaybackState() == Player.STATE_IDLE ? Player.STATE_IDLE : STATE_BUFFERING;
|
||||
int oldMaskingMediaItemIndex = getCurrentMediaItemIndex();
|
||||
PlaybackInfo newPlaybackInfo = playbackInfo.copyWithPlaybackState(newPlaybackState);
|
||||
newPlaybackInfo =
|
||||
maskTimelineAndPosition(
|
||||
newPlaybackInfo,
|
||||
timeline,
|
||||
getPeriodPositionOrMaskWindowPosition(timeline, mediaItemIndex, positionMs));
|
||||
maskWindowPositionMsOrGetPeriodPositionUs(timeline, mediaItemIndex, positionMs));
|
||||
internalPlayer.seekTo(timeline, mediaItemIndex, Util.msToUs(positionMs));
|
||||
updatePlaybackInfo(
|
||||
newPlaybackInfo,
|
||||
|
|
@ -681,22 +692,18 @@ import java.util.concurrent.CopyOnWriteArraySet;
|
|||
oldMaskingMediaItemIndex);
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getSeekBackIncrement() {
|
||||
return seekBackIncrementMs;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getSeekForwardIncrement() {
|
||||
return seekForwardIncrementMs;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getMaxSeekToPreviousPosition() {
|
||||
return C.DEFAULT_MAX_SEEK_TO_PREVIOUS_POSITION_MS;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setPlaybackParameters(PlaybackParameters playbackParameters) {
|
||||
if (playbackParameters == null) {
|
||||
playbackParameters = PlaybackParameters.DEFAULT;
|
||||
|
|
@ -718,7 +725,6 @@ import java.util.concurrent.CopyOnWriteArraySet;
|
|||
/* ignored */ C.INDEX_UNSET);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PlaybackParameters getPlaybackParameters() {
|
||||
return playbackInfo.playbackParameters;
|
||||
}
|
||||
|
|
@ -751,13 +757,6 @@ import java.util.concurrent.CopyOnWriteArraySet;
|
|||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop() {
|
||||
stop(/* reset= */ false);
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
@Override
|
||||
public void stop(boolean reset) {
|
||||
stop(reset, /* error= */ null);
|
||||
}
|
||||
|
|
@ -800,7 +799,6 @@ import java.util.concurrent.CopyOnWriteArraySet;
|
|||
/* ignored */ C.INDEX_UNSET);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void release() {
|
||||
Log.i(
|
||||
TAG,
|
||||
|
|
@ -825,9 +823,7 @@ import java.util.concurrent.CopyOnWriteArraySet;
|
|||
}
|
||||
listeners.release();
|
||||
playbackInfoUpdateHandler.removeCallbacksAndMessages(null);
|
||||
if (analyticsCollector != null) {
|
||||
bandwidthMeter.removeEventListener(analyticsCollector);
|
||||
}
|
||||
bandwidthMeter.removeEventListener(analyticsCollector);
|
||||
playbackInfo = playbackInfo.copyWithPlaybackState(Player.STATE_IDLE);
|
||||
playbackInfo = playbackInfo.copyWithLoadingMediaPeriodId(playbackInfo.periodId);
|
||||
playbackInfo.bufferedPositionUs = playbackInfo.positionUs;
|
||||
|
|
@ -844,7 +840,6 @@ import java.util.concurrent.CopyOnWriteArraySet;
|
|||
internalPlayer.getPlaybackLooper());
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getCurrentPeriodIndex() {
|
||||
if (playbackInfo.timeline.isEmpty()) {
|
||||
return maskingPeriodIndex;
|
||||
|
|
@ -853,13 +848,11 @@ import java.util.concurrent.CopyOnWriteArraySet;
|
|||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getCurrentMediaItemIndex() {
|
||||
int currentWindowIndex = getCurrentWindowIndexInternal();
|
||||
return currentWindowIndex == C.INDEX_UNSET ? 0 : currentWindowIndex;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getDuration() {
|
||||
if (isPlayingAd()) {
|
||||
MediaPeriodId periodId = playbackInfo.periodId;
|
||||
|
|
@ -870,12 +863,17 @@ import java.util.concurrent.CopyOnWriteArraySet;
|
|||
return getContentDuration();
|
||||
}
|
||||
|
||||
@Override
|
||||
private long getContentDuration() {
|
||||
Timeline timeline = getCurrentTimeline();
|
||||
return timeline.isEmpty()
|
||||
? C.TIME_UNSET
|
||||
: timeline.getWindow(getCurrentMediaItemIndex(), window).getDurationMs();
|
||||
}
|
||||
|
||||
public long getCurrentPosition() {
|
||||
return Util.usToMs(getCurrentPositionUsInternal(playbackInfo));
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getBufferedPosition() {
|
||||
if (isPlayingAd()) {
|
||||
return playbackInfo.loadingMediaPeriodId.equals(playbackInfo.periodId)
|
||||
|
|
@ -885,27 +883,22 @@ import java.util.concurrent.CopyOnWriteArraySet;
|
|||
return getContentBufferedPosition();
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getTotalBufferedDuration() {
|
||||
return Util.usToMs(playbackInfo.totalBufferedDurationUs);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isPlayingAd() {
|
||||
return playbackInfo.periodId.isAd();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getCurrentAdGroupIndex() {
|
||||
return isPlayingAd() ? playbackInfo.periodId.adGroupIndex : C.INDEX_UNSET;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getCurrentAdIndexInAdGroup() {
|
||||
return isPlayingAd() ? playbackInfo.periodId.adIndexInAdGroup : C.INDEX_UNSET;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getContentPosition() {
|
||||
if (isPlayingAd()) {
|
||||
playbackInfo.timeline.getPeriodByUid(playbackInfo.periodId.periodUid, period);
|
||||
|
|
@ -920,7 +913,6 @@ import java.util.concurrent.CopyOnWriteArraySet;
|
|||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getContentBufferedPosition() {
|
||||
if (playbackInfo.timeline.isEmpty()) {
|
||||
return maskingWindowPositionMs;
|
||||
|
|
@ -952,32 +944,26 @@ import java.util.concurrent.CopyOnWriteArraySet;
|
|||
return renderers[index].getTrackType();
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public TrackSelector getTrackSelector() {
|
||||
return trackSelector;
|
||||
}
|
||||
|
||||
@Override
|
||||
public TrackGroupArray getCurrentTrackGroups() {
|
||||
return playbackInfo.trackGroups;
|
||||
}
|
||||
|
||||
@Override
|
||||
public TrackSelectionArray getCurrentTrackSelections() {
|
||||
return new TrackSelectionArray(playbackInfo.trackSelectorResult.selections);
|
||||
}
|
||||
|
||||
@Override
|
||||
public TracksInfo getCurrentTracksInfo() {
|
||||
return playbackInfo.trackSelectorResult.tracksInfo;
|
||||
}
|
||||
|
||||
@Override
|
||||
public TrackSelectionParameters getTrackSelectionParameters() {
|
||||
return trackSelector.getParameters();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setTrackSelectionParameters(TrackSelectionParameters parameters) {
|
||||
if (!trackSelector.isSetParametersSupported()
|
||||
|| parameters.equals(trackSelector.getParameters())) {
|
||||
|
|
@ -989,7 +975,6 @@ import java.util.concurrent.CopyOnWriteArraySet;
|
|||
listener -> listener.onTrackSelectionParametersChanged(parameters));
|
||||
}
|
||||
|
||||
@Override
|
||||
public MediaMetadata getMediaMetadata() {
|
||||
return mediaMetadata;
|
||||
}
|
||||
|
|
@ -1008,12 +993,10 @@ import java.util.concurrent.CopyOnWriteArraySet;
|
|||
EVENT_MEDIA_METADATA_CHANGED, listener -> listener.onMediaMetadataChanged(mediaMetadata));
|
||||
}
|
||||
|
||||
@Override
|
||||
public MediaMetadata getPlaylistMetadata() {
|
||||
return playlistMetadata;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setPlaylistMetadata(MediaMetadata playlistMetadata) {
|
||||
checkNotNull(playlistMetadata);
|
||||
if (playlistMetadata.equals(this.playlistMetadata)) {
|
||||
|
|
@ -1025,109 +1008,10 @@ import java.util.concurrent.CopyOnWriteArraySet;
|
|||
listener -> listener.onPlaylistMetadataChanged(this.playlistMetadata));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Timeline getCurrentTimeline() {
|
||||
return playbackInfo.timeline;
|
||||
}
|
||||
|
||||
/** This method is not supported and returns {@link AudioAttributes#DEFAULT}. */
|
||||
@Override
|
||||
public AudioAttributes getAudioAttributes() {
|
||||
return AudioAttributes.DEFAULT;
|
||||
}
|
||||
|
||||
/** This method is not supported and does nothing. */
|
||||
@Override
|
||||
public void setVolume(float volume) {}
|
||||
|
||||
/** This method is not supported and returns 1. */
|
||||
@Override
|
||||
public float getVolume() {
|
||||
return 1;
|
||||
}
|
||||
|
||||
/** This method is not supported and does nothing. */
|
||||
@Override
|
||||
public void clearVideoSurface() {}
|
||||
|
||||
/** This method is not supported and does nothing. */
|
||||
@Override
|
||||
public void clearVideoSurface(@Nullable Surface surface) {}
|
||||
|
||||
/** This method is not supported and does nothing. */
|
||||
@Override
|
||||
public void setVideoSurface(@Nullable Surface surface) {}
|
||||
|
||||
/** This method is not supported and does nothing. */
|
||||
@Override
|
||||
public void setVideoSurfaceHolder(@Nullable SurfaceHolder surfaceHolder) {}
|
||||
|
||||
/** This method is not supported and does nothing. */
|
||||
@Override
|
||||
public void clearVideoSurfaceHolder(@Nullable SurfaceHolder surfaceHolder) {}
|
||||
|
||||
/** This method is not supported and does nothing. */
|
||||
@Override
|
||||
public void setVideoSurfaceView(@Nullable SurfaceView surfaceView) {}
|
||||
|
||||
/** This method is not supported and does nothing. */
|
||||
@Override
|
||||
public void clearVideoSurfaceView(@Nullable SurfaceView surfaceView) {}
|
||||
|
||||
/** This method is not supported and does nothing. */
|
||||
@Override
|
||||
public void setVideoTextureView(@Nullable TextureView textureView) {}
|
||||
|
||||
/** This method is not supported and does nothing. */
|
||||
@Override
|
||||
public void clearVideoTextureView(@Nullable TextureView textureView) {}
|
||||
|
||||
/** This method is not supported and returns {@link VideoSize#UNKNOWN}. */
|
||||
@Override
|
||||
public VideoSize getVideoSize() {
|
||||
return VideoSize.UNKNOWN;
|
||||
}
|
||||
|
||||
/** This method is not supported and returns an empty list. */
|
||||
@Override
|
||||
public ImmutableList<Cue> getCurrentCues() {
|
||||
return ImmutableList.of();
|
||||
}
|
||||
|
||||
/** This method is not supported and always returns {@link DeviceInfo#UNKNOWN}. */
|
||||
@Override
|
||||
public DeviceInfo getDeviceInfo() {
|
||||
return DeviceInfo.UNKNOWN;
|
||||
}
|
||||
|
||||
/** This method is not supported and always returns {@code 0}. */
|
||||
@Override
|
||||
public int getDeviceVolume() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
/** This method is not supported and always returns {@link false}. */
|
||||
@Override
|
||||
public boolean isDeviceMuted() {
|
||||
return false;
|
||||
}
|
||||
|
||||
/** This method is not supported and does nothing. */
|
||||
@Override
|
||||
public void setDeviceVolume(int volume) {}
|
||||
|
||||
/** This method is not supported and does nothing. */
|
||||
@Override
|
||||
public void increaseDeviceVolume() {}
|
||||
|
||||
/** This method is not supported and does nothing. */
|
||||
@Override
|
||||
public void decreaseDeviceVolume() {}
|
||||
|
||||
/** This method is not supported and does nothing. */
|
||||
@Override
|
||||
public void setDeviceMuted(boolean muted) {}
|
||||
|
||||
private int getCurrentWindowIndexInternal() {
|
||||
if (playbackInfo.timeline.isEmpty()) {
|
||||
return maskingWindowIndex;
|
||||
|
|
@ -1310,7 +1194,7 @@ import java.util.concurrent.CopyOnWriteArraySet;
|
|||
if (metadataChanged) {
|
||||
final MediaMetadata finalMediaMetadata = mediaMetadata;
|
||||
listeners.queueEvent(
|
||||
Player.EVENT_MEDIA_METADATA_CHANGED,
|
||||
EVENT_MEDIA_METADATA_CHANGED,
|
||||
listener -> listener.onMediaMetadataChanged(finalMediaMetadata));
|
||||
}
|
||||
if (previousPlaybackInfo.isLoading != newPlaybackInfo.isLoading) {
|
||||
|
|
@ -1518,7 +1402,7 @@ import java.util.concurrent.CopyOnWriteArraySet;
|
|||
|
||||
private void updateAvailableCommands() {
|
||||
Commands previousAvailableCommands = availableCommands;
|
||||
availableCommands = getAvailableCommands(permanentAvailableCommands);
|
||||
availableCommands = Util.getAvailableCommands(wrappingPlayer, permanentAvailableCommands);
|
||||
if (!availableCommands.equals(previousAvailableCommands)) {
|
||||
listeners.queueEvent(
|
||||
Player.EVENT_AVAILABLE_COMMANDS_CHANGED,
|
||||
|
|
@ -1556,7 +1440,7 @@ import java.util.concurrent.CopyOnWriteArraySet;
|
|||
maskTimelineAndPosition(
|
||||
playbackInfo,
|
||||
timeline,
|
||||
getPeriodPositionOrMaskWindowPosition(timeline, startWindowIndex, startPositionMs));
|
||||
maskWindowPositionMsOrGetPeriodPositionUs(timeline, startWindowIndex, startPositionMs));
|
||||
// Mask the playback state.
|
||||
int maskingPlaybackState = newPlaybackInfo.playbackState;
|
||||
if (startWindowIndex != C.INDEX_UNSET && newPlaybackInfo.playbackState != STATE_IDLE) {
|
||||
|
|
@ -1580,7 +1464,7 @@ import java.util.concurrent.CopyOnWriteArraySet;
|
|||
/* ignored */ PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST,
|
||||
/* seekProcessed= */ false,
|
||||
/* positionDiscontinuity= */ positionDiscontinuity,
|
||||
Player.DISCONTINUITY_REASON_REMOVE,
|
||||
DISCONTINUITY_REASON_REMOVE,
|
||||
/* discontinuityWindowStartPositionUs= */ getCurrentPositionUsInternal(newPlaybackInfo),
|
||||
/* ignored */ C.INDEX_UNSET);
|
||||
}
|
||||
|
|
@ -1614,7 +1498,7 @@ import java.util.concurrent.CopyOnWriteArraySet;
|
|||
maskTimelineAndPosition(
|
||||
playbackInfo,
|
||||
newTimeline,
|
||||
getPeriodPositionAfterTimelineChanged(oldTimeline, newTimeline));
|
||||
getPeriodPositionUsAfterTimelineChanged(oldTimeline, newTimeline));
|
||||
// Player transitions to STATE_ENDED if the current index is part of the removed tail.
|
||||
final boolean transitionsToEnded =
|
||||
newPlaybackInfo.playbackState != STATE_IDLE
|
||||
|
|
@ -1641,8 +1525,8 @@ import java.util.concurrent.CopyOnWriteArraySet;
|
|||
}
|
||||
|
||||
private PlaybackInfo maskTimelineAndPosition(
|
||||
PlaybackInfo playbackInfo, Timeline timeline, @Nullable Pair<Object, Long> periodPosition) {
|
||||
Assertions.checkArgument(timeline.isEmpty() || periodPosition != null);
|
||||
PlaybackInfo playbackInfo, Timeline timeline, @Nullable Pair<Object, Long> periodPositionUs) {
|
||||
Assertions.checkArgument(timeline.isEmpty() || periodPositionUs != null);
|
||||
Timeline oldTimeline = playbackInfo.timeline;
|
||||
// Mask the timeline.
|
||||
playbackInfo = playbackInfo.copyWithTimeline(timeline);
|
||||
|
|
@ -1667,10 +1551,10 @@ import java.util.concurrent.CopyOnWriteArraySet;
|
|||
}
|
||||
|
||||
Object oldPeriodUid = playbackInfo.periodId.periodUid;
|
||||
boolean playingPeriodChanged = !oldPeriodUid.equals(castNonNull(periodPosition).first);
|
||||
boolean playingPeriodChanged = !oldPeriodUid.equals(castNonNull(periodPositionUs).first);
|
||||
MediaPeriodId newPeriodId =
|
||||
playingPeriodChanged ? new MediaPeriodId(periodPosition.first) : playbackInfo.periodId;
|
||||
long newContentPositionUs = periodPosition.second;
|
||||
playingPeriodChanged ? new MediaPeriodId(periodPositionUs.first) : playbackInfo.periodId;
|
||||
long newContentPositionUs = periodPositionUs.second;
|
||||
long oldContentPositionUs = Util.msToUs(getContentPosition());
|
||||
if (!oldTimeline.isEmpty()) {
|
||||
oldContentPositionUs -=
|
||||
|
|
@ -1746,25 +1630,25 @@ import java.util.concurrent.CopyOnWriteArraySet;
|
|||
}
|
||||
|
||||
@Nullable
|
||||
private Pair<Object, Long> getPeriodPositionAfterTimelineChanged(
|
||||
private Pair<Object, Long> getPeriodPositionUsAfterTimelineChanged(
|
||||
Timeline oldTimeline, Timeline newTimeline) {
|
||||
long currentPositionMs = getContentPosition();
|
||||
if (oldTimeline.isEmpty() || newTimeline.isEmpty()) {
|
||||
boolean isCleared = !oldTimeline.isEmpty() && newTimeline.isEmpty();
|
||||
return getPeriodPositionOrMaskWindowPosition(
|
||||
return maskWindowPositionMsOrGetPeriodPositionUs(
|
||||
newTimeline,
|
||||
isCleared ? C.INDEX_UNSET : getCurrentWindowIndexInternal(),
|
||||
isCleared ? C.TIME_UNSET : currentPositionMs);
|
||||
}
|
||||
int currentMediaItemIndex = getCurrentMediaItemIndex();
|
||||
@Nullable
|
||||
Pair<Object, Long> oldPeriodPosition =
|
||||
oldTimeline.getPeriodPosition(
|
||||
Pair<Object, Long> oldPeriodPositionUs =
|
||||
oldTimeline.getPeriodPositionUs(
|
||||
window, period, currentMediaItemIndex, Util.msToUs(currentPositionMs));
|
||||
Object periodUid = castNonNull(oldPeriodPosition).first;
|
||||
Object periodUid = castNonNull(oldPeriodPositionUs).first;
|
||||
if (newTimeline.getIndexOfPeriod(periodUid) != C.INDEX_UNSET) {
|
||||
// The old period position is still available in the new timeline.
|
||||
return oldPeriodPosition;
|
||||
return oldPeriodPositionUs;
|
||||
}
|
||||
// Period uid not found in new timeline. Try to get subsequent period.
|
||||
@Nullable
|
||||
|
|
@ -1774,19 +1658,19 @@ import java.util.concurrent.CopyOnWriteArraySet;
|
|||
if (nextPeriodUid != null) {
|
||||
// Reset position to the default position of the window of the subsequent period.
|
||||
newTimeline.getPeriodByUid(nextPeriodUid, period);
|
||||
return getPeriodPositionOrMaskWindowPosition(
|
||||
return maskWindowPositionMsOrGetPeriodPositionUs(
|
||||
newTimeline,
|
||||
period.windowIndex,
|
||||
newTimeline.getWindow(period.windowIndex, window).getDefaultPositionMs());
|
||||
} else {
|
||||
// No subsequent period found and the new timeline is not empty. Use the default position.
|
||||
return getPeriodPositionOrMaskWindowPosition(
|
||||
return maskWindowPositionMsOrGetPeriodPositionUs(
|
||||
newTimeline, /* windowIndex= */ C.INDEX_UNSET, /* windowPositionMs= */ C.TIME_UNSET);
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private Pair<Object, Long> getPeriodPositionOrMaskWindowPosition(
|
||||
private Pair<Object, Long> maskWindowPositionMsOrGetPeriodPositionUs(
|
||||
Timeline timeline, int windowIndex, long windowPositionMs) {
|
||||
if (timeline.isEmpty()) {
|
||||
// If empty we store the initial seek in the masking variables.
|
||||
|
|
@ -1801,7 +1685,7 @@ import java.util.concurrent.CopyOnWriteArraySet;
|
|||
windowIndex = timeline.getFirstWindowIndex(shuffleModeEnabled);
|
||||
windowPositionMs = timeline.getWindow(windowIndex, window).getDefaultPositionMs();
|
||||
}
|
||||
return timeline.getPeriodPosition(window, period, windowIndex, Util.msToUs(windowPositionMs));
|
||||
return timeline.getPeriodPositionUs(window, period, windowIndex, Util.msToUs(windowPositionMs));
|
||||
}
|
||||
|
||||
private long periodPositionUsToWindowPositionUs(
|
||||
|
|
@ -1819,12 +1703,11 @@ import java.util.concurrent.CopyOnWriteArraySet;
|
|||
* #onMetadata(Metadata)}) sources.
|
||||
*/
|
||||
private MediaMetadata buildUpdatedMediaMetadata() {
|
||||
@Nullable MediaItem mediaItem = getCurrentMediaItem();
|
||||
|
||||
if (mediaItem == null) {
|
||||
Timeline timeline = getCurrentTimeline();
|
||||
if (timeline.isEmpty()) {
|
||||
return staticAndDynamicMediaMetadata;
|
||||
}
|
||||
|
||||
MediaItem mediaItem = timeline.getWindow(getCurrentMediaItemIndex(), window).mediaItem;
|
||||
// MediaItem metadata is prioritized over metadata within the media.
|
||||
return staticAndDynamicMediaMetadata.buildUpon().populate(mediaItem.mediaMetadata).build();
|
||||
}
|
||||
|
|
@ -1856,4 +1739,15 @@ import java.util.concurrent.CopyOnWriteArraySet;
|
|||
return timeline;
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(31)
|
||||
private static final class Api31 {
|
||||
private Api31() {}
|
||||
|
||||
@DoNotInline
|
||||
public static PlayerId createPlayerId() {
|
||||
// TODO: Create a MediaMetricsListener and obtain LogSessionId from it.
|
||||
return new PlayerId(LogSessionId.LOG_SESSION_ID_NONE);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@ import com.google.android.exoplayer2.Player.PlayWhenReadyChangeReason;
|
|||
import com.google.android.exoplayer2.Player.PlaybackSuppressionReason;
|
||||
import com.google.android.exoplayer2.Player.RepeatMode;
|
||||
import com.google.android.exoplayer2.analytics.AnalyticsCollector;
|
||||
import com.google.android.exoplayer2.analytics.PlayerId;
|
||||
import com.google.android.exoplayer2.drm.DrmSession;
|
||||
import com.google.android.exoplayer2.metadata.Metadata;
|
||||
import com.google.android.exoplayer2.source.BehindLiveWindowException;
|
||||
|
|
@ -43,6 +44,7 @@ import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId;
|
|||
import com.google.android.exoplayer2.source.SampleStream;
|
||||
import com.google.android.exoplayer2.source.ShuffleOrder;
|
||||
import com.google.android.exoplayer2.source.TrackGroupArray;
|
||||
import com.google.android.exoplayer2.source.ads.AdPlaybackState;
|
||||
import com.google.android.exoplayer2.text.TextRenderer;
|
||||
import com.google.android.exoplayer2.trackselection.ExoTrackSelection;
|
||||
import com.google.android.exoplayer2.trackselection.TrackSelector;
|
||||
|
|
@ -229,7 +231,8 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
|||
boolean pauseAtEndOfWindow,
|
||||
Looper applicationLooper,
|
||||
Clock clock,
|
||||
PlaybackInfoUpdateListener playbackInfoUpdateListener) {
|
||||
PlaybackInfoUpdateListener playbackInfoUpdateListener,
|
||||
PlayerId playerId) {
|
||||
this.playbackInfoUpdateListener = playbackInfoUpdateListener;
|
||||
this.renderers = renderers;
|
||||
this.trackSelector = trackSelector;
|
||||
|
|
@ -252,7 +255,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
|||
playbackInfoUpdate = new PlaybackInfoUpdate(playbackInfo);
|
||||
rendererCapabilities = new RendererCapabilities[renderers.length];
|
||||
for (int i = 0; i < renderers.length; i++) {
|
||||
renderers[i].setIndex(i);
|
||||
renderers[i].init(/* index= */ i, playerId);
|
||||
rendererCapabilities[i] = renderers[i].getCapabilities();
|
||||
}
|
||||
mediaClock = new DefaultMediaClock(this, clock);
|
||||
|
|
@ -266,7 +269,8 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
|||
|
||||
Handler eventHandler = new Handler(applicationLooper);
|
||||
queue = new MediaPeriodQueue(analyticsCollector, eventHandler);
|
||||
mediaSourceList = new MediaSourceList(/* listener= */ this, analyticsCollector, eventHandler);
|
||||
mediaSourceList =
|
||||
new MediaSourceList(/* listener= */ this, analyticsCollector, eventHandler, playerId);
|
||||
|
||||
// Note: The documentation for Process.THREAD_PRIORITY_AUDIO that states "Applications can
|
||||
// not normally change to this priority" is incorrect.
|
||||
|
|
@ -1119,7 +1123,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
|||
boolean seekPositionAdjusted;
|
||||
@Nullable
|
||||
Pair<Object, Long> resolvedSeekPosition =
|
||||
resolveSeekPosition(
|
||||
resolveSeekPositionUs(
|
||||
playbackInfo.timeline,
|
||||
seekPosition,
|
||||
/* trySubsequentPeriods= */ true,
|
||||
|
|
@ -1130,10 +1134,10 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
|||
if (resolvedSeekPosition == null) {
|
||||
// The seek position was valid for the timeline that it was performed into, but the
|
||||
// timeline has changed or is not ready and a suitable seek position could not be resolved.
|
||||
Pair<MediaPeriodId, Long> firstPeriodAndPosition =
|
||||
getPlaceholderFirstMediaPeriodPosition(playbackInfo.timeline);
|
||||
periodId = firstPeriodAndPosition.first;
|
||||
periodPositionUs = firstPeriodAndPosition.second;
|
||||
Pair<MediaPeriodId, Long> firstPeriodAndPositionUs =
|
||||
getPlaceholderFirstMediaPeriodPositionUs(playbackInfo.timeline);
|
||||
periodId = firstPeriodAndPositionUs.first;
|
||||
periodPositionUs = firstPeriodAndPositionUs.second;
|
||||
requestedContentPositionUs = C.TIME_UNSET;
|
||||
seekPositionAdjusted = !playbackInfo.timeline.isEmpty();
|
||||
} else {
|
||||
|
|
@ -1408,10 +1412,10 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
|||
boolean resetTrackInfo = false;
|
||||
if (resetPosition) {
|
||||
pendingInitialSeekPosition = null;
|
||||
Pair<MediaPeriodId, Long> firstPeriodAndPosition =
|
||||
getPlaceholderFirstMediaPeriodPosition(playbackInfo.timeline);
|
||||
mediaPeriodId = firstPeriodAndPosition.first;
|
||||
startPositionUs = firstPeriodAndPosition.second;
|
||||
Pair<MediaPeriodId, Long> firstPeriodAndPositionUs =
|
||||
getPlaceholderFirstMediaPeriodPositionUs(playbackInfo.timeline);
|
||||
mediaPeriodId = firstPeriodAndPositionUs.first;
|
||||
startPositionUs = firstPeriodAndPositionUs.second;
|
||||
requestedContentPositionUs = C.TIME_UNSET;
|
||||
if (!mediaPeriodId.equals(playbackInfo.periodId)) {
|
||||
resetTrackInfo = true;
|
||||
|
|
@ -1447,19 +1451,19 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
|||
}
|
||||
}
|
||||
|
||||
private Pair<MediaPeriodId, Long> getPlaceholderFirstMediaPeriodPosition(Timeline timeline) {
|
||||
private Pair<MediaPeriodId, Long> getPlaceholderFirstMediaPeriodPositionUs(Timeline timeline) {
|
||||
if (timeline.isEmpty()) {
|
||||
return Pair.create(PlaybackInfo.getDummyPeriodForEmptyTimeline(), 0L);
|
||||
}
|
||||
int firstWindowIndex = timeline.getFirstWindowIndex(shuffleModeEnabled);
|
||||
Pair<Object, Long> firstPeriodAndPosition =
|
||||
timeline.getPeriodPosition(
|
||||
Pair<Object, Long> firstPeriodAndPositionUs =
|
||||
timeline.getPeriodPositionUs(
|
||||
window, period, firstWindowIndex, /* windowPositionUs= */ C.TIME_UNSET);
|
||||
// Add ad metadata if any and propagate the window sequence number to new period id.
|
||||
MediaPeriodId firstPeriodId =
|
||||
queue.resolveMediaPeriodIdForAds(
|
||||
timeline, firstPeriodAndPosition.first, /* positionUs= */ 0);
|
||||
long positionUs = firstPeriodAndPosition.second;
|
||||
timeline, firstPeriodAndPositionUs.first, /* positionUs= */ 0);
|
||||
long positionUs = firstPeriodAndPositionUs.second;
|
||||
if (firstPeriodId.isAd()) {
|
||||
timeline.getPeriodByUid(firstPeriodId.periodUid, period);
|
||||
positionUs =
|
||||
|
|
@ -2539,7 +2543,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
|||
// Resolve initial seek position.
|
||||
@Nullable
|
||||
Pair<Object, Long> periodPosition =
|
||||
resolveSeekPosition(
|
||||
resolveSeekPositionUs(
|
||||
timeline,
|
||||
pendingInitialSeekPosition,
|
||||
/* trySubsequentPeriods= */ true,
|
||||
|
|
@ -2604,10 +2608,10 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
|||
// at position 0 and don't need to be resolved.
|
||||
long windowPositionUs = oldContentPositionUs + period.getPositionInWindowUs();
|
||||
int windowIndex = timeline.getPeriodByUid(newPeriodUid, period).windowIndex;
|
||||
Pair<Object, Long> periodPosition =
|
||||
timeline.getPeriodPosition(window, period, windowIndex, windowPositionUs);
|
||||
newPeriodUid = periodPosition.first;
|
||||
newContentPositionUs = periodPosition.second;
|
||||
Pair<Object, Long> periodPositionUs =
|
||||
timeline.getPeriodPositionUs(window, period, windowIndex, windowPositionUs);
|
||||
newPeriodUid = periodPositionUs.first;
|
||||
newContentPositionUs = periodPositionUs.second;
|
||||
}
|
||||
// Use an explicitly requested content position as new target live offset.
|
||||
setTargetLiveOffset = true;
|
||||
|
|
@ -2616,14 +2620,14 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
|||
// Set period uid for default positions and resolve position for ad resolution.
|
||||
long contentPositionForAdResolutionUs = newContentPositionUs;
|
||||
if (startAtDefaultPositionWindowIndex != C.INDEX_UNSET) {
|
||||
Pair<Object, Long> defaultPosition =
|
||||
timeline.getPeriodPosition(
|
||||
Pair<Object, Long> defaultPositionUs =
|
||||
timeline.getPeriodPositionUs(
|
||||
window,
|
||||
period,
|
||||
startAtDefaultPositionWindowIndex,
|
||||
/* windowPositionUs= */ C.TIME_UNSET);
|
||||
newPeriodUid = defaultPosition.first;
|
||||
contentPositionForAdResolutionUs = defaultPosition.second;
|
||||
newPeriodUid = defaultPositionUs.first;
|
||||
contentPositionForAdResolutionUs = defaultPositionUs.second;
|
||||
newContentPositionUs = C.TIME_UNSET;
|
||||
}
|
||||
|
||||
|
|
@ -2645,15 +2649,14 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
|||
&& earliestCuePointIsUnchangedOrLater;
|
||||
// Drop update if the change is from/to server-side inserted ads at the same content position to
|
||||
// avoid any unintentional renderer reset.
|
||||
timeline.getPeriodByUid(newPeriodUid, period);
|
||||
boolean isInStreamAdChange =
|
||||
sameOldAndNewPeriodUid
|
||||
&& !isUsingPlaceholderPeriod
|
||||
&& oldContentPositionUs == newContentPositionUs
|
||||
&& ((periodIdWithAds.isAd()
|
||||
&& period.isServerSideInsertedAdGroup(periodIdWithAds.adGroupIndex))
|
||||
|| (oldPeriodId.isAd()
|
||||
&& period.isServerSideInsertedAdGroup(oldPeriodId.adGroupIndex)));
|
||||
isIgnorableServerSideAdInsertionPeriodChange(
|
||||
isUsingPlaceholderPeriod,
|
||||
oldPeriodId,
|
||||
oldContentPositionUs,
|
||||
periodIdWithAds,
|
||||
timeline.getPeriodByUid(newPeriodUid, period),
|
||||
newContentPositionUs);
|
||||
MediaPeriodId newPeriodId =
|
||||
onlyNextAdGroupIndexIncreased || isInStreamAdChange ? oldPeriodId : periodIdWithAds;
|
||||
|
||||
|
|
@ -2679,6 +2682,30 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
|||
setTargetLiveOffset);
|
||||
}
|
||||
|
||||
private static boolean isIgnorableServerSideAdInsertionPeriodChange(
|
||||
boolean isUsingPlaceholderPeriod,
|
||||
MediaPeriodId oldPeriodId,
|
||||
long oldContentPositionUs,
|
||||
MediaPeriodId newPeriodId,
|
||||
Timeline.Period newPeriod,
|
||||
long newContentPositionUs) {
|
||||
if (isUsingPlaceholderPeriod
|
||||
|| oldContentPositionUs != newContentPositionUs
|
||||
|| !oldPeriodId.periodUid.equals(newPeriodId.periodUid)) {
|
||||
// The period position changed.
|
||||
return false;
|
||||
}
|
||||
if (oldPeriodId.isAd() && newPeriod.isServerSideInsertedAdGroup(oldPeriodId.adGroupIndex)) {
|
||||
// Whether the old period was a server side ad that doesn't need skipping to the content.
|
||||
return newPeriod.getAdState(oldPeriodId.adGroupIndex, oldPeriodId.adIndexInAdGroup)
|
||||
!= AdPlaybackState.AD_STATE_ERROR
|
||||
&& newPeriod.getAdState(oldPeriodId.adGroupIndex, oldPeriodId.adIndexInAdGroup)
|
||||
!= AdPlaybackState.AD_STATE_SKIPPED;
|
||||
}
|
||||
// If the new period is a server side inserted ad, we can just continue playing.
|
||||
return newPeriodId.isAd() && newPeriod.isServerSideInsertedAdGroup(newPeriodId.adGroupIndex);
|
||||
}
|
||||
|
||||
private static boolean isUsingPlaceholderPeriod(
|
||||
PlaybackInfo playbackInfo, Timeline.Period period) {
|
||||
MediaPeriodId periodId = playbackInfo.periodId;
|
||||
|
|
@ -2714,7 +2741,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
|||
: Util.msToUs(pendingMessageInfo.message.getPositionMs());
|
||||
@Nullable
|
||||
Pair<Object, Long> periodPosition =
|
||||
resolveSeekPosition(
|
||||
resolveSeekPositionUs(
|
||||
newTimeline,
|
||||
new SeekPosition(
|
||||
pendingMessageInfo.message.getTimeline(),
|
||||
|
|
@ -2759,12 +2786,12 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
|||
pendingMessageInfo.resolvedPeriodTimeUs + period.getPositionInWindowUs();
|
||||
int windowIndex =
|
||||
newTimeline.getPeriodByUid(pendingMessageInfo.resolvedPeriodUid, period).windowIndex;
|
||||
Pair<Object, Long> periodPosition =
|
||||
newTimeline.getPeriodPosition(window, period, windowIndex, windowPositionUs);
|
||||
Pair<Object, Long> periodPositionUs =
|
||||
newTimeline.getPeriodPositionUs(window, period, windowIndex, windowPositionUs);
|
||||
pendingMessageInfo.setResolvedPosition(
|
||||
/* periodIndex= */ newTimeline.getIndexOfPeriod(periodPosition.first),
|
||||
/* periodTimeUs= */ periodPosition.second,
|
||||
/* periodUid= */ periodPosition.first);
|
||||
/* periodIndex= */ newTimeline.getIndexOfPeriod(periodPositionUs.first),
|
||||
/* periodTimeUs= */ periodPositionUs.second,
|
||||
/* periodUid= */ periodPositionUs.first);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
|
@ -2793,7 +2820,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
|||
* bounds of the timeline.
|
||||
*/
|
||||
@Nullable
|
||||
private static Pair<Object, Long> resolveSeekPosition(
|
||||
private static Pair<Object, Long> resolveSeekPositionUs(
|
||||
Timeline timeline,
|
||||
SeekPosition seekPosition,
|
||||
boolean trySubsequentPeriods,
|
||||
|
|
@ -2812,10 +2839,10 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
|||
seekTimeline = timeline;
|
||||
}
|
||||
// Map the SeekPosition to a position in the corresponding timeline.
|
||||
Pair<Object, Long> periodPosition;
|
||||
Pair<Object, Long> periodPositionUs;
|
||||
try {
|
||||
periodPosition =
|
||||
seekTimeline.getPeriodPosition(
|
||||
periodPositionUs =
|
||||
seekTimeline.getPeriodPositionUs(
|
||||
window, period, seekPosition.windowIndex, seekPosition.windowPositionUs);
|
||||
} catch (IndexOutOfBoundsException e) {
|
||||
// The window index of the seek position was outside the bounds of the timeline.
|
||||
|
|
@ -2823,24 +2850,24 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
|||
}
|
||||
if (timeline.equals(seekTimeline)) {
|
||||
// Our internal timeline is the seek timeline, so the mapped position is correct.
|
||||
return periodPosition;
|
||||
return periodPositionUs;
|
||||
}
|
||||
// Attempt to find the mapped period in the internal timeline.
|
||||
int periodIndex = timeline.getIndexOfPeriod(periodPosition.first);
|
||||
int periodIndex = timeline.getIndexOfPeriod(periodPositionUs.first);
|
||||
if (periodIndex != C.INDEX_UNSET) {
|
||||
// We successfully located the period in the internal timeline.
|
||||
if (seekTimeline.getPeriodByUid(periodPosition.first, period).isPlaceholder
|
||||
if (seekTimeline.getPeriodByUid(periodPositionUs.first, period).isPlaceholder
|
||||
&& seekTimeline.getWindow(period.windowIndex, window).firstPeriodIndex
|
||||
== seekTimeline.getIndexOfPeriod(periodPosition.first)) {
|
||||
== seekTimeline.getIndexOfPeriod(periodPositionUs.first)) {
|
||||
// The seek timeline was using a placeholder, so we need to re-resolve using the updated
|
||||
// timeline in case the resolved position changed. Only resolve the first period in a window
|
||||
// because subsequent periods must start at position 0 and don't need to be resolved.
|
||||
int newWindowIndex = timeline.getPeriodByUid(periodPosition.first, period).windowIndex;
|
||||
periodPosition =
|
||||
timeline.getPeriodPosition(
|
||||
int newWindowIndex = timeline.getPeriodByUid(periodPositionUs.first, period).windowIndex;
|
||||
periodPositionUs =
|
||||
timeline.getPeriodPositionUs(
|
||||
window, period, newWindowIndex, seekPosition.windowPositionUs);
|
||||
}
|
||||
return periodPosition;
|
||||
return periodPositionUs;
|
||||
}
|
||||
if (trySubsequentPeriods) {
|
||||
// Try and find a subsequent period from the seek timeline in the internal timeline.
|
||||
|
|
@ -2851,12 +2878,12 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
|||
period,
|
||||
repeatMode,
|
||||
shuffleModeEnabled,
|
||||
periodPosition.first,
|
||||
periodPositionUs.first,
|
||||
seekTimeline,
|
||||
timeline);
|
||||
if (periodUid != null) {
|
||||
// We found one. Use the default position of the corresponding window.
|
||||
return timeline.getPeriodPosition(
|
||||
return timeline.getPeriodPositionUs(
|
||||
window,
|
||||
period,
|
||||
timeline.getPeriodByUid(periodUid, period).windowIndex,
|
||||
|
|
|
|||
|
|
@ -661,18 +661,18 @@ import com.google.common.collect.ImmutableList;
|
|||
// forward by the duration of the buffer, and start buffering from this point.
|
||||
contentPositionUs = C.TIME_UNSET;
|
||||
@Nullable
|
||||
Pair<Object, Long> defaultPosition =
|
||||
timeline.getPeriodPosition(
|
||||
Pair<Object, Long> defaultPositionUs =
|
||||
timeline.getPeriodPositionUs(
|
||||
window,
|
||||
period,
|
||||
nextWindowIndex,
|
||||
/* windowPositionUs= */ C.TIME_UNSET,
|
||||
/* defaultPositionProjectionUs= */ max(0, bufferedDurationUs));
|
||||
if (defaultPosition == null) {
|
||||
if (defaultPositionUs == null) {
|
||||
return null;
|
||||
}
|
||||
nextPeriodUid = defaultPosition.first;
|
||||
startPositionUs = defaultPosition.second;
|
||||
nextPeriodUid = defaultPositionUs.first;
|
||||
startPositionUs = defaultPositionUs.second;
|
||||
MediaPeriodHolder nextMediaPeriodHolder = mediaPeriodHolder.getNext();
|
||||
if (nextMediaPeriodHolder != null && nextMediaPeriodHolder.uid.equals(nextPeriodUid)) {
|
||||
windowSequenceNumber = nextMediaPeriodHolder.info.id.windowSequenceNumber;
|
||||
|
|
@ -716,17 +716,17 @@ import com.google.common.collect.ImmutableList;
|
|||
// If we're transitioning from an ad group to content starting from its default position,
|
||||
// project the start position forward as if this were a transition to a new window.
|
||||
@Nullable
|
||||
Pair<Object, Long> defaultPosition =
|
||||
timeline.getPeriodPosition(
|
||||
Pair<Object, Long> defaultPositionUs =
|
||||
timeline.getPeriodPositionUs(
|
||||
window,
|
||||
period,
|
||||
period.windowIndex,
|
||||
/* windowPositionUs= */ C.TIME_UNSET,
|
||||
/* defaultPositionProjectionUs= */ max(0, bufferedDurationUs));
|
||||
if (defaultPosition == null) {
|
||||
if (defaultPositionUs == null) {
|
||||
return null;
|
||||
}
|
||||
startPositionUs = defaultPosition.second;
|
||||
startPositionUs = defaultPositionUs.second;
|
||||
}
|
||||
long minStartPositionUs =
|
||||
getMinStartPositionAfterAdGroupUs(
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ import static java.lang.Math.min;
|
|||
import android.os.Handler;
|
||||
import androidx.annotation.Nullable;
|
||||
import com.google.android.exoplayer2.analytics.AnalyticsCollector;
|
||||
import com.google.android.exoplayer2.analytics.PlayerId;
|
||||
import com.google.android.exoplayer2.drm.DrmSession;
|
||||
import com.google.android.exoplayer2.drm.DrmSessionEventListener;
|
||||
import com.google.android.exoplayer2.source.LoadEventInfo;
|
||||
|
|
@ -70,6 +71,7 @@ import java.util.Set;
|
|||
|
||||
private static final String TAG = "MediaSourceList";
|
||||
|
||||
private final PlayerId playerId;
|
||||
private final List<MediaSourceHolder> mediaSourceHolders;
|
||||
private final IdentityHashMap<MediaPeriod, MediaSourceHolder> mediaSourceByMediaPeriod;
|
||||
private final Map<Object, MediaSourceHolder> mediaSourceByUid;
|
||||
|
|
@ -93,11 +95,14 @@ import java.util.Set;
|
|||
* source events.
|
||||
* @param analyticsCollectorHandler The {@link Handler} to call {@link AnalyticsCollector} methods
|
||||
* on.
|
||||
* @param playerId The {@link PlayerId} of the player using this list.
|
||||
*/
|
||||
public MediaSourceList(
|
||||
MediaSourceListInfoRefreshListener listener,
|
||||
@Nullable AnalyticsCollector analyticsCollector,
|
||||
Handler analyticsCollectorHandler) {
|
||||
Handler analyticsCollectorHandler,
|
||||
PlayerId playerId) {
|
||||
this.playerId = playerId;
|
||||
mediaSourceListInfoListener = listener;
|
||||
shuffleOrder = new DefaultShuffleOrder(0);
|
||||
mediaSourceByMediaPeriod = new IdentityHashMap<>();
|
||||
|
|
@ -440,7 +445,7 @@ import java.util.Set;
|
|||
childSources.put(holder, new MediaSourceAndListener(mediaSource, caller, eventListener));
|
||||
mediaSource.addEventListener(Util.createHandlerForCurrentOrMainLooper(), eventListener);
|
||||
mediaSource.addDrmEventListener(Util.createHandlerForCurrentOrMainLooper(), eventListener);
|
||||
mediaSource.prepareSource(caller, mediaTransferListener);
|
||||
mediaSource.prepareSource(caller, mediaTransferListener, playerId);
|
||||
}
|
||||
|
||||
private void maybeReleaseChildSource(MediaSourceHolder mediaSourceHolder) {
|
||||
|
|
|
|||
|
|
@ -23,13 +23,13 @@ import android.os.Handler;
|
|||
import android.os.HandlerThread;
|
||||
import android.os.Message;
|
||||
import androidx.annotation.VisibleForTesting;
|
||||
import com.google.android.exoplayer2.analytics.PlayerId;
|
||||
import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory;
|
||||
import com.google.android.exoplayer2.extractor.ExtractorsFactory;
|
||||
import com.google.android.exoplayer2.extractor.mp4.Mp4Extractor;
|
||||
import com.google.android.exoplayer2.source.DefaultMediaSourceFactory;
|
||||
import com.google.android.exoplayer2.source.MediaPeriod;
|
||||
import com.google.android.exoplayer2.source.MediaSource;
|
||||
import com.google.android.exoplayer2.source.MediaSourceFactory;
|
||||
import com.google.android.exoplayer2.source.TrackGroupArray;
|
||||
import com.google.android.exoplayer2.upstream.Allocator;
|
||||
import com.google.android.exoplayer2.upstream.DefaultAllocator;
|
||||
|
|
@ -48,7 +48,7 @@ public final class MetadataRetriever {
|
|||
/**
|
||||
* Retrieves the {@link TrackGroupArray} corresponding to a {@link MediaItem}.
|
||||
*
|
||||
* <p>This is equivalent to using {@link #retrieveMetadata(MediaSourceFactory, MediaItem)} with a
|
||||
* <p>This is equivalent to using {@link #retrieveMetadata(MediaSource.Factory, MediaItem)} with a
|
||||
* {@link DefaultMediaSourceFactory} and a {@link DefaultExtractorsFactory} with {@link
|
||||
* Mp4Extractor#FLAG_READ_MOTION_PHOTO_METADATA} and {@link Mp4Extractor#FLAG_READ_SEF_DATA} set.
|
||||
*
|
||||
|
|
@ -66,13 +66,13 @@ public final class MetadataRetriever {
|
|||
*
|
||||
* <p>This method is thread-safe.
|
||||
*
|
||||
* @param mediaSourceFactory mediaSourceFactory The {@link MediaSourceFactory} to use to read the
|
||||
* @param mediaSourceFactory mediaSourceFactory The {@link MediaSource.Factory} to use to read the
|
||||
* data.
|
||||
* @param mediaItem The {@link MediaItem} whose metadata should be retrieved.
|
||||
* @return A {@link ListenableFuture} of the result.
|
||||
*/
|
||||
public static ListenableFuture<TrackGroupArray> retrieveMetadata(
|
||||
MediaSourceFactory mediaSourceFactory, MediaItem mediaItem) {
|
||||
MediaSource.Factory mediaSourceFactory, MediaItem mediaItem) {
|
||||
return retrieveMetadata(mediaSourceFactory, mediaItem, Clock.DEFAULT);
|
||||
}
|
||||
|
||||
|
|
@ -83,13 +83,13 @@ public final class MetadataRetriever {
|
|||
new DefaultExtractorsFactory()
|
||||
.setMp4ExtractorFlags(
|
||||
Mp4Extractor.FLAG_READ_MOTION_PHOTO_METADATA | Mp4Extractor.FLAG_READ_SEF_DATA);
|
||||
MediaSourceFactory mediaSourceFactory =
|
||||
MediaSource.Factory mediaSourceFactory =
|
||||
new DefaultMediaSourceFactory(context, extractorsFactory);
|
||||
return retrieveMetadata(mediaSourceFactory, mediaItem, clock);
|
||||
}
|
||||
|
||||
private static ListenableFuture<TrackGroupArray> retrieveMetadata(
|
||||
MediaSourceFactory mediaSourceFactory, MediaItem mediaItem, Clock clock) {
|
||||
MediaSource.Factory mediaSourceFactory, MediaItem mediaItem, Clock clock) {
|
||||
// Recreate thread and handler every time this method is called so that it can be used
|
||||
// concurrently.
|
||||
return new MetadataRetrieverInternal(mediaSourceFactory, clock).retrieveMetadata(mediaItem);
|
||||
|
|
@ -102,12 +102,12 @@ public final class MetadataRetriever {
|
|||
private static final int MESSAGE_CONTINUE_LOADING = 2;
|
||||
private static final int MESSAGE_RELEASE = 3;
|
||||
|
||||
private final MediaSourceFactory mediaSourceFactory;
|
||||
private final MediaSource.Factory mediaSourceFactory;
|
||||
private final HandlerThread mediaSourceThread;
|
||||
private final HandlerWrapper mediaSourceHandler;
|
||||
private final SettableFuture<TrackGroupArray> trackGroupsFuture;
|
||||
|
||||
public MetadataRetrieverInternal(MediaSourceFactory mediaSourceFactory, Clock clock) {
|
||||
public MetadataRetrieverInternal(MediaSource.Factory mediaSourceFactory, Clock clock) {
|
||||
this.mediaSourceFactory = mediaSourceFactory;
|
||||
mediaSourceThread = new HandlerThread("ExoPlayer:MetadataRetriever");
|
||||
mediaSourceThread.start();
|
||||
|
|
@ -140,7 +140,8 @@ public final class MetadataRetriever {
|
|||
case MESSAGE_PREPARE_SOURCE:
|
||||
MediaItem mediaItem = (MediaItem) msg.obj;
|
||||
mediaSource = mediaSourceFactory.createMediaSource(mediaItem);
|
||||
mediaSource.prepareSource(mediaSourceCaller, /* mediaTransferListener= */ null);
|
||||
mediaSource.prepareSource(
|
||||
mediaSourceCaller, /* mediaTransferListener= */ null, PlayerId.UNSET);
|
||||
mediaSourceHandler.sendEmptyMessage(MESSAGE_CHECK_FOR_FAILURE);
|
||||
return true;
|
||||
case MESSAGE_CHECK_FOR_FAILURE:
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@
|
|||
package com.google.android.exoplayer2;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import com.google.android.exoplayer2.analytics.PlayerId;
|
||||
import com.google.android.exoplayer2.source.SampleStream;
|
||||
import com.google.android.exoplayer2.util.Assertions;
|
||||
import com.google.android.exoplayer2.util.MediaClock;
|
||||
|
|
@ -45,7 +46,7 @@ public abstract class NoSampleRenderer implements Renderer, RendererCapabilities
|
|||
}
|
||||
|
||||
@Override
|
||||
public final void setIndex(int index) {
|
||||
public final void init(int index, PlayerId playerId) {
|
||||
this.index = index;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ import android.view.Surface;
|
|||
import androidx.annotation.IntDef;
|
||||
import androidx.annotation.Nullable;
|
||||
import com.google.android.exoplayer2.PlayerMessage.Target;
|
||||
import com.google.android.exoplayer2.analytics.PlayerId;
|
||||
import com.google.android.exoplayer2.audio.AudioAttributes;
|
||||
import com.google.android.exoplayer2.audio.AuxEffectInfo;
|
||||
import com.google.android.exoplayer2.source.SampleStream;
|
||||
|
|
@ -248,11 +249,12 @@ public interface Renderer extends PlayerMessage.Target {
|
|||
RendererCapabilities getCapabilities();
|
||||
|
||||
/**
|
||||
* Sets the index of this renderer within the player.
|
||||
* Initializes the renderer for playback with a player.
|
||||
*
|
||||
* @param index The renderer index.
|
||||
* @param index The renderer index within the player.
|
||||
* @param playerId The {@link PlayerId} of the player.
|
||||
*/
|
||||
void setIndex(int index);
|
||||
void init(int index, PlayerId playerId);
|
||||
|
||||
/**
|
||||
* If the renderer advances its own playback position then this method returns a corresponding
|
||||
|
|
|
|||
|
|
@ -60,16 +60,16 @@ public interface RendererCapabilities {
|
|||
@interface AdaptiveSupport {}
|
||||
|
||||
/** A mask to apply to {@link Capabilities} to obtain the {@link AdaptiveSupport} only. */
|
||||
int ADAPTIVE_SUPPORT_MASK = 0b11000;
|
||||
int ADAPTIVE_SUPPORT_MASK = 0b11 << 3;
|
||||
/** The {@link Renderer} can seamlessly adapt between formats. */
|
||||
int ADAPTIVE_SEAMLESS = 0b10000;
|
||||
int ADAPTIVE_SEAMLESS = 0b10 << 3;
|
||||
/**
|
||||
* The {@link Renderer} can adapt between formats, but may suffer a brief discontinuity
|
||||
* (~50-100ms) when adaptation occurs.
|
||||
*/
|
||||
int ADAPTIVE_NOT_SEAMLESS = 0b01000;
|
||||
int ADAPTIVE_NOT_SEAMLESS = 0b01 << 3;
|
||||
/** The {@link Renderer} does not support adaptation between formats. */
|
||||
int ADAPTIVE_NOT_SUPPORTED = 0b00000;
|
||||
int ADAPTIVE_NOT_SUPPORTED = 0;
|
||||
|
||||
/**
|
||||
* Level of renderer support for tunneling. One of {@link #TUNNELING_SUPPORTED} or {@link
|
||||
|
|
@ -80,20 +80,62 @@ public interface RendererCapabilities {
|
|||
@IntDef({TUNNELING_SUPPORTED, TUNNELING_NOT_SUPPORTED})
|
||||
@interface TunnelingSupport {}
|
||||
|
||||
/** A mask to apply to {@link Capabilities} to obtain the {@link TunnelingSupport} only. */
|
||||
int TUNNELING_SUPPORT_MASK = 0b100000;
|
||||
/** A mask to apply to {@link Capabilities} to obtain {@link TunnelingSupport} only. */
|
||||
int TUNNELING_SUPPORT_MASK = 0b1 << 5;
|
||||
/** The {@link Renderer} supports tunneled output. */
|
||||
int TUNNELING_SUPPORTED = 0b100000;
|
||||
int TUNNELING_SUPPORTED = 0b1 << 5;
|
||||
/** The {@link Renderer} does not support tunneled output. */
|
||||
int TUNNELING_NOT_SUPPORTED = 0b000000;
|
||||
int TUNNELING_NOT_SUPPORTED = 0;
|
||||
|
||||
/**
|
||||
* Level of renderer support for hardware acceleration. One of {@link
|
||||
* #HARDWARE_ACCELERATION_SUPPORTED} and {@link #HARDWARE_ACCELERATION_NOT_SUPPORTED}.
|
||||
*
|
||||
* <p>For video renderers, the level of support is indicated for non-tunneled output.
|
||||
*/
|
||||
@Documented
|
||||
@Retention(RetentionPolicy.SOURCE)
|
||||
@IntDef({
|
||||
HARDWARE_ACCELERATION_SUPPORTED,
|
||||
HARDWARE_ACCELERATION_NOT_SUPPORTED,
|
||||
})
|
||||
@interface HardwareAccelerationSupport {}
|
||||
/** A mask to apply to {@link Capabilities} to obtain {@link HardwareAccelerationSupport} only. */
|
||||
int HARDWARE_ACCELERATION_SUPPORT_MASK = 0b1 << 6;
|
||||
/** The renderer is able to use hardware acceleration. */
|
||||
int HARDWARE_ACCELERATION_SUPPORTED = 0b1 << 6;
|
||||
/** The renderer is not able to use hardware acceleration. */
|
||||
int HARDWARE_ACCELERATION_NOT_SUPPORTED = 0;
|
||||
|
||||
/**
|
||||
* Level of decoder support. One of {@link #DECODER_SUPPORT_PRIMARY} and {@link
|
||||
* #DECODER_SUPPORT_FALLBACK}.
|
||||
*
|
||||
* <p>For video renderers, the level of support is indicated for non-tunneled output.
|
||||
*/
|
||||
@Documented
|
||||
@Retention(RetentionPolicy.SOURCE)
|
||||
@IntDef({
|
||||
DECODER_SUPPORT_PRIMARY,
|
||||
DECODER_SUPPORT_FALLBACK,
|
||||
})
|
||||
@interface DecoderSupport {}
|
||||
/** A mask to apply to {@link Capabilities} to obtain {@link DecoderSupport} only. */
|
||||
int MODE_SUPPORT_MASK = 0b1 << 7;
|
||||
/** The renderer is able to use the primary decoder for the format's MIME type. */
|
||||
int DECODER_SUPPORT_PRIMARY = 0b1 << 7;
|
||||
/** The renderer will use a fallback decoder. */
|
||||
int DECODER_SUPPORT_FALLBACK = 0;
|
||||
|
||||
/**
|
||||
* Combined renderer capabilities.
|
||||
*
|
||||
* <p>This is a bitwise OR of {@link C.FormatSupport}, {@link AdaptiveSupport} and {@link
|
||||
* TunnelingSupport}. Use {@link #getFormatSupport(int)}, {@link #getAdaptiveSupport(int)} or
|
||||
* {@link #getTunnelingSupport(int)} to obtain the individual flags. And use {@link #create(int)}
|
||||
* or {@link #create(int, int, int)} to create the combined capabilities.
|
||||
* <p>This is a bitwise OR of {@link C.FormatSupport}, {@link AdaptiveSupport}, {@link
|
||||
* TunnelingSupport}, {@link HardwareAccelerationSupport} and {@link DecoderSupport}. Use {@link
|
||||
* #getFormatSupport}, {@link #getAdaptiveSupport}, {@link #getTunnelingSupport}, {@link
|
||||
* #getHardwareAccelerationSupport} and {@link #getDecoderSupport} to obtain individual
|
||||
* components. Use {@link #create(int)}, {@link #create(int, int, int)} or {@link #create(int,
|
||||
* int, int, int, int)} to create combined capabilities from individual components.
|
||||
*
|
||||
* <p>Possible values:
|
||||
*
|
||||
|
|
@ -111,6 +153,11 @@ public interface RendererCapabilities {
|
|||
* #TUNNELING_SUPPORTED} and {@link #TUNNELING_NOT_SUPPORTED}. Only set if the level of
|
||||
* support for the format itself is {@link C#FORMAT_HANDLED} or {@link
|
||||
* C#FORMAT_EXCEEDS_CAPABILITIES}.
|
||||
* <li>{@link HardwareAccelerationSupport}: The level of support for hardware acceleration. One
|
||||
* of {@link #HARDWARE_ACCELERATION_SUPPORTED} and {@link
|
||||
* #HARDWARE_ACCELERATION_NOT_SUPPORTED}.
|
||||
* <li>{@link DecoderSupport}: The level of decoder support. One of {@link
|
||||
* #DECODER_SUPPORT_PRIMARY} and {@link #DECODER_SUPPORT_FALLBACK}.
|
||||
* </ul>
|
||||
*/
|
||||
@Documented
|
||||
|
|
@ -122,8 +169,10 @@ public interface RendererCapabilities {
|
|||
/**
|
||||
* Returns {@link Capabilities} for the given {@link C.FormatSupport}.
|
||||
*
|
||||
* <p>The {@link AdaptiveSupport} is set to {@link #ADAPTIVE_NOT_SUPPORTED} and {{@link
|
||||
* TunnelingSupport} is set to {@link #TUNNELING_NOT_SUPPORTED}.
|
||||
* <p>{@link AdaptiveSupport} is set to {@link #ADAPTIVE_NOT_SUPPORTED}, {@link TunnelingSupport}
|
||||
* is set to {@link #TUNNELING_NOT_SUPPORTED}, {@link HardwareAccelerationSupport} is set to
|
||||
* {@link #HARDWARE_ACCELERATION_NOT_SUPPORTED} and {@link DecoderSupport} is set to {@link
|
||||
* #DECODER_SUPPORT_PRIMARY}.
|
||||
*
|
||||
* @param formatSupport The {@link C.FormatSupport}.
|
||||
* @return The combined {@link Capabilities} of the given {@link C.FormatSupport}, {@link
|
||||
|
|
@ -138,19 +187,53 @@ public interface RendererCapabilities {
|
|||
* Returns {@link Capabilities} combining the given {@link C.FormatSupport}, {@link
|
||||
* AdaptiveSupport} and {@link TunnelingSupport}.
|
||||
*
|
||||
* <p>{@link HardwareAccelerationSupport} is set to {@link #HARDWARE_ACCELERATION_NOT_SUPPORTED}
|
||||
* and {@link DecoderSupport} is set to {@link #DECODER_SUPPORT_PRIMARY}.
|
||||
*
|
||||
* @param formatSupport The {@link C.FormatSupport}.
|
||||
* @param adaptiveSupport The {@link AdaptiveSupport}.
|
||||
* @param tunnelingSupport The {@link TunnelingSupport}.
|
||||
* @return The combined {@link Capabilities}.
|
||||
*/
|
||||
@Capabilities
|
||||
static int create(
|
||||
@C.FormatSupport int formatSupport,
|
||||
@AdaptiveSupport int adaptiveSupport,
|
||||
@TunnelingSupport int tunnelingSupport) {
|
||||
return create(
|
||||
formatSupport,
|
||||
adaptiveSupport,
|
||||
tunnelingSupport,
|
||||
HARDWARE_ACCELERATION_NOT_SUPPORTED,
|
||||
DECODER_SUPPORT_PRIMARY);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns {@link Capabilities} combining the given {@link C.FormatSupport}, {@link
|
||||
* AdaptiveSupport}, {@link TunnelingSupport}, {@link HardwareAccelerationSupport} and {@link
|
||||
* DecoderSupport}.
|
||||
*
|
||||
* @param formatSupport The {@link C.FormatSupport}.
|
||||
* @param adaptiveSupport The {@link AdaptiveSupport}.
|
||||
* @param tunnelingSupport The {@link TunnelingSupport}.
|
||||
* @param hardwareAccelerationSupport The {@link HardwareAccelerationSupport}.
|
||||
* @param decoderSupport The {@link DecoderSupport}.
|
||||
* @return The combined {@link Capabilities}.
|
||||
*/
|
||||
// Suppression needed for IntDef casting.
|
||||
@SuppressLint("WrongConstant")
|
||||
@Capabilities
|
||||
static int create(
|
||||
@C.FormatSupport int formatSupport,
|
||||
@AdaptiveSupport int adaptiveSupport,
|
||||
@TunnelingSupport int tunnelingSupport) {
|
||||
return formatSupport | adaptiveSupport | tunnelingSupport;
|
||||
@TunnelingSupport int tunnelingSupport,
|
||||
@HardwareAccelerationSupport int hardwareAccelerationSupport,
|
||||
@DecoderSupport int decoderSupport) {
|
||||
return formatSupport
|
||||
| adaptiveSupport
|
||||
| tunnelingSupport
|
||||
| hardwareAccelerationSupport
|
||||
| decoderSupport;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -192,6 +275,32 @@ public interface RendererCapabilities {
|
|||
return supportFlags & TUNNELING_SUPPORT_MASK;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the {@link HardwareAccelerationSupport} from the combined {@link Capabilities}.
|
||||
*
|
||||
* @param supportFlags The combined {@link Capabilities}.
|
||||
* @return The {@link HardwareAccelerationSupport} only.
|
||||
*/
|
||||
// Suppression needed for IntDef casting.
|
||||
@SuppressLint("WrongConstant")
|
||||
@HardwareAccelerationSupport
|
||||
static int getHardwareAccelerationSupport(@Capabilities int supportFlags) {
|
||||
return supportFlags & HARDWARE_ACCELERATION_SUPPORT_MASK;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the {@link DecoderSupport} from the combined {@link Capabilities}.
|
||||
*
|
||||
* @param supportFlags The combined {@link Capabilities}.
|
||||
* @return The {@link DecoderSupport} only.
|
||||
*/
|
||||
// Suppression needed for IntDef casting.
|
||||
@SuppressLint("WrongConstant")
|
||||
@DecoderSupport
|
||||
static int getDecoderSupport(@Capabilities int supportFlags) {
|
||||
return supportFlags & MODE_SUPPORT_MASK;
|
||||
}
|
||||
|
||||
/** Returns the name of the {@link Renderer}. */
|
||||
String getName();
|
||||
|
||||
|
|
|
|||
|
|
@ -58,7 +58,6 @@ import com.google.android.exoplayer2.metadata.Metadata;
|
|||
import com.google.android.exoplayer2.metadata.MetadataOutput;
|
||||
import com.google.android.exoplayer2.source.DefaultMediaSourceFactory;
|
||||
import com.google.android.exoplayer2.source.MediaSource;
|
||||
import com.google.android.exoplayer2.source.MediaSourceFactory;
|
||||
import com.google.android.exoplayer2.source.ShuffleOrder;
|
||||
import com.google.android.exoplayer2.source.TrackGroupArray;
|
||||
import com.google.android.exoplayer2.text.Cue;
|
||||
|
|
@ -113,7 +112,7 @@ public class SimpleExoPlayer extends BasePlayer
|
|||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use {@link ExoPlayer.Builder#Builder(Context, MediaSourceFactory)} and {@link
|
||||
* @deprecated Use {@link ExoPlayer.Builder#Builder(Context, MediaSource.Factory)} and {@link
|
||||
* DefaultMediaSourceFactory#DefaultMediaSourceFactory(Context, ExtractorsFactory)} instead.
|
||||
*/
|
||||
@Deprecated
|
||||
|
|
@ -124,7 +123,7 @@ public class SimpleExoPlayer extends BasePlayer
|
|||
|
||||
/**
|
||||
* @deprecated Use {@link ExoPlayer.Builder#Builder(Context, RenderersFactory,
|
||||
* MediaSourceFactory)} and {@link
|
||||
* MediaSource.Factory)} and {@link
|
||||
* DefaultMediaSourceFactory#DefaultMediaSourceFactory(Context, ExtractorsFactory)} instead.
|
||||
*/
|
||||
@Deprecated
|
||||
|
|
@ -137,7 +136,7 @@ public class SimpleExoPlayer extends BasePlayer
|
|||
|
||||
/**
|
||||
* @deprecated Use {@link ExoPlayer.Builder#Builder(Context, RenderersFactory,
|
||||
* MediaSourceFactory, TrackSelector, LoadControl, BandwidthMeter, AnalyticsCollector)}
|
||||
* MediaSource.Factory, TrackSelector, LoadControl, BandwidthMeter, AnalyticsCollector)}
|
||||
* instead.
|
||||
*/
|
||||
@Deprecated
|
||||
|
|
@ -145,7 +144,7 @@ public class SimpleExoPlayer extends BasePlayer
|
|||
Context context,
|
||||
RenderersFactory renderersFactory,
|
||||
TrackSelector trackSelector,
|
||||
MediaSourceFactory mediaSourceFactory,
|
||||
MediaSource.Factory mediaSourceFactory,
|
||||
LoadControl loadControl,
|
||||
BandwidthMeter bandwidthMeter,
|
||||
AnalyticsCollector analyticsCollector) {
|
||||
|
|
@ -178,10 +177,10 @@ public class SimpleExoPlayer extends BasePlayer
|
|||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use {@link ExoPlayer.Builder#setMediaSourceFactory(MediaSourceFactory)} instead.
|
||||
* @deprecated Use {@link ExoPlayer.Builder#setMediaSourceFactory(MediaSource.Factory)} instead.
|
||||
*/
|
||||
@Deprecated
|
||||
public Builder setMediaSourceFactory(MediaSourceFactory mediaSourceFactory) {
|
||||
public Builder setMediaSourceFactory(MediaSource.Factory mediaSourceFactory) {
|
||||
wrappedBuilder.setMediaSourceFactory(mediaSourceFactory);
|
||||
return this;
|
||||
}
|
||||
|
|
@ -400,7 +399,7 @@ public class SimpleExoPlayer extends BasePlayer
|
|||
Context context,
|
||||
RenderersFactory renderersFactory,
|
||||
TrackSelector trackSelector,
|
||||
MediaSourceFactory mediaSourceFactory,
|
||||
MediaSource.Factory mediaSourceFactory,
|
||||
LoadControl loadControl,
|
||||
BandwidthMeter bandwidthMeter,
|
||||
AnalyticsCollector analyticsCollector,
|
||||
|
|
@ -1351,7 +1350,6 @@ public class SimpleExoPlayer extends BasePlayer
|
|||
}
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public TrackSelector getTrackSelector() {
|
||||
verifyApplicationThread();
|
||||
return player.getTrackSelector();
|
||||
|
|
|
|||
|
|
@ -0,0 +1,858 @@
|
|||
/*
|
||||
* Copyright 2021 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.analytics;
|
||||
|
||||
import static com.google.android.exoplayer2.util.Assertions.checkNotNull;
|
||||
import static com.google.android.exoplayer2.util.Util.castNonNull;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import android.media.DeniedByServerException;
|
||||
import android.media.MediaCodec;
|
||||
import android.media.MediaDrm;
|
||||
import android.media.MediaDrmResetException;
|
||||
import android.media.NotProvisionedException;
|
||||
import android.media.metrics.LogSessionId;
|
||||
import android.media.metrics.MediaMetricsManager;
|
||||
import android.media.metrics.NetworkEvent;
|
||||
import android.media.metrics.PlaybackErrorEvent;
|
||||
import android.media.metrics.PlaybackMetrics;
|
||||
import android.media.metrics.PlaybackSession;
|
||||
import android.media.metrics.PlaybackStateEvent;
|
||||
import android.media.metrics.TrackChangeEvent;
|
||||
import android.os.SystemClock;
|
||||
import android.system.ErrnoException;
|
||||
import android.system.OsConstants;
|
||||
import android.util.Pair;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.RequiresApi;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.ExoPlaybackException;
|
||||
import com.google.android.exoplayer2.ExoPlayerLibraryInfo;
|
||||
import com.google.android.exoplayer2.Format;
|
||||
import com.google.android.exoplayer2.MediaItem;
|
||||
import com.google.android.exoplayer2.ParserException;
|
||||
import com.google.android.exoplayer2.PlaybackException;
|
||||
import com.google.android.exoplayer2.Player;
|
||||
import com.google.android.exoplayer2.Timeline;
|
||||
import com.google.android.exoplayer2.TracksInfo;
|
||||
import com.google.android.exoplayer2.TracksInfo.TrackGroupInfo;
|
||||
import com.google.android.exoplayer2.audio.AudioSink;
|
||||
import com.google.android.exoplayer2.decoder.DecoderCounters;
|
||||
import com.google.android.exoplayer2.drm.DefaultDrmSessionManager;
|
||||
import com.google.android.exoplayer2.drm.DrmInitData;
|
||||
import com.google.android.exoplayer2.drm.DrmSession;
|
||||
import com.google.android.exoplayer2.drm.UnsupportedDrmException;
|
||||
import com.google.android.exoplayer2.mediacodec.MediaCodecDecoderException;
|
||||
import com.google.android.exoplayer2.mediacodec.MediaCodecRenderer;
|
||||
import com.google.android.exoplayer2.source.LoadEventInfo;
|
||||
import com.google.android.exoplayer2.source.MediaLoadData;
|
||||
import com.google.android.exoplayer2.source.MediaSource;
|
||||
import com.google.android.exoplayer2.source.TrackGroup;
|
||||
import com.google.android.exoplayer2.upstream.FileDataSource;
|
||||
import com.google.android.exoplayer2.upstream.HttpDataSource;
|
||||
import com.google.android.exoplayer2.upstream.UdpDataSource;
|
||||
import com.google.android.exoplayer2.util.MimeTypes;
|
||||
import com.google.android.exoplayer2.util.NetworkTypeObserver;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
import com.google.android.exoplayer2.video.VideoSize;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.net.SocketTimeoutException;
|
||||
import java.net.UnknownHostException;
|
||||
import java.util.UUID;
|
||||
import org.checkerframework.checker.nullness.compatqual.NullableType;
|
||||
import org.checkerframework.checker.nullness.qual.EnsuresNonNullIf;
|
||||
import org.checkerframework.checker.nullness.qual.RequiresNonNull;
|
||||
|
||||
/**
|
||||
* An {@link AnalyticsListener} that interacts with the Android {@link MediaMetricsManager}.
|
||||
*
|
||||
* <p>It listens to playback events and forwards them to a {@link PlaybackSession}. The {@link
|
||||
* LogSessionId} of the playback session can be obtained with {@link #getLogSessionId()}.
|
||||
*/
|
||||
@RequiresApi(31)
|
||||
public final class MediaMetricsListener
|
||||
implements AnalyticsListener, PlaybackSessionManager.Listener {
|
||||
|
||||
/**
|
||||
* Creates a media metrics listener.
|
||||
*
|
||||
* @param context A context.
|
||||
* @return The {@link MediaMetricsListener}, or null if the {@link Context#MEDIA_METRICS_SERVICE
|
||||
* media metrics service} isn't available.
|
||||
*/
|
||||
@Nullable
|
||||
public static MediaMetricsListener create(Context context) {
|
||||
@Nullable
|
||||
MediaMetricsManager mediaMetricsManager =
|
||||
(MediaMetricsManager) context.getSystemService(Context.MEDIA_METRICS_SERVICE);
|
||||
return mediaMetricsManager == null
|
||||
? null
|
||||
: new MediaMetricsListener(context, mediaMetricsManager.createPlaybackSession());
|
||||
}
|
||||
|
||||
private final Context context;
|
||||
private final PlaybackSessionManager sessionManager;
|
||||
private final PlaybackSession playbackSession;
|
||||
private final long startTimeMs;
|
||||
private final Timeline.Window window;
|
||||
private final Timeline.Period period;
|
||||
|
||||
@Nullable private PlaybackMetrics.Builder metricsBuilder;
|
||||
@Player.DiscontinuityReason private int discontinuityReason;
|
||||
private int currentPlaybackState;
|
||||
private int currentNetworkType;
|
||||
@Nullable private PlaybackException pendingPlayerError;
|
||||
@Nullable private PendingFormatUpdate pendingVideoFormat;
|
||||
@Nullable private PendingFormatUpdate pendingAudioFormat;
|
||||
@Nullable private PendingFormatUpdate pendingTextFormat;
|
||||
@Nullable private Format currentVideoFormat;
|
||||
@Nullable private Format currentAudioFormat;
|
||||
@Nullable private Format currentTextFormat;
|
||||
private boolean isSeeking;
|
||||
private int ioErrorType;
|
||||
private boolean hasFatalError;
|
||||
private int droppedFrames;
|
||||
private int playedFrames;
|
||||
private long bandwidthTimeMs;
|
||||
private long bandwidthBytes;
|
||||
private int audioUnderruns;
|
||||
|
||||
/**
|
||||
* Creates the listener.
|
||||
*
|
||||
* @param context A {@link Context}.
|
||||
*/
|
||||
private MediaMetricsListener(Context context, PlaybackSession playbackSession) {
|
||||
context = context.getApplicationContext();
|
||||
this.context = context;
|
||||
this.playbackSession = playbackSession;
|
||||
window = new Timeline.Window();
|
||||
period = new Timeline.Period();
|
||||
startTimeMs = SystemClock.elapsedRealtime();
|
||||
currentPlaybackState = PlaybackStateEvent.STATE_NOT_STARTED;
|
||||
currentNetworkType = NetworkEvent.NETWORK_TYPE_UNKNOWN;
|
||||
sessionManager = new DefaultPlaybackSessionManager();
|
||||
sessionManager.setListener(this);
|
||||
}
|
||||
|
||||
/** Returns the {@link LogSessionId} used by this listener. */
|
||||
public LogSessionId getLogSessionId() {
|
||||
return playbackSession.getSessionId();
|
||||
}
|
||||
|
||||
// PlaybackSessionManager.Listener implementation.
|
||||
|
||||
@Override
|
||||
public void onSessionCreated(EventTime eventTime, String sessionId) {}
|
||||
|
||||
@Override
|
||||
public void onSessionActive(EventTime eventTime, String sessionId) {
|
||||
if (eventTime.mediaPeriodId != null && eventTime.mediaPeriodId.isAd()) {
|
||||
// Ignore ad sessions.
|
||||
return;
|
||||
}
|
||||
finishCurrentSession();
|
||||
metricsBuilder =
|
||||
new PlaybackMetrics.Builder()
|
||||
.setPlayerName(ExoPlayerLibraryInfo.TAG)
|
||||
.setPlayerVersion(ExoPlayerLibraryInfo.VERSION);
|
||||
maybeUpdateTimelineMetadata(eventTime.timeline, eventTime.mediaPeriodId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAdPlaybackStarted(
|
||||
EventTime eventTime, String contentSessionId, String adSessionId) {}
|
||||
|
||||
@Override
|
||||
public void onSessionFinished(
|
||||
EventTime eventTime, String sessionId, boolean automaticTransitionToNextPlayback) {
|
||||
if (eventTime.mediaPeriodId != null && eventTime.mediaPeriodId.isAd()) {
|
||||
// Ignore ad sessions.
|
||||
return;
|
||||
}
|
||||
finishCurrentSession();
|
||||
}
|
||||
|
||||
// AnalyticsListener implementation.
|
||||
|
||||
@Override
|
||||
public void onPositionDiscontinuity(
|
||||
EventTime eventTime,
|
||||
Player.PositionInfo oldPosition,
|
||||
Player.PositionInfo newPosition,
|
||||
@Player.DiscontinuityReason int reason) {
|
||||
if (reason == Player.DISCONTINUITY_REASON_SEEK) {
|
||||
isSeeking = true;
|
||||
}
|
||||
discontinuityReason = reason;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onVideoDisabled(EventTime eventTime, DecoderCounters decoderCounters) {
|
||||
// TODO(b/181122234): DecoderCounters are not re-reported at period boundaries.
|
||||
droppedFrames += decoderCounters.droppedBufferCount;
|
||||
playedFrames += decoderCounters.renderedOutputBufferCount;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBandwidthEstimate(
|
||||
EventTime eventTime, int totalLoadTimeMs, long totalBytesLoaded, long bitrateEstimate) {
|
||||
bandwidthTimeMs += totalLoadTimeMs;
|
||||
bandwidthBytes += totalBytesLoaded;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDownstreamFormatChanged(EventTime eventTime, MediaLoadData mediaLoadData) {
|
||||
PendingFormatUpdate update =
|
||||
new PendingFormatUpdate(
|
||||
checkNotNull(mediaLoadData.trackFormat),
|
||||
mediaLoadData.trackSelectionReason,
|
||||
sessionManager.getSessionForMediaPeriodId(
|
||||
eventTime.timeline, checkNotNull(eventTime.mediaPeriodId)));
|
||||
switch (mediaLoadData.trackType) {
|
||||
case C.TRACK_TYPE_VIDEO:
|
||||
case C.TRACK_TYPE_DEFAULT:
|
||||
pendingVideoFormat = update;
|
||||
break;
|
||||
case C.TRACK_TYPE_AUDIO:
|
||||
pendingAudioFormat = update;
|
||||
break;
|
||||
case C.TRACK_TYPE_TEXT:
|
||||
pendingTextFormat = update;
|
||||
break;
|
||||
default:
|
||||
// Other track type. Ignore.
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onVideoSizeChanged(EventTime eventTime, VideoSize videoSize) {
|
||||
@Nullable PendingFormatUpdate pendingVideoFormat = this.pendingVideoFormat;
|
||||
if (pendingVideoFormat != null && pendingVideoFormat.format.height == Format.NO_VALUE) {
|
||||
Format formatWithHeightAndWidth =
|
||||
pendingVideoFormat
|
||||
.format
|
||||
.buildUpon()
|
||||
.setWidth(videoSize.width)
|
||||
.setHeight(videoSize.height)
|
||||
.build();
|
||||
this.pendingVideoFormat =
|
||||
new PendingFormatUpdate(
|
||||
formatWithHeightAndWidth,
|
||||
pendingVideoFormat.selectionReason,
|
||||
pendingVideoFormat.sessionId);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoadError(
|
||||
EventTime eventTime,
|
||||
LoadEventInfo loadEventInfo,
|
||||
MediaLoadData mediaLoadData,
|
||||
IOException error,
|
||||
boolean wasCanceled) {
|
||||
ioErrorType = mediaLoadData.dataType;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPlayerError(EventTime eventTime, PlaybackException error) {
|
||||
pendingPlayerError = error;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEvents(Player player, Events events) {
|
||||
if (events.size() == 0) {
|
||||
return;
|
||||
}
|
||||
maybeAddSessions(events);
|
||||
|
||||
long realtimeMs = SystemClock.elapsedRealtime();
|
||||
maybeUpdateMetricsBuilderValues(player, events);
|
||||
maybeReportPlaybackError(realtimeMs);
|
||||
maybeReportTrackChanges(player, events, realtimeMs);
|
||||
maybeReportNetworkChange(realtimeMs);
|
||||
maybeReportPlaybackStateChange(player, events, realtimeMs);
|
||||
|
||||
if (events.contains(AnalyticsListener.EVENT_PLAYER_RELEASED)) {
|
||||
sessionManager.finishAllSessions(events.getEventTime(EVENT_PLAYER_RELEASED));
|
||||
}
|
||||
}
|
||||
|
||||
private void maybeAddSessions(Events events) {
|
||||
for (int i = 0; i < events.size(); i++) {
|
||||
@EventFlags int event = events.get(i);
|
||||
EventTime eventTime = events.getEventTime(event);
|
||||
if (event == EVENT_TIMELINE_CHANGED) {
|
||||
sessionManager.updateSessionsWithTimelineChange(eventTime);
|
||||
} else if (event == EVENT_POSITION_DISCONTINUITY) {
|
||||
sessionManager.updateSessionsWithDiscontinuity(eventTime, discontinuityReason);
|
||||
} else {
|
||||
sessionManager.updateSessions(eventTime);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void maybeUpdateMetricsBuilderValues(Player player, Events events) {
|
||||
if (events.contains(EVENT_TIMELINE_CHANGED)) {
|
||||
EventTime eventTime = events.getEventTime(EVENT_TIMELINE_CHANGED);
|
||||
if (metricsBuilder != null) {
|
||||
maybeUpdateTimelineMetadata(eventTime.timeline, eventTime.mediaPeriodId);
|
||||
}
|
||||
}
|
||||
if (events.contains(EVENT_TRACKS_CHANGED) && metricsBuilder != null) {
|
||||
@Nullable
|
||||
DrmInitData drmInitData = getDrmInitData(player.getCurrentTracksInfo().getTrackGroupInfos());
|
||||
if (drmInitData != null) {
|
||||
castNonNull(metricsBuilder).setDrmType(getDrmType(drmInitData));
|
||||
}
|
||||
}
|
||||
if (events.contains(EVENT_AUDIO_UNDERRUN)) {
|
||||
audioUnderruns++;
|
||||
}
|
||||
}
|
||||
|
||||
private void maybeReportPlaybackError(long realtimeMs) {
|
||||
@Nullable PlaybackException error = pendingPlayerError;
|
||||
if (error == null) {
|
||||
return;
|
||||
}
|
||||
ErrorInfo errorInfo =
|
||||
getErrorInfo(
|
||||
error, context, /* lastIoErrorForManifest= */ ioErrorType == C.DATA_TYPE_MANIFEST);
|
||||
playbackSession.reportPlaybackErrorEvent(
|
||||
new PlaybackErrorEvent.Builder()
|
||||
.setTimeSinceCreatedMillis(realtimeMs - startTimeMs)
|
||||
.setErrorCode(errorInfo.errorCode)
|
||||
.setSubErrorCode(errorInfo.subErrorCode)
|
||||
.setException(error)
|
||||
.build());
|
||||
pendingPlayerError = null;
|
||||
}
|
||||
|
||||
private void maybeReportTrackChanges(Player player, Events events, long realtimeMs) {
|
||||
if (events.contains(EVENT_TRACKS_CHANGED)) {
|
||||
TracksInfo tracksInfo = player.getCurrentTracksInfo();
|
||||
boolean isVideoSelected = tracksInfo.isTypeSelected(C.TRACK_TYPE_VIDEO);
|
||||
boolean isAudioSelected = tracksInfo.isTypeSelected(C.TRACK_TYPE_AUDIO);
|
||||
boolean isTextSelected = tracksInfo.isTypeSelected(C.TRACK_TYPE_TEXT);
|
||||
if (isVideoSelected || isAudioSelected || isTextSelected) {
|
||||
// Ignore updates with insufficient information where no tracks are selected.
|
||||
if (!isVideoSelected) {
|
||||
maybeUpdateVideoFormat(realtimeMs, /* videoFormat= */ null, C.SELECTION_REASON_UNKNOWN);
|
||||
}
|
||||
if (!isAudioSelected) {
|
||||
maybeUpdateAudioFormat(realtimeMs, /* audioFormat= */ null, C.SELECTION_REASON_UNKNOWN);
|
||||
}
|
||||
if (!isTextSelected) {
|
||||
maybeUpdateTextFormat(realtimeMs, /* textFormat= */ null, C.SELECTION_REASON_UNKNOWN);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (canReportPendingFormatUpdate(pendingVideoFormat)
|
||||
&& pendingVideoFormat.format.height != Format.NO_VALUE) {
|
||||
maybeUpdateVideoFormat(
|
||||
realtimeMs, pendingVideoFormat.format, pendingVideoFormat.selectionReason);
|
||||
pendingVideoFormat = null;
|
||||
}
|
||||
if (canReportPendingFormatUpdate(pendingAudioFormat)) {
|
||||
maybeUpdateAudioFormat(
|
||||
realtimeMs, pendingAudioFormat.format, pendingAudioFormat.selectionReason);
|
||||
pendingAudioFormat = null;
|
||||
}
|
||||
if (canReportPendingFormatUpdate(pendingTextFormat)) {
|
||||
maybeUpdateTextFormat(
|
||||
realtimeMs, pendingTextFormat.format, pendingTextFormat.selectionReason);
|
||||
pendingTextFormat = null;
|
||||
}
|
||||
}
|
||||
|
||||
@EnsuresNonNullIf(result = true, expression = "#1")
|
||||
private boolean canReportPendingFormatUpdate(@Nullable PendingFormatUpdate pendingFormatUpdate) {
|
||||
return pendingFormatUpdate != null
|
||||
&& pendingFormatUpdate.sessionId.equals(sessionManager.getActiveSessionId());
|
||||
}
|
||||
|
||||
private void maybeReportNetworkChange(long realtimeMs) {
|
||||
int networkType = getNetworkType(context);
|
||||
if (networkType != currentNetworkType) {
|
||||
currentNetworkType = networkType;
|
||||
playbackSession.reportNetworkEvent(
|
||||
new NetworkEvent.Builder()
|
||||
.setNetworkType(networkType)
|
||||
.setTimeSinceCreatedMillis(realtimeMs - startTimeMs)
|
||||
.build());
|
||||
}
|
||||
}
|
||||
|
||||
private void maybeReportPlaybackStateChange(Player player, Events events, long realtimeMs) {
|
||||
if (player.getPlaybackState() != Player.STATE_BUFFERING) {
|
||||
isSeeking = false;
|
||||
}
|
||||
if (player.getPlayerError() == null) {
|
||||
hasFatalError = false;
|
||||
} else if (events.contains(EVENT_PLAYER_ERROR)) {
|
||||
hasFatalError = true;
|
||||
}
|
||||
int newPlaybackState = resolveNewPlaybackState(player);
|
||||
if (currentPlaybackState != newPlaybackState) {
|
||||
currentPlaybackState = newPlaybackState;
|
||||
playbackSession.reportPlaybackStateEvent(
|
||||
new PlaybackStateEvent.Builder()
|
||||
.setState(currentPlaybackState)
|
||||
.setTimeSinceCreatedMillis(realtimeMs - startTimeMs)
|
||||
.build());
|
||||
}
|
||||
}
|
||||
|
||||
private int resolveNewPlaybackState(Player player) {
|
||||
@Player.State int playerPlaybackState = player.getPlaybackState();
|
||||
if (isSeeking) {
|
||||
// Seeking takes precedence over errors such that we report a seek while in error state.
|
||||
return PlaybackStateEvent.STATE_SEEKING;
|
||||
} else if (hasFatalError) {
|
||||
return PlaybackStateEvent.STATE_FAILED;
|
||||
} else if (playerPlaybackState == Player.STATE_ENDED) {
|
||||
return PlaybackStateEvent.STATE_ENDED;
|
||||
} else if (playerPlaybackState == Player.STATE_BUFFERING) {
|
||||
if (currentPlaybackState == PlaybackStateEvent.STATE_NOT_STARTED
|
||||
|| currentPlaybackState == PlaybackStateEvent.STATE_JOINING_FOREGROUND) {
|
||||
return PlaybackStateEvent.STATE_JOINING_FOREGROUND;
|
||||
}
|
||||
if (!player.getPlayWhenReady()) {
|
||||
return PlaybackStateEvent.STATE_PAUSED_BUFFERING;
|
||||
}
|
||||
return player.getPlaybackSuppressionReason() != Player.PLAYBACK_SUPPRESSION_REASON_NONE
|
||||
? PlaybackStateEvent.STATE_SUPPRESSED_BUFFERING
|
||||
: PlaybackStateEvent.STATE_BUFFERING;
|
||||
} else if (playerPlaybackState == Player.STATE_READY) {
|
||||
if (!player.getPlayWhenReady()) {
|
||||
return PlaybackStateEvent.STATE_PAUSED;
|
||||
}
|
||||
return player.getPlaybackSuppressionReason() != Player.PLAYBACK_SUPPRESSION_REASON_NONE
|
||||
? PlaybackStateEvent.STATE_SUPPRESSED
|
||||
: PlaybackStateEvent.STATE_PLAYING;
|
||||
} else if (playerPlaybackState == Player.STATE_IDLE
|
||||
&& currentPlaybackState != PlaybackStateEvent.STATE_NOT_STARTED) {
|
||||
// This case only applies for calls to player.stop(). All other IDLE cases are handled by
|
||||
// !isForeground, hasFatalError or isSuspended. NOT_STARTED is deliberately ignored.
|
||||
return PlaybackStateEvent.STATE_STOPPED;
|
||||
}
|
||||
return currentPlaybackState;
|
||||
}
|
||||
|
||||
private void maybeUpdateVideoFormat(
|
||||
long realtimeMs, @Nullable Format videoFormat, @C.SelectionReason int trackSelectionReason) {
|
||||
if (Util.areEqual(currentVideoFormat, videoFormat)) {
|
||||
return;
|
||||
}
|
||||
if (currentVideoFormat == null && trackSelectionReason == C.SELECTION_REASON_UNKNOWN) {
|
||||
trackSelectionReason = C.SELECTION_REASON_INITIAL;
|
||||
}
|
||||
currentVideoFormat = videoFormat;
|
||||
reportTrackChangeEvent(
|
||||
TrackChangeEvent.TRACK_TYPE_VIDEO, realtimeMs, videoFormat, trackSelectionReason);
|
||||
}
|
||||
|
||||
private void maybeUpdateAudioFormat(
|
||||
long realtimeMs, @Nullable Format audioFormat, @C.SelectionReason int trackSelectionReason) {
|
||||
if (Util.areEqual(currentAudioFormat, audioFormat)) {
|
||||
return;
|
||||
}
|
||||
if (currentAudioFormat == null && trackSelectionReason == C.SELECTION_REASON_UNKNOWN) {
|
||||
trackSelectionReason = C.SELECTION_REASON_INITIAL;
|
||||
}
|
||||
currentAudioFormat = audioFormat;
|
||||
reportTrackChangeEvent(
|
||||
TrackChangeEvent.TRACK_TYPE_AUDIO, realtimeMs, audioFormat, trackSelectionReason);
|
||||
}
|
||||
|
||||
private void maybeUpdateTextFormat(
|
||||
long realtimeMs, @Nullable Format textFormat, @C.SelectionReason int trackSelectionReason) {
|
||||
if (Util.areEqual(currentTextFormat, textFormat)) {
|
||||
return;
|
||||
}
|
||||
if (currentTextFormat == null && trackSelectionReason == C.SELECTION_REASON_UNKNOWN) {
|
||||
trackSelectionReason = C.SELECTION_REASON_INITIAL;
|
||||
}
|
||||
currentTextFormat = textFormat;
|
||||
reportTrackChangeEvent(
|
||||
TrackChangeEvent.TRACK_TYPE_TEXT, realtimeMs, textFormat, trackSelectionReason);
|
||||
}
|
||||
|
||||
private void reportTrackChangeEvent(
|
||||
int type,
|
||||
long realtimeMs,
|
||||
@Nullable Format format,
|
||||
@C.SelectionReason int trackSelectionReason) {
|
||||
TrackChangeEvent.Builder builder =
|
||||
new TrackChangeEvent.Builder(type).setTimeSinceCreatedMillis(realtimeMs - startTimeMs);
|
||||
if (format != null) {
|
||||
builder.setTrackState(TrackChangeEvent.TRACK_STATE_ON);
|
||||
builder.setTrackChangeReason(getTrackChangeReason(trackSelectionReason));
|
||||
if (format.containerMimeType != null) {
|
||||
// TODO(b/181121074): Progressive container mime type is not filled in by MediaSource.
|
||||
builder.setContainerMimeType(format.containerMimeType);
|
||||
}
|
||||
if (format.sampleMimeType != null) {
|
||||
builder.setSampleMimeType(format.sampleMimeType);
|
||||
}
|
||||
if (format.codecs != null) {
|
||||
builder.setCodecName(format.codecs);
|
||||
}
|
||||
if (format.bitrate != Format.NO_VALUE) {
|
||||
builder.setBitrate(format.bitrate);
|
||||
}
|
||||
if (format.width != Format.NO_VALUE) {
|
||||
builder.setWidth(format.width);
|
||||
}
|
||||
if (format.height != Format.NO_VALUE) {
|
||||
builder.setHeight(format.height);
|
||||
}
|
||||
if (format.channelCount != Format.NO_VALUE) {
|
||||
builder.setChannelCount(format.channelCount);
|
||||
}
|
||||
if (format.sampleRate != Format.NO_VALUE) {
|
||||
builder.setAudioSampleRate(format.sampleRate);
|
||||
}
|
||||
if (format.language != null) {
|
||||
Pair<String, @NullableType String> languageAndRegion =
|
||||
getLanguageAndRegion(format.language);
|
||||
builder.setLanguage(languageAndRegion.first);
|
||||
if (languageAndRegion.second != null) {
|
||||
builder.setLanguageRegion(languageAndRegion.second);
|
||||
}
|
||||
}
|
||||
if (format.frameRate != Format.NO_VALUE) {
|
||||
builder.setVideoFrameRate(format.frameRate);
|
||||
}
|
||||
} else {
|
||||
builder.setTrackState(TrackChangeEvent.TRACK_STATE_OFF);
|
||||
}
|
||||
playbackSession.reportTrackChangeEvent(builder.build());
|
||||
}
|
||||
|
||||
@RequiresNonNull("metricsBuilder")
|
||||
private void maybeUpdateTimelineMetadata(
|
||||
Timeline timeline, @Nullable MediaSource.MediaPeriodId mediaPeriodId) {
|
||||
PlaybackMetrics.Builder metricsBuilder = this.metricsBuilder;
|
||||
if (mediaPeriodId == null) {
|
||||
return;
|
||||
}
|
||||
int periodIndex = timeline.getIndexOfPeriod(mediaPeriodId.periodUid);
|
||||
if (periodIndex == C.INDEX_UNSET) {
|
||||
return;
|
||||
}
|
||||
timeline.getPeriod(periodIndex, period);
|
||||
timeline.getWindow(period.windowIndex, window);
|
||||
metricsBuilder.setStreamType(getStreamType(window.mediaItem));
|
||||
if (window.durationUs != C.TIME_UNSET
|
||||
&& !window.isPlaceholder
|
||||
&& !window.isDynamic
|
||||
&& !window.isLive()) {
|
||||
metricsBuilder.setMediaDurationMillis(window.getDurationMs());
|
||||
}
|
||||
metricsBuilder.setPlaybackType(
|
||||
window.isLive() ? PlaybackMetrics.PLAYBACK_TYPE_LIVE : PlaybackMetrics.PLAYBACK_TYPE_VOD);
|
||||
}
|
||||
|
||||
private void finishCurrentSession() {
|
||||
if (metricsBuilder == null) {
|
||||
return;
|
||||
}
|
||||
metricsBuilder.setAudioUnderrunCount(audioUnderruns);
|
||||
metricsBuilder.setVideoFramesDropped(droppedFrames);
|
||||
metricsBuilder.setVideoFramesPlayed(playedFrames);
|
||||
metricsBuilder.setNetworkTransferDurationMillis(bandwidthTimeMs);
|
||||
// TODO(b/181121847): Report localBytesRead. This requires additional callbacks or plumbing.
|
||||
metricsBuilder.setNetworkBytesRead(bandwidthBytes);
|
||||
// TODO(b/181121847): Detect stream sources mixed and local depending on localBytesRead.
|
||||
metricsBuilder.setStreamSource(
|
||||
bandwidthBytes > 0
|
||||
? PlaybackMetrics.STREAM_SOURCE_NETWORK
|
||||
: PlaybackMetrics.STREAM_SOURCE_UNKNOWN);
|
||||
playbackSession.reportPlaybackMetrics(metricsBuilder.build());
|
||||
metricsBuilder = null;
|
||||
}
|
||||
|
||||
private static int getTrackChangeReason(@C.SelectionReason int trackSelectionReason) {
|
||||
switch (trackSelectionReason) {
|
||||
case C.SELECTION_REASON_INITIAL:
|
||||
return TrackChangeEvent.TRACK_CHANGE_REASON_INITIAL;
|
||||
case C.SELECTION_REASON_ADAPTIVE:
|
||||
return TrackChangeEvent.TRACK_CHANGE_REASON_ADAPTIVE;
|
||||
case C.SELECTION_REASON_MANUAL:
|
||||
return TrackChangeEvent.TRACK_CHANGE_REASON_MANUAL;
|
||||
case C.SELECTION_REASON_TRICK_PLAY:
|
||||
case C.SELECTION_REASON_UNKNOWN:
|
||||
default:
|
||||
return TrackChangeEvent.TRACK_CHANGE_REASON_OTHER;
|
||||
}
|
||||
}
|
||||
|
||||
private static Pair<String, @NullableType String> getLanguageAndRegion(String languageCode) {
|
||||
String[] parts = Util.split(languageCode, "-");
|
||||
return Pair.create(parts[0], parts.length >= 2 ? parts[1] : null);
|
||||
}
|
||||
|
||||
private static int getNetworkType(Context context) {
|
||||
switch (NetworkTypeObserver.getInstance(context).getNetworkType()) {
|
||||
case C.NETWORK_TYPE_WIFI:
|
||||
return NetworkEvent.NETWORK_TYPE_WIFI;
|
||||
case C.NETWORK_TYPE_2G:
|
||||
return NetworkEvent.NETWORK_TYPE_2G;
|
||||
case C.NETWORK_TYPE_3G:
|
||||
return NetworkEvent.NETWORK_TYPE_3G;
|
||||
case C.NETWORK_TYPE_4G:
|
||||
return NetworkEvent.NETWORK_TYPE_4G;
|
||||
case C.NETWORK_TYPE_5G_SA:
|
||||
return NetworkEvent.NETWORK_TYPE_5G_SA;
|
||||
case C.NETWORK_TYPE_5G_NSA:
|
||||
return NetworkEvent.NETWORK_TYPE_5G_NSA;
|
||||
case C.NETWORK_TYPE_ETHERNET:
|
||||
return NetworkEvent.NETWORK_TYPE_ETHERNET;
|
||||
case C.NETWORK_TYPE_OFFLINE:
|
||||
return NetworkEvent.NETWORK_TYPE_OFFLINE;
|
||||
case C.NETWORK_TYPE_UNKNOWN:
|
||||
return NetworkEvent.NETWORK_TYPE_UNKNOWN;
|
||||
default:
|
||||
return NetworkEvent.NETWORK_TYPE_OTHER;
|
||||
}
|
||||
}
|
||||
|
||||
private static int getStreamType(MediaItem mediaItem) {
|
||||
if (mediaItem.localConfiguration == null || mediaItem.localConfiguration.mimeType == null) {
|
||||
return PlaybackMetrics.STREAM_TYPE_UNKNOWN;
|
||||
}
|
||||
String mimeType = mediaItem.localConfiguration.mimeType;
|
||||
switch (mimeType) {
|
||||
case MimeTypes.APPLICATION_M3U8:
|
||||
return PlaybackMetrics.STREAM_TYPE_HLS;
|
||||
case MimeTypes.APPLICATION_MPD:
|
||||
return PlaybackMetrics.STREAM_TYPE_DASH;
|
||||
case MimeTypes.APPLICATION_SS:
|
||||
return PlaybackMetrics.STREAM_TYPE_SS;
|
||||
default:
|
||||
return PlaybackMetrics.STREAM_TYPE_PROGRESSIVE;
|
||||
}
|
||||
}
|
||||
|
||||
private static ErrorInfo getErrorInfo(
|
||||
PlaybackException error, Context context, boolean lastIoErrorForManifest) {
|
||||
if (error.errorCode == PlaybackException.ERROR_CODE_REMOTE_ERROR) {
|
||||
return new ErrorInfo(PlaybackErrorEvent.ERROR_PLAYER_REMOTE, /* subErrorCode= */ 0);
|
||||
}
|
||||
// Unpack the PlaybackException.
|
||||
// TODO(b/190203080): Use error codes instead of the Exception's cause where possible.
|
||||
boolean isRendererExoPlaybackException = false;
|
||||
int rendererFormatSupport = C.FORMAT_UNSUPPORTED_TYPE;
|
||||
if (error instanceof ExoPlaybackException) {
|
||||
ExoPlaybackException exoPlaybackException = (ExoPlaybackException) error;
|
||||
isRendererExoPlaybackException =
|
||||
exoPlaybackException.type == ExoPlaybackException.TYPE_RENDERER;
|
||||
rendererFormatSupport = exoPlaybackException.rendererFormatSupport;
|
||||
}
|
||||
Throwable cause = checkNotNull(error.getCause());
|
||||
if (cause instanceof IOException) {
|
||||
if (cause instanceof HttpDataSource.InvalidResponseCodeException) {
|
||||
int responseCode = ((HttpDataSource.InvalidResponseCodeException) cause).responseCode;
|
||||
return new ErrorInfo(
|
||||
PlaybackErrorEvent.ERROR_IO_BAD_HTTP_STATUS, /* subErrorCode= */ responseCode);
|
||||
} else if (cause instanceof HttpDataSource.InvalidContentTypeException
|
||||
|| cause instanceof ParserException) {
|
||||
return new ErrorInfo(
|
||||
lastIoErrorForManifest
|
||||
? PlaybackErrorEvent.ERROR_PARSING_MANIFEST_MALFORMED
|
||||
: PlaybackErrorEvent.ERROR_PARSING_CONTAINER_MALFORMED,
|
||||
/* subErrorCode= */ 0);
|
||||
} else if (cause instanceof HttpDataSource.HttpDataSourceException
|
||||
|| cause instanceof UdpDataSource.UdpDataSourceException) {
|
||||
if (NetworkTypeObserver.getInstance(context).getNetworkType() == C.NETWORK_TYPE_OFFLINE) {
|
||||
return new ErrorInfo(
|
||||
PlaybackErrorEvent.ERROR_IO_NETWORK_UNAVAILABLE, /* subErrorCode= */ 0);
|
||||
} else {
|
||||
@Nullable Throwable detailedCause = cause.getCause();
|
||||
if (detailedCause instanceof UnknownHostException) {
|
||||
return new ErrorInfo(PlaybackErrorEvent.ERROR_IO_DNS_FAILED, /* subErrorCode= */ 0);
|
||||
} else if (detailedCause instanceof SocketTimeoutException) {
|
||||
return new ErrorInfo(
|
||||
PlaybackErrorEvent.ERROR_IO_CONNECTION_TIMEOUT, /* subErrorCode= */ 0);
|
||||
} else if (cause instanceof HttpDataSource.HttpDataSourceException
|
||||
&& ((HttpDataSource.HttpDataSourceException) cause).type
|
||||
== HttpDataSource.HttpDataSourceException.TYPE_OPEN) {
|
||||
return new ErrorInfo(
|
||||
PlaybackErrorEvent.ERROR_IO_NETWORK_CONNECTION_FAILED, /* subErrorCode= */ 0);
|
||||
} else {
|
||||
return new ErrorInfo(
|
||||
PlaybackErrorEvent.ERROR_IO_CONNECTION_CLOSED, /* subErrorCode= */ 0);
|
||||
}
|
||||
}
|
||||
} else if (error.errorCode == PlaybackException.ERROR_CODE_BEHIND_LIVE_WINDOW) {
|
||||
return new ErrorInfo(
|
||||
PlaybackErrorEvent.ERROR_PLAYER_BEHIND_LIVE_WINDOW, /* subErrorCode= */ 0);
|
||||
} else if (cause instanceof DrmSession.DrmSessionException) {
|
||||
// Unpack DrmSessionException.
|
||||
cause = checkNotNull(cause.getCause());
|
||||
if (Util.SDK_INT >= 21 && cause instanceof MediaDrm.MediaDrmStateException) {
|
||||
String diagnosticsInfo = ((MediaDrm.MediaDrmStateException) cause).getDiagnosticInfo();
|
||||
int subErrorCode = Util.getErrorCodeFromPlatformDiagnosticsInfo(diagnosticsInfo);
|
||||
int errorCode = getDrmErrorCode(subErrorCode);
|
||||
return new ErrorInfo(errorCode, subErrorCode);
|
||||
} else if (Util.SDK_INT >= 23 && cause instanceof MediaDrmResetException) {
|
||||
return new ErrorInfo(PlaybackErrorEvent.ERROR_DRM_SYSTEM_ERROR, /* subErrorCode= */ 0);
|
||||
} else if (Util.SDK_INT >= 18 && cause instanceof NotProvisionedException) {
|
||||
return new ErrorInfo(
|
||||
PlaybackErrorEvent.ERROR_DRM_PROVISIONING_FAILED, /* subErrorCode= */ 0);
|
||||
} else if (Util.SDK_INT >= 18 && cause instanceof DeniedByServerException) {
|
||||
return new ErrorInfo(PlaybackErrorEvent.ERROR_DRM_DEVICE_REVOKED, /* subErrorCode= */ 0);
|
||||
} else if (cause instanceof UnsupportedDrmException) {
|
||||
return new ErrorInfo(
|
||||
PlaybackErrorEvent.ERROR_DRM_SCHEME_UNSUPPORTED, /* subErrorCode= */ 0);
|
||||
} else if (cause instanceof DefaultDrmSessionManager.MissingSchemeDataException) {
|
||||
return new ErrorInfo(PlaybackErrorEvent.ERROR_DRM_CONTENT_ERROR, /* subErrorCode= */ 0);
|
||||
} else {
|
||||
return new ErrorInfo(PlaybackErrorEvent.ERROR_DRM_OTHER, /* subErrorCode= */ 0);
|
||||
}
|
||||
} else if (cause instanceof FileDataSource.FileDataSourceException
|
||||
&& cause.getCause() instanceof FileNotFoundException) {
|
||||
@Nullable Throwable notFoundCause = checkNotNull(cause.getCause()).getCause();
|
||||
if (Util.SDK_INT >= 21
|
||||
&& notFoundCause instanceof ErrnoException
|
||||
&& ((ErrnoException) notFoundCause).errno == OsConstants.EACCES) {
|
||||
return new ErrorInfo(PlaybackErrorEvent.ERROR_IO_NO_PERMISSION, /* subErrorCode= */ 0);
|
||||
} else {
|
||||
return new ErrorInfo(PlaybackErrorEvent.ERROR_IO_FILE_NOT_FOUND, /* subErrorCode= */ 0);
|
||||
}
|
||||
} else {
|
||||
return new ErrorInfo(PlaybackErrorEvent.ERROR_IO_OTHER, /* subErrorCode= */ 0);
|
||||
}
|
||||
} else if (isRendererExoPlaybackException
|
||||
&& (rendererFormatSupport == C.FORMAT_UNSUPPORTED_TYPE
|
||||
|| rendererFormatSupport == C.FORMAT_UNSUPPORTED_SUBTYPE)) {
|
||||
return new ErrorInfo(
|
||||
PlaybackErrorEvent.ERROR_DECODING_FORMAT_UNSUPPORTED, /* subErrorCode= */ 0);
|
||||
} else if (isRendererExoPlaybackException
|
||||
&& rendererFormatSupport == C.FORMAT_EXCEEDS_CAPABILITIES) {
|
||||
return new ErrorInfo(
|
||||
PlaybackErrorEvent.ERROR_DECODING_FORMAT_EXCEEDS_CAPABILITIES, /* subErrorCode= */ 0);
|
||||
} else if (isRendererExoPlaybackException
|
||||
&& rendererFormatSupport == C.FORMAT_UNSUPPORTED_DRM) {
|
||||
return new ErrorInfo(PlaybackErrorEvent.ERROR_DRM_SCHEME_UNSUPPORTED, /* subErrorCode= */ 0);
|
||||
} else if (cause instanceof MediaCodecRenderer.DecoderInitializationException) {
|
||||
@Nullable
|
||||
String diagnosticsInfo =
|
||||
((MediaCodecRenderer.DecoderInitializationException) cause).diagnosticInfo;
|
||||
int subErrorCode = Util.getErrorCodeFromPlatformDiagnosticsInfo(diagnosticsInfo);
|
||||
return new ErrorInfo(PlaybackErrorEvent.ERROR_DECODER_INIT_FAILED, subErrorCode);
|
||||
} else if (cause instanceof MediaCodecDecoderException) {
|
||||
@Nullable String diagnosticsInfo = ((MediaCodecDecoderException) cause).diagnosticInfo;
|
||||
int subErrorCode = Util.getErrorCodeFromPlatformDiagnosticsInfo(diagnosticsInfo);
|
||||
return new ErrorInfo(PlaybackErrorEvent.ERROR_DECODING_FAILED, subErrorCode);
|
||||
} else if (cause instanceof OutOfMemoryError) {
|
||||
return new ErrorInfo(PlaybackErrorEvent.ERROR_DECODING_FAILED, /* subErrorCode= */ 0);
|
||||
} else if (cause instanceof AudioSink.InitializationException) {
|
||||
int subErrorCode = ((AudioSink.InitializationException) cause).audioTrackState;
|
||||
return new ErrorInfo(PlaybackErrorEvent.ERROR_AUDIO_TRACK_INIT_FAILED, subErrorCode);
|
||||
} else if (cause instanceof AudioSink.WriteException) {
|
||||
int subErrorCode = ((AudioSink.WriteException) cause).errorCode;
|
||||
return new ErrorInfo(PlaybackErrorEvent.ERROR_AUDIO_TRACK_WRITE_FAILED, subErrorCode);
|
||||
} else if (Util.SDK_INT >= 16 && cause instanceof MediaCodec.CryptoException) {
|
||||
int subErrorCode = ((MediaCodec.CryptoException) cause).getErrorCode();
|
||||
int errorCode = getDrmErrorCode(subErrorCode);
|
||||
return new ErrorInfo(errorCode, subErrorCode);
|
||||
} else {
|
||||
return new ErrorInfo(PlaybackErrorEvent.ERROR_PLAYER_OTHER, /* subErrorCode= */ 0);
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private static DrmInitData getDrmInitData(ImmutableList<TrackGroupInfo> trackGroupInfos) {
|
||||
for (TrackGroupInfo trackGroupInfo : trackGroupInfos) {
|
||||
TrackGroup trackGroup = trackGroupInfo.getTrackGroup();
|
||||
for (int trackIndex = 0; trackIndex < trackGroup.length; trackIndex++) {
|
||||
if (trackGroupInfo.isTrackSelected(trackIndex)) {
|
||||
@Nullable DrmInitData drmInitData = trackGroup.getFormat(trackIndex).drmInitData;
|
||||
if (drmInitData != null) {
|
||||
return drmInitData;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static int getDrmType(DrmInitData drmInitData) {
|
||||
for (int i = 0; i < drmInitData.schemeDataCount; i++) {
|
||||
UUID uuid = drmInitData.get(i).uuid;
|
||||
if (uuid.equals(C.WIDEVINE_UUID)) {
|
||||
// TODO(b/77625596): Forward MediaDrm metrics to distinguish between L1 and L3 and to set
|
||||
// the drm session id.
|
||||
return PlaybackMetrics.DRM_TYPE_WIDEVINE_L1;
|
||||
}
|
||||
if (uuid.equals(C.PLAYREADY_UUID)) {
|
||||
return PlaybackMetrics.DRM_TYPE_PLAY_READY;
|
||||
}
|
||||
if (uuid.equals(C.CLEARKEY_UUID)) {
|
||||
return PlaybackMetrics.DRM_TYPE_CLEARKEY;
|
||||
}
|
||||
}
|
||||
return PlaybackMetrics.DRM_TYPE_OTHER;
|
||||
}
|
||||
|
||||
@SuppressLint("SwitchIntDef") // Only DRM error codes are relevant here.
|
||||
private static int getDrmErrorCode(int mediaDrmErrorCode) {
|
||||
switch (Util.getErrorCodeForMediaDrmErrorCode(mediaDrmErrorCode)) {
|
||||
case PlaybackException.ERROR_CODE_DRM_PROVISIONING_FAILED:
|
||||
return PlaybackErrorEvent.ERROR_DRM_PROVISIONING_FAILED;
|
||||
case PlaybackException.ERROR_CODE_DRM_LICENSE_ACQUISITION_FAILED:
|
||||
return PlaybackErrorEvent.ERROR_DRM_LICENSE_ACQUISITION_FAILED;
|
||||
case PlaybackException.ERROR_CODE_DRM_DISALLOWED_OPERATION:
|
||||
return PlaybackErrorEvent.ERROR_DRM_DISALLOWED_OPERATION;
|
||||
case PlaybackException.ERROR_CODE_DRM_CONTENT_ERROR:
|
||||
return PlaybackErrorEvent.ERROR_DRM_CONTENT_ERROR;
|
||||
case PlaybackException.ERROR_CODE_DRM_SYSTEM_ERROR:
|
||||
default:
|
||||
return PlaybackErrorEvent.ERROR_DRM_SYSTEM_ERROR;
|
||||
}
|
||||
}
|
||||
|
||||
private static final class ErrorInfo {
|
||||
|
||||
public final int errorCode;
|
||||
public final int subErrorCode;
|
||||
|
||||
public ErrorInfo(int errorCode, int subErrorCode) {
|
||||
this.errorCode = errorCode;
|
||||
this.subErrorCode = subErrorCode;
|
||||
}
|
||||
}
|
||||
|
||||
private static final class PendingFormatUpdate {
|
||||
|
||||
public final Format format;
|
||||
@C.SelectionReason public final int selectionReason;
|
||||
public final String sessionId;
|
||||
|
||||
public PendingFormatUpdate(
|
||||
Format format, @C.SelectionReason int selectionReason, String sessionId) {
|
||||
this.format = format;
|
||||
this.selectionReason = selectionReason;
|
||||
this.sessionId = sessionId;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,75 @@
|
|||
/*
|
||||
* Copyright 2021 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.analytics;
|
||||
|
||||
import static com.google.android.exoplayer2.util.Assertions.checkNotNull;
|
||||
import static com.google.android.exoplayer2.util.Assertions.checkState;
|
||||
|
||||
import android.media.metrics.LogSessionId;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.RequiresApi;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
|
||||
/** Identifier for a player instance. */
|
||||
public final class PlayerId {
|
||||
|
||||
/**
|
||||
* A player identifier with unset default values that can be used as a placeholder or for testing.
|
||||
*/
|
||||
public static final PlayerId UNSET =
|
||||
Util.SDK_INT < 31 ? new PlayerId() : new PlayerId(LogSessionIdApi31.UNSET);
|
||||
|
||||
@Nullable private final LogSessionIdApi31 logSessionIdApi31;
|
||||
|
||||
/** Creates an instance for API < 31. */
|
||||
public PlayerId() {
|
||||
this(/* logSessionIdApi31= */ (LogSessionIdApi31) null);
|
||||
checkState(Util.SDK_INT < 31);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an instance for API ≥ 31.
|
||||
*
|
||||
* @param logSessionId The {@link LogSessionId} used for this player.
|
||||
*/
|
||||
@RequiresApi(31)
|
||||
public PlayerId(LogSessionId logSessionId) {
|
||||
this(new LogSessionIdApi31(logSessionId));
|
||||
}
|
||||
|
||||
private PlayerId(@Nullable LogSessionIdApi31 logSessionIdApi31) {
|
||||
this.logSessionIdApi31 = logSessionIdApi31;
|
||||
}
|
||||
|
||||
/** Returns the {@link LogSessionId} for this player instance. */
|
||||
@RequiresApi(31)
|
||||
public LogSessionId getLogSessionId() {
|
||||
return checkNotNull(logSessionIdApi31).logSessionId;
|
||||
}
|
||||
|
||||
@RequiresApi(31)
|
||||
private static final class LogSessionIdApi31 {
|
||||
|
||||
public static final LogSessionIdApi31 UNSET =
|
||||
new LogSessionIdApi31(LogSessionId.LOG_SESSION_ID_NONE);
|
||||
|
||||
public final LogSessionId logSessionId;
|
||||
|
||||
public LogSessionIdApi31(LogSessionId logSessionId) {
|
||||
this.logSessionId = logSessionId;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -24,6 +24,7 @@ import com.google.android.exoplayer2.Format;
|
|||
import com.google.android.exoplayer2.PlaybackException;
|
||||
import com.google.android.exoplayer2.PlaybackParameters;
|
||||
import com.google.android.exoplayer2.Player;
|
||||
import com.google.android.exoplayer2.analytics.PlayerId;
|
||||
import java.lang.annotation.Documented;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
|
|
@ -281,6 +282,13 @@ public interface AudioSink {
|
|||
*/
|
||||
void setListener(Listener listener);
|
||||
|
||||
/**
|
||||
* Sets the {@link PlayerId} of the player using this audio sink.
|
||||
*
|
||||
* @param playerId The {@link PlayerId}, or null to clear a previously set id.
|
||||
*/
|
||||
default void setPlayerId(@Nullable PlayerId playerId) {}
|
||||
|
||||
/**
|
||||
* Returns whether the sink supports a given {@link Format}.
|
||||
*
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ import static com.google.android.exoplayer2.decoder.DecoderReuseEvaluation.DISCA
|
|||
import static com.google.android.exoplayer2.decoder.DecoderReuseEvaluation.DISCARD_REASON_REUSE_NOT_IMPLEMENTED;
|
||||
import static com.google.android.exoplayer2.decoder.DecoderReuseEvaluation.REUSE_RESULT_NO;
|
||||
import static com.google.android.exoplayer2.source.SampleStream.FLAG_REQUIRE_FORMAT;
|
||||
import static com.google.common.base.MoreObjects.firstNonNull;
|
||||
import static java.lang.Math.max;
|
||||
|
||||
import android.os.Handler;
|
||||
|
|
@ -162,16 +163,24 @@ public abstract class DecoderAudioRenderer<
|
|||
* @param eventHandler A handler to use when delivering events to {@code eventListener}. May be
|
||||
* null if delivery of events is not required.
|
||||
* @param eventListener A listener of events. May be null if delivery of events is not required.
|
||||
* @param audioCapabilities The audio capabilities for playback on this device. May be null if the
|
||||
* default capabilities (no encoded audio passthrough support) should be assumed.
|
||||
* @param audioCapabilities The audio capabilities for playback on this device. Use {@link
|
||||
* AudioCapabilities#DEFAULT_AUDIO_CAPABILITIES} if default capabilities (no encoded audio
|
||||
* passthrough support) should be assumed.
|
||||
* @param audioProcessors Optional {@link AudioProcessor}s that will process audio before output.
|
||||
*/
|
||||
public DecoderAudioRenderer(
|
||||
@Nullable Handler eventHandler,
|
||||
@Nullable AudioRendererEventListener eventListener,
|
||||
@Nullable AudioCapabilities audioCapabilities,
|
||||
AudioCapabilities audioCapabilities,
|
||||
AudioProcessor... audioProcessors) {
|
||||
this(eventHandler, eventListener, new DefaultAudioSink(audioCapabilities, audioProcessors));
|
||||
this(
|
||||
eventHandler,
|
||||
eventListener,
|
||||
new DefaultAudioSink.Builder()
|
||||
.setAudioCapabilities( // For backward compatibility, null == default.
|
||||
firstNonNull(audioCapabilities, AudioCapabilities.DEFAULT_AUDIO_CAPABILITIES))
|
||||
.setAudioProcessors(audioProcessors)
|
||||
.build());
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -463,7 +472,7 @@ public abstract class DecoderAudioRenderer<
|
|||
onQueueInputBuffer(inputBuffer);
|
||||
decoder.queueInputBuffer(inputBuffer);
|
||||
decoderReceivedBuffers = true;
|
||||
decoderCounters.inputBufferCount++;
|
||||
decoderCounters.queuedInputBufferCount++;
|
||||
inputBuffer = null;
|
||||
return true;
|
||||
default:
|
||||
|
|
@ -530,6 +539,7 @@ public abstract class DecoderAudioRenderer<
|
|||
} else {
|
||||
audioSink.disableTunneling();
|
||||
}
|
||||
audioSink.setPlayerId(getPlayerId());
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
|||
|
|
@ -15,6 +15,9 @@
|
|||
*/
|
||||
package com.google.android.exoplayer2.audio;
|
||||
|
||||
import static com.google.android.exoplayer2.audio.AudioCapabilities.DEFAULT_AUDIO_CAPABILITIES;
|
||||
import static com.google.android.exoplayer2.util.Assertions.checkNotNull;
|
||||
import static com.google.common.base.MoreObjects.firstNonNull;
|
||||
import static java.lang.Math.max;
|
||||
import static java.lang.Math.min;
|
||||
|
||||
|
|
@ -23,21 +26,26 @@ import android.media.AudioFormat;
|
|||
import android.media.AudioManager;
|
||||
import android.media.AudioTrack;
|
||||
import android.media.PlaybackParams;
|
||||
import android.media.metrics.LogSessionId;
|
||||
import android.os.ConditionVariable;
|
||||
import android.os.Handler;
|
||||
import android.os.SystemClock;
|
||||
import android.util.Pair;
|
||||
import androidx.annotation.DoNotInline;
|
||||
import androidx.annotation.IntDef;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.RequiresApi;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.Format;
|
||||
import com.google.android.exoplayer2.PlaybackParameters;
|
||||
import com.google.android.exoplayer2.analytics.PlayerId;
|
||||
import com.google.android.exoplayer2.audio.AudioProcessor.UnhandledAudioFormatException;
|
||||
import com.google.android.exoplayer2.util.Assertions;
|
||||
import com.google.android.exoplayer2.util.Log;
|
||||
import com.google.android.exoplayer2.util.MimeTypes;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
import com.google.errorprone.annotations.InlineMe;
|
||||
import com.google.errorprone.annotations.InlineMeValidationDisabled;
|
||||
import java.lang.annotation.Documented;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
|
|
@ -47,6 +55,7 @@ import java.util.ArrayDeque;
|
|||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
||||
import org.checkerframework.checker.nullness.qual.RequiresNonNull;
|
||||
|
||||
/**
|
||||
* Plays audio data. The implementation delegates to an {@link AudioTrack} and handles playback
|
||||
|
|
@ -200,6 +209,108 @@ public final class DefaultAudioSink implements AudioSink {
|
|||
}
|
||||
}
|
||||
|
||||
/** A builder to create {@link DefaultAudioSink} instances. */
|
||||
public static final class Builder {
|
||||
|
||||
private AudioCapabilities audioCapabilities;
|
||||
@Nullable private AudioProcessorChain audioProcessorChain;
|
||||
private boolean enableFloatOutput;
|
||||
private boolean enableAudioTrackPlaybackParams;
|
||||
private int offloadMode;
|
||||
|
||||
/** Creates a new builder. */
|
||||
public Builder() {
|
||||
audioCapabilities = DEFAULT_AUDIO_CAPABILITIES;
|
||||
offloadMode = OFFLOAD_MODE_DISABLED;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets audio capabilities for playback on this device. May be {@code null} if the default
|
||||
* capabilities (no encoded audio passthrough support) should be assumed.
|
||||
*
|
||||
* <p>Default is {@link AudioCapabilities#DEFAULT_AUDIO_CAPABILITIES}.
|
||||
*/
|
||||
public Builder setAudioCapabilities(AudioCapabilities audioCapabilities) {
|
||||
checkNotNull(audioCapabilities);
|
||||
this.audioCapabilities = audioCapabilities;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets an array of {@link AudioProcessor AudioProcessors}s that will process PCM audio before
|
||||
* output. May be empty. Equivalent of {@code setAudioProcessorChain(new
|
||||
* DefaultAudioProcessorChain(audioProcessors)}.
|
||||
*
|
||||
* <p>The default value is an empty array.
|
||||
*/
|
||||
public Builder setAudioProcessors(AudioProcessor[] audioProcessors) {
|
||||
checkNotNull(audioProcessors);
|
||||
return setAudioProcessorChain(new DefaultAudioProcessorChain(audioProcessors));
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the {@link AudioProcessorChain} to process audio before playback. The instance passed in
|
||||
* must not be reused in other sinks. Processing chains are only supported for PCM playback (not
|
||||
* passthrough or offload).
|
||||
*
|
||||
* <p>By default, no processing will be applied.
|
||||
*/
|
||||
public Builder setAudioProcessorChain(AudioProcessorChain audioProcessorChain) {
|
||||
checkNotNull(audioProcessorChain);
|
||||
this.audioProcessorChain = audioProcessorChain;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets whether to enable 32-bit float output or integer output. Where possible, 32-bit float
|
||||
* output will be used if the input is 32-bit float, and also if the input is high resolution
|
||||
* (24-bit or 32-bit) integer PCM. Float output is supported from API level 21. Audio processing
|
||||
* (for example, speed adjustment) will not be available when float output is in use.
|
||||
*
|
||||
* <p>The default value is {@code false}.
|
||||
*/
|
||||
public Builder setEnableFloatOutput(boolean enableFloatOutput) {
|
||||
this.enableFloatOutput = enableFloatOutput;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets whether to control the playback speed using the platform implementation (see {@link
|
||||
* AudioTrack#setPlaybackParams(PlaybackParams)}), if supported. If set to {@code false}, speed
|
||||
* up/down of the audio will be done by ExoPlayer (see {@link SonicAudioProcessor}). Platform
|
||||
* speed adjustment is lower latency, but less reliable.
|
||||
*
|
||||
* <p>The default value is {@code false}.
|
||||
*/
|
||||
public Builder setEnableAudioTrackPlaybackParams(boolean enableAudioTrackPlaybackParams) {
|
||||
this.enableAudioTrackPlaybackParams = enableAudioTrackPlaybackParams;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the offload mode. If an audio format can be both played with offload and encoded audio
|
||||
* passthrough, it will be played in offload. Audio offload is supported from API level 29. Most
|
||||
* Android devices can only support one offload {@link AudioTrack} at a time and can invalidate
|
||||
* it at any time. Thus an app can never be guaranteed that it will be able to play in offload.
|
||||
* Audio processing (for example, speed adjustment) will not be available when offload is in
|
||||
* use.
|
||||
*
|
||||
* <p>The default value is {@link #OFFLOAD_MODE_DISABLED}.
|
||||
*/
|
||||
public Builder setOffloadMode(@OffloadMode int offloadMode) {
|
||||
this.offloadMode = offloadMode;
|
||||
return this;
|
||||
}
|
||||
|
||||
/** Builds the {@link DefaultAudioSink}. Must only be called once per Builder instance. */
|
||||
public DefaultAudioSink build() {
|
||||
if (audioProcessorChain == null) {
|
||||
audioProcessorChain = new DefaultAudioProcessorChain();
|
||||
}
|
||||
return new DefaultAudioSink(this);
|
||||
}
|
||||
}
|
||||
|
||||
/** The default playback speed. */
|
||||
public static final float DEFAULT_PLAYBACK_SPEED = 1f;
|
||||
/** The minimum allowed playback speed. Lower values will be constrained to fall in range. */
|
||||
|
|
@ -306,7 +417,7 @@ public final class DefaultAudioSink implements AudioSink {
|
|||
*/
|
||||
public static boolean failOnSpuriousAudioTimestamp = false;
|
||||
|
||||
@Nullable private final AudioCapabilities audioCapabilities;
|
||||
private final AudioCapabilities audioCapabilities;
|
||||
private final AudioProcessorChain audioProcessorChain;
|
||||
private final boolean enableFloatOutput;
|
||||
private final ChannelMappingAudioProcessor channelMappingAudioProcessor;
|
||||
|
|
@ -323,6 +434,7 @@ public final class DefaultAudioSink implements AudioSink {
|
|||
initializationExceptionPendingExceptionHolder;
|
||||
private final PendingExceptionHolder<WriteException> writeExceptionPendingExceptionHolder;
|
||||
|
||||
@Nullable private PlayerId playerId;
|
||||
@Nullable private Listener listener;
|
||||
@Nullable private Configuration pendingConfiguration;
|
||||
@MonotonicNonNull private Configuration configuration;
|
||||
|
|
@ -366,76 +478,81 @@ public final class DefaultAudioSink implements AudioSink {
|
|||
private boolean offloadDisabledUntilNextConfiguration;
|
||||
private boolean isWaitingForOffloadEndOfStreamHandled;
|
||||
|
||||
/**
|
||||
* Creates a new default audio sink.
|
||||
*
|
||||
* @param audioCapabilities The audio capabilities for playback on this device. May be null if the
|
||||
* default capabilities (no encoded audio passthrough support) should be assumed.
|
||||
* @param audioProcessors An array of {@link AudioProcessor}s that will process PCM audio before
|
||||
* output. May be empty.
|
||||
*/
|
||||
/** @deprecated Use {@link Builder}. */
|
||||
@Deprecated
|
||||
@InlineMeValidationDisabled("Migrate constructor to Builder")
|
||||
@InlineMe(
|
||||
replacement =
|
||||
"new DefaultAudioSink.Builder()"
|
||||
+ ".setAudioCapabilities(audioCapabilities)"
|
||||
+ ".setAudioProcessors(audioProcessors)"
|
||||
+ ".build()",
|
||||
imports = "com.google.android.exoplayer2.audio.DefaultAudioSink")
|
||||
public DefaultAudioSink(
|
||||
@Nullable AudioCapabilities audioCapabilities, AudioProcessor[] audioProcessors) {
|
||||
this(audioCapabilities, audioProcessors, /* enableFloatOutput= */ false);
|
||||
this(
|
||||
new Builder()
|
||||
.setAudioCapabilities(firstNonNull(audioCapabilities, DEFAULT_AUDIO_CAPABILITIES))
|
||||
.setAudioProcessors(audioProcessors));
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new default audio sink, optionally using float output for high resolution PCM.
|
||||
*
|
||||
* @param audioCapabilities The audio capabilities for playback on this device. May be null if the
|
||||
* default capabilities (no encoded audio passthrough support) should be assumed.
|
||||
* @param audioProcessors An array of {@link AudioProcessor}s that will process PCM audio before
|
||||
* output. May be empty.
|
||||
* @param enableFloatOutput Whether to enable 32-bit float output. Where possible, 32-bit float
|
||||
* output will be used if the input is 32-bit float, and also if the input is high resolution
|
||||
* (24-bit or 32-bit) integer PCM. Audio processing (for example, speed adjustment) will not
|
||||
* be available when float output is in use.
|
||||
*/
|
||||
/** @deprecated Use {@link Builder}. */
|
||||
@Deprecated
|
||||
@InlineMeValidationDisabled("Migrate constructor to Builder")
|
||||
@InlineMe(
|
||||
replacement =
|
||||
"new DefaultAudioSink.Builder()"
|
||||
+ ".setAudioCapabilities(audioCapabilities)"
|
||||
+ ".setAudioProcessors(audioProcessors)"
|
||||
+ ".setEnableFloatOutput(enableFloatOutput)"
|
||||
+ ".build()",
|
||||
imports = "com.google.android.exoplayer2.audio.DefaultAudioSink")
|
||||
public DefaultAudioSink(
|
||||
@Nullable AudioCapabilities audioCapabilities,
|
||||
AudioProcessor[] audioProcessors,
|
||||
boolean enableFloatOutput) {
|
||||
this(
|
||||
audioCapabilities,
|
||||
new DefaultAudioProcessorChain(audioProcessors),
|
||||
enableFloatOutput,
|
||||
/* enableAudioTrackPlaybackParams= */ false,
|
||||
OFFLOAD_MODE_DISABLED);
|
||||
new Builder()
|
||||
.setAudioCapabilities(firstNonNull(audioCapabilities, DEFAULT_AUDIO_CAPABILITIES))
|
||||
.setAudioProcessors(audioProcessors)
|
||||
.setEnableFloatOutput(enableFloatOutput));
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new default audio sink, optionally using float output for high resolution PCM and
|
||||
* with the specified {@code audioProcessorChain}.
|
||||
*
|
||||
* @param audioCapabilities The audio capabilities for playback on this device. May be null if the
|
||||
* default capabilities (no encoded audio passthrough support) should be assumed.
|
||||
* @param audioProcessorChain An {@link AudioProcessorChain} which is used to apply playback
|
||||
* parameters adjustments. The instance passed in must not be reused in other sinks.
|
||||
* @param enableFloatOutput Whether to enable 32-bit float output. Where possible, 32-bit float
|
||||
* output will be used if the input is 32-bit float, and also if the input is high resolution
|
||||
* (24-bit or 32-bit) integer PCM. Float output is supported from API level 21. Audio
|
||||
* processing (for example, speed adjustment) will not be available when float output is in
|
||||
* use.
|
||||
* @param enableAudioTrackPlaybackParams Whether to enable setting playback speed using {@link
|
||||
* android.media.AudioTrack#setPlaybackParams(PlaybackParams)}, if supported.
|
||||
* @param offloadMode Audio offload configuration. If an audio format can be both played with
|
||||
* offload and encoded audio passthrough, it will be played in offload. Audio offload is
|
||||
* supported from API level 29. Most Android devices can only support one offload {@link
|
||||
* android.media.AudioTrack} at a time and can invalidate it at any time. Thus an app can
|
||||
* never be guaranteed that it will be able to play in offload. Audio processing (for example,
|
||||
* speed adjustment) will not be available when offload is in use.
|
||||
*/
|
||||
/** @deprecated Use {@link Builder}. */
|
||||
@Deprecated
|
||||
@InlineMeValidationDisabled("Migrate constructor to Builder")
|
||||
@InlineMe(
|
||||
replacement =
|
||||
"new DefaultAudioSink.Builder()"
|
||||
+ ".setAudioCapabilities(audioCapabilities)"
|
||||
+ ".setAudioProcessorChain(audioProcessorChain)"
|
||||
+ ".setEnableFloatOutput(enableFloatOutput)"
|
||||
+ ".setEnableAudioTrackPlaybackParams(enableAudioTrackPlaybackParams)"
|
||||
+ ".setOffloadMode(offloadMode)"
|
||||
+ ".build()",
|
||||
imports = "com.google.android.exoplayer2.audio.DefaultAudioSink")
|
||||
public DefaultAudioSink(
|
||||
@Nullable AudioCapabilities audioCapabilities,
|
||||
AudioProcessorChain audioProcessorChain,
|
||||
boolean enableFloatOutput,
|
||||
boolean enableAudioTrackPlaybackParams,
|
||||
@OffloadMode int offloadMode) {
|
||||
this.audioCapabilities = audioCapabilities;
|
||||
this.audioProcessorChain = Assertions.checkNotNull(audioProcessorChain);
|
||||
this.enableFloatOutput = Util.SDK_INT >= 21 && enableFloatOutput;
|
||||
this.enableAudioTrackPlaybackParams = Util.SDK_INT >= 23 && enableAudioTrackPlaybackParams;
|
||||
this.offloadMode = Util.SDK_INT >= 29 ? offloadMode : OFFLOAD_MODE_DISABLED;
|
||||
this(
|
||||
new Builder()
|
||||
.setAudioCapabilities(firstNonNull(audioCapabilities, DEFAULT_AUDIO_CAPABILITIES))
|
||||
.setAudioProcessorChain(audioProcessorChain)
|
||||
.setEnableFloatOutput(enableFloatOutput)
|
||||
.setEnableAudioTrackPlaybackParams(enableAudioTrackPlaybackParams)
|
||||
.setOffloadMode(offloadMode));
|
||||
}
|
||||
|
||||
@RequiresNonNull("#1.audioProcessorChain")
|
||||
private DefaultAudioSink(Builder builder) {
|
||||
audioCapabilities = builder.audioCapabilities;
|
||||
audioProcessorChain = builder.audioProcessorChain;
|
||||
enableFloatOutput = Util.SDK_INT >= 21 && builder.enableFloatOutput;
|
||||
enableAudioTrackPlaybackParams = Util.SDK_INT >= 23 && builder.enableAudioTrackPlaybackParams;
|
||||
offloadMode = Util.SDK_INT >= 29 ? builder.offloadMode : OFFLOAD_MODE_DISABLED;
|
||||
releasingConditionVariable = new ConditionVariable(true);
|
||||
audioTrackPositionTracker = new AudioTrackPositionTracker(new PositionTrackerListener());
|
||||
channelMappingAudioProcessor = new ChannelMappingAudioProcessor();
|
||||
|
|
@ -477,6 +594,11 @@ public final class DefaultAudioSink implements AudioSink {
|
|||
this.listener = listener;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setPlayerId(@Nullable PlayerId playerId) {
|
||||
this.playerId = playerId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsFormat(Format format) {
|
||||
return getFormatSupport(format) != SINK_FORMAT_UNSUPPORTED;
|
||||
|
|
@ -578,8 +700,7 @@ public final class DefaultAudioSink implements AudioSink {
|
|||
if (useOffloadedPlayback(inputFormat, audioAttributes)) {
|
||||
outputMode = OUTPUT_MODE_OFFLOAD;
|
||||
outputEncoding =
|
||||
MimeTypes.getEncoding(
|
||||
Assertions.checkNotNull(inputFormat.sampleMimeType), inputFormat.codecs);
|
||||
MimeTypes.getEncoding(checkNotNull(inputFormat.sampleMimeType), inputFormat.codecs);
|
||||
outputChannelConfig = Util.getAudioTrackChannelConfig(inputFormat.channelCount);
|
||||
} else {
|
||||
outputMode = OUTPUT_MODE_PASSTHROUGH;
|
||||
|
|
@ -665,6 +786,9 @@ public final class DefaultAudioSink implements AudioSink {
|
|||
configuration.inputFormat.encoderDelay, configuration.inputFormat.encoderPadding);
|
||||
}
|
||||
}
|
||||
if (Util.SDK_INT >= 31 && playerId != null) {
|
||||
Api31.setLogSessionIdOnAudioTrack(audioTrack, playerId);
|
||||
}
|
||||
audioSessionId = audioTrack.getAudioSessionId();
|
||||
audioTrackPositionTracker.setAudioTrack(
|
||||
audioTrack,
|
||||
|
|
@ -850,7 +974,7 @@ public final class DefaultAudioSink implements AudioSink {
|
|||
|
||||
private AudioTrack buildAudioTrack() throws InitializationException {
|
||||
try {
|
||||
return Assertions.checkNotNull(configuration)
|
||||
return checkNotNull(configuration)
|
||||
.buildAudioTrack(tunneling, audioAttributes, audioSessionId);
|
||||
} catch (InitializationException e) {
|
||||
maybeDisableOffload();
|
||||
|
|
@ -1195,7 +1319,7 @@ public final class DefaultAudioSink implements AudioSink {
|
|||
audioTrack.pause();
|
||||
}
|
||||
if (isOffloadedPlayback(audioTrack)) {
|
||||
Assertions.checkNotNull(offloadStreamEventCallbackV29).unregister(audioTrack);
|
||||
checkNotNull(offloadStreamEventCallbackV29).unregister(audioTrack);
|
||||
}
|
||||
// AudioTrack.release can take some time, so we call it on a background thread.
|
||||
final AudioTrack toRelease = audioTrack;
|
||||
|
|
@ -1475,7 +1599,7 @@ public final class DefaultAudioSink implements AudioSink {
|
|||
}
|
||||
|
||||
private static boolean isPassthroughPlaybackSupported(
|
||||
Format format, @Nullable AudioCapabilities audioCapabilities) {
|
||||
Format format, AudioCapabilities audioCapabilities) {
|
||||
return getEncodingAndChannelConfigForPassthrough(format, audioCapabilities) != null;
|
||||
}
|
||||
|
||||
|
|
@ -1491,14 +1615,9 @@ public final class DefaultAudioSink implements AudioSink {
|
|||
*/
|
||||
@Nullable
|
||||
private static Pair<Integer, Integer> getEncodingAndChannelConfigForPassthrough(
|
||||
Format format, @Nullable AudioCapabilities audioCapabilities) {
|
||||
if (audioCapabilities == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Format format, AudioCapabilities audioCapabilities) {
|
||||
@C.Encoding
|
||||
int encoding =
|
||||
MimeTypes.getEncoding(Assertions.checkNotNull(format.sampleMimeType), format.codecs);
|
||||
int encoding = MimeTypes.getEncoding(checkNotNull(format.sampleMimeType), format.codecs);
|
||||
// Check for encodings that are known to work for passthrough with the implementation in this
|
||||
// class. This avoids trying to use passthrough with an encoding where the device/app reports
|
||||
// it's capable but it is untested or known to be broken (for example AAC-LC).
|
||||
|
|
@ -1609,8 +1728,7 @@ public final class DefaultAudioSink implements AudioSink {
|
|||
return false;
|
||||
}
|
||||
@C.Encoding
|
||||
int encoding =
|
||||
MimeTypes.getEncoding(Assertions.checkNotNull(format.sampleMimeType), format.codecs);
|
||||
int encoding = MimeTypes.getEncoding(checkNotNull(format.sampleMimeType), format.codecs);
|
||||
if (encoding == C.ENCODING_INVALID) {
|
||||
return false;
|
||||
}
|
||||
|
|
@ -2094,6 +2212,7 @@ public final class DefaultAudioSink implements AudioSink {
|
|||
audioSessionId);
|
||||
}
|
||||
|
||||
@SuppressWarnings("deprecation") // Using deprecated AudioTrack constructor.
|
||||
private AudioTrack createAudioTrackV9(AudioAttributes audioAttributes, int audioSessionId) {
|
||||
int streamType = Util.getStreamTypeForAudioUsage(audioAttributes.usage);
|
||||
if (audioSessionId == C.AUDIO_SESSION_ID_UNSET) {
|
||||
|
|
@ -2216,4 +2335,17 @@ public final class DefaultAudioSink implements AudioSink {
|
|||
pendingException = null;
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(31)
|
||||
private static final class Api31 {
|
||||
private Api31() {}
|
||||
|
||||
@DoNotInline
|
||||
public static void setLogSessionIdOnAudioTrack(AudioTrack audioTrack, PlayerId playerId) {
|
||||
LogSessionId logSessionId = playerId.getLogSessionId();
|
||||
if (!logSessionId.equals(LogSessionId.LOG_SESSION_ID_NONE)) {
|
||||
audioTrack.setLogSessionId(logSessionId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ package com.google.android.exoplayer2.audio;
|
|||
import androidx.annotation.Nullable;
|
||||
import com.google.android.exoplayer2.Format;
|
||||
import com.google.android.exoplayer2.PlaybackParameters;
|
||||
import com.google.android.exoplayer2.analytics.PlayerId;
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
/** An overridable {@link AudioSink} implementation forwarding all methods to another sink. */
|
||||
|
|
@ -34,6 +35,11 @@ public class ForwardingAudioSink implements AudioSink {
|
|||
sink.setListener(listener);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setPlayerId(@Nullable PlayerId playerId) {
|
||||
sink.setPlayerId(playerId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsFormat(Format format) {
|
||||
return sink.supportsFormat(format);
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ package com.google.android.exoplayer2.audio;
|
|||
import static com.google.android.exoplayer2.decoder.DecoderReuseEvaluation.DISCARD_REASON_MAX_INPUT_SIZE_EXCEEDED;
|
||||
import static com.google.android.exoplayer2.decoder.DecoderReuseEvaluation.REUSE_RESULT_NO;
|
||||
import static com.google.android.exoplayer2.util.Assertions.checkNotNull;
|
||||
import static com.google.common.base.MoreObjects.firstNonNull;
|
||||
import static java.lang.Math.max;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
|
|
@ -129,7 +130,12 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
|
|||
MediaCodecSelector mediaCodecSelector,
|
||||
@Nullable Handler eventHandler,
|
||||
@Nullable AudioRendererEventListener eventListener) {
|
||||
this(context, mediaCodecSelector, eventHandler, eventListener, (AudioCapabilities) null);
|
||||
this(
|
||||
context,
|
||||
mediaCodecSelector,
|
||||
eventHandler,
|
||||
eventListener,
|
||||
AudioCapabilities.DEFAULT_AUDIO_CAPABILITIES);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -138,8 +144,9 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
|
|||
* @param eventHandler A handler to use when delivering events to {@code eventListener}. May be
|
||||
* null if delivery of events is not required.
|
||||
* @param eventListener A listener of events. May be null if delivery of events is not required.
|
||||
* @param audioCapabilities The audio capabilities for playback on this device. May be null if the
|
||||
* default capabilities (no encoded audio passthrough support) should be assumed.
|
||||
* @param audioCapabilities The audio capabilities for playback on this device. Use {@link
|
||||
* AudioCapabilities#DEFAULT_AUDIO_CAPABILITIES} if default capabilities (no encoded audio
|
||||
* passthrough support) should be assumed.
|
||||
* @param audioProcessors Optional {@link AudioProcessor}s that will process PCM audio before
|
||||
* output.
|
||||
*/
|
||||
|
|
@ -148,14 +155,18 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
|
|||
MediaCodecSelector mediaCodecSelector,
|
||||
@Nullable Handler eventHandler,
|
||||
@Nullable AudioRendererEventListener eventListener,
|
||||
@Nullable AudioCapabilities audioCapabilities,
|
||||
AudioCapabilities audioCapabilities,
|
||||
AudioProcessor... audioProcessors) {
|
||||
this(
|
||||
context,
|
||||
mediaCodecSelector,
|
||||
eventHandler,
|
||||
eventListener,
|
||||
new DefaultAudioSink(audioCapabilities, audioProcessors));
|
||||
new DefaultAudioSink.Builder()
|
||||
.setAudioCapabilities( // For backward compatibility, null == default.
|
||||
firstNonNull(audioCapabilities, AudioCapabilities.DEFAULT_AUDIO_CAPABILITIES))
|
||||
.setAudioProcessors(audioProcessors)
|
||||
.build());
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -292,30 +303,82 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
|
|||
return RendererCapabilities.create(C.FORMAT_UNSUPPORTED_SUBTYPE);
|
||||
}
|
||||
List<MediaCodecInfo> decoderInfos =
|
||||
getDecoderInfos(mediaCodecSelector, format, /* requiresSecureDecoder= */ false);
|
||||
getDecoderInfos(mediaCodecSelector, format, /* requiresSecureDecoder= */ false, audioSink);
|
||||
if (decoderInfos.isEmpty()) {
|
||||
return RendererCapabilities.create(C.FORMAT_UNSUPPORTED_SUBTYPE);
|
||||
}
|
||||
if (!supportsFormatDrm) {
|
||||
return RendererCapabilities.create(C.FORMAT_UNSUPPORTED_DRM);
|
||||
}
|
||||
// Check capabilities for the first decoder in the list, which takes priority.
|
||||
// Check whether the first decoder supports the format. This is the preferred decoder for the
|
||||
// format's MIME type, according to the MediaCodecSelector.
|
||||
MediaCodecInfo decoderInfo = decoderInfos.get(0);
|
||||
boolean isFormatSupported = decoderInfo.isFormatSupported(format);
|
||||
boolean isPreferredDecoder = true;
|
||||
if (!isFormatSupported) {
|
||||
// Check whether any of the other decoders support the format.
|
||||
for (int i = 1; i < decoderInfos.size(); i++) {
|
||||
MediaCodecInfo otherDecoderInfo = decoderInfos.get(i);
|
||||
if (otherDecoderInfo.isFormatSupported(format)) {
|
||||
decoderInfo = otherDecoderInfo;
|
||||
isFormatSupported = true;
|
||||
isPreferredDecoder = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@C.FormatSupport
|
||||
int formatSupport = isFormatSupported ? C.FORMAT_HANDLED : C.FORMAT_EXCEEDS_CAPABILITIES;
|
||||
@AdaptiveSupport
|
||||
int adaptiveSupport =
|
||||
isFormatSupported && decoderInfo.isSeamlessAdaptationSupported(format)
|
||||
? ADAPTIVE_SEAMLESS
|
||||
: ADAPTIVE_NOT_SEAMLESS;
|
||||
@C.FormatSupport
|
||||
int formatSupport = isFormatSupported ? C.FORMAT_HANDLED : C.FORMAT_EXCEEDS_CAPABILITIES;
|
||||
return RendererCapabilities.create(formatSupport, adaptiveSupport, tunnelingSupport);
|
||||
@HardwareAccelerationSupport
|
||||
int hardwareAccelerationSupport =
|
||||
decoderInfo.hardwareAccelerated
|
||||
? HARDWARE_ACCELERATION_SUPPORTED
|
||||
: HARDWARE_ACCELERATION_NOT_SUPPORTED;
|
||||
@DecoderSupport
|
||||
int decoderSupport = isPreferredDecoder ? DECODER_SUPPORT_PRIMARY : DECODER_SUPPORT_FALLBACK;
|
||||
return RendererCapabilities.create(
|
||||
formatSupport,
|
||||
adaptiveSupport,
|
||||
tunnelingSupport,
|
||||
hardwareAccelerationSupport,
|
||||
decoderSupport);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<MediaCodecInfo> getDecoderInfos(
|
||||
MediaCodecSelector mediaCodecSelector, Format format, boolean requiresSecureDecoder)
|
||||
throws DecoderQueryException {
|
||||
return MediaCodecUtil.getDecoderInfosSortedByFormatSupport(
|
||||
getDecoderInfos(mediaCodecSelector, format, requiresSecureDecoder, audioSink), format);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of decoders that can decode media in the specified format, in the priority order
|
||||
* specified by the {@link MediaCodecSelector}. Note that since the {@link MediaCodecSelector}
|
||||
* only has access to {@link Format#sampleMimeType}, the list is not ordered to account for
|
||||
* whether each decoder supports the details of the format (e.g., taking into account the format's
|
||||
* profile, level, channel count and so on). {@link
|
||||
* MediaCodecUtil#getDecoderInfosSortedByFormatSupport} can be used to further sort the list into
|
||||
* an order where decoders that fully support the format come first.
|
||||
*
|
||||
* @param mediaCodecSelector The decoder selector.
|
||||
* @param format The {@link Format} for which a decoder is required.
|
||||
* @param requiresSecureDecoder Whether a secure decoder is required.
|
||||
* @param audioSink The {@link AudioSink} to which audio will be output.
|
||||
* @return A list of {@link MediaCodecInfo}s corresponding to decoders. May be empty.
|
||||
* @throws DecoderQueryException Thrown if there was an error querying decoders.
|
||||
*/
|
||||
private static List<MediaCodecInfo> getDecoderInfos(
|
||||
MediaCodecSelector mediaCodecSelector,
|
||||
Format format,
|
||||
boolean requiresSecureDecoder,
|
||||
AudioSink audioSink)
|
||||
throws DecoderQueryException {
|
||||
@Nullable String mimeType = format.sampleMimeType;
|
||||
if (mimeType == null) {
|
||||
return Collections.emptyList();
|
||||
|
|
@ -330,7 +393,6 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
|
|||
List<MediaCodecInfo> decoderInfos =
|
||||
mediaCodecSelector.getDecoderInfos(
|
||||
mimeType, requiresSecureDecoder, /* requiresTunnelingDecoder= */ false);
|
||||
decoderInfos = MediaCodecUtil.getDecoderInfosSortedByFormatSupport(decoderInfos, format);
|
||||
if (MimeTypes.AUDIO_E_AC3_JOC.equals(mimeType)) {
|
||||
// E-AC3 decoders can decode JOC streams, but in 2-D rather than 3-D.
|
||||
List<MediaCodecInfo> decoderInfosWithEac3 = new ArrayList<>(decoderInfos);
|
||||
|
|
@ -452,10 +514,7 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
|
|||
} else {
|
||||
// If the format is anything other than PCM then we assume that the audio decoder will
|
||||
// output 16-bit PCM.
|
||||
pcmEncoding =
|
||||
MimeTypes.AUDIO_RAW.equals(format.sampleMimeType)
|
||||
? format.pcmEncoding
|
||||
: C.ENCODING_PCM_16BIT;
|
||||
pcmEncoding = C.ENCODING_PCM_16BIT;
|
||||
}
|
||||
audioSinkInputFormat =
|
||||
new Format.Builder()
|
||||
|
|
@ -500,6 +559,7 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
|
|||
} else {
|
||||
audioSink.disableTunneling();
|
||||
}
|
||||
audioSink.setPlayerId(getPlayerId());
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -787,6 +847,14 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
|
|||
== AudioSink.SINK_FORMAT_SUPPORTED_DIRECTLY) {
|
||||
mediaFormat.setInteger(MediaFormat.KEY_PCM_ENCODING, AudioFormat.ENCODING_PCM_FLOAT);
|
||||
}
|
||||
|
||||
if (Util.SDK_INT >= 32) {
|
||||
// Disable down-mixing in the decoder (for decoders that read the max-output-channel-count
|
||||
// key).
|
||||
// TODO[b/190759307]: Update key to use MediaFormat.KEY_MAX_OUTPUT_CHANNEL_COUNT once the
|
||||
// compile SDK target is set to 32.
|
||||
mediaFormat.setInteger("max-output-channel-count", 99);
|
||||
}
|
||||
return mediaFormat;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,217 @@
|
|||
/*
|
||||
* Copyright 2021 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.audio;
|
||||
|
||||
import static com.google.android.exoplayer2.util.Assertions.checkStateNotNull;
|
||||
import static java.lang.annotation.ElementType.FIELD;
|
||||
import static java.lang.annotation.ElementType.LOCAL_VARIABLE;
|
||||
import static java.lang.annotation.ElementType.METHOD;
|
||||
import static java.lang.annotation.ElementType.PARAMETER;
|
||||
import static java.lang.annotation.ElementType.TYPE_USE;
|
||||
|
||||
import android.content.Context;
|
||||
import android.media.AudioAttributes;
|
||||
import android.media.AudioFormat;
|
||||
import android.media.AudioManager;
|
||||
import androidx.annotation.IntDef;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.RequiresApi;
|
||||
import com.google.android.exoplayer2.util.Assertions;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
import java.lang.annotation.Documented;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
import java.lang.reflect.InvocationHandler;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
import java.lang.reflect.Proxy;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.Executor;
|
||||
|
||||
/**
|
||||
* Exposes the android.media.Spatializer API via reflection. This is so that we can use the
|
||||
* Spatializer while the compile SDK target is set to 31.
|
||||
*/
|
||||
@RequiresApi(31)
|
||||
/* package */ final class SpatializerDelegate {
|
||||
/** Level of support for audio spatialization. */
|
||||
@Documented
|
||||
@Retention(RetentionPolicy.SOURCE)
|
||||
@Target({FIELD, METHOD, PARAMETER, LOCAL_VARIABLE, TYPE_USE})
|
||||
@IntDef({
|
||||
SPATIALIZER_IMMERSIVE_LEVEL_MULTICHANNEL,
|
||||
SPATIALIZER_IMMERSIVE_LEVEL_NONE,
|
||||
SPATIALIZER_IMMERSIVE_LEVEL_OTHER
|
||||
})
|
||||
@interface ImmersiveAudioLevel {}
|
||||
|
||||
/** See Spatializer#SPATIALIZER_IMMERSIVE_LEVEL_MULTICHANNEL */
|
||||
public static final int SPATIALIZER_IMMERSIVE_LEVEL_MULTICHANNEL = 1;
|
||||
/** See Spatializer#SPATIALIZER_IMMERSIVE_LEVEL_NONE */
|
||||
public static final int SPATIALIZER_IMMERSIVE_LEVEL_NONE = 0;
|
||||
/** See Spatializer#SPATIALIZER_IMMERSIVE_LEVEL_OTHER */
|
||||
public static final int SPATIALIZER_IMMERSIVE_LEVEL_OTHER = -1;
|
||||
|
||||
/** Wrapper for Spatializer.OnSpatializerStateChangedListener */
|
||||
public interface Listener {
|
||||
/** See Spatializer.OnSpatializerStateChangedListener.onSpatializerEnabledChanged */
|
||||
void onSpatializerEnabledChanged(SpatializerDelegate spatializer, boolean enabled);
|
||||
|
||||
/** See Spatializer.OnSpatializerStateChangedListener.onSpatializerAvailableChanged */
|
||||
void onSpatializerAvailableChanged(SpatializerDelegate spatializer, boolean available);
|
||||
}
|
||||
|
||||
private final Object spatializer;
|
||||
private final Class<?> spatializerClass;
|
||||
private final Class<?> spatializerListenerClass;
|
||||
private final Method isEnabled;
|
||||
private final Method isAvailable;
|
||||
private final Method getImmersiveAudioLevel;
|
||||
private final Method canBeSpatialized;
|
||||
private final Method addListener;
|
||||
private final Method removeListener;
|
||||
private final Map<Listener, Object> listeners;
|
||||
|
||||
/** Creates an instance. */
|
||||
public SpatializerDelegate(Context context)
|
||||
throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException,
|
||||
IllegalAccessException {
|
||||
Method getSpatializerMethod = AudioManager.class.getMethod("getSpatializer");
|
||||
AudioManager manager =
|
||||
Assertions.checkNotNull(
|
||||
(AudioManager) context.getApplicationContext().getSystemService(Context.AUDIO_SERVICE));
|
||||
spatializer = checkStateNotNull(getSpatializerMethod.invoke(manager));
|
||||
spatializerClass = Class.forName("android.media.Spatializer");
|
||||
spatializerListenerClass =
|
||||
Class.forName("android.media.Spatializer$OnSpatializerStateChangedListener");
|
||||
isEnabled = spatializerClass.getMethod("isEnabled");
|
||||
isAvailable = spatializerClass.getMethod("isAvailable");
|
||||
getImmersiveAudioLevel = spatializerClass.getMethod("getImmersiveAudioLevel");
|
||||
canBeSpatialized =
|
||||
spatializerClass.getMethod(
|
||||
"canBeSpatialized", android.media.AudioAttributes.class, AudioFormat.class);
|
||||
addListener =
|
||||
spatializerClass.getMethod(
|
||||
"addOnSpatializerStateChangedListener", Executor.class, spatializerListenerClass);
|
||||
removeListener =
|
||||
spatializerClass.getMethod(
|
||||
"removeOnSpatializerStateChangedListener", spatializerListenerClass);
|
||||
listeners = new HashMap<>();
|
||||
}
|
||||
|
||||
/** Delegates to Spatializer.isEnabled() */
|
||||
public boolean isEnabled() {
|
||||
try {
|
||||
return (boolean) Util.castNonNull(isEnabled.invoke(spatializer));
|
||||
} catch (IllegalAccessException | InvocationTargetException e) {
|
||||
throw new IllegalStateException(e);
|
||||
}
|
||||
}
|
||||
|
||||
/** Delegates to Spatializer.isAvailable() */
|
||||
public boolean isAvailable() {
|
||||
try {
|
||||
return (boolean) Util.castNonNull(isAvailable.invoke(spatializer));
|
||||
} catch (IllegalAccessException | InvocationTargetException e) {
|
||||
throw new IllegalStateException(e);
|
||||
}
|
||||
}
|
||||
|
||||
/** Delegates to Spatializer.getImmersiveAudioLevel() */
|
||||
@ImmersiveAudioLevel
|
||||
public int getImmersiveAudioLevel() {
|
||||
try {
|
||||
return (int) Util.castNonNull(getImmersiveAudioLevel.invoke(spatializer));
|
||||
} catch (IllegalAccessException | InvocationTargetException e) {
|
||||
throw new IllegalStateException(e);
|
||||
}
|
||||
}
|
||||
|
||||
/** Delegates to Spatializer.canBeSpatialized() */
|
||||
public boolean canBeSpatialized(AudioAttributes attributes, AudioFormat format) {
|
||||
try {
|
||||
return (boolean) Util.castNonNull(canBeSpatialized.invoke(spatializer, attributes, format));
|
||||
} catch (IllegalAccessException | InvocationTargetException e) {
|
||||
throw new IllegalStateException(e);
|
||||
}
|
||||
}
|
||||
|
||||
/** Delegates to Spatializer.addOnSpatializerStateChangedListener() */
|
||||
public void addOnSpatializerStateChangedListener(Executor executor, Listener listener) {
|
||||
if (listeners.containsKey(listener)) {
|
||||
return;
|
||||
}
|
||||
Object listenerProxy = createSpatializerListenerProxy(listener);
|
||||
try {
|
||||
addListener.invoke(spatializer, executor, listenerProxy);
|
||||
listeners.put(listener, listenerProxy);
|
||||
} catch (IllegalAccessException | InvocationTargetException e) {
|
||||
throw new IllegalStateException(e);
|
||||
}
|
||||
}
|
||||
|
||||
/** Delegates to Spatializer.removeOnSpatializerStateChangedListener() */
|
||||
public void removeOnSpatializerStateChangedListener(Listener listener) {
|
||||
@Nullable Object proxy = listeners.get(listener);
|
||||
if (proxy == null) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
removeListener.invoke(spatializer, proxy);
|
||||
listeners.remove(listener);
|
||||
} catch (IllegalAccessException | InvocationTargetException e) {
|
||||
throw new IllegalStateException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private Object createSpatializerListenerProxy(Listener listener) {
|
||||
return Proxy.newProxyInstance(
|
||||
spatializerListenerClass.getClassLoader(),
|
||||
new Class<?>[] {spatializerListenerClass},
|
||||
new ProxySpatializerListener(this, listener));
|
||||
}
|
||||
|
||||
/** Proxy-based implementation of Spatializer.OnSpatializerStateChangedListener. */
|
||||
private static final class ProxySpatializerListener implements InvocationHandler {
|
||||
private final SpatializerDelegate spatializerDelegate;
|
||||
private final Listener listener;
|
||||
|
||||
private ProxySpatializerListener(SpatializerDelegate spatializerDelegate, Listener listener) {
|
||||
this.spatializerDelegate = spatializerDelegate;
|
||||
this.listener = listener;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object invoke(Object o, Method method, Object[] objects) {
|
||||
String methodName = method.getName();
|
||||
Class<?>[] parameterTypes = method.getParameterTypes();
|
||||
if (methodName.equals("onSpatializerAvailableChanged")
|
||||
&& parameterTypes.length == 2
|
||||
&& spatializerDelegate.spatializerClass.isAssignableFrom(parameterTypes[0])
|
||||
&& parameterTypes[1].equals(Boolean.TYPE)) {
|
||||
listener.onSpatializerAvailableChanged(spatializerDelegate, (boolean) objects[1]);
|
||||
} else if (methodName.equals("onSpatializerEnabledChanged")
|
||||
&& parameterTypes.length == 2
|
||||
&& spatializerDelegate.spatializerClass.isAssignableFrom(parameterTypes[0])
|
||||
&& parameterTypes[1].equals(Boolean.TYPE)) {
|
||||
listener.onSpatializerEnabledChanged(spatializerDelegate, (boolean) objects[1]);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -17,6 +17,8 @@ package com.google.android.exoplayer2.decoder;
|
|||
|
||||
import static java.lang.Math.max;
|
||||
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
|
||||
/**
|
||||
* Maintains decoder event counts, for debugging purposes only.
|
||||
*
|
||||
|
|
@ -30,12 +32,12 @@ public final class DecoderCounters {
|
|||
public int decoderInitCount;
|
||||
/** The number of times a decoder has been released. */
|
||||
public int decoderReleaseCount;
|
||||
/** The number of queued input buffers. */
|
||||
public int inputBufferCount;
|
||||
/** The number of input buffers queued to the decoder. */
|
||||
public int queuedInputBufferCount;
|
||||
/**
|
||||
* The number of skipped input buffers.
|
||||
*
|
||||
* <p>A skipped input buffer is an input buffer that was deliberately not sent to the decoder.
|
||||
* <p>A skipped input buffer is an input buffer that was deliberately not queued to the decoder.
|
||||
*/
|
||||
public int skippedInputBufferCount;
|
||||
/** The number of rendered output buffers. */
|
||||
|
|
@ -43,28 +45,47 @@ public final class DecoderCounters {
|
|||
/**
|
||||
* The number of skipped output buffers.
|
||||
*
|
||||
* <p>A skipped output buffer is an output buffer that was deliberately not rendered.
|
||||
* <p>A skipped output buffer is an output buffer that was deliberately not rendered. This
|
||||
* includes buffers that were never dequeued from the decoder and instead skipped while 'inside'
|
||||
* the codec due to a flush.
|
||||
*/
|
||||
public int skippedOutputBufferCount;
|
||||
/**
|
||||
* The number of dropped buffers.
|
||||
*
|
||||
* <p>A dropped buffer is an buffer that was supposed to be decoded/rendered, but was instead
|
||||
* <p>A dropped buffer is a buffer that was supposed to be decoded/rendered, but was instead
|
||||
* dropped because it could not be rendered in time.
|
||||
*
|
||||
* <p>This includes all of {@link #droppedInputBufferCount} in addition to buffers dropped after
|
||||
* being queued to the decoder.
|
||||
*/
|
||||
public int droppedBufferCount;
|
||||
/**
|
||||
* The number of input buffers dropped.
|
||||
*
|
||||
* <p>A dropped input buffer is a buffer that was not queued to the decoder because it would not
|
||||
* be rendered in time.
|
||||
*/
|
||||
public int droppedInputBufferCount;
|
||||
/**
|
||||
* The maximum number of dropped buffers without an interleaving rendered output buffer.
|
||||
*
|
||||
* <p>Skipped output buffers are ignored for the purposes of calculating this value.
|
||||
* <p>Skipped buffers are ignored for the purposes of calculating this value.
|
||||
*/
|
||||
public int maxConsecutiveDroppedBufferCount;
|
||||
/**
|
||||
* The number of times all buffers to a keyframe were dropped.
|
||||
*
|
||||
* <p>Each time buffers to a keyframe are dropped, this counter is increased by one, and the
|
||||
* dropped buffer counters are increased by one (for the current output buffer) plus the number of
|
||||
* buffers dropped from the source to advance to the keyframe.
|
||||
* <p>Each time buffers to a keyframe are dropped:
|
||||
*
|
||||
* <ul>
|
||||
* <li>This counter is incremented by one.
|
||||
* <li>{@link #droppedInputBufferCount} is incremented by the number of buffers dropped from the
|
||||
* source to advance to the keyframe.
|
||||
* <li>{@link #droppedBufferCount} is incremented by the sum of the number of buffers dropped
|
||||
* from the source to advance to the keyframe and the number of buffers 'inside' the
|
||||
* decoder.
|
||||
* </ul>
|
||||
*/
|
||||
public int droppedToKeyframeCount;
|
||||
/**
|
||||
|
|
@ -106,11 +127,12 @@ public final class DecoderCounters {
|
|||
public void merge(DecoderCounters other) {
|
||||
decoderInitCount += other.decoderInitCount;
|
||||
decoderReleaseCount += other.decoderReleaseCount;
|
||||
inputBufferCount += other.inputBufferCount;
|
||||
queuedInputBufferCount += other.queuedInputBufferCount;
|
||||
skippedInputBufferCount += other.skippedInputBufferCount;
|
||||
renderedOutputBufferCount += other.renderedOutputBufferCount;
|
||||
skippedOutputBufferCount += other.skippedOutputBufferCount;
|
||||
droppedBufferCount += other.droppedBufferCount;
|
||||
droppedInputBufferCount += other.droppedInputBufferCount;
|
||||
maxConsecutiveDroppedBufferCount =
|
||||
max(maxConsecutiveDroppedBufferCount, other.maxConsecutiveDroppedBufferCount);
|
||||
droppedToKeyframeCount += other.droppedToKeyframeCount;
|
||||
|
|
@ -134,4 +156,34 @@ public final class DecoderCounters {
|
|||
totalVideoFrameProcessingOffsetUs += totalProcessingOffsetUs;
|
||||
videoFrameProcessingOffsetCount += count;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return Util.formatInvariant(
|
||||
"DecoderCounters {\n "
|
||||
+ "decoderInits=%s,\n "
|
||||
+ "decoderReleases=%s\n "
|
||||
+ "queuedInputBuffers=%s\n "
|
||||
+ "skippedInputBuffers=%s\n "
|
||||
+ "renderedOutputBuffers=%s\n "
|
||||
+ "skippedOutputBuffers=%s\n "
|
||||
+ "droppedBuffers=%s\n "
|
||||
+ "droppedInputBuffers=%s\n "
|
||||
+ "maxConsecutiveDroppedBuffers=%s\n "
|
||||
+ "droppedToKeyframeEvents=%s\n "
|
||||
+ "totalVideoFrameProcessingOffsetUs=%s\n "
|
||||
+ "videoFrameProcessingOffsetCount=%s\n}",
|
||||
decoderInitCount,
|
||||
decoderReleaseCount,
|
||||
queuedInputBufferCount,
|
||||
skippedInputBufferCount,
|
||||
renderedOutputBufferCount,
|
||||
skippedOutputBufferCount,
|
||||
droppedBufferCount,
|
||||
droppedInputBufferCount,
|
||||
maxConsecutiveDroppedBufferCount,
|
||||
droppedToKeyframeCount,
|
||||
totalVideoFrameProcessingOffsetUs,
|
||||
videoFrameProcessingOffsetCount);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ import androidx.annotation.GuardedBy;
|
|||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.RequiresApi;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.analytics.PlayerId;
|
||||
import com.google.android.exoplayer2.decoder.CryptoConfig;
|
||||
import com.google.android.exoplayer2.drm.DrmInitData.SchemeData;
|
||||
import com.google.android.exoplayer2.drm.ExoMediaDrm.KeyRequest;
|
||||
|
|
@ -133,6 +134,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
|
|||
private final HashMap<String, String> keyRequestParameters;
|
||||
private final CopyOnWriteMultiset<DrmSessionEventListener.EventDispatcher> eventDispatchers;
|
||||
private final LoadErrorHandlingPolicy loadErrorHandlingPolicy;
|
||||
private final PlayerId playerId;
|
||||
|
||||
/* package */ final MediaDrmCallback callback;
|
||||
/* package */ final UUID uuid;
|
||||
|
|
@ -182,7 +184,8 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
|
|||
HashMap<String, String> keyRequestParameters,
|
||||
MediaDrmCallback callback,
|
||||
Looper playbackLooper,
|
||||
LoadErrorHandlingPolicy loadErrorHandlingPolicy) {
|
||||
LoadErrorHandlingPolicy loadErrorHandlingPolicy,
|
||||
PlayerId playerId) {
|
||||
if (mode == DefaultDrmSessionManager.MODE_QUERY
|
||||
|| mode == DefaultDrmSessionManager.MODE_RELEASE) {
|
||||
Assertions.checkNotNull(offlineLicenseKeySetId);
|
||||
|
|
@ -204,6 +207,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
|
|||
this.callback = callback;
|
||||
this.eventDispatchers = new CopyOnWriteMultiset<>();
|
||||
this.loadErrorHandlingPolicy = loadErrorHandlingPolicy;
|
||||
this.playerId = playerId;
|
||||
state = STATE_OPENING;
|
||||
responseHandler = new ResponseHandler(playbackLooper);
|
||||
}
|
||||
|
|
@ -370,6 +374,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
|
|||
|
||||
try {
|
||||
sessionId = mediaDrm.openSession();
|
||||
mediaDrm.setPlayerIdForSession(sessionId, playerId);
|
||||
cryptoConfig = mediaDrm.createCryptoConfig(sessionId);
|
||||
state = STATE_OPENED;
|
||||
// Capture state into a local so a consistent value is seen by the lambda.
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ package com.google.android.exoplayer2.drm;
|
|||
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 static com.google.android.exoplayer2.util.Assertions.checkStateNotNull;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.media.ResourceBusyException;
|
||||
|
|
@ -31,6 +32,7 @@ import androidx.annotation.RequiresApi;
|
|||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.Format;
|
||||
import com.google.android.exoplayer2.PlaybackException;
|
||||
import com.google.android.exoplayer2.analytics.PlayerId;
|
||||
import com.google.android.exoplayer2.drm.DrmInitData.SchemeData;
|
||||
import com.google.android.exoplayer2.drm.DrmSession.DrmSessionException;
|
||||
import com.google.android.exoplayer2.drm.ExoMediaDrm.OnEventListener;
|
||||
|
|
@ -59,7 +61,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
|||
* A {@link DrmSessionManager} that supports playbacks using {@link ExoMediaDrm}.
|
||||
*
|
||||
* <p>This implementation supports pre-acquisition of sessions using {@link
|
||||
* #preacquireSession(Looper, DrmSessionEventListener.EventDispatcher, Format)}.
|
||||
* #preacquireSession(DrmSessionEventListener.EventDispatcher, Format)}.
|
||||
*/
|
||||
@RequiresApi(18)
|
||||
public class DefaultDrmSessionManager implements DrmSessionManager {
|
||||
|
|
@ -302,6 +304,7 @@ public class DefaultDrmSessionManager implements DrmSessionManager {
|
|||
private @MonotonicNonNull Handler playbackHandler;
|
||||
private int mode;
|
||||
@Nullable private byte[] offlineLicenseKeySetId;
|
||||
private @MonotonicNonNull PlayerId playerId;
|
||||
|
||||
/* package */ volatile @Nullable MediaDrmHandler mediaDrmHandler;
|
||||
|
||||
|
|
@ -419,8 +422,8 @@ public class DefaultDrmSessionManager implements DrmSessionManager {
|
|||
|
||||
/**
|
||||
* Sets the mode, which determines the role of sessions acquired from the instance. This must be
|
||||
* called before {@link #acquireSession(Looper, DrmSessionEventListener.EventDispatcher, Format)}
|
||||
* is called.
|
||||
* called before {@link #acquireSession(DrmSessionEventListener.EventDispatcher, Format)} is
|
||||
* called.
|
||||
*
|
||||
* <p>By default, the mode is {@link #MODE_PLAYBACK} and a streaming license is requested when
|
||||
* required.
|
||||
|
|
@ -488,12 +491,16 @@ public class DefaultDrmSessionManager implements DrmSessionManager {
|
|||
}
|
||||
|
||||
@Override
|
||||
public DrmSessionReference preacquireSession(
|
||||
Looper playbackLooper,
|
||||
@Nullable DrmSessionEventListener.EventDispatcher eventDispatcher,
|
||||
Format format) {
|
||||
checkState(prepareCallsCount > 0);
|
||||
public void setPlayer(Looper playbackLooper, PlayerId playerId) {
|
||||
initPlaybackLooper(playbackLooper);
|
||||
this.playerId = playerId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DrmSessionReference preacquireSession(
|
||||
@Nullable DrmSessionEventListener.EventDispatcher eventDispatcher, Format format) {
|
||||
checkState(prepareCallsCount > 0);
|
||||
checkStateNotNull(playbackLooper);
|
||||
PreacquiredSessionReference preacquiredSessionReference =
|
||||
new PreacquiredSessionReference(eventDispatcher);
|
||||
preacquiredSessionReference.acquire(format);
|
||||
|
|
@ -503,11 +510,9 @@ public class DefaultDrmSessionManager implements DrmSessionManager {
|
|||
@Override
|
||||
@Nullable
|
||||
public DrmSession acquireSession(
|
||||
Looper playbackLooper,
|
||||
@Nullable DrmSessionEventListener.EventDispatcher eventDispatcher,
|
||||
Format format) {
|
||||
@Nullable DrmSessionEventListener.EventDispatcher eventDispatcher, Format format) {
|
||||
checkState(prepareCallsCount > 0);
|
||||
initPlaybackLooper(playbackLooper);
|
||||
checkStateNotNull(playbackLooper);
|
||||
return acquireSession(
|
||||
playbackLooper,
|
||||
eventDispatcher,
|
||||
|
|
@ -774,7 +779,8 @@ public class DefaultDrmSessionManager implements DrmSessionManager {
|
|||
keyRequestParameters,
|
||||
callback,
|
||||
checkNotNull(playbackLooper),
|
||||
loadErrorHandlingPolicy);
|
||||
loadErrorHandlingPolicy,
|
||||
checkNotNull(playerId));
|
||||
// Acquire the session once on behalf of the caller to DrmSessionManager - this is the
|
||||
// reference 'assigned' to the caller which they're responsible for releasing. Do this first,
|
||||
// to ensure that eventDispatcher receives all events related to the initial
|
||||
|
|
@ -977,7 +983,7 @@ public class DefaultDrmSessionManager implements DrmSessionManager {
|
|||
* Constructs an instance.
|
||||
*
|
||||
* @param eventDispatcher The {@link DrmSessionEventListener.EventDispatcher} passed to {@link
|
||||
* #acquireSession(Looper, DrmSessionEventListener.EventDispatcher, Format)}.
|
||||
* #acquireSession(DrmSessionEventListener.EventDispatcher, Format)}.
|
||||
*/
|
||||
public PreacquiredSessionReference(
|
||||
@Nullable DrmSessionEventListener.EventDispatcher eventDispatcher) {
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ import androidx.annotation.Nullable;
|
|||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.Format;
|
||||
import com.google.android.exoplayer2.PlaybackException;
|
||||
import com.google.android.exoplayer2.analytics.PlayerId;
|
||||
|
||||
/** Manages a DRM session. */
|
||||
public interface DrmSessionManager {
|
||||
|
|
@ -45,12 +46,13 @@ public interface DrmSessionManager {
|
|||
DrmSessionManager DRM_UNSUPPORTED =
|
||||
new DrmSessionManager() {
|
||||
|
||||
@Override
|
||||
public void setPlayer(Looper playbackLooper, PlayerId playerId) {}
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public DrmSession acquireSession(
|
||||
Looper playbackLooper,
|
||||
@Nullable DrmSessionEventListener.EventDispatcher eventDispatcher,
|
||||
Format format) {
|
||||
@Nullable DrmSessionEventListener.EventDispatcher eventDispatcher, Format format) {
|
||||
if (format.drmInitData == null) {
|
||||
return null;
|
||||
} else {
|
||||
|
|
@ -100,25 +102,33 @@ public interface DrmSessionManager {
|
|||
// Do nothing.
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets information about the player using this DRM session manager.
|
||||
*
|
||||
* @param playbackLooper The {@link Looper} associated with the player's playback thread.
|
||||
* @param playerId The {@link PlayerId} of the player.
|
||||
*/
|
||||
void setPlayer(Looper playbackLooper, PlayerId playerId);
|
||||
|
||||
/**
|
||||
* Pre-acquires a DRM session for the specified {@link Format}.
|
||||
*
|
||||
* <p>This notifies the manager that a subsequent call to {@link #acquireSession(Looper,
|
||||
* <p>This notifies the manager that a subsequent call to {@link #acquireSession(
|
||||
* DrmSessionEventListener.EventDispatcher, Format)} with the same {@link Format} is likely,
|
||||
* allowing a manager that supports pre-acquisition to get the required {@link DrmSession} ready
|
||||
* in the background.
|
||||
*
|
||||
* <p>The caller must call {@link DrmSessionReference#release()} on the returned instance when
|
||||
* they no longer require the pre-acquisition (i.e. they know they won't be making a matching call
|
||||
* to {@link #acquireSession(Looper, DrmSessionEventListener.EventDispatcher, Format)} in the near
|
||||
* to {@link #acquireSession(DrmSessionEventListener.EventDispatcher, Format)} in the near
|
||||
* future).
|
||||
*
|
||||
* <p>This manager may silently release the underlying session in order to allow another operation
|
||||
* to complete. This will result in a subsequent call to {@link #acquireSession(Looper,
|
||||
* to complete. This will result in a subsequent call to {@link #acquireSession(
|
||||
* DrmSessionEventListener.EventDispatcher, Format)} re-initializing a new session, including
|
||||
* repeating key loads and other async initialization steps.
|
||||
*
|
||||
* <p>The caller must separately call {@link #acquireSession(Looper,
|
||||
* <p>The caller must separately call {@link #acquireSession(
|
||||
* DrmSessionEventListener.EventDispatcher, Format)} in order to obtain a session suitable for
|
||||
* playback. The pre-acquired {@link DrmSessionReference} and full {@link DrmSession} instances
|
||||
* are distinct. The caller must release both, and can release the {@link DrmSessionReference}
|
||||
|
|
@ -129,19 +139,15 @@ public interface DrmSessionManager {
|
|||
* <p>Implementations that do not support pre-acquisition always return an empty {@link
|
||||
* DrmSessionReference} instance.
|
||||
*
|
||||
* @param playbackLooper The looper associated with the media playback thread.
|
||||
* @param eventDispatcher The {@link DrmSessionEventListener.EventDispatcher} used to distribute
|
||||
* events, and passed on to {@link
|
||||
* DrmSession#acquire(DrmSessionEventListener.EventDispatcher)}.
|
||||
* @param format The {@link Format} for which to pre-acquire a {@link DrmSession}.
|
||||
* @return A releaser for the pre-acquired session. Guaranteed to be non-null even if the matching
|
||||
* {@link #acquireSession(Looper, DrmSessionEventListener.EventDispatcher, Format)} would
|
||||
* return null.
|
||||
* {@link #acquireSession(DrmSessionEventListener.EventDispatcher, Format)} would return null.
|
||||
*/
|
||||
default DrmSessionReference preacquireSession(
|
||||
Looper playbackLooper,
|
||||
@Nullable DrmSessionEventListener.EventDispatcher eventDispatcher,
|
||||
Format format) {
|
||||
@Nullable DrmSessionEventListener.EventDispatcher eventDispatcher, Format format) {
|
||||
return DrmSessionReference.EMPTY;
|
||||
}
|
||||
|
||||
|
|
@ -158,7 +164,6 @@ public interface DrmSessionManager {
|
|||
* used to configure secure decoders for playback of clear content periods, which can reduce the
|
||||
* cost of transitioning between clear and encrypted content.
|
||||
*
|
||||
* @param playbackLooper The looper associated with the media playback thread.
|
||||
* @param eventDispatcher The {@link DrmSessionEventListener.EventDispatcher} used to distribute
|
||||
* events, and passed on to {@link
|
||||
* DrmSession#acquire(DrmSessionEventListener.EventDispatcher)}.
|
||||
|
|
@ -167,9 +172,7 @@ public interface DrmSessionManager {
|
|||
*/
|
||||
@Nullable
|
||||
DrmSession acquireSession(
|
||||
Looper playbackLooper,
|
||||
@Nullable DrmSessionEventListener.EventDispatcher eventDispatcher,
|
||||
Format format);
|
||||
@Nullable DrmSessionEventListener.EventDispatcher eventDispatcher, Format format);
|
||||
|
||||
/**
|
||||
* Returns the {@link C.CryptoType} that the DRM session manager will use for a given {@link
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ import android.os.PersistableBundle;
|
|||
import androidx.annotation.IntDef;
|
||||
import androidx.annotation.Nullable;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.analytics.PlayerId;
|
||||
import com.google.android.exoplayer2.decoder.CryptoConfig;
|
||||
import com.google.android.exoplayer2.drm.DrmInitData.SchemeData;
|
||||
import java.lang.annotation.Documented;
|
||||
|
|
@ -395,6 +396,14 @@ public interface ExoMediaDrm {
|
|||
*/
|
||||
void closeSession(byte[] sessionId);
|
||||
|
||||
/**
|
||||
* Sets the {@link PlayerId} of the player using a session.
|
||||
*
|
||||
* @param sessionId The ID of the session.
|
||||
* @param playerId The {@link PlayerId} of the player using the session.
|
||||
*/
|
||||
default void setPlayerIdForSession(byte[] sessionId, PlayerId playerId) {}
|
||||
|
||||
/**
|
||||
* Generates a key request.
|
||||
*
|
||||
|
|
|
|||
|
|
@ -15,6 +15,8 @@
|
|||
*/
|
||||
package com.google.android.exoplayer2.drm;
|
||||
|
||||
import static com.google.android.exoplayer2.util.Assertions.checkNotNull;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.media.DeniedByServerException;
|
||||
import android.media.MediaCrypto;
|
||||
|
|
@ -23,12 +25,14 @@ import android.media.MediaDrm;
|
|||
import android.media.MediaDrmException;
|
||||
import android.media.NotProvisionedException;
|
||||
import android.media.UnsupportedSchemeException;
|
||||
import android.media.metrics.LogSessionId;
|
||||
import android.os.PersistableBundle;
|
||||
import android.text.TextUtils;
|
||||
import androidx.annotation.DoNotInline;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.RequiresApi;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.analytics.PlayerId;
|
||||
import com.google.android.exoplayer2.drm.DrmInitData.SchemeData;
|
||||
import com.google.android.exoplayer2.extractor.mp4.PsshAtomUtil;
|
||||
import com.google.android.exoplayer2.util.Assertions;
|
||||
|
|
@ -182,6 +186,13 @@ public final class FrameworkMediaDrm implements ExoMediaDrm {
|
|||
mediaDrm.closeSession(sessionId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setPlayerIdForSession(byte[] sessionId, PlayerId playerId) {
|
||||
if (Util.SDK_INT >= 31) {
|
||||
Api31.setLogSessionIdOnMediaDrmSession(mediaDrm, sessionId, playerId);
|
||||
}
|
||||
}
|
||||
|
||||
// Return values of MediaDrm.KeyRequest.getRequestType are equal to KeyRequest.RequestType.
|
||||
@SuppressLint("WrongConstant")
|
||||
@Override
|
||||
|
|
@ -504,9 +515,22 @@ public final class FrameworkMediaDrm implements ExoMediaDrm {
|
|||
|
||||
@RequiresApi(31)
|
||||
private static class Api31 {
|
||||
private Api31() {}
|
||||
|
||||
@DoNotInline
|
||||
public static boolean requiresSecureDecoder(MediaDrm mediaDrm, String mimeType) {
|
||||
return mediaDrm.requiresSecureDecoder(mimeType);
|
||||
}
|
||||
|
||||
@DoNotInline
|
||||
public static void setLogSessionIdOnMediaDrmSession(
|
||||
MediaDrm mediaDrm, byte[] drmSessionId, PlayerId playerId) {
|
||||
LogSessionId logSessionId = playerId.getLogSessionId();
|
||||
if (!logSessionId.equals(LogSessionId.LOG_SESSION_ID_NONE)) {
|
||||
MediaDrm.PlaybackComponent playbackComponent =
|
||||
checkNotNull(mediaDrm.getPlaybackComponent(drmSessionId));
|
||||
playbackComponent.setLogSessionId(logSessionId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ import android.util.Pair;
|
|||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.RequiresApi;
|
||||
import com.google.android.exoplayer2.Format;
|
||||
import com.google.android.exoplayer2.analytics.PlayerId;
|
||||
import com.google.android.exoplayer2.drm.DefaultDrmSessionManager.Mode;
|
||||
import com.google.android.exoplayer2.drm.DrmSession.DrmSessionException;
|
||||
import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId;
|
||||
|
|
@ -235,6 +236,7 @@ public final class OfflineLicenseHelper {
|
|||
public synchronized Pair<Long, Long> getLicenseDurationRemainingSec(byte[] offlineLicenseKeySetId)
|
||||
throws DrmSessionException {
|
||||
Assertions.checkNotNull(offlineLicenseKeySetId);
|
||||
drmSessionManager.setPlayer(handlerThread.getLooper(), PlayerId.UNSET);
|
||||
drmSessionManager.prepare();
|
||||
DrmSession drmSession =
|
||||
openBlockingKeyRequest(
|
||||
|
|
@ -263,6 +265,7 @@ public final class OfflineLicenseHelper {
|
|||
private byte[] blockingKeyRequest(
|
||||
@Mode int licenseMode, @Nullable byte[] offlineLicenseKeySetId, Format format)
|
||||
throws DrmSessionException {
|
||||
drmSessionManager.setPlayer(handlerThread.getLooper(), PlayerId.UNSET);
|
||||
drmSessionManager.prepare();
|
||||
DrmSession drmSession = openBlockingKeyRequest(licenseMode, offlineLicenseKeySetId, format);
|
||||
DrmSessionException error = drmSession.getError();
|
||||
|
|
@ -280,8 +283,7 @@ public final class OfflineLicenseHelper {
|
|||
Assertions.checkNotNull(format.drmInitData);
|
||||
drmSessionManager.setMode(licenseMode, offlineLicenseKeySetId);
|
||||
conditionVariable.close();
|
||||
DrmSession drmSession =
|
||||
drmSessionManager.acquireSession(handlerThread.getLooper(), eventDispatcher, format);
|
||||
DrmSession drmSession = drmSessionManager.acquireSession(eventDispatcher, format);
|
||||
// Block current thread until key loading is finished
|
||||
conditionVariable.block();
|
||||
return Assertions.checkNotNull(drmSession);
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ import android.media.MediaFormat;
|
|||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.os.HandlerThread;
|
||||
import android.os.PersistableBundle;
|
||||
import android.view.Surface;
|
||||
import androidx.annotation.IntDef;
|
||||
import androidx.annotation.Nullable;
|
||||
|
|
@ -306,6 +307,13 @@ import java.nio.ByteBuffer;
|
|||
codec.signalEndOfInputStream();
|
||||
}
|
||||
|
||||
@Override
|
||||
@RequiresApi(26)
|
||||
public PersistableBundle getMetrics() {
|
||||
maybeBlockOnQueueing();
|
||||
return codec.getMetrics();
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
/* package */ void onError(MediaCodec.CodecException error) {
|
||||
asynchronousMediaCodecCallback.onError(codec, error);
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ public final class DefaultMediaCodecAdapterFactory implements MediaCodecAdapter.
|
|||
private static final int MODE_ENABLED = 1;
|
||||
private static final int MODE_DISABLED = 2;
|
||||
|
||||
private static final String TAG = "DefaultMediaCodecAdapterFactory";
|
||||
private static final String TAG = "DMCodecAdapterFactory";
|
||||
|
||||
@Mode private int asynchronousMode;
|
||||
private boolean enableSynchronizeCodecInteractionsWithQueueing;
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ import android.media.MediaCrypto;
|
|||
import android.media.MediaFormat;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.os.PersistableBundle;
|
||||
import android.view.Surface;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.RequiresApi;
|
||||
|
|
@ -336,4 +337,12 @@ public interface MediaCodecAdapter {
|
|||
*/
|
||||
@RequiresApi(18)
|
||||
void signalEndOfInputStream();
|
||||
|
||||
/**
|
||||
* Returns metrics data about the current codec instance.
|
||||
*
|
||||
* @see MediaCodec#getMetrics()
|
||||
*/
|
||||
@RequiresApi(26)
|
||||
PersistableBundle getMetrics();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -37,10 +37,12 @@ import android.media.MediaCodec.CryptoException;
|
|||
import android.media.MediaCrypto;
|
||||
import android.media.MediaCryptoException;
|
||||
import android.media.MediaFormat;
|
||||
import android.media.metrics.LogSessionId;
|
||||
import android.os.Bundle;
|
||||
import android.os.SystemClock;
|
||||
import androidx.annotation.CallSuper;
|
||||
import androidx.annotation.CheckResult;
|
||||
import androidx.annotation.DoNotInline;
|
||||
import androidx.annotation.IntDef;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.RequiresApi;
|
||||
|
|
@ -50,6 +52,7 @@ import com.google.android.exoplayer2.ExoPlaybackException;
|
|||
import com.google.android.exoplayer2.Format;
|
||||
import com.google.android.exoplayer2.FormatHolder;
|
||||
import com.google.android.exoplayer2.PlaybackException;
|
||||
import com.google.android.exoplayer2.analytics.PlayerId;
|
||||
import com.google.android.exoplayer2.decoder.CryptoConfig;
|
||||
import com.google.android.exoplayer2.decoder.DecoderCounters;
|
||||
import com.google.android.exoplayer2.decoder.DecoderInputBuffer;
|
||||
|
|
@ -973,13 +976,27 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
|
|||
DecoderInitializationException.NO_SUITABLE_DECODER_ERROR);
|
||||
}
|
||||
|
||||
MediaCodecInfo preferredCodecInfo = availableCodecInfos.peekFirst();
|
||||
while (codec == null) {
|
||||
MediaCodecInfo codecInfo = availableCodecInfos.peekFirst();
|
||||
if (!shouldInitCodec(codecInfo)) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
initCodec(codecInfo, crypto);
|
||||
try {
|
||||
initCodec(codecInfo, crypto);
|
||||
} catch (Exception e) {
|
||||
if (codecInfo == preferredCodecInfo) {
|
||||
// If creating the preferred decoder failed then sleep briefly before retrying.
|
||||
// Workaround for [internal b/191966399].
|
||||
// See also https://github.com/google/ExoPlayer/issues/8696.
|
||||
Log.w(TAG, "Preferred decoder instantiation failed. Sleeping for 50ms then retrying.");
|
||||
Thread.sleep(/* millis= */ 50);
|
||||
initCodec(codecInfo, crypto);
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.w(TAG, "Failed to initialize decoder: " + codecInfo, e);
|
||||
// This codec failed to initialize, so fall back to the next codec in the list (if any). We
|
||||
|
|
@ -1057,10 +1074,17 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
|
|||
codecOperatingRate = CODEC_OPERATING_RATE_UNSET;
|
||||
}
|
||||
codecInitializingTimestamp = SystemClock.elapsedRealtime();
|
||||
TraceUtil.beginSection("createCodec:" + codecName);
|
||||
MediaCodecAdapter.Configuration configuration =
|
||||
getMediaCodecConfiguration(codecInfo, inputFormat, crypto, codecOperatingRate);
|
||||
codec = codecAdapterFactory.createAdapter(configuration);
|
||||
if (Util.SDK_INT >= 31) {
|
||||
Api31.setLogSessionIdToMediaCodecFormat(configuration, getPlayerId());
|
||||
}
|
||||
try {
|
||||
TraceUtil.beginSection("createCodec:" + codecName);
|
||||
codec = codecAdapterFactory.createAdapter(configuration);
|
||||
} finally {
|
||||
TraceUtil.endSection();
|
||||
}
|
||||
codecInitializedTimestamp = SystemClock.elapsedRealtime();
|
||||
|
||||
this.codecInfo = codecInfo;
|
||||
|
|
@ -1318,7 +1342,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
|
|||
resetInputBuffer();
|
||||
codecReceivedBuffers = true;
|
||||
codecReconfigurationState = RECONFIGURATION_STATE_NONE;
|
||||
decoderCounters.inputBufferCount++;
|
||||
decoderCounters.queuedInputBufferCount++;
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
@ -2421,4 +2445,18 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
|
|||
&& format.channelCount == 1
|
||||
&& "OMX.MTK.AUDIO.DECODER.MP3".equals(name);
|
||||
}
|
||||
|
||||
@RequiresApi(31)
|
||||
private static final class Api31 {
|
||||
private Api31() {}
|
||||
|
||||
@DoNotInline
|
||||
public static void setLogSessionIdToMediaCodecFormat(
|
||||
MediaCodecAdapter.Configuration codecConfiguration, PlayerId playerId) {
|
||||
LogSessionId logSessionId = playerId.getLogSessionId();
|
||||
if (!logSessionId.equals(LogSessionId.LOG_SESSION_ID_NONE)) {
|
||||
codecConfiguration.mediaFormat.setString("log-session-id", logSessionId.getStringId());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ import androidx.annotation.CheckResult;
|
|||
import androidx.annotation.GuardedBy;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.RequiresApi;
|
||||
import androidx.annotation.VisibleForTesting;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.Format;
|
||||
import com.google.android.exoplayer2.util.Log;
|
||||
|
|
@ -105,11 +106,8 @@ public final class MediaCodecUtil {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the codec cache.
|
||||
*
|
||||
* <p>This method should only be called in tests.
|
||||
*/
|
||||
/* Clears the codec cache.*/
|
||||
@VisibleForTesting
|
||||
public static synchronized void clearDecoderInfoCache() {
|
||||
decoderInfosCache.clear();
|
||||
}
|
||||
|
|
@ -322,8 +320,8 @@ public final class MediaCodecUtil {
|
|||
if ((!key.secure && secureRequired) || (key.secure && !secureSupported)) {
|
||||
continue;
|
||||
}
|
||||
boolean hardwareAccelerated = isHardwareAccelerated(codecInfo);
|
||||
boolean softwareOnly = isSoftwareOnly(codecInfo);
|
||||
boolean hardwareAccelerated = isHardwareAccelerated(codecInfo, mimeType);
|
||||
boolean softwareOnly = isSoftwareOnly(codecInfo, mimeType);
|
||||
boolean vendor = isVendor(codecInfo);
|
||||
if ((secureDecodersExplicit && key.secure == secureSupported)
|
||||
|| (!secureDecodersExplicit && !key.secure)) {
|
||||
|
|
@ -514,8 +512,10 @@ public final class MediaCodecUtil {
|
|||
return false;
|
||||
}
|
||||
|
||||
// MTK E-AC3 decoder doesn't support decoding JOC streams in 2-D. See [Internal: b/69400041].
|
||||
if (MimeTypes.AUDIO_E_AC3_JOC.equals(mimeType) && "OMX.MTK.AUDIO.DECODER.DSPAC3".equals(name)) {
|
||||
// MTK AC3 decoder doesn't support decoding JOC streams in 2-D. See [Internal: b/69400041].
|
||||
if (Util.SDK_INT <= 23
|
||||
&& MimeTypes.AUDIO_E_AC3_JOC.equals(mimeType)
|
||||
&& "OMX.MTK.AUDIO.DECODER.DSPAC3".equals(name)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
@ -603,13 +603,14 @@ public final class MediaCodecUtil {
|
|||
* The result of {@link android.media.MediaCodecInfo#isHardwareAccelerated()} for API levels 29+,
|
||||
* or a best-effort approximation for lower levels.
|
||||
*/
|
||||
private static boolean isHardwareAccelerated(android.media.MediaCodecInfo codecInfo) {
|
||||
private static boolean isHardwareAccelerated(
|
||||
android.media.MediaCodecInfo codecInfo, String mimeType) {
|
||||
if (Util.SDK_INT >= 29) {
|
||||
return isHardwareAcceleratedV29(codecInfo);
|
||||
}
|
||||
// codecInfo.isHardwareAccelerated() != codecInfo.isSoftwareOnly() is not necessarily true.
|
||||
// However, we assume this to be true as an approximation.
|
||||
return !isSoftwareOnly(codecInfo);
|
||||
return !isSoftwareOnly(codecInfo, mimeType);
|
||||
}
|
||||
|
||||
@RequiresApi(29)
|
||||
|
|
@ -621,12 +622,17 @@ public final class MediaCodecUtil {
|
|||
* The result of {@link android.media.MediaCodecInfo#isSoftwareOnly()} for API levels 29+, or a
|
||||
* best-effort approximation for lower levels.
|
||||
*/
|
||||
private static boolean isSoftwareOnly(android.media.MediaCodecInfo codecInfo) {
|
||||
private static boolean isSoftwareOnly(android.media.MediaCodecInfo codecInfo, String mimeType) {
|
||||
if (Util.SDK_INT >= 29) {
|
||||
return isSoftwareOnlyV29(codecInfo);
|
||||
}
|
||||
if (MimeTypes.isAudio(mimeType)) {
|
||||
// Assume audio decoders are software only.
|
||||
return true;
|
||||
}
|
||||
String codecName = Ascii.toLowerCase(codecInfo.getName());
|
||||
if (codecName.startsWith("arc.")) { // App Runtime for Chrome (ARC) codecs
|
||||
if (codecName.startsWith("arc.")) {
|
||||
// App Runtime for Chrome (ARC) codecs
|
||||
return false;
|
||||
}
|
||||
return codecName.startsWith("omx.google.")
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ import android.media.MediaCodec;
|
|||
import android.media.MediaFormat;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.os.PersistableBundle;
|
||||
import android.view.Surface;
|
||||
import androidx.annotation.DoNotInline;
|
||||
import androidx.annotation.Nullable;
|
||||
|
|
@ -37,7 +38,7 @@ import java.nio.ByteBuffer;
|
|||
/**
|
||||
* A {@link MediaCodecAdapter} that operates the underlying {@link MediaCodec} in synchronous mode.
|
||||
*/
|
||||
public class SynchronousMediaCodecAdapter implements MediaCodecAdapter {
|
||||
public final class SynchronousMediaCodecAdapter implements MediaCodecAdapter {
|
||||
|
||||
/** A factory for {@link SynchronousMediaCodecAdapter} instances. */
|
||||
public static class Factory implements MediaCodecAdapter.Factory {
|
||||
|
|
@ -231,8 +232,16 @@ public class SynchronousMediaCodecAdapter implements MediaCodecAdapter {
|
|||
codec.setVideoScalingMode(scalingMode);
|
||||
}
|
||||
|
||||
@Override
|
||||
@RequiresApi(26)
|
||||
public PersistableBundle getMetrics() {
|
||||
return codec.getMetrics();
|
||||
}
|
||||
|
||||
@RequiresApi(18)
|
||||
private static final class Api18 {
|
||||
private Api18() {}
|
||||
|
||||
@DoNotInline
|
||||
public static Surface createCodecInputSurface(MediaCodec codec) {
|
||||
return codec.createInputSurface();
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ import com.google.android.exoplayer2.Renderer;
|
|||
import com.google.android.exoplayer2.RendererCapabilities;
|
||||
import com.google.android.exoplayer2.RenderersFactory;
|
||||
import com.google.android.exoplayer2.Timeline;
|
||||
import com.google.android.exoplayer2.analytics.PlayerId;
|
||||
import com.google.android.exoplayer2.audio.AudioRendererEventListener;
|
||||
import com.google.android.exoplayer2.drm.DrmSessionManager;
|
||||
import com.google.android.exoplayer2.extractor.ExtractorsFactory;
|
||||
|
|
@ -831,8 +832,6 @@ public final class DownloadHelper {
|
|||
* Runs the track selection for a given period index with the current parameters. The selected
|
||||
* tracks will be added to {@link #trackSelectionsByPeriodAndRenderer}.
|
||||
*/
|
||||
// Intentional reference comparison of track group instances.
|
||||
@SuppressWarnings("ReferenceEquality")
|
||||
@RequiresNonNull({
|
||||
"trackGroupArrays",
|
||||
"trackSelectionsByPeriodAndRenderer",
|
||||
|
|
@ -857,7 +856,7 @@ public final class DownloadHelper {
|
|||
boolean mergedWithExistingSelection = false;
|
||||
for (int j = 0; j < existingSelectionList.size(); j++) {
|
||||
ExoTrackSelection existingSelection = existingSelectionList.get(j);
|
||||
if (existingSelection.getTrackGroup() == newSelection.getTrackGroup()) {
|
||||
if (existingSelection.getTrackGroup().equals(newSelection.getTrackGroup())) {
|
||||
// Merge with existing selection.
|
||||
scratchSet.clear();
|
||||
for (int k = 0; k < existingSelection.length(); k++) {
|
||||
|
|
@ -891,8 +890,10 @@ public final class DownloadHelper {
|
|||
MediaItem mediaItem,
|
||||
DataSource.Factory dataSourceFactory,
|
||||
@Nullable DrmSessionManager drmSessionManager) {
|
||||
return new DefaultMediaSourceFactory(dataSourceFactory, ExtractorsFactory.EMPTY)
|
||||
.setDrmSessionManager(drmSessionManager)
|
||||
return new DefaultMediaSourceFactory(
|
||||
dataSourceFactory, ExtractorsFactory.EMPTY, /* serverSideDaiMediaSourceFactory= */ null)
|
||||
.setDrmSessionManagerProvider(
|
||||
drmSessionManager != null ? unusedMediaItem -> drmSessionManager : null)
|
||||
.createMediaSource(mediaItem);
|
||||
}
|
||||
|
||||
|
|
@ -955,7 +956,8 @@ public final class DownloadHelper {
|
|||
public boolean handleMessage(Message msg) {
|
||||
switch (msg.what) {
|
||||
case MESSAGE_PREPARE_SOURCE:
|
||||
mediaSource.prepareSource(/* caller= */ this, /* mediaTransferListener= */ null);
|
||||
mediaSource.prepareSource(
|
||||
/* caller= */ this, /* mediaTransferListener= */ null, PlayerId.UNSET);
|
||||
mediaSourceHandler.sendEmptyMessage(MESSAGE_CHECK_FOR_FAILURE);
|
||||
return true;
|
||||
case MESSAGE_CHECK_FOR_FAILURE:
|
||||
|
|
|
|||
|
|
@ -15,10 +15,13 @@
|
|||
*/
|
||||
package com.google.android.exoplayer2.source;
|
||||
|
||||
import static com.google.android.exoplayer2.util.Assertions.checkStateNotNull;
|
||||
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import androidx.annotation.Nullable;
|
||||
import com.google.android.exoplayer2.Timeline;
|
||||
import com.google.android.exoplayer2.analytics.PlayerId;
|
||||
import com.google.android.exoplayer2.drm.DrmSessionEventListener;
|
||||
import com.google.android.exoplayer2.upstream.TransferListener;
|
||||
import com.google.android.exoplayer2.util.Assertions;
|
||||
|
|
@ -41,6 +44,7 @@ public abstract class BaseMediaSource implements MediaSource {
|
|||
|
||||
@Nullable private Looper looper;
|
||||
@Nullable private Timeline timeline;
|
||||
@Nullable private PlayerId playerId;
|
||||
|
||||
public BaseMediaSource() {
|
||||
mediaSourceCallers = new ArrayList<>(/* initialCapacity= */ 1);
|
||||
|
|
@ -51,7 +55,7 @@ public abstract class BaseMediaSource implements MediaSource {
|
|||
|
||||
/**
|
||||
* Starts source preparation and enables the source, see {@link #prepareSource(MediaSourceCaller,
|
||||
* TransferListener)}. This method is called at most once until the next call to {@link
|
||||
* TransferListener, PlayerId)}. This method is called at most once until the next call to {@link
|
||||
* #releaseSourceInternal()}.
|
||||
*
|
||||
* @param mediaTransferListener The transfer listener which should be informed of any media data
|
||||
|
|
@ -160,6 +164,16 @@ public abstract class BaseMediaSource implements MediaSource {
|
|||
return !enabledMediaSourceCallers.isEmpty();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the {@link PlayerId} of the player using this media source.
|
||||
*
|
||||
* <p>Must only be used when the media source is {@link #prepareSourceInternal(TransferListener)
|
||||
* prepared}.
|
||||
*/
|
||||
protected final PlayerId getPlayerId() {
|
||||
return checkStateNotNull(playerId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public final void addEventListener(Handler handler, MediaSourceEventListener eventListener) {
|
||||
Assertions.checkNotNull(handler);
|
||||
|
|
@ -186,9 +200,12 @@ public abstract class BaseMediaSource implements MediaSource {
|
|||
|
||||
@Override
|
||||
public final void prepareSource(
|
||||
MediaSourceCaller caller, @Nullable TransferListener mediaTransferListener) {
|
||||
MediaSourceCaller caller,
|
||||
@Nullable TransferListener mediaTransferListener,
|
||||
PlayerId playerId) {
|
||||
Looper looper = Looper.myLooper();
|
||||
Assertions.checkArgument(this.looper == null || this.looper == looper);
|
||||
this.playerId = playerId;
|
||||
@Nullable Timeline timeline = this.timeline;
|
||||
mediaSourceCallers.add(caller);
|
||||
if (this.looper == null) {
|
||||
|
|
@ -226,6 +243,7 @@ public abstract class BaseMediaSource implements MediaSource {
|
|||
if (mediaSourceCallers.isEmpty()) {
|
||||
looper = null;
|
||||
timeline = null;
|
||||
playerId = null;
|
||||
enabledMediaSourceCallers.clear();
|
||||
releaseSourceInternal();
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -117,7 +117,7 @@ public abstract class CompositeMediaSource<T> extends BaseMediaSource {
|
|||
childSources.put(id, new MediaSourceAndListener<>(mediaSource, caller, eventListener));
|
||||
mediaSource.addEventListener(Assertions.checkNotNull(eventHandler), eventListener);
|
||||
mediaSource.addDrmEventListener(Assertions.checkNotNull(eventHandler), eventListener);
|
||||
mediaSource.prepareSource(caller, mediaTransferListener);
|
||||
mediaSource.prepareSource(caller, mediaTransferListener, getPlayerId());
|
||||
if (!isEnabled()) {
|
||||
mediaSource.disable(caller);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,7 +24,6 @@ import androidx.annotation.Nullable;
|
|||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.Format;
|
||||
import com.google.android.exoplayer2.MediaItem;
|
||||
import com.google.android.exoplayer2.drm.DrmSessionManager;
|
||||
import com.google.android.exoplayer2.drm.DrmSessionManagerProvider;
|
||||
import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory;
|
||||
import com.google.android.exoplayer2.extractor.Extractor;
|
||||
|
|
@ -34,7 +33,6 @@ import com.google.android.exoplayer2.extractor.ExtractorsFactory;
|
|||
import com.google.android.exoplayer2.extractor.PositionHolder;
|
||||
import com.google.android.exoplayer2.extractor.SeekMap;
|
||||
import com.google.android.exoplayer2.extractor.TrackOutput;
|
||||
import com.google.android.exoplayer2.offline.StreamKey;
|
||||
import com.google.android.exoplayer2.source.ads.AdsLoader;
|
||||
import com.google.android.exoplayer2.source.ads.AdsMediaSource;
|
||||
import com.google.android.exoplayer2.text.SubtitleDecoderFactory;
|
||||
|
|
@ -43,8 +41,8 @@ import com.google.android.exoplayer2.ui.AdViewProvider;
|
|||
import com.google.android.exoplayer2.upstream.DataSource;
|
||||
import com.google.android.exoplayer2.upstream.DataSpec;
|
||||
import com.google.android.exoplayer2.upstream.DefaultDataSource;
|
||||
import com.google.android.exoplayer2.upstream.HttpDataSource;
|
||||
import com.google.android.exoplayer2.upstream.LoadErrorHandlingPolicy;
|
||||
import com.google.android.exoplayer2.util.Assertions;
|
||||
import com.google.android.exoplayer2.util.Log;
|
||||
import com.google.android.exoplayer2.util.MimeTypes;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
|
|
@ -60,7 +58,7 @@ import java.util.Set;
|
|||
import org.checkerframework.checker.nullness.compatqual.NullableType;
|
||||
|
||||
/**
|
||||
* The default {@link MediaSourceFactory} implementation.
|
||||
* The default {@link MediaSource.Factory} implementation.
|
||||
*
|
||||
* <p>This implementation delegates calls to {@link #createMediaSource(MediaItem)} to the following
|
||||
* factories:
|
||||
|
|
@ -94,32 +92,20 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
|
|||
* configuration}, {@link #setAdsLoaderProvider} and {@link #setAdViewProvider} need to be called to
|
||||
* configure the factory with the required providers.
|
||||
*/
|
||||
@SuppressWarnings("deprecation") // Implement deprecated type for backwards compatibility.
|
||||
public final class DefaultMediaSourceFactory implements MediaSourceFactory {
|
||||
|
||||
/**
|
||||
* Provides {@link AdsLoader} instances for media items that have {@link
|
||||
* MediaItem.LocalConfiguration#adsConfiguration ad tag URIs}.
|
||||
*/
|
||||
public interface AdsLoaderProvider {
|
||||
/** @deprecated Use {@link AdsLoader.Provider} instead. */
|
||||
@Deprecated
|
||||
public interface AdsLoaderProvider extends AdsLoader.Provider {}
|
||||
|
||||
/**
|
||||
* Returns an {@link AdsLoader} for the given {@link
|
||||
* MediaItem.LocalConfiguration#adsConfiguration ads configuration}, or {@code null} if no ads
|
||||
* loader is available for the given ads configuration.
|
||||
*
|
||||
* <p>This method is called each time a {@link MediaSource} is created from a {@link MediaItem}
|
||||
* that defines an {@link MediaItem.LocalConfiguration#adsConfiguration ads configuration}.
|
||||
*/
|
||||
@Nullable
|
||||
AdsLoader getAdsLoader(MediaItem.AdsConfiguration adsConfiguration);
|
||||
}
|
||||
|
||||
private static final String TAG = "DefaultMediaSourceFactory";
|
||||
private static final String TAG = "DMediaSourceFactory";
|
||||
|
||||
private final DataSource.Factory dataSourceFactory;
|
||||
private final DelegateFactoryLoader delegateFactoryLoader;
|
||||
|
||||
@Nullable private AdsLoaderProvider adsLoaderProvider;
|
||||
@Nullable private final MediaSource.Factory serverSideDaiMediaSourceFactory;
|
||||
@Nullable private AdsLoader.Provider adsLoaderProvider;
|
||||
@Nullable private AdViewProvider adViewProvider;
|
||||
@Nullable private LoadErrorHandlingPolicy loadErrorHandlingPolicy;
|
||||
private long liveTargetOffsetMs;
|
||||
|
|
@ -146,7 +132,10 @@ public final class DefaultMediaSourceFactory implements MediaSourceFactory {
|
|||
* its container.
|
||||
*/
|
||||
public DefaultMediaSourceFactory(Context context, ExtractorsFactory extractorsFactory) {
|
||||
this(new DefaultDataSource.Factory(context), extractorsFactory);
|
||||
this(
|
||||
new DefaultDataSource.Factory(context),
|
||||
extractorsFactory,
|
||||
/* serverSideDaiMediaSourceFactory= */ null);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -156,7 +145,10 @@ public final class DefaultMediaSourceFactory implements MediaSourceFactory {
|
|||
* for requesting media data.
|
||||
*/
|
||||
public DefaultMediaSourceFactory(DataSource.Factory dataSourceFactory) {
|
||||
this(dataSourceFactory, new DefaultExtractorsFactory());
|
||||
this(
|
||||
dataSourceFactory,
|
||||
new DefaultExtractorsFactory(),
|
||||
/* serverSideDaiMediaSourceFactory= */ null);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -166,10 +158,17 @@ public final class DefaultMediaSourceFactory implements MediaSourceFactory {
|
|||
* for requesting media data.
|
||||
* @param extractorsFactory An {@link ExtractorsFactory} used to extract progressive media from
|
||||
* its container.
|
||||
* @param serverSideDaiMediaSourceFactory A {@link MediaSource.Factory} for creating server side
|
||||
* inserted ad media sources.
|
||||
*/
|
||||
public DefaultMediaSourceFactory(
|
||||
DataSource.Factory dataSourceFactory, ExtractorsFactory extractorsFactory) {
|
||||
DataSource.Factory dataSourceFactory,
|
||||
ExtractorsFactory extractorsFactory,
|
||||
@Nullable MediaSource.Factory serverSideDaiMediaSourceFactory) {
|
||||
this.dataSourceFactory = dataSourceFactory;
|
||||
// Temporary until factory registration is agreed upon.
|
||||
this.serverSideDaiMediaSourceFactory = serverSideDaiMediaSourceFactory;
|
||||
|
||||
delegateFactoryLoader = new DelegateFactoryLoader(dataSourceFactory, extractorsFactory);
|
||||
liveTargetOffsetMs = C.TIME_UNSET;
|
||||
liveMinOffsetMs = C.TIME_UNSET;
|
||||
|
|
@ -196,14 +195,14 @@ public final class DefaultMediaSourceFactory implements MediaSourceFactory {
|
|||
}
|
||||
|
||||
/**
|
||||
* Sets the {@link AdsLoaderProvider} that provides {@link AdsLoader} instances for media items
|
||||
* Sets the {@link AdsLoader.Provider} that provides {@link AdsLoader} instances for media items
|
||||
* that have {@link MediaItem.LocalConfiguration#adsConfiguration ads configurations}.
|
||||
*
|
||||
* @param adsLoaderProvider A provider for {@link AdsLoader} instances.
|
||||
* @return This factory, for convenience.
|
||||
*/
|
||||
public DefaultMediaSourceFactory setAdsLoaderProvider(
|
||||
@Nullable AdsLoaderProvider adsLoaderProvider) {
|
||||
@Nullable AdsLoader.Provider adsLoaderProvider) {
|
||||
this.adsLoaderProvider = adsLoaderProvider;
|
||||
return this;
|
||||
}
|
||||
|
|
@ -279,29 +278,6 @@ public final class DefaultMediaSourceFactory implements MediaSourceFactory {
|
|||
return this;
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
@Override
|
||||
public DefaultMediaSourceFactory setDrmHttpDataSourceFactory(
|
||||
@Nullable HttpDataSource.Factory drmHttpDataSourceFactory) {
|
||||
delegateFactoryLoader.setDrmHttpDataSourceFactory(drmHttpDataSourceFactory);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
@Override
|
||||
public DefaultMediaSourceFactory setDrmUserAgent(@Nullable String userAgent) {
|
||||
delegateFactoryLoader.setDrmUserAgent(userAgent);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
@Override
|
||||
public DefaultMediaSourceFactory setDrmSessionManager(
|
||||
@Nullable DrmSessionManager drmSessionManager) {
|
||||
delegateFactoryLoader.setDrmSessionManager(drmSessionManager);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DefaultMediaSourceFactory setDrmSessionManagerProvider(
|
||||
@Nullable DrmSessionManagerProvider drmSessionManagerProvider) {
|
||||
|
|
@ -317,18 +293,6 @@ public final class DefaultMediaSourceFactory implements MediaSourceFactory {
|
|||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use {@link MediaItem.Builder#setStreamKeys(List)} and {@link
|
||||
* #createMediaSource(MediaItem)} instead.
|
||||
*/
|
||||
@SuppressWarnings("deprecation") // Calling through to the same deprecated method.
|
||||
@Deprecated
|
||||
@Override
|
||||
public DefaultMediaSourceFactory setStreamKeys(@Nullable List<StreamKey> streamKeys) {
|
||||
delegateFactoryLoader.setStreamKeys(streamKeys);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int[] getSupportedTypes() {
|
||||
return delegateFactoryLoader.getSupportedTypes();
|
||||
|
|
@ -336,13 +300,17 @@ public final class DefaultMediaSourceFactory implements MediaSourceFactory {
|
|||
|
||||
@Override
|
||||
public MediaSource createMediaSource(MediaItem mediaItem) {
|
||||
checkNotNull(mediaItem.localConfiguration);
|
||||
Assertions.checkNotNull(mediaItem.localConfiguration);
|
||||
@Nullable String scheme = mediaItem.localConfiguration.uri.getScheme();
|
||||
if (scheme != null && scheme.equals("imadai")) {
|
||||
return checkNotNull(serverSideDaiMediaSourceFactory).createMediaSource(mediaItem);
|
||||
}
|
||||
@C.ContentType
|
||||
int type =
|
||||
Util.inferContentTypeForUriAndMimeType(
|
||||
mediaItem.localConfiguration.uri, mediaItem.localConfiguration.mimeType);
|
||||
@Nullable
|
||||
MediaSourceFactory mediaSourceFactory = delegateFactoryLoader.getMediaSourceFactory(type);
|
||||
MediaSource.Factory mediaSourceFactory = delegateFactoryLoader.getMediaSourceFactory(type);
|
||||
checkStateNotNull(
|
||||
mediaSourceFactory, "No suitable media source factory found for content type: " + type);
|
||||
|
||||
|
|
@ -385,6 +353,7 @@ public final class DefaultMediaSourceFactory implements MediaSourceFactory {
|
|||
.setSelectionFlags(subtitleConfigurations.get(i).selectionFlags)
|
||||
.setRoleFlags(subtitleConfigurations.get(i).roleFlags)
|
||||
.setLabel(subtitleConfigurations.get(i).label)
|
||||
.setId(subtitleConfigurations.get(i).id)
|
||||
.build();
|
||||
ExtractorsFactory extractorsFactory =
|
||||
() ->
|
||||
|
|
@ -437,7 +406,7 @@ public final class DefaultMediaSourceFactory implements MediaSourceFactory {
|
|||
if (adsConfiguration == null) {
|
||||
return mediaSource;
|
||||
}
|
||||
@Nullable AdsLoaderProvider adsLoaderProvider = this.adsLoaderProvider;
|
||||
@Nullable AdsLoader.Provider adsLoaderProvider = this.adsLoaderProvider;
|
||||
@Nullable AdViewProvider adViewProvider = this.adViewProvider;
|
||||
if (adsLoaderProvider == null || adViewProvider == null) {
|
||||
Log.w(
|
||||
|
|
@ -467,17 +436,13 @@ public final class DefaultMediaSourceFactory implements MediaSourceFactory {
|
|||
private static final class DelegateFactoryLoader {
|
||||
private final DataSource.Factory dataSourceFactory;
|
||||
private final ExtractorsFactory extractorsFactory;
|
||||
private final Map<Integer, @NullableType Supplier<MediaSourceFactory>>
|
||||
private final Map<Integer, @NullableType Supplier<MediaSource.Factory>>
|
||||
mediaSourceFactorySuppliers;
|
||||
private final Set<Integer> supportedTypes;
|
||||
private final Map<Integer, MediaSourceFactory> mediaSourceFactories;
|
||||
private final Map<Integer, MediaSource.Factory> mediaSourceFactories;
|
||||
|
||||
@Nullable private HttpDataSource.Factory drmHttpDataSourceFactory;
|
||||
@Nullable private String userAgent;
|
||||
@Nullable private DrmSessionManager drmSessionManager;
|
||||
@Nullable private DrmSessionManagerProvider drmSessionManagerProvider;
|
||||
@Nullable private LoadErrorHandlingPolicy loadErrorHandlingPolicy;
|
||||
@Nullable private List<StreamKey> streamKeys;
|
||||
|
||||
public DelegateFactoryLoader(
|
||||
DataSource.Factory dataSourceFactory, ExtractorsFactory extractorsFactory) {
|
||||
|
|
@ -496,69 +461,32 @@ public final class DefaultMediaSourceFactory implements MediaSourceFactory {
|
|||
|
||||
@SuppressWarnings("deprecation") // Forwarding to deprecated methods.
|
||||
@Nullable
|
||||
public MediaSourceFactory getMediaSourceFactory(@C.ContentType int contentType) {
|
||||
@Nullable MediaSourceFactory mediaSourceFactory = mediaSourceFactories.get(contentType);
|
||||
public MediaSource.Factory getMediaSourceFactory(@C.ContentType int contentType) {
|
||||
@Nullable MediaSource.Factory mediaSourceFactory = mediaSourceFactories.get(contentType);
|
||||
if (mediaSourceFactory != null) {
|
||||
return mediaSourceFactory;
|
||||
}
|
||||
@Nullable
|
||||
Supplier<MediaSourceFactory> mediaSourceFactorySupplier = maybeLoadSupplier(contentType);
|
||||
Supplier<MediaSource.Factory> mediaSourceFactorySupplier = maybeLoadSupplier(contentType);
|
||||
if (mediaSourceFactorySupplier == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
mediaSourceFactory = mediaSourceFactorySupplier.get();
|
||||
if (drmHttpDataSourceFactory != null) {
|
||||
mediaSourceFactory.setDrmHttpDataSourceFactory(drmHttpDataSourceFactory);
|
||||
}
|
||||
if (userAgent != null) {
|
||||
mediaSourceFactory.setDrmUserAgent(userAgent);
|
||||
}
|
||||
if (drmSessionManager != null) {
|
||||
mediaSourceFactory.setDrmSessionManager(drmSessionManager);
|
||||
}
|
||||
if (drmSessionManagerProvider != null) {
|
||||
mediaSourceFactory.setDrmSessionManagerProvider(drmSessionManagerProvider);
|
||||
}
|
||||
if (loadErrorHandlingPolicy != null) {
|
||||
mediaSourceFactory.setLoadErrorHandlingPolicy(loadErrorHandlingPolicy);
|
||||
}
|
||||
if (streamKeys != null) {
|
||||
mediaSourceFactory.setStreamKeys(streamKeys);
|
||||
}
|
||||
mediaSourceFactories.put(contentType, mediaSourceFactory);
|
||||
return mediaSourceFactory;
|
||||
}
|
||||
|
||||
@SuppressWarnings("deprecation") // Forwarding to deprecated method.
|
||||
public void setDrmHttpDataSourceFactory(
|
||||
@Nullable HttpDataSource.Factory drmHttpDataSourceFactory) {
|
||||
this.drmHttpDataSourceFactory = drmHttpDataSourceFactory;
|
||||
for (MediaSourceFactory mediaSourceFactory : mediaSourceFactories.values()) {
|
||||
mediaSourceFactory.setDrmHttpDataSourceFactory(drmHttpDataSourceFactory);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("deprecation") // Forwarding to deprecated method.
|
||||
public void setDrmUserAgent(@Nullable String userAgent) {
|
||||
this.userAgent = userAgent;
|
||||
for (MediaSourceFactory mediaSourceFactory : mediaSourceFactories.values()) {
|
||||
mediaSourceFactory.setDrmUserAgent(userAgent);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("deprecation") // Forwarding to deprecated method.
|
||||
public void setDrmSessionManager(@Nullable DrmSessionManager drmSessionManager) {
|
||||
this.drmSessionManager = drmSessionManager;
|
||||
for (MediaSourceFactory mediaSourceFactory : mediaSourceFactories.values()) {
|
||||
mediaSourceFactory.setDrmSessionManager(drmSessionManager);
|
||||
}
|
||||
}
|
||||
|
||||
public void setDrmSessionManagerProvider(
|
||||
@Nullable DrmSessionManagerProvider drmSessionManagerProvider) {
|
||||
this.drmSessionManagerProvider = drmSessionManagerProvider;
|
||||
for (MediaSourceFactory mediaSourceFactory : mediaSourceFactories.values()) {
|
||||
for (MediaSource.Factory mediaSourceFactory : mediaSourceFactories.values()) {
|
||||
mediaSourceFactory.setDrmSessionManagerProvider(drmSessionManagerProvider);
|
||||
}
|
||||
}
|
||||
|
|
@ -566,19 +494,11 @@ public final class DefaultMediaSourceFactory implements MediaSourceFactory {
|
|||
public void setLoadErrorHandlingPolicy(
|
||||
@Nullable LoadErrorHandlingPolicy loadErrorHandlingPolicy) {
|
||||
this.loadErrorHandlingPolicy = loadErrorHandlingPolicy;
|
||||
for (MediaSourceFactory mediaSourceFactory : mediaSourceFactories.values()) {
|
||||
for (MediaSource.Factory mediaSourceFactory : mediaSourceFactories.values()) {
|
||||
mediaSourceFactory.setLoadErrorHandlingPolicy(loadErrorHandlingPolicy);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("deprecation") // Forwarding to deprecated method.
|
||||
public void setStreamKeys(@Nullable List<StreamKey> streamKeys) {
|
||||
this.streamKeys = streamKeys;
|
||||
for (MediaSourceFactory mediaSourceFactory : mediaSourceFactories.values()) {
|
||||
mediaSourceFactory.setStreamKeys(streamKeys);
|
||||
}
|
||||
}
|
||||
|
||||
private void ensureAllSuppliersAreLoaded() {
|
||||
maybeLoadSupplier(C.TYPE_DASH);
|
||||
maybeLoadSupplier(C.TYPE_SS);
|
||||
|
|
@ -588,38 +508,38 @@ public final class DefaultMediaSourceFactory implements MediaSourceFactory {
|
|||
}
|
||||
|
||||
@Nullable
|
||||
private Supplier<MediaSourceFactory> maybeLoadSupplier(@C.ContentType int contentType) {
|
||||
private Supplier<MediaSource.Factory> maybeLoadSupplier(@C.ContentType int contentType) {
|
||||
if (mediaSourceFactorySuppliers.containsKey(contentType)) {
|
||||
return mediaSourceFactorySuppliers.get(contentType);
|
||||
}
|
||||
|
||||
@Nullable Supplier<MediaSourceFactory> mediaSourceFactorySupplier = null;
|
||||
@Nullable Supplier<MediaSource.Factory> mediaSourceFactorySupplier = null;
|
||||
try {
|
||||
Class<? extends MediaSourceFactory> clazz;
|
||||
Class<? extends MediaSource.Factory> clazz;
|
||||
switch (contentType) {
|
||||
case C.TYPE_DASH:
|
||||
clazz =
|
||||
Class.forName("com.google.android.exoplayer2.source.dash.DashMediaSource$Factory")
|
||||
.asSubclass(MediaSourceFactory.class);
|
||||
.asSubclass(MediaSource.Factory.class);
|
||||
mediaSourceFactorySupplier = () -> newInstance(clazz, dataSourceFactory);
|
||||
break;
|
||||
case C.TYPE_SS:
|
||||
clazz =
|
||||
Class.forName(
|
||||
"com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource$Factory")
|
||||
.asSubclass(MediaSourceFactory.class);
|
||||
.asSubclass(MediaSource.Factory.class);
|
||||
mediaSourceFactorySupplier = () -> newInstance(clazz, dataSourceFactory);
|
||||
break;
|
||||
case C.TYPE_HLS:
|
||||
clazz =
|
||||
Class.forName("com.google.android.exoplayer2.source.hls.HlsMediaSource$Factory")
|
||||
.asSubclass(MediaSourceFactory.class);
|
||||
.asSubclass(MediaSource.Factory.class);
|
||||
mediaSourceFactorySupplier = () -> newInstance(clazz, dataSourceFactory);
|
||||
break;
|
||||
case C.TYPE_RTSP:
|
||||
clazz =
|
||||
Class.forName("com.google.android.exoplayer2.source.rtsp.RtspMediaSource$Factory")
|
||||
.asSubclass(MediaSourceFactory.class);
|
||||
.asSubclass(MediaSource.Factory.class);
|
||||
mediaSourceFactorySupplier = () -> newInstance(clazz);
|
||||
break;
|
||||
case C.TYPE_OTHER:
|
||||
|
|
@ -681,8 +601,8 @@ public final class DefaultMediaSourceFactory implements MediaSourceFactory {
|
|||
public void release() {}
|
||||
}
|
||||
|
||||
private static MediaSourceFactory newInstance(
|
||||
Class<? extends MediaSourceFactory> clazz, DataSource.Factory dataSourceFactory) {
|
||||
private static MediaSource.Factory newInstance(
|
||||
Class<? extends MediaSource.Factory> clazz, DataSource.Factory dataSourceFactory) {
|
||||
try {
|
||||
return clazz.getConstructor(DataSource.Factory.class).newInstance(dataSourceFactory);
|
||||
} catch (Exception e) {
|
||||
|
|
@ -690,7 +610,7 @@ public final class DefaultMediaSourceFactory implements MediaSourceFactory {
|
|||
}
|
||||
}
|
||||
|
||||
private static MediaSourceFactory newInstance(Class<? extends MediaSourceFactory> clazz) {
|
||||
private static MediaSource.Factory newInstance(Class<? extends MediaSource.Factory> clazz) {
|
||||
try {
|
||||
return clazz.getConstructor().newInstance();
|
||||
} catch (Exception e) {
|
||||
|
|
|
|||
|
|
@ -179,11 +179,11 @@ public final class MaskingMediaSource extends CompositeMediaSource<Void> {
|
|||
windowStartPositionUs = windowPreparePositionUs;
|
||||
}
|
||||
}
|
||||
Pair<Object, Long> periodPosition =
|
||||
newTimeline.getPeriodPosition(
|
||||
Pair<Object, Long> periodUidAndPositionUs =
|
||||
newTimeline.getPeriodPositionUs(
|
||||
window, period, /* windowIndex= */ 0, windowStartPositionUs);
|
||||
Object periodUid = periodPosition.first;
|
||||
long periodPositionUs = periodPosition.second;
|
||||
Object periodUid = periodUidAndPositionUs.first;
|
||||
long periodPositionUs = periodUidAndPositionUs.second;
|
||||
timeline =
|
||||
hasRealTimeline
|
||||
? timeline.cloneWithUpdatedTimeline(newTimeline)
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue