Merge branch 'dev-v2' into trackSelectionView
This commit is contained in:
commit
869981a360
@ -1,4 +1,4 @@
|
||||
# ExoPlayer #
|
||||
# ExoPlayer <img src="https://img.shields.io/github/v/release/google/ExoPlayer.svg?label=latest"/> #
|
||||
|
||||
ExoPlayer is an application level media player for Android. It provides an
|
||||
alternative to Android’s MediaPlayer API for playing audio and video both
|
||||
|
326
RELEASENOTES.md
326
RELEASENOTES.md
@ -2,138 +2,137 @@
|
||||
|
||||
### dev-v2 (not yet released)
|
||||
|
||||
* New release notes go here!
|
||||
|
||||
### 2.12.0 (not yet released - targeted for 2020-08-TBD) ###
|
||||
### 2.12.0 (not yet released - targeted for 2020-09-03) ###
|
||||
|
||||
* Core library:
|
||||
* Implement getTag for SilenceMediaSource.
|
||||
* Added `TextComponent.getCurrentCues` because the current cues are no
|
||||
longer forwarded to a new `TextOutput` in `SimpleExoPlayer`
|
||||
automatically.
|
||||
* Add additional options to `SimpleExoPlayer.Builder` that were previously
|
||||
only accessible via setters.
|
||||
* Add opt-in to verify correct thread usage with
|
||||
`SimpleExoPlayer.setThrowsWhenUsingWrongThread(true)`
|
||||
([#4463](https://github.com/google/ExoPlayer/issues/4463)).
|
||||
* Add playbackPositionUs parameter to 'LoadControl.shouldContinueLoading'.
|
||||
* The `DefaultLoadControl` default minimum buffer is set to 50 seconds,
|
||||
equal to the default maximum buffer. `DefaultLoadControl` applies the
|
||||
same behavior for audio and video.
|
||||
* Add API in `AnalyticsListener` to report video frame processing offset.
|
||||
`MediaCodecVideoRenderer` reports the event.
|
||||
* Add fields `videoFrameProcessingOffsetUsSum` and
|
||||
`videoFrameProcessingOffsetUsCount` in `DecoderCounters` to compute the
|
||||
average video frame processing offset.
|
||||
* Add playlist API
|
||||
([#6161](https://github.com/google/ExoPlayer/issues/6161)).
|
||||
* `Player`:
|
||||
* Add a top level playlist API based on a new `MediaItem` class
|
||||
([#6161](https://github.com/google/ExoPlayer/issues/6161)). The
|
||||
new methods for playlist manipulation are `setMediaItem(s)`,
|
||||
`addMediaItem(s)`, `moveMediaItem(s)`, `removeMediaItem(s)` and
|
||||
`clearMediaItems`. This API should be used instead of
|
||||
`ConcatenatingMediaSource` in most cases.
|
||||
* Add `getCurrentMediaItem` for getting the currently playing item
|
||||
in the playlist.
|
||||
* Add `EventListener.onMediaItemTransition` to report when
|
||||
playback transitions from one item to another in the playlist.
|
||||
* Add `play` and `pause` convenience methods. They are equivalent to
|
||||
`setPlayWhenReady(true)` and `setPlayWhenReady(false)` respectively.
|
||||
* Add `getCurrentLiveOffset` for getting the offset of the current
|
||||
playback position from the live edge of a live stream.
|
||||
* Add `getTrackSelector` for getting the `TrackSelector` used by the
|
||||
player.
|
||||
* Add `AudioComponent.setAudioSessionId` to set the audio session ID.
|
||||
This method is also available on `SimpleExoPlayer`.
|
||||
* Add `TextComponent.getCurrentCues` to get the current cues. This
|
||||
method is also available on `SimpleExoPlayer`. The current cues are
|
||||
no longer automatically forwarded to a `TextOutput` when it's added
|
||||
to a `SimpleExoPlayer`.
|
||||
* Add `Player.DeviceComponent` to query and control the device volume.
|
||||
`SimpleExoPlayer` implements this interface.
|
||||
* Deprecate and rename `getPlaybackError` to `getPlayerError` for
|
||||
consistency.
|
||||
* Deprecate and rename `onLoadingChanged` to `onIsLoadingChanged` for
|
||||
consistency.
|
||||
* Deprecate `EventListener.onPlayerStateChanged`, replacing it with
|
||||
`EventListener.onPlayWhenReadyChanged` and
|
||||
`EventListener.onPlaybackStateChanged`.
|
||||
* Deprecate `EventListener.onSeekProcessed` because seek changes now
|
||||
happen instantly and listening to `onPositionDiscontinuity` is
|
||||
sufficient.
|
||||
* `ExoPlayer`:
|
||||
* Add `setMediaSource(s)` and `addMediaSource(s)` to `ExoPlayer`, for
|
||||
adding `MediaSource` instances directly to the playlist.
|
||||
* Add `ExoPlayer.setPauseAtEndOfMediaItems` to let the player pause at
|
||||
the end of each media item
|
||||
([#5660](https://github.com/google/ExoPlayer/issues/5660)).
|
||||
* Allow passing `C.TIME_END_OF_SOURCE` to `PlayerMessage.setPosition`
|
||||
to send a `PlayerMessage` at the end of a stream.
|
||||
* `SimpleExoPlayer`:
|
||||
* `SimpleExoPlayer` implements the new `MediaItem` based playlist API,
|
||||
using a `MediaSourceFactory` to convert `MediaItem` instances to
|
||||
playable `MediaSource` instances. A `DefaultMediaSourceFactory` is
|
||||
used by default. `Builder.setMediaSourceFactory` allows setting a
|
||||
custom factory.
|
||||
* Add additional options to `Builder` that were previously only
|
||||
accessible via setters.
|
||||
* Add opt-in to verify correct thread usage with
|
||||
`setThrowsWhenUsingWrongThread(true)`
|
||||
([#4463](https://github.com/google/ExoPlayer/issues/4463)).
|
||||
* `Format`:
|
||||
* Add a `Builder` and deprecate all `create` methods and most
|
||||
`Format.copyWith` methods.
|
||||
* Split `bitrate` into `averageBitrate` and `peakBitrate`
|
||||
([#2863](https://github.com/google/ExoPlayer/issues/2863)).
|
||||
* `LoadControl`:
|
||||
* Add a `playbackPositionUs` parameter to `shouldContinueLoading`.
|
||||
* Set the default minimum buffer duration in `DefaultLoadControl` to
|
||||
50 seconds (equal to the default maximum buffer), and treat audio
|
||||
and video the same.
|
||||
* Add a `MetadataRetriever` API for retrieving track information and
|
||||
static metadata for a media item
|
||||
([#3609](https://github.com/google/ExoPlayer/issues/3609)).
|
||||
* Attach an identifier and extra information to load error events passed
|
||||
to `LoadErrorHandlingPolicy`. `LoadErrorHandlingPolicy` implementations
|
||||
must migrate to overriding the non-deprecated methods of the interface
|
||||
in preparation for deprecated methods' removal in a future ExoPlayer
|
||||
version ([#7309](https://github.com/google/ExoPlayer/issues/7309)).
|
||||
* Add `play` and `pause` methods to `Player`.
|
||||
* Add `Player.getCurrentLiveOffset` to conveniently return the live
|
||||
offset.
|
||||
* Add `Player.EventListener.onPlayWhenReadyChanged` with reasons.
|
||||
* Add `Player.EventListener.onPlaybackStateChanged` and deprecate
|
||||
`Player.EventListener.onPlayerStateChanged`.
|
||||
* Add `Player.EventListener.onMediaItemTransition` with reasons.
|
||||
* Add `Player.setAudioSessionId` to set the session ID attached to the
|
||||
`AudioTrack`.
|
||||
* Add `Player.getTrackSelector`.
|
||||
* Deprecate and rename `getPlaybackError` to `getPlayerError` for
|
||||
consistency.
|
||||
* Deprecate and rename `onLoadingChanged` to `onIsLoadingChanged` for
|
||||
consistency.
|
||||
* Deprecate `onSeekProcessed` because all seek changes happen instantly
|
||||
now and listening to `onPositionDiscontinuity` is sufficient.
|
||||
* Add `ExoPlayer.setPauseAtEndOfMediaItems` to let the player pause at the
|
||||
end of each media item
|
||||
([#5660](https://github.com/google/ExoPlayer/issues/5660)).
|
||||
* Split `setPlaybackParameter` into `setPlaybackSpeed` and
|
||||
`AudioComponent.setSkipSilenceEnabled` with callbacks
|
||||
`onPlaybackSpeedChanged` and
|
||||
`AudioListener.onSkipSilenceEnabledChanged`.
|
||||
* Make `MediaSourceEventListener.LoadEventInfo` and
|
||||
`MediaSourceEventListener.MediaLoadData` top-level classes.
|
||||
* Rename `MediaCodecRenderer.onOutputFormatChanged` to
|
||||
`MediaCodecRenderer.onOutputMediaFormatChanged`, further clarifying the
|
||||
distinction between `Format` and `MediaFormat`.
|
||||
* Improve `Format` propagation within the media codec renderer
|
||||
([#6646](https://github.com/google/ExoPlayer/issues/6646)).
|
||||
* Move player message-related constants from `C` to `Renderer`, to avoid
|
||||
having the constants class depend on player/renderer classes.
|
||||
* Split out `common` and `extractor` submodules.
|
||||
* Allow to explicitly send `PlayerMessage`s at the end of a stream.
|
||||
* Add `DataSpec.Builder` and deprecate most `DataSpec` constructors.
|
||||
* Add `DataSpec.customData` to allow applications to pass custom data
|
||||
through `DataSource` chains.
|
||||
* Add a `Format.Builder` and deprecate all `Format.create*` methods and
|
||||
most `Format.copyWith*` methods.
|
||||
* Split `Format.bitrate` into `Format.averageBitrate` and
|
||||
`Format.peakBitrate`
|
||||
([#2863](https://github.com/google/ExoPlayer/issues/2863)).
|
||||
* Add option to `MergingMediaSource` to adjust the time offsets between
|
||||
the merged sources
|
||||
to `LoadErrorHandlingPolicy`
|
||||
([#7309](https://github.com/google/ExoPlayer/issues/7309)).
|
||||
`LoadErrorHandlingPolicy` implementations should migrate to implementing
|
||||
the non-deprecated methods of the interface.
|
||||
* Add an option to `MergingMediaSource` to adjust the time offsets
|
||||
between the merged sources
|
||||
([#6103](https://github.com/google/ExoPlayer/issues/6103)).
|
||||
* `SimpleDecoderVideoRenderer` and `SimpleDecoderAudioRenderer` renamed to
|
||||
* Move `MediaSourceEventListener.LoadEventInfo` and
|
||||
`MediaSourceEventListener.MediaLoadData` to be top-level classes in
|
||||
`com.google.android.exoplayer2.source`.
|
||||
* Move `SimpleDecoderVideoRenderer` and `SimpleDecoderAudioRenderer` to
|
||||
`DecoderVideoRenderer` and `DecoderAudioRenderer` respectively, and
|
||||
generalized to work with `Decoder` rather than `SimpleDecoder`.
|
||||
* Add media item based playlist API to `Player`.
|
||||
* Add `getCurrentMediaItem` to `Player`.
|
||||
* Remove deprecated members in `DefaultTrackSelector`.
|
||||
* Add `DefaultTrackSelector` constraints for minimum video resolution,
|
||||
bitrate and frame rate
|
||||
([#4511](https://github.com/google/ExoPlayer/issues/4511)).
|
||||
* Add `Player.DeviceComponent` and implement it for `SimpleExoPlayer` so
|
||||
that the device volume can be controlled by player.
|
||||
* Parse track titles from Matroska files
|
||||
([#7247](https://github.com/google/ExoPlayer/pull/7247)).
|
||||
* Replace `CacheDataSinkFactory` and `CacheDataSourceFactory` with
|
||||
`CacheDataSink.Factory` and `CacheDataSource.Factory` respectively.
|
||||
* Extend `EventTime` with more details about the current player state for
|
||||
easier access
|
||||
([#7332](https://github.com/google/ExoPlayer/issues/7332)).
|
||||
* Add `HttpDataSource.InvalidResponseCodeException#responseBody` field
|
||||
([#6853](https://github.com/google/ExoPlayer/issues/6853)).
|
||||
* Add `TrackSelection.shouldCancelMediaChunkLoad` to check whether an
|
||||
ongoing load should be canceled. Only supported by HLS streams so far.
|
||||
([#2848](https://github.com/google/ExoPlayer/issues/2848)).
|
||||
* Remove throws clause from Renderer.stop.
|
||||
* Don't clear `exception` in `SimpleDecoder#flush()`
|
||||
([#7590](https://github.com/google/ExoPlayer/issues/7590)).
|
||||
* Remove `AdaptiveTrackSelection.minTimeBetweenBufferReevaluationMs`
|
||||
parameter ([#7582](https://github.com/google/ExoPlayer/issues/7582)).
|
||||
* Fix wrong `MediaPeriodId` for some renderer errors reported by
|
||||
`AnalyticsListener.onPlayerError`.
|
||||
* Remove onMediaPeriodCreated/Released/ReadingStarted from
|
||||
`MediaSourceEventListener` and `AnalyticsListener`.
|
||||
* Dispatch previous, next, fast forward and rewind actions through
|
||||
`ControlDispatcher`
|
||||
([#6926](https://github.com/google/ExoPlayer/issues/6926)).
|
||||
* Add Guava dependency.
|
||||
* Add MetadataRetriever API to retrieve the static metadata of a media
|
||||
item ([#3609](https://github.com/google/ExoPlayer/issues/3609)).
|
||||
* Video: Pass frame rate hint to `Surface.setFrameRate` on Android R devices.
|
||||
generalize them to work with `Decoder` rather than `SimpleDecoder`.
|
||||
* Deprecate `C.MSG_*` constants, replacing them with constants in
|
||||
`Renderer`.
|
||||
* Split the `library-core` module into `library-core`,
|
||||
`library-common` and `library-extractor`. The `library-core` module
|
||||
has an API dependency on both of the new modules, so this change
|
||||
should be transparent to developers including ExoPlayer using Gradle
|
||||
dependencies.
|
||||
* Add a dependency on Guava.
|
||||
* Video:
|
||||
* Pass frame rate hint to `Surface.setFrameRate` on Android 11.
|
||||
* Fix incorrect aspect ratio when transitioning from one video to another
|
||||
with the same resolution, but a different pixel aspect ratio
|
||||
([#6646](https://github.com/google/ExoPlayer/issues/6646)).
|
||||
* Audio:
|
||||
* Add a sample count parameter to `MediaCodecRenderer.processOutputBuffer`
|
||||
and `AudioSink.handleBuffer` to allow batching multiple encoded frames
|
||||
in one buffer.
|
||||
* No longer use a `MediaCodec` in audio passthrough mode.
|
||||
* Add experimental support for power efficient playback using audio
|
||||
offload.
|
||||
* Add support for using framework audio speed adjustment instead of
|
||||
ExoPlayer's implementation
|
||||
([#7502](https://github.com/google/ExoPlayer/issues/7502)). This option
|
||||
can be set using
|
||||
`DefaultRenderersFactory.setEnableAudioTrackPlaybackParams`.
|
||||
* Add an event for the audio position starting to advance, to make it
|
||||
easier for apps to determine when audio playout started
|
||||
([#7577](https://github.com/google/ExoPlayer/issues/7577)).
|
||||
* Generalize support for floating point audio.
|
||||
* Add an option to `DefaultAudioSink` for enabling floating point
|
||||
output. This option can also be set using
|
||||
`DefaultRenderersFactory.setEnableAudioFloatOutput`.
|
||||
* Add floating point output capability to `MediaCodecAudioRenderer`
|
||||
and `LibopusAudioRenderer`, which is enabled automatically if the
|
||||
audio sink supports floating point output and if it makes sense for
|
||||
the content being played.
|
||||
* Enable the floating point output capability of `FfmpegAudioRenderer`
|
||||
automatically if the audio sink supports floating point output and
|
||||
if it makes sense for the content being played. The option to
|
||||
manually enable floating point output has been removed, since this
|
||||
now done with the generalized option on `DefaultAudioSink`.
|
||||
* In `MediaCodecAudioRenderer`, stop passing audio samples through
|
||||
`MediaCodec` when playing PCM audio or encoded audio using passthrough
|
||||
mode.
|
||||
* Reuse audio decoders when transitioning through playlists of gapless
|
||||
audio, rather than reinstantiating them.
|
||||
* Check `DefaultAudioSink` supports passthrough, in addition to checking
|
||||
the `AudioCapabilities`
|
||||
* Add an experimental scheduling mode to save power in offload.
|
||||
([#7404](https://github.com/google/ExoPlayer/issues/7404)).
|
||||
* Adjust input timestamps in `MediaCodecRenderer` to account for the
|
||||
Codec2 MP3 decoder having lower timestamps on the output side.
|
||||
* Propagate gapless audio metadata without the need to recreate the audio
|
||||
decoders.
|
||||
* Add floating point PCM output capability in `MediaCodecAudioRenderer`,
|
||||
and `LibopusAudioRenderer`.
|
||||
* Do not use a MediaCodec for PCM formats if AudioTrack supports it.
|
||||
* Add optional support for using framework audio speed adjustment instead
|
||||
of application-level audio speed adjustment
|
||||
([#7502](https://github.com/google/ExoPlayer/issues/7502)).
|
||||
* Text:
|
||||
* Add a WebView-based output option to `SubtitleView`. This can display
|
||||
some features not supported by the existing Canvas-based output such as
|
||||
@ -209,21 +208,19 @@
|
||||
`Mp3Extractor`. A significant portion of the file may need to be scanned
|
||||
when a seek is performed, which may be costly for large files.
|
||||
* MP4: Fix playback of MP4 streams that contain Opus audio.
|
||||
* FMP4:
|
||||
* Add support for partially fragmented MP4s
|
||||
([#7308](https://github.com/google/ExoPlayer/issues/7308)).
|
||||
* Fix handling of `traf` boxes containing multiple `sbgp` or `sgpd`
|
||||
boxes ([#7716](https://github.com/google/ExoPlayer/issues/7716)).
|
||||
* Matroska: Remove support for the `Invisible` block header flag.
|
||||
* FMP4: Add support for partially fragmented MP4s
|
||||
([#7308](https://github.com/google/ExoPlayer/issues/7308)).
|
||||
* Matroska:
|
||||
* Support Dolby Vision
|
||||
([#7267](https://github.com/google/ExoPlayer/issues/7267).
|
||||
* Populate `Format.label` with track titles.
|
||||
* Remove support for the `Invisible` block header flag.
|
||||
* MPEG-TS: Add support for MPEG-4 Part 2 and H.263
|
||||
([#1603](https://github.com/google/ExoPlayer/issues/1603),
|
||||
[#5107](https://github.com/google/ExoPlayer/issues/5107)).
|
||||
* Ogg: Fix handling of non-contiguous pages
|
||||
([#7230](https://github.com/google/ExoPlayer/issues/7230)).
|
||||
* FLV: Ignore `SCRIPTDATA` segments with invalid name types, rather than
|
||||
failing playback
|
||||
([#7675](https://github.com/google/ExoPlayer/issues/7675)).
|
||||
* UI
|
||||
* UI:
|
||||
* Add `StyledPlayerView` and `StyledPlayerControlView`, which provide a
|
||||
more polished user experience than `PlayerView` and `PlayerControlView`
|
||||
at the cost of decreased customizability.
|
||||
@ -238,6 +235,9 @@
|
||||
* Update `TrackSelectionDialogBuilder` to use the AndroidX app compat
|
||||
`AlertDialog` rather than the platform version, if available
|
||||
([#7357](https://github.com/google/ExoPlayer/issues/7357)).
|
||||
* Make UI components dispatch previous, next, fast forward and rewind
|
||||
actions via their `ControlDispatcher`
|
||||
([#6926](https://github.com/google/ExoPlayer/issues/6926)).
|
||||
* Downloads and caching:
|
||||
* Add `DownloadRequest.Builder`.
|
||||
* Add `DownloadRequest.keySetId` to make it easier to store an offline
|
||||
@ -269,7 +269,39 @@
|
||||
([#7011](https://github.com/google/ExoPlayer/issues/7011),
|
||||
[#6725](https://github.com/google/ExoPlayer/issues/6725),
|
||||
[#7066](https://github.com/google/ExoPlayer/issues/7066)).
|
||||
* Remove support for `cbc1` and `cens` encrytion schemes. Support for
|
||||
these schemes was removed from the Android platform from API level 30,
|
||||
and the range of API levels for which they are supported is too small to
|
||||
be useful.
|
||||
* Remove generic types from DRM components.
|
||||
* Track selection:
|
||||
* Add `TrackSelection.shouldCancelMediaChunkLoad` to check whether an
|
||||
ongoing load should be canceled
|
||||
([#2848](https://github.com/google/ExoPlayer/issues/2848)).
|
||||
* Add `DefaultTrackSelector` constraints for minimum video resolution,
|
||||
bitrate and frame rate
|
||||
([#4511](https://github.com/google/ExoPlayer/issues/4511)).
|
||||
* Remove previously deprecated `DefaultTrackSelector` members.
|
||||
* Data sources:
|
||||
* Add `HttpDataSource.InvalidResponseCodeException#responseBody` field
|
||||
([#6853](https://github.com/google/ExoPlayer/issues/6853)).
|
||||
* Add `DataSpec.Builder` and deprecate most `DataSpec` constructors.
|
||||
* Add `DataSpec.customData` to allow applications to pass custom data
|
||||
through `DataSource` chains.
|
||||
* Deprecate `CacheDataSinkFactory` and `CacheDataSourceFactory`, which are
|
||||
replaced by `CacheDataSink.Factory` and `CacheDataSource.Factory`
|
||||
respectively.
|
||||
* Analytics:
|
||||
* Extend `EventTime` with more details about the current player state
|
||||
([#7332](https://github.com/google/ExoPlayer/issues/7332)).
|
||||
* Add `AnalyticsListener.onVideoFrameProcessingOffset` to report how
|
||||
early or late video frames are processed relative to them needing to be
|
||||
presented. Video frame processing offset fields are also added to
|
||||
`DecoderCounters`.
|
||||
* Fix incorrect `MediaPeriodId` for some renderer errors reported by
|
||||
`AnalyticsListener.onPlayerError`.
|
||||
* Remove `onMediaPeriodCreated`, `onMediaPeriodReleased` and
|
||||
`onReadingStarted` from `AnalyticsListener`.
|
||||
* Test utils: Add `TestExoPlayer`, a utility class with APIs to create
|
||||
`SimpleExoPlayer` instances with fake components for testing.
|
||||
* Media2 extension: This is a new extension that makes it easy to use
|
||||
@ -277,8 +309,6 @@
|
||||
* Cast extension: Implement playlist API and deprecate the old queue
|
||||
manipulation API.
|
||||
* IMA extension:
|
||||
* Upgrade to IMA SDK 3.19.4, bringing in a fix for setting the media load
|
||||
timeout ([#7170](https://github.com/google/ExoPlayer/issues/7170)).
|
||||
* Migrate to new 'friendly obstruction' IMA SDK APIs, and allow apps to
|
||||
register a purpose and detail reason for overlay views via
|
||||
`AdsLoader.AdViewProvider`.
|
||||
@ -304,8 +334,32 @@
|
||||
* Remove support for media tunneling, random ABR and playback of
|
||||
spherical video. Developers wishing to experiment with these features
|
||||
can enable them by modifying the demo app source code.
|
||||
* Fix playback of ClearKey protected content on API level 26 and earlier
|
||||
([#7735](https://github.com/google/ExoPlayer/issues/7735)).
|
||||
|
||||
### 2.11.8 (2020-08-25) ###
|
||||
|
||||
* Fix distorted playback of floating point audio when samples exceed the
|
||||
`[-1, 1]` nominal range.
|
||||
* MP4:
|
||||
* Add support for `piff` and `isml` brands
|
||||
([#7584](https://github.com/google/ExoPlayer/issues/7584)).
|
||||
* Fix playback of very short MP4 files.
|
||||
* FMP4:
|
||||
* Fix `saiz` and `senc` sample count checks, resolving a "length
|
||||
mismatch" `ParserException` when playing certain protected FMP4 streams
|
||||
([#7592](https://github.com/google/ExoPlayer/issues/7592)).
|
||||
* Fix handling of `traf` boxes containing multiple `sbgp` or `sgpd`
|
||||
boxes.
|
||||
* FLV: Ignore `SCRIPTDATA` segments with invalid name types, rather than
|
||||
failing playback ([#7675](https://github.com/google/ExoPlayer/issues/7675)).
|
||||
* Better infer the content type of `.ism` and `.isml` streaming URLs.
|
||||
* Workaround an issue on Broadcom based devices where playbacks would not
|
||||
transition to `STATE_ENDED` when using video tunneling mode
|
||||
([#7647](https://github.com/google/ExoPlayer/issues/7647)).
|
||||
* IMA extension: Upgrade to IMA SDK 3.19.4, bringing in a fix for setting the
|
||||
media load timeout
|
||||
([#7170](https://github.com/google/ExoPlayer/issues/7170)).
|
||||
* Demo app: Fix playback of ClearKey protected content on API level 26 and
|
||||
earlier ([#7735](https://github.com/google/ExoPlayer/issues/7735)).
|
||||
|
||||
### 2.11.7 (2020-06-29) ###
|
||||
|
||||
|
@ -124,18 +124,6 @@
|
||||
"drm_scheme": "widevine",
|
||||
"drm_license_uri": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
|
||||
},
|
||||
{
|
||||
"name": "Secure (cbc1)",
|
||||
"uri": "https://storage.googleapis.com/wvmedia/cbc1/h264/tears/tears_aes_cbc1.mpd",
|
||||
"drm_scheme": "widevine",
|
||||
"drm_license_uri": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
|
||||
},
|
||||
{
|
||||
"name": "Secure UHD (cbc1)",
|
||||
"uri": "https://storage.googleapis.com/wvmedia/cbc1/h264/tears/tears_aes_cbc1_uhd.mpd",
|
||||
"drm_scheme": "widevine",
|
||||
"drm_license_uri": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
|
||||
},
|
||||
{
|
||||
"name": "Secure (cbcs)",
|
||||
"uri": "https://storage.googleapis.com/wvmedia/cbcs/h264/tears/tears_aes_cbcs.mpd",
|
||||
|
@ -458,32 +458,16 @@ public final class CastPlayer extends BasePlayer {
|
||||
flushNotifications();
|
||||
}
|
||||
|
||||
/** @deprecated Use {@link #setPlaybackSpeed(float)} instead. */
|
||||
@SuppressWarnings("deprecation")
|
||||
@Deprecated
|
||||
@Override
|
||||
public void setPlaybackParameters(@Nullable PlaybackParameters playbackParameters) {
|
||||
// Unsupported by the RemoteMediaClient API. Do nothing.
|
||||
}
|
||||
|
||||
/** @deprecated Use {@link #getPlaybackSpeed()} instead. */
|
||||
@SuppressWarnings("deprecation")
|
||||
@Deprecated
|
||||
@Override
|
||||
public PlaybackParameters getPlaybackParameters() {
|
||||
return PlaybackParameters.DEFAULT;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setPlaybackSpeed(float playbackSpeed) {
|
||||
// Unsupported by the RemoteMediaClient API. Do nothing.
|
||||
}
|
||||
|
||||
@Override
|
||||
public float getPlaybackSpeed() {
|
||||
return Player.DEFAULT_PLAYBACK_SPEED;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop(boolean reset) {
|
||||
playbackState = STATE_IDLE;
|
||||
|
@ -19,7 +19,6 @@ import static com.google.common.truth.Truth.assertThat;
|
||||
import static java.util.concurrent.TimeUnit.MILLISECONDS;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Looper;
|
||||
import android.support.v4.media.session.MediaControllerCompat;
|
||||
import android.support.v4.media.session.MediaSessionCompat;
|
||||
import androidx.annotation.NonNull;
|
||||
@ -46,13 +45,6 @@ public class MediaSessionUtilTest {
|
||||
|
||||
@Test
|
||||
public void getSessionCompatToken_withMediaControllerCompat_returnsValidToken() throws Exception {
|
||||
// Workaround to instantiate MediaSession with public androidx.media dependency.
|
||||
// TODO(b/146536708): Remove this workaround when the relevant change is released via
|
||||
// androidx.media 1.2.0.
|
||||
if (Looper.myLooper() == null) {
|
||||
Looper.prepare();
|
||||
}
|
||||
|
||||
Context context = ApplicationProvider.getApplicationContext();
|
||||
|
||||
SessionPlayerConnector sessionPlayerConnector = playerTestRule.getSessionPlayerConnector();
|
||||
|
@ -13,7 +13,6 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.google.android.exoplayer2.ext.media2;
|
||||
|
||||
import android.content.Context;
|
||||
@ -55,15 +54,23 @@ import org.junit.rules.ExternalResource;
|
||||
|
||||
@Override
|
||||
protected void before() {
|
||||
// Workaround limitation in androidx.media2.session:1.0.3 which session can only be instantiated
|
||||
// on thread with prepared Looper.
|
||||
// TODO: Remove when androidx.media2.session:1.1.0 is released without the limitation
|
||||
// [Internal: b/146536708]
|
||||
if (Looper.myLooper() == null) {
|
||||
Looper.prepare();
|
||||
}
|
||||
|
||||
context = ApplicationProvider.getApplicationContext();
|
||||
executor = Executors.newFixedThreadPool(1);
|
||||
|
||||
InstrumentationRegistry.getInstrumentation()
|
||||
.runOnMainSync(
|
||||
() -> {
|
||||
// Initialize AudioManager on the main thread to workaround b/78617702 that
|
||||
// Initialize AudioManager on the main thread to workaround that
|
||||
// audio focus listener is called on the thread where the AudioManager was
|
||||
// originally initialized.
|
||||
// originally initialized. [Internal: b/78617702]
|
||||
// Without posting this, audio focus listeners wouldn't be called because the
|
||||
// listeners would be posted to the test thread (here) where it waits until the
|
||||
// tests are finished.
|
||||
@ -75,8 +82,7 @@ import org.junit.rules.ExternalResource;
|
||||
.setLooper(Looper.myLooper())
|
||||
.setMediaSourceFactory(new DefaultMediaSourceFactory(dataSourceFactory, null))
|
||||
.build();
|
||||
sessionPlayerConnector =
|
||||
new SessionPlayerConnector(exoPlayer, new DefaultMediaItemConverter());
|
||||
sessionPlayerConnector = new SessionPlayerConnector(exoPlayer);
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -13,7 +13,6 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.google.android.exoplayer2.ext.media2;
|
||||
|
||||
import static com.google.android.exoplayer2.ext.media2.TestUtils.assertPlayerResultSuccess;
|
||||
@ -29,6 +28,7 @@ import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.media2.common.MediaItem;
|
||||
import androidx.media2.common.MediaMetadata;
|
||||
import androidx.media2.common.Rating;
|
||||
import androidx.media2.common.SessionPlayer;
|
||||
import androidx.media2.common.UriMediaItem;
|
||||
@ -136,7 +136,7 @@ public class SessionCallbackBuilderTest {
|
||||
SessionResult.RESULT_ERROR_BAD_VALUE)
|
||||
.setRewindIncrementMs(testRewindIncrementMs)
|
||||
.setFastForwardIncrementMs(testFastForwardIncrementMs)
|
||||
.setMediaItemProvider(new SessionCallbackBuilder.DefaultMediaItemProvider())
|
||||
.setMediaItemProvider(new SessionCallbackBuilder.MediaIdMediaItemProvider())
|
||||
.build())) {
|
||||
assertPlayerResultSuccess(sessionPlayerConnector.setMediaItem(TestUtils.createMediaItem()));
|
||||
assertPlayerResultSuccess(sessionPlayerConnector.prepare());
|
||||
@ -179,7 +179,7 @@ public class SessionCallbackBuilderTest {
|
||||
SessionResult.RESULT_ERROR_BAD_VALUE)
|
||||
.setRewindIncrementMs(testRewindIncrementMs)
|
||||
.setFastForwardIncrementMs(testFastForwardIncrementMs)
|
||||
.setMediaItemProvider(new SessionCallbackBuilder.DefaultMediaItemProvider())
|
||||
.setMediaItemProvider(new SessionCallbackBuilder.MediaIdMediaItemProvider())
|
||||
.build())) {
|
||||
|
||||
assertPlayerResultSuccess(sessionPlayerConnector.setPlaylist(testPlaylist, null));
|
||||
@ -455,13 +455,13 @@ public class SessionCallbackBuilderTest {
|
||||
Uri testMediaUri = RawResourceDataSource.buildRawResourceUri(R.raw.audio);
|
||||
|
||||
CountDownLatch providerLatch = new CountDownLatch(1);
|
||||
SessionCallbackBuilder.DefaultMediaItemProvider defaultMediaItemProvider =
|
||||
new SessionCallbackBuilder.DefaultMediaItemProvider();
|
||||
SessionCallbackBuilder.MediaIdMediaItemProvider mediaIdMediaItemProvider =
|
||||
new SessionCallbackBuilder.MediaIdMediaItemProvider();
|
||||
SessionCallbackBuilder.MediaItemProvider provider =
|
||||
(session, controllerInfo, mediaId) -> {
|
||||
assertThat(mediaId).isEqualTo(testMediaUri.toString());
|
||||
providerLatch.countDown();
|
||||
return defaultMediaItemProvider.onCreateMediaItem(session, controllerInfo, mediaId);
|
||||
return mediaIdMediaItemProvider.onCreateMediaItem(session, controllerInfo, mediaId);
|
||||
};
|
||||
|
||||
CountDownLatch currentMediaItemChangedLatch = new CountDownLatch(1);
|
||||
@ -471,7 +471,9 @@ public class SessionCallbackBuilderTest {
|
||||
@Override
|
||||
public void onCurrentMediaItemChanged(
|
||||
@NonNull SessionPlayer player, @NonNull MediaItem item) {
|
||||
assertThat(((UriMediaItem) item).getUri()).isEqualTo(testMediaUri);
|
||||
MediaMetadata metadata = item.getMetadata();
|
||||
assertThat(metadata.getString(MediaMetadata.METADATA_KEY_MEDIA_ID))
|
||||
.isEqualTo(testMediaUri.toString());
|
||||
currentMediaItemChangedLatch.countDown();
|
||||
}
|
||||
});
|
||||
|
@ -13,7 +13,6 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.google.android.exoplayer2.ext.media2;
|
||||
|
||||
import static androidx.media2.common.SessionPlayer.PLAYER_STATE_PAUSED;
|
||||
@ -33,6 +32,7 @@ import android.os.Build.VERSION_CODES;
|
||||
import android.os.Looper;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.util.ObjectsCompat;
|
||||
import androidx.media.AudioAttributesCompat;
|
||||
import androidx.media2.common.MediaItem;
|
||||
import androidx.media2.common.MediaMetadata;
|
||||
@ -61,6 +61,7 @@ import java.util.concurrent.CopyOnWriteArrayList;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import org.junit.Before;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
@ -273,17 +274,17 @@ public class SessionPlayerConnectorTest {
|
||||
@Test
|
||||
@SmallTest
|
||||
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT)
|
||||
public void getCurrentPosition_whenIdleState_returnsUnknownTime() {
|
||||
public void getCurrentPosition_whenIdleState_returnsDefaultPosition() {
|
||||
assertThat(sessionPlayerConnector.getPlayerState()).isEqualTo(SessionPlayer.PLAYER_STATE_IDLE);
|
||||
assertThat(sessionPlayerConnector.getCurrentPosition()).isEqualTo(SessionPlayer.UNKNOWN_TIME);
|
||||
assertThat(sessionPlayerConnector.getCurrentPosition()).isEqualTo(0);
|
||||
}
|
||||
|
||||
@Test
|
||||
@SmallTest
|
||||
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT)
|
||||
public void getBufferedPosition_whenIdleState_returnsUnknownTime() {
|
||||
public void getBufferedPosition_whenIdleState_returnsDefaultPosition() {
|
||||
assertThat(sessionPlayerConnector.getPlayerState()).isEqualTo(SessionPlayer.PLAYER_STATE_IDLE);
|
||||
assertThat(sessionPlayerConnector.getBufferedPosition()).isEqualTo(SessionPlayer.UNKNOWN_TIME);
|
||||
assertThat(sessionPlayerConnector.getBufferedPosition()).isEqualTo(0);
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -795,6 +796,73 @@ public class SessionPlayerConnectorTest {
|
||||
assertThat(onPlaylistChangedLatch.getCount()).isEqualTo(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
@LargeTest
|
||||
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT)
|
||||
public void setPlaylist_byUnderlyingPlayerBeforePrepare_notifiesOnPlaylistChanged()
|
||||
throws Exception {
|
||||
List<MediaItem> playlistToSessionPlayer = TestUtils.createPlaylist(2);
|
||||
List<MediaItem> playlistToExoPlayer = TestUtils.createPlaylist(4);
|
||||
DefaultMediaItemConverter converter = new DefaultMediaItemConverter();
|
||||
List<com.google.android.exoplayer2.MediaItem> exoMediaItems = new ArrayList<>();
|
||||
for (MediaItem mediaItem : playlistToExoPlayer) {
|
||||
exoMediaItems.add(converter.convertToExoPlayerMediaItem(mediaItem));
|
||||
}
|
||||
|
||||
CountDownLatch onPlaylistChangedLatch = new CountDownLatch(1);
|
||||
sessionPlayerConnector.registerPlayerCallback(
|
||||
executor,
|
||||
new SessionPlayer.PlayerCallback() {
|
||||
@Override
|
||||
public void onPlaylistChanged(
|
||||
@NonNull SessionPlayer player,
|
||||
@Nullable List<MediaItem> list,
|
||||
@Nullable MediaMetadata metadata) {
|
||||
if (ObjectsCompat.equals(list, playlistToExoPlayer)) {
|
||||
onPlaylistChangedLatch.countDown();
|
||||
}
|
||||
}
|
||||
});
|
||||
sessionPlayerConnector.setPlaylist(playlistToSessionPlayer, /* metadata= */ null);
|
||||
InstrumentationRegistry.getInstrumentation()
|
||||
.runOnMainSync(() -> playerTestRule.getSimpleExoPlayer().setMediaItems(exoMediaItems));
|
||||
assertThat(onPlaylistChangedLatch.await(PLAYLIST_CHANGE_WAIT_TIME_MS, MILLISECONDS)).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
@LargeTest
|
||||
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT)
|
||||
public void setPlaylist_byUnderlyingPlayerAfterPrepare_notifiesOnPlaylistChanged()
|
||||
throws Exception {
|
||||
List<MediaItem> playlistToSessionPlayer = TestUtils.createPlaylist(2);
|
||||
List<MediaItem> playlistToExoPlayer = TestUtils.createPlaylist(4);
|
||||
DefaultMediaItemConverter converter = new DefaultMediaItemConverter();
|
||||
List<com.google.android.exoplayer2.MediaItem> exoMediaItems = new ArrayList<>();
|
||||
for (MediaItem mediaItem : playlistToExoPlayer) {
|
||||
exoMediaItems.add(converter.convertToExoPlayerMediaItem(mediaItem));
|
||||
}
|
||||
|
||||
CountDownLatch onPlaylistChangedLatch = new CountDownLatch(1);
|
||||
sessionPlayerConnector.registerPlayerCallback(
|
||||
executor,
|
||||
new SessionPlayer.PlayerCallback() {
|
||||
@Override
|
||||
public void onPlaylistChanged(
|
||||
@NonNull SessionPlayer player,
|
||||
@Nullable List<MediaItem> list,
|
||||
@Nullable MediaMetadata metadata) {
|
||||
if (ObjectsCompat.equals(list, playlistToExoPlayer)) {
|
||||
onPlaylistChangedLatch.countDown();
|
||||
}
|
||||
}
|
||||
});
|
||||
sessionPlayerConnector.prepare();
|
||||
sessionPlayerConnector.setPlaylist(playlistToSessionPlayer, /* metadata= */ null);
|
||||
InstrumentationRegistry.getInstrumentation()
|
||||
.runOnMainSync(() -> playerTestRule.getSimpleExoPlayer().setMediaItems(exoMediaItems));
|
||||
assertThat(onPlaylistChangedLatch.await(PLAYLIST_CHANGE_WAIT_TIME_MS, MILLISECONDS)).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
@LargeTest
|
||||
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT)
|
||||
@ -862,7 +930,7 @@ public class SessionPlayerConnectorTest {
|
||||
|
||||
CountDownLatch onPlaylistChangedLatch = new CountDownLatch(2);
|
||||
int replaceIndex = 2;
|
||||
MediaItem newMediaItem = TestUtils.createMediaItem();
|
||||
MediaItem newMediaItem = TestUtils.createMediaItem(R.raw.video_big_buck_bunny);
|
||||
playlist.set(replaceIndex, newMediaItem);
|
||||
sessionPlayerConnector.registerPlayerCallback(
|
||||
executor,
|
||||
@ -1185,6 +1253,32 @@ public class SessionPlayerConnectorTest {
|
||||
assertThat(sessionPlayerConnector.getPlayerState()).isEqualTo(PLAYER_STATE_PLAYING);
|
||||
}
|
||||
|
||||
@Test
|
||||
@LargeTest
|
||||
public void getPlaylist_returnsPlaylistInUnderlyingPlayer() {
|
||||
List<MediaItem> playlistToExoPlayer = TestUtils.createPlaylist(4);
|
||||
DefaultMediaItemConverter converter = new DefaultMediaItemConverter();
|
||||
List<com.google.android.exoplayer2.MediaItem> exoMediaItems = new ArrayList<>();
|
||||
for (MediaItem mediaItem : playlistToExoPlayer) {
|
||||
exoMediaItems.add(converter.convertToExoPlayerMediaItem(mediaItem));
|
||||
}
|
||||
|
||||
AtomicReference<List<MediaItem>> playlistFromSessionPlayer = new AtomicReference<>();
|
||||
InstrumentationRegistry.getInstrumentation()
|
||||
.runOnMainSync(
|
||||
() -> {
|
||||
SimpleExoPlayer simpleExoPlayer = playerTestRule.getSimpleExoPlayer();
|
||||
simpleExoPlayer.setMediaItems(exoMediaItems);
|
||||
|
||||
try (SessionPlayerConnector sessionPlayer =
|
||||
new SessionPlayerConnector(simpleExoPlayer)) {
|
||||
List<MediaItem> playlist = sessionPlayer.getPlaylist();
|
||||
playlistFromSessionPlayer.set(playlist);
|
||||
}
|
||||
});
|
||||
assertThat(playlistFromSessionPlayer.get()).isEqualTo(playlistToExoPlayer);
|
||||
}
|
||||
|
||||
private class PlayerCallbackForPlaylist extends SessionPlayer.PlayerCallback {
|
||||
private List<MediaItem> playlist;
|
||||
private CountDownLatch onCurrentMediaItemChangedLatch;
|
||||
|
@ -13,7 +13,6 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.google.android.exoplayer2.ext.media2;
|
||||
|
||||
import static androidx.media2.common.SessionPlayer.PlayerResult.RESULT_SUCCESS;
|
||||
|
@ -13,79 +13,125 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.google.android.exoplayer2.ext.media2;
|
||||
|
||||
import static androidx.media2.common.MediaMetadata.METADATA_KEY_DISPLAY_TITLE;
|
||||
import static androidx.media2.common.MediaMetadata.METADATA_KEY_MEDIA_ID;
|
||||
import static androidx.media2.common.MediaMetadata.METADATA_KEY_MEDIA_URI;
|
||||
import static androidx.media2.common.MediaMetadata.METADATA_KEY_TITLE;
|
||||
|
||||
import android.net.Uri;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.media2.common.CallbackMediaItem;
|
||||
import androidx.media2.common.FileMediaItem;
|
||||
import androidx.media2.common.MediaMetadata;
|
||||
import androidx.media2.common.UriMediaItem;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.MediaItem;
|
||||
import com.google.android.exoplayer2.util.Assertions;
|
||||
|
||||
/** Default implementation of {@link MediaItemConverter}. */
|
||||
public final class DefaultMediaItemConverter implements MediaItemConverter {
|
||||
/**
|
||||
* Default implementation of {@link MediaItemConverter}.
|
||||
*
|
||||
* <p>Note that {@link #getMetadata} can be overridden to fill in additional metadata when
|
||||
* converting {@link MediaItem ExoPlayer MediaItems} to their AndroidX equivalents.
|
||||
*/
|
||||
public class DefaultMediaItemConverter implements MediaItemConverter {
|
||||
|
||||
@Override
|
||||
public MediaItem convertToExoPlayerMediaItem(androidx.media2.common.MediaItem androidXMediaItem) {
|
||||
if (androidXMediaItem instanceof FileMediaItem) {
|
||||
public MediaItem convertToExoPlayerMediaItem(androidx.media2.common.MediaItem media2MediaItem) {
|
||||
if (media2MediaItem instanceof FileMediaItem) {
|
||||
throw new IllegalStateException("FileMediaItem isn't supported");
|
||||
}
|
||||
if (androidXMediaItem instanceof CallbackMediaItem) {
|
||||
if (media2MediaItem instanceof CallbackMediaItem) {
|
||||
throw new IllegalStateException("CallbackMediaItem isn't supported");
|
||||
}
|
||||
|
||||
MediaItem.Builder exoplayerMediaItemBuilder = new MediaItem.Builder();
|
||||
|
||||
// Set mediaItem as tag for creating MediaSource via MediaSourceFactory methods.
|
||||
exoplayerMediaItemBuilder.setTag(androidXMediaItem);
|
||||
|
||||
// Media ID or URI must be present. Get it from androidx.MediaItem if possible.
|
||||
|
||||
@Nullable Uri uri = null;
|
||||
@Nullable String mediaId = null;
|
||||
if (androidXMediaItem instanceof UriMediaItem) {
|
||||
UriMediaItem uriMediaItem = (UriMediaItem) androidXMediaItem;
|
||||
@Nullable String title = null;
|
||||
if (media2MediaItem instanceof UriMediaItem) {
|
||||
UriMediaItem uriMediaItem = (UriMediaItem) media2MediaItem;
|
||||
uri = uriMediaItem.getUri();
|
||||
}
|
||||
@Nullable MediaMetadata metadata = androidXMediaItem.getMetadata();
|
||||
@Nullable androidx.media2.common.MediaMetadata metadata = media2MediaItem.getMetadata();
|
||||
if (metadata != null) {
|
||||
mediaId = metadata.getString(MediaMetadata.METADATA_KEY_MEDIA_ID);
|
||||
@Nullable String uriString = metadata.getString(MediaMetadata.METADATA_KEY_MEDIA_URI);
|
||||
if (uri == null && uriString != null) {
|
||||
uri = Uri.parse(uriString);
|
||||
@Nullable String uriString = metadata.getString(METADATA_KEY_MEDIA_URI);
|
||||
mediaId = metadata.getString(METADATA_KEY_MEDIA_ID);
|
||||
if (uri == null) {
|
||||
if (uriString != null) {
|
||||
uri = Uri.parse(uriString);
|
||||
} else if (mediaId != null) {
|
||||
uri = Uri.parse("media2:///" + mediaId);
|
||||
}
|
||||
}
|
||||
title = metadata.getString(METADATA_KEY_DISPLAY_TITLE);
|
||||
if (title == null) {
|
||||
title = metadata.getString(METADATA_KEY_TITLE);
|
||||
}
|
||||
}
|
||||
if (uri == null) {
|
||||
// Generate a Uri to make it non-null. If not, tag will be ignored.
|
||||
uri = Uri.parse("exoplayer://" + androidXMediaItem.hashCode());
|
||||
// Generate a URI to make it non-null. If not, then the tag passed to setTag will be ignored.
|
||||
uri = Uri.parse("media2:///");
|
||||
}
|
||||
exoplayerMediaItemBuilder.setUri(uri);
|
||||
exoplayerMediaItemBuilder.setMediaId(mediaId);
|
||||
|
||||
if (androidXMediaItem.getStartPosition() != androidx.media2.common.MediaItem.POSITION_UNKNOWN) {
|
||||
exoplayerMediaItemBuilder.setClipStartPositionMs(androidXMediaItem.getStartPosition());
|
||||
exoplayerMediaItemBuilder.setClipRelativeToDefaultPosition(true);
|
||||
long startPositionMs = media2MediaItem.getStartPosition();
|
||||
if (startPositionMs == androidx.media2.common.MediaItem.POSITION_UNKNOWN) {
|
||||
startPositionMs = 0;
|
||||
}
|
||||
if (androidXMediaItem.getEndPosition() != androidx.media2.common.MediaItem.POSITION_UNKNOWN) {
|
||||
exoplayerMediaItemBuilder.setClipEndPositionMs(androidXMediaItem.getEndPosition());
|
||||
exoplayerMediaItemBuilder.setClipRelativeToDefaultPosition(true);
|
||||
long endPositionMs = media2MediaItem.getEndPosition();
|
||||
if (endPositionMs == androidx.media2.common.MediaItem.POSITION_UNKNOWN) {
|
||||
endPositionMs = C.TIME_END_OF_SOURCE;
|
||||
}
|
||||
|
||||
return exoplayerMediaItemBuilder.build();
|
||||
return new MediaItem.Builder()
|
||||
.setUri(uri)
|
||||
.setMediaId(mediaId)
|
||||
.setMediaMetadata(
|
||||
new com.google.android.exoplayer2.MediaMetadata.Builder().setTitle(title).build())
|
||||
.setTag(media2MediaItem)
|
||||
.setClipStartPositionMs(startPositionMs)
|
||||
.setClipEndPositionMs(endPositionMs)
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public androidx.media2.common.MediaItem convertToAndroidXMediaItem(MediaItem exoplayerMediaItem) {
|
||||
Assertions.checkNotNull(exoplayerMediaItem);
|
||||
public androidx.media2.common.MediaItem convertToMedia2MediaItem(MediaItem exoPlayerMediaItem) {
|
||||
Assertions.checkNotNull(exoPlayerMediaItem);
|
||||
MediaItem.PlaybackProperties playbackProperties =
|
||||
Assertions.checkNotNull(exoplayerMediaItem.playbackProperties);
|
||||
Assertions.checkNotNull(exoPlayerMediaItem.playbackProperties);
|
||||
|
||||
@Nullable Object tag = playbackProperties.tag;
|
||||
if (!(tag instanceof androidx.media2.common.MediaItem)) {
|
||||
throw new IllegalStateException(
|
||||
"MediaItem tag must be an instance of androidx.media2.common.MediaItem");
|
||||
if (tag instanceof androidx.media2.common.MediaItem) {
|
||||
return (androidx.media2.common.MediaItem) tag;
|
||||
}
|
||||
return (androidx.media2.common.MediaItem) tag;
|
||||
|
||||
androidx.media2.common.MediaMetadata metadata = getMetadata(exoPlayerMediaItem);
|
||||
long startPositionMs = exoPlayerMediaItem.clippingProperties.startPositionMs;
|
||||
long endPositionMs = exoPlayerMediaItem.clippingProperties.endPositionMs;
|
||||
if (endPositionMs == C.TIME_END_OF_SOURCE) {
|
||||
endPositionMs = androidx.media2.common.MediaItem.POSITION_UNKNOWN;
|
||||
}
|
||||
|
||||
return new androidx.media2.common.MediaItem.Builder()
|
||||
.setMetadata(metadata)
|
||||
.setStartPosition(startPositionMs)
|
||||
.setEndPosition(endPositionMs)
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a {@link androidx.media2.common.MediaMetadata} corresponding to the given {@link
|
||||
* MediaItem ExoPlayer MediaItem}.
|
||||
*/
|
||||
protected androidx.media2.common.MediaMetadata getMetadata(MediaItem exoPlayerMediaItem) {
|
||||
@Nullable String title = exoPlayerMediaItem.mediaMetadata.title;
|
||||
|
||||
androidx.media2.common.MediaMetadata.Builder metadataBuilder =
|
||||
new androidx.media2.common.MediaMetadata.Builder()
|
||||
.putString(METADATA_KEY_MEDIA_ID, exoPlayerMediaItem.mediaId);
|
||||
if (title != null) {
|
||||
metadataBuilder.putString(METADATA_KEY_TITLE, title);
|
||||
metadataBuilder.putString(METADATA_KEY_DISPLAY_TITLE, title);
|
||||
}
|
||||
return metadataBuilder.build();
|
||||
}
|
||||
}
|
||||
|
@ -13,25 +13,24 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.google.android.exoplayer2.ext.media2;
|
||||
|
||||
import com.google.android.exoplayer2.MediaItem;
|
||||
|
||||
/**
|
||||
* Converter for between {@link MediaItem AndroidX MediaItem} and {@link
|
||||
* com.google.android.exoplayer2.MediaItem ExoPlayer MediaItem}.
|
||||
* Converts between {@link androidx.media2.common.MediaItem Media2 MediaItem} and {@link MediaItem
|
||||
* ExoPlayer MediaItem}.
|
||||
*/
|
||||
public interface MediaItemConverter {
|
||||
/**
|
||||
* Converts {@link androidx.media2.common.MediaItem AndroidX MediaItem} to {@link MediaItem
|
||||
* Converts an {@link androidx.media2.common.MediaItem Media2 MediaItem} to an {@link MediaItem
|
||||
* ExoPlayer MediaItem}.
|
||||
*/
|
||||
MediaItem convertToExoPlayerMediaItem(androidx.media2.common.MediaItem androidXMediaItem);
|
||||
MediaItem convertToExoPlayerMediaItem(androidx.media2.common.MediaItem media2MediaItem);
|
||||
|
||||
/**
|
||||
* Converts {@link MediaItem ExoPlayer MediaItem} to {@link androidx.media2.common.MediaItem
|
||||
* AndroidX MediaItem}.
|
||||
* Converts an {@link MediaItem ExoPlayer MediaItem} to an {@link androidx.media2.common.MediaItem
|
||||
* Media2 MediaItem}.
|
||||
*/
|
||||
androidx.media2.common.MediaItem convertToAndroidXMediaItem(MediaItem exoplayerMediaItem);
|
||||
androidx.media2.common.MediaItem convertToMedia2MediaItem(MediaItem exoPlayerMediaItem);
|
||||
}
|
||||
|
@ -13,7 +13,6 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.google.android.exoplayer2.ext.media2;
|
||||
|
||||
import androidx.annotation.IntRange;
|
||||
@ -27,6 +26,7 @@ import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.ControlDispatcher;
|
||||
import com.google.android.exoplayer2.ExoPlaybackException;
|
||||
import com.google.android.exoplayer2.MediaItem;
|
||||
import com.google.android.exoplayer2.PlaybackParameters;
|
||||
import com.google.android.exoplayer2.Player;
|
||||
import com.google.android.exoplayer2.Timeline;
|
||||
import com.google.android.exoplayer2.audio.AudioAttributes;
|
||||
@ -56,44 +56,44 @@ import java.util.List;
|
||||
void onPlayerStateChanged(/* @SessionPlayer.PlayerState */ int playerState);
|
||||
|
||||
/** Called when the player is prepared. */
|
||||
void onPrepared(androidx.media2.common.MediaItem androidXMediaItem, int bufferingPercentage);
|
||||
void onPrepared(androidx.media2.common.MediaItem media2MediaItem, int bufferingPercentage);
|
||||
|
||||
/** Called when a seek request has completed. */
|
||||
void onSeekCompleted();
|
||||
|
||||
/** Called when the player rebuffers. */
|
||||
void onBufferingStarted(androidx.media2.common.MediaItem androidXMediaItem);
|
||||
void onBufferingStarted(androidx.media2.common.MediaItem media2MediaItem);
|
||||
|
||||
/** Called when the player becomes ready again after rebuffering. */
|
||||
void onBufferingEnded(
|
||||
androidx.media2.common.MediaItem androidXMediaItem, int bufferingPercentage);
|
||||
androidx.media2.common.MediaItem media2MediaItem, int bufferingPercentage);
|
||||
|
||||
/** Called periodically with the player's buffered position as a percentage. */
|
||||
void onBufferingUpdate(
|
||||
androidx.media2.common.MediaItem androidXMediaItem, int bufferingPercentage);
|
||||
androidx.media2.common.MediaItem media2MediaItem, int bufferingPercentage);
|
||||
|
||||
/** Called when current media item is changed. */
|
||||
void onCurrentMediaItemChanged(androidx.media2.common.MediaItem androidXMediaItem);
|
||||
void onCurrentMediaItemChanged(androidx.media2.common.MediaItem media2MediaItem);
|
||||
|
||||
/** Called when playback of the item list has ended. */
|
||||
void onPlaybackEnded();
|
||||
|
||||
/** Called when the player encounters an error. */
|
||||
void onError(@Nullable androidx.media2.common.MediaItem androidXMediaItem);
|
||||
void onError(@Nullable androidx.media2.common.MediaItem media2MediaItem);
|
||||
|
||||
/** Called when the playlist is changed */
|
||||
/** Called when the playlist is changed. */
|
||||
void onPlaylistChanged();
|
||||
|
||||
/** Called when the shuffle mode is changed */
|
||||
/** Called when the shuffle mode is changed. */
|
||||
void onShuffleModeChanged(int shuffleMode);
|
||||
|
||||
/** Called when the repeat mode is changed */
|
||||
/** Called when the repeat mode is changed. */
|
||||
void onRepeatModeChanged(int repeatMode);
|
||||
|
||||
/** Called when the audio attributes is changed */
|
||||
/** Called when the audio attributes is changed. */
|
||||
void onAudioAttributesChanged(AudioAttributesCompat audioAttributes);
|
||||
|
||||
/** Called when the playback speed is changed */
|
||||
/** Called when the playback speed is changed. */
|
||||
void onPlaybackSpeedChanged(float playbackSpeed);
|
||||
}
|
||||
|
||||
@ -108,14 +108,15 @@ import java.util.List;
|
||||
private final ControlDispatcher controlDispatcher;
|
||||
private final ComponentListener componentListener;
|
||||
|
||||
private final List<androidx.media2.common.MediaItem> cachedPlaylist;
|
||||
@Nullable private MediaMetadata playlistMetadata;
|
||||
private final List<MediaItem> cachedMediaItems;
|
||||
|
||||
// These should be only updated in TimelineChanges.
|
||||
private final List<androidx.media2.common.MediaItem> media2Playlist;
|
||||
private final List<MediaItem> exoPlayerPlaylist;
|
||||
|
||||
private boolean prepared;
|
||||
private boolean rebuffering;
|
||||
private int currentWindowIndex;
|
||||
private boolean loggedUnexpectedTimelineChanges;
|
||||
private boolean ignoreTimelineUpdates;
|
||||
|
||||
/**
|
||||
@ -146,79 +147,69 @@ import java.util.List;
|
||||
handler = new PlayerHandler(player.getApplicationLooper());
|
||||
pollBufferRunnable = new PollBufferRunnable();
|
||||
|
||||
cachedPlaylist = new ArrayList<>();
|
||||
cachedMediaItems = new ArrayList<>();
|
||||
media2Playlist = new ArrayList<>();
|
||||
exoPlayerPlaylist = new ArrayList<>();
|
||||
currentWindowIndex = C.INDEX_UNSET;
|
||||
|
||||
prepared = player.getPlaybackState() != Player.STATE_IDLE;
|
||||
rebuffering = player.getPlaybackState() == Player.STATE_BUFFERING;
|
||||
|
||||
updatePlaylist(player.getCurrentTimeline());
|
||||
}
|
||||
|
||||
public boolean setMediaItem(androidx.media2.common.MediaItem androidXMediaItem) {
|
||||
return setPlaylist(Collections.singletonList(androidXMediaItem), /* metadata= */ null);
|
||||
public boolean setMediaItem(androidx.media2.common.MediaItem media2MediaItem) {
|
||||
return setPlaylist(Collections.singletonList(media2MediaItem), /* metadata= */ null);
|
||||
}
|
||||
|
||||
public boolean setPlaylist(
|
||||
List<androidx.media2.common.MediaItem> playlist, @Nullable MediaMetadata metadata) {
|
||||
// Check for duplication.
|
||||
for (int i = 0; i < playlist.size(); i++) {
|
||||
androidx.media2.common.MediaItem androidXMediaItem = playlist.get(i);
|
||||
Assertions.checkArgument(playlist.indexOf(androidXMediaItem) == i);
|
||||
androidx.media2.common.MediaItem media2MediaItem = playlist.get(i);
|
||||
Assertions.checkArgument(playlist.indexOf(media2MediaItem) == i);
|
||||
}
|
||||
|
||||
this.cachedPlaylist.clear();
|
||||
this.cachedPlaylist.addAll(playlist);
|
||||
this.playlistMetadata = metadata;
|
||||
this.cachedMediaItems.clear();
|
||||
List<MediaItem> exoplayerMediaItems = new ArrayList<>();
|
||||
List<MediaItem> exoPlayerMediaItems = new ArrayList<>();
|
||||
for (int i = 0; i < playlist.size(); i++) {
|
||||
androidx.media2.common.MediaItem androidXMediaItem = playlist.get(i);
|
||||
MediaItem exoplayerMediaItem =
|
||||
Assertions.checkNotNull(
|
||||
mediaItemConverter.convertToExoPlayerMediaItem(androidXMediaItem));
|
||||
exoplayerMediaItems.add(exoplayerMediaItem);
|
||||
androidx.media2.common.MediaItem media2MediaItem = playlist.get(i);
|
||||
MediaItem exoPlayerMediaItem =
|
||||
Assertions.checkNotNull(mediaItemConverter.convertToExoPlayerMediaItem(media2MediaItem));
|
||||
exoPlayerMediaItems.add(exoPlayerMediaItem);
|
||||
}
|
||||
this.cachedMediaItems.addAll(exoplayerMediaItems);
|
||||
|
||||
player.setMediaItems(exoplayerMediaItems, /* resetPosition= */ true);
|
||||
player.setMediaItems(exoPlayerMediaItems, /* resetPosition= */ true);
|
||||
|
||||
currentWindowIndex = getCurrentMediaItemIndex();
|
||||
return true;
|
||||
}
|
||||
|
||||
public boolean addPlaylistItem(int index, androidx.media2.common.MediaItem androidXMediaItem) {
|
||||
Assertions.checkArgument(!cachedPlaylist.contains(androidXMediaItem));
|
||||
index = Util.constrainValue(index, 0, cachedPlaylist.size());
|
||||
public boolean addPlaylistItem(int index, androidx.media2.common.MediaItem media2MediaItem) {
|
||||
Assertions.checkArgument(!media2Playlist.contains(media2MediaItem));
|
||||
index = Util.constrainValue(index, 0, media2Playlist.size());
|
||||
|
||||
cachedPlaylist.add(index, androidXMediaItem);
|
||||
MediaItem exoplayerMediaItem =
|
||||
Assertions.checkNotNull(mediaItemConverter.convertToExoPlayerMediaItem(androidXMediaItem));
|
||||
cachedMediaItems.add(index, exoplayerMediaItem);
|
||||
player.addMediaItem(index, exoplayerMediaItem);
|
||||
MediaItem exoPlayerMediaItem =
|
||||
Assertions.checkNotNull(mediaItemConverter.convertToExoPlayerMediaItem(media2MediaItem));
|
||||
player.addMediaItem(index, exoPlayerMediaItem);
|
||||
return true;
|
||||
}
|
||||
|
||||
public boolean removePlaylistItem(@IntRange(from = 0) int index) {
|
||||
androidx.media2.common.MediaItem androidXMediaItemToRemove = cachedPlaylist.remove(index);
|
||||
releaseMediaItem(androidXMediaItemToRemove);
|
||||
cachedMediaItems.remove(index);
|
||||
player.removeMediaItem(index);
|
||||
return true;
|
||||
}
|
||||
|
||||
public boolean replacePlaylistItem(
|
||||
int index, androidx.media2.common.MediaItem androidXMediaItem) {
|
||||
Assertions.checkArgument(!cachedPlaylist.contains(androidXMediaItem));
|
||||
index = Util.constrainValue(index, 0, cachedPlaylist.size());
|
||||
public boolean replacePlaylistItem(int index, androidx.media2.common.MediaItem media2MediaItem) {
|
||||
Assertions.checkArgument(!media2Playlist.contains(media2MediaItem));
|
||||
index = Util.constrainValue(index, 0, media2Playlist.size());
|
||||
|
||||
androidx.media2.common.MediaItem androidXMediaItemToRemove = cachedPlaylist.get(index);
|
||||
cachedPlaylist.set(index, androidXMediaItem);
|
||||
releaseMediaItem(androidXMediaItemToRemove);
|
||||
MediaItem exoplayerMediaItemToAdd =
|
||||
Assertions.checkNotNull(mediaItemConverter.convertToExoPlayerMediaItem(androidXMediaItem));
|
||||
cachedMediaItems.set(index, exoplayerMediaItemToAdd);
|
||||
MediaItem exoPlayerMediaItemToAdd =
|
||||
Assertions.checkNotNull(mediaItemConverter.convertToExoPlayerMediaItem(media2MediaItem));
|
||||
|
||||
ignoreTimelineUpdates = true;
|
||||
player.removeMediaItem(index);
|
||||
ignoreTimelineUpdates = false;
|
||||
player.addMediaItem(index, exoplayerMediaItemToAdd);
|
||||
player.addMediaItem(index, exoPlayerMediaItemToAdd);
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -272,8 +263,8 @@ import java.util.List;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public List<androidx.media2.common.MediaItem> getCachedPlaylist() {
|
||||
return new ArrayList<>(cachedPlaylist);
|
||||
public List<androidx.media2.common.MediaItem> getPlaylist() {
|
||||
return new ArrayList<>(media2Playlist);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@ -290,7 +281,7 @@ import java.util.List;
|
||||
}
|
||||
|
||||
public int getCurrentMediaItemIndex() {
|
||||
return cachedPlaylist.isEmpty() ? C.INDEX_UNSET : player.getCurrentWindowIndex();
|
||||
return media2Playlist.isEmpty() ? C.INDEX_UNSET : player.getCurrentWindowIndex();
|
||||
}
|
||||
|
||||
public int getPreviousMediaItemIndex() {
|
||||
@ -304,7 +295,7 @@ import java.util.List;
|
||||
@Nullable
|
||||
public androidx.media2.common.MediaItem getCurrentMediaItem() {
|
||||
int index = getCurrentMediaItemIndex();
|
||||
return (index != C.INDEX_UNSET) ? cachedPlaylist.get(index) : null;
|
||||
return index == C.INDEX_UNSET ? null : media2Playlist.get(index);
|
||||
}
|
||||
|
||||
public boolean prepare() {
|
||||
@ -317,9 +308,9 @@ import java.util.List;
|
||||
|
||||
public boolean play() {
|
||||
if (player.getPlaybackState() == Player.STATE_ENDED) {
|
||||
int currentWindowIndex = getCurrentMediaItemIndex();
|
||||
boolean seekHandled =
|
||||
controlDispatcher.dispatchSeekTo(player, currentWindowIndex, /* positionMs= */ 0);
|
||||
controlDispatcher.dispatchSeekTo(
|
||||
player, player.getCurrentWindowIndex(), /* positionMs= */ 0);
|
||||
if (!seekHandled) {
|
||||
return false;
|
||||
}
|
||||
@ -342,23 +333,19 @@ import java.util.List;
|
||||
}
|
||||
|
||||
public boolean seekTo(long position) {
|
||||
int currentWindowIndex = getCurrentMediaItemIndex();
|
||||
return controlDispatcher.dispatchSeekTo(player, currentWindowIndex, position);
|
||||
return controlDispatcher.dispatchSeekTo(player, player.getCurrentWindowIndex(), position);
|
||||
}
|
||||
|
||||
public long getCurrentPosition() {
|
||||
Assertions.checkState(getState() != SessionPlayer.PLAYER_STATE_IDLE);
|
||||
return Math.max(0, player.getCurrentPosition());
|
||||
return player.getCurrentPosition();
|
||||
}
|
||||
|
||||
public long getDuration() {
|
||||
Assertions.checkState(getState() != SessionPlayer.PLAYER_STATE_IDLE);
|
||||
long duration = player.getDuration();
|
||||
return duration == C.TIME_UNSET ? -1 : duration;
|
||||
return duration == C.TIME_UNSET ? SessionPlayer.UNKNOWN_TIME : duration;
|
||||
}
|
||||
|
||||
public long getBufferedPosition() {
|
||||
Assertions.checkState(getState() != SessionPlayer.PLAYER_STATE_IDLE);
|
||||
return player.getBufferedPosition();
|
||||
}
|
||||
|
||||
@ -397,11 +384,11 @@ import java.util.List;
|
||||
}
|
||||
|
||||
public void setPlaybackSpeed(float playbackSpeed) {
|
||||
player.setPlaybackSpeed(playbackSpeed);
|
||||
player.setPlaybackParameters(new PlaybackParameters(playbackSpeed));
|
||||
}
|
||||
|
||||
public float getPlaybackSpeed() {
|
||||
return player.getPlaybackSpeed();
|
||||
return player.getPlaybackParameters().speed;
|
||||
}
|
||||
|
||||
public void reset() {
|
||||
@ -427,7 +414,7 @@ import java.util.List;
|
||||
}
|
||||
|
||||
public boolean canSkipToPlaylistItem() {
|
||||
@Nullable List<androidx.media2.common.MediaItem> playlist = getCachedPlaylist();
|
||||
@Nullable List<androidx.media2.common.MediaItem> playlist = getPlaylist();
|
||||
return playlist != null && playlist.size() > 1;
|
||||
}
|
||||
|
||||
@ -497,57 +484,57 @@ import java.util.List;
|
||||
listener.onShuffleModeChanged(Utils.getShuffleMode(shuffleModeEnabled));
|
||||
}
|
||||
|
||||
private void handlePlaybackSpeedChanged(float playbackSpeed) {
|
||||
listener.onPlaybackSpeedChanged(playbackSpeed);
|
||||
private void handlePlaybackParametersChanged(PlaybackParameters playbackParameters) {
|
||||
listener.onPlaybackSpeedChanged(playbackParameters.speed);
|
||||
}
|
||||
|
||||
private void handleTimelineChanged(Timeline timeline) {
|
||||
if (ignoreTimelineUpdates) {
|
||||
return;
|
||||
}
|
||||
updateCachedPlaylistAndMediaItems(timeline);
|
||||
if (!isExoPlayerMediaItemsChanged(timeline)) {
|
||||
return;
|
||||
}
|
||||
updatePlaylist(timeline);
|
||||
listener.onPlaylistChanged();
|
||||
}
|
||||
|
||||
// Update cached playlist, if the ExoPlayer Player's Timeline is unexpectedly changed without
|
||||
// using SessionPlayer interface.
|
||||
private void updateCachedPlaylistAndMediaItems(Timeline currentTimeline) {
|
||||
// Check whether ExoPlayer media items are the same as expected.
|
||||
// Check whether Timeline is changed by media item changes or not
|
||||
private boolean isExoPlayerMediaItemsChanged(Timeline timeline) {
|
||||
if (exoPlayerPlaylist.size() != timeline.getWindowCount()) {
|
||||
return true;
|
||||
}
|
||||
Timeline.Window window = new Timeline.Window();
|
||||
int windowCount = currentTimeline.getWindowCount();
|
||||
int windowCount = timeline.getWindowCount();
|
||||
for (int i = 0; i < windowCount; i++) {
|
||||
currentTimeline.getWindow(i, window);
|
||||
if (i >= cachedMediaItems.size()
|
||||
|| !ObjectsCompat.equals(cachedMediaItems.get(i), window.mediaItem)) {
|
||||
if (!loggedUnexpectedTimelineChanges) {
|
||||
Log.w(TAG, "Timeline was unexpectedly changed. Playlist will be rebuilt.");
|
||||
loggedUnexpectedTimelineChanges = true;
|
||||
}
|
||||
|
||||
androidx.media2.common.MediaItem oldAndroidXMediaItem = cachedPlaylist.get(i);
|
||||
releaseMediaItem(oldAndroidXMediaItem);
|
||||
|
||||
androidx.media2.common.MediaItem androidXMediaItem =
|
||||
Assertions.checkNotNull(
|
||||
mediaItemConverter.convertToAndroidXMediaItem(window.mediaItem));
|
||||
if (i < cachedMediaItems.size()) {
|
||||
cachedMediaItems.set(i, window.mediaItem);
|
||||
cachedPlaylist.set(i, androidXMediaItem);
|
||||
} else {
|
||||
cachedMediaItems.add(window.mediaItem);
|
||||
cachedPlaylist.add(androidXMediaItem);
|
||||
}
|
||||
timeline.getWindow(i, window);
|
||||
if (!ObjectsCompat.equals(exoPlayerPlaylist.get(i), window.mediaItem)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if (cachedMediaItems.size() > windowCount) {
|
||||
if (!loggedUnexpectedTimelineChanges) {
|
||||
Log.w(TAG, "Timeline was unexpectedly changed. Playlist will be rebuilt.");
|
||||
loggedUnexpectedTimelineChanges = true;
|
||||
}
|
||||
while (cachedMediaItems.size() > windowCount) {
|
||||
cachedMediaItems.remove(windowCount);
|
||||
cachedPlaylist.remove(windowCount);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private void updatePlaylist(Timeline timeline) {
|
||||
List<androidx.media2.common.MediaItem> media2MediaItemToBeRemoved =
|
||||
new ArrayList<>(media2Playlist);
|
||||
media2Playlist.clear();
|
||||
exoPlayerPlaylist.clear();
|
||||
|
||||
Timeline.Window window = new Timeline.Window();
|
||||
int windowCount = timeline.getWindowCount();
|
||||
for (int i = 0; i < windowCount; i++) {
|
||||
timeline.getWindow(i, window);
|
||||
MediaItem exoPlayerMediaItem = window.mediaItem;
|
||||
androidx.media2.common.MediaItem media2MediaItem =
|
||||
Assertions.checkNotNull(mediaItemConverter.convertToMedia2MediaItem(exoPlayerMediaItem));
|
||||
exoPlayerPlaylist.add(exoPlayerMediaItem);
|
||||
media2Playlist.add(media2MediaItem);
|
||||
media2MediaItemToBeRemoved.remove(media2MediaItem);
|
||||
}
|
||||
|
||||
for (androidx.media2.common.MediaItem item : media2MediaItemToBeRemoved) {
|
||||
releaseMediaItem(item);
|
||||
}
|
||||
}
|
||||
|
||||
@ -556,35 +543,35 @@ import java.util.List;
|
||||
}
|
||||
|
||||
private void updateBufferingAndScheduleNextPollBuffer() {
|
||||
androidx.media2.common.MediaItem androidXMediaItem =
|
||||
androidx.media2.common.MediaItem media2MediaItem =
|
||||
Assertions.checkNotNull(getCurrentMediaItem());
|
||||
listener.onBufferingUpdate(androidXMediaItem, player.getBufferedPercentage());
|
||||
listener.onBufferingUpdate(media2MediaItem, player.getBufferedPercentage());
|
||||
handler.removeCallbacks(pollBufferRunnable);
|
||||
handler.postDelayed(pollBufferRunnable, POLL_BUFFER_INTERVAL_MS);
|
||||
}
|
||||
|
||||
private void maybeNotifyBufferingEvents() {
|
||||
androidx.media2.common.MediaItem androidXMediaItem =
|
||||
androidx.media2.common.MediaItem media2MediaItem =
|
||||
Assertions.checkNotNull(getCurrentMediaItem());
|
||||
if (prepared && !rebuffering) {
|
||||
rebuffering = true;
|
||||
listener.onBufferingStarted(androidXMediaItem);
|
||||
listener.onBufferingStarted(media2MediaItem);
|
||||
}
|
||||
}
|
||||
|
||||
private void maybeNotifyReadyEvents() {
|
||||
androidx.media2.common.MediaItem androidXMediaItem =
|
||||
androidx.media2.common.MediaItem media2MediaItem =
|
||||
Assertions.checkNotNull(getCurrentMediaItem());
|
||||
boolean prepareComplete = !prepared;
|
||||
if (prepareComplete) {
|
||||
prepared = true;
|
||||
handlePositionDiscontinuity(Player.DISCONTINUITY_REASON_PERIOD_TRANSITION);
|
||||
listener.onPlayerStateChanged(SessionPlayer.PLAYER_STATE_PAUSED);
|
||||
listener.onPrepared(androidXMediaItem, player.getBufferedPercentage());
|
||||
listener.onPrepared(media2MediaItem, player.getBufferedPercentage());
|
||||
}
|
||||
if (rebuffering) {
|
||||
rebuffering = false;
|
||||
listener.onBufferingEnded(androidXMediaItem, player.getBufferedPercentage());
|
||||
listener.onBufferingEnded(media2MediaItem, player.getBufferedPercentage());
|
||||
}
|
||||
}
|
||||
|
||||
@ -596,13 +583,13 @@ import java.util.List;
|
||||
}
|
||||
}
|
||||
|
||||
private void releaseMediaItem(androidx.media2.common.MediaItem androidXMediaItem) {
|
||||
private void releaseMediaItem(androidx.media2.common.MediaItem media2MediaItem) {
|
||||
try {
|
||||
if (androidXMediaItem instanceof CallbackMediaItem) {
|
||||
((CallbackMediaItem) androidXMediaItem).getDataSourceCallback().close();
|
||||
if (media2MediaItem instanceof CallbackMediaItem) {
|
||||
((CallbackMediaItem) media2MediaItem).getDataSourceCallback().close();
|
||||
}
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, "Error releasing media item " + androidXMediaItem, e);
|
||||
Log.w(TAG, "Error releasing media item " + media2MediaItem, e);
|
||||
}
|
||||
}
|
||||
|
||||
@ -641,8 +628,8 @@ import java.util.List;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPlaybackSpeedChanged(float playbackSpeed) {
|
||||
handlePlaybackSpeedChanged(playbackSpeed);
|
||||
public void onPlaybackParametersChanged(PlaybackParameters playbackParameters) {
|
||||
handlePlaybackParametersChanged(playbackParameters);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -13,7 +13,6 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.google.android.exoplayer2.ext.media2;
|
||||
|
||||
import static java.util.concurrent.TimeUnit.MILLISECONDS;
|
||||
|
@ -13,14 +13,12 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.google.android.exoplayer2.ext.media2;
|
||||
|
||||
import android.Manifest;
|
||||
import android.content.ComponentName;
|
||||
import android.content.Context;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.provider.Settings;
|
||||
import android.text.TextUtils;
|
||||
@ -31,7 +29,6 @@ import androidx.media2.common.MediaItem;
|
||||
import androidx.media2.common.MediaMetadata;
|
||||
import androidx.media2.common.Rating;
|
||||
import androidx.media2.common.SessionPlayer;
|
||||
import androidx.media2.common.UriMediaItem;
|
||||
import androidx.media2.session.MediaController;
|
||||
import androidx.media2.session.MediaSession;
|
||||
import androidx.media2.session.MediaSession.ControllerInfo;
|
||||
@ -39,13 +36,11 @@ import androidx.media2.session.SessionCommand;
|
||||
import androidx.media2.session.SessionCommandGroup;
|
||||
import androidx.media2.session.SessionResult;
|
||||
import com.google.android.exoplayer2.util.Assertions;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Builds {@link MediaSession.SessionCallback} with various collaborators.
|
||||
* Builds a {@link MediaSession.SessionCallback} with various collaborators.
|
||||
*
|
||||
* @see MediaSession.SessionCallback
|
||||
*/
|
||||
@ -351,10 +346,8 @@ public final class SessionCallbackBuilder {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Default implementation of {@link MediaItemProvider} that assumes the media id is a URI string.
|
||||
*/
|
||||
public static final class DefaultMediaItemProvider implements MediaItemProvider {
|
||||
/** A {@link MediaItemProvider} that creates media items containing only a media ID. */
|
||||
public static final class MediaIdMediaItemProvider implements MediaItemProvider {
|
||||
@Override
|
||||
@Nullable
|
||||
public MediaItem onCreateMediaItem(
|
||||
@ -362,17 +355,11 @@ public final class SessionCallbackBuilder {
|
||||
if (TextUtils.isEmpty(mediaId)) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
new URI(mediaId);
|
||||
} catch (URISyntaxException e) {
|
||||
// Ignore if mediaId isn't a URI.
|
||||
return null;
|
||||
}
|
||||
MediaMetadata metadata =
|
||||
new MediaMetadata.Builder()
|
||||
.putString(MediaMetadata.METADATA_KEY_MEDIA_ID, mediaId)
|
||||
.build();
|
||||
return new UriMediaItem.Builder(Uri.parse(mediaId)).setMetadata(metadata).build();
|
||||
return new MediaItem.Builder().setMetadata(metadata).build();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -13,7 +13,6 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.google.android.exoplayer2.ext.media2;
|
||||
|
||||
import androidx.annotation.FloatRange;
|
||||
@ -23,6 +22,7 @@ import androidx.annotation.Nullable;
|
||||
import androidx.core.util.ObjectsCompat;
|
||||
import androidx.core.util.Pair;
|
||||
import androidx.media.AudioAttributesCompat;
|
||||
import androidx.media2.common.CallbackMediaItem;
|
||||
import androidx.media2.common.FileMediaItem;
|
||||
import androidx.media2.common.MediaItem;
|
||||
import androidx.media2.common.MediaMetadata;
|
||||
@ -41,25 +41,11 @@ import java.util.Map;
|
||||
import java.util.concurrent.Callable;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.Executor;
|
||||
import org.checkerframework.checker.initialization.qual.Initialized;
|
||||
import org.checkerframework.checker.nullness.compatqual.NullableType;
|
||||
|
||||
/**
|
||||
* An implementation of {@link SessionPlayer} that wraps a given ExoPlayer {@link Player} instance.
|
||||
*
|
||||
* <h3>Ownership</h3>
|
||||
*
|
||||
* <p>{@code SessionPlayerConnector} takes ownership of the provided ExoPlayer {@link Player}
|
||||
* instance between when it's constructed and when it's {@link #close() closed}. No other components
|
||||
* should interact with the wrapped player (otherwise, unexpected event callbacks from the wrapped
|
||||
* player may put the session player in an inconsistent state).
|
||||
*
|
||||
* <p>Call {@link SessionPlayer#close()} when the {@code SessionPlayerConnector} is no longer needed
|
||||
* to regain ownership of the wrapped player. It is the caller's responsibility to release the
|
||||
* wrapped player via {@link Player#release()}.
|
||||
*
|
||||
* <h3>Threading model</h3>
|
||||
*
|
||||
* <p>Internally this implementation posts operations to and receives callbacks on the thread
|
||||
* associated with {@link Player#getApplicationLooper()}, so it is important not to block this
|
||||
* thread. In particular, when awaiting the result of an asynchronous session player operation, apps
|
||||
@ -95,16 +81,15 @@ public final class SessionPlayerConnector extends SessionPlayer {
|
||||
|
||||
// Should be only accessed on the executor, which is currently single-threaded.
|
||||
@Nullable private MediaItem currentMediaItem;
|
||||
@Nullable private List<MediaItem> currentPlaylist;
|
||||
|
||||
/**
|
||||
* Creates an instance using {@link DefaultControlDispatcher} to dispatch player commands.
|
||||
* Creates an instance using {@link DefaultMediaItemConverter} to convert between ExoPlayer and
|
||||
* media2 MediaItems and {@link DefaultControlDispatcher} to dispatch player commands.
|
||||
*
|
||||
* @param player The player to wrap.
|
||||
* @param mediaItemConverter The {@link MediaItemConverter}.
|
||||
*/
|
||||
public SessionPlayerConnector(Player player, MediaItemConverter mediaItemConverter) {
|
||||
this(player, mediaItemConverter, new DefaultControlDispatcher());
|
||||
public SessionPlayerConnector(Player player) {
|
||||
this(player, new DefaultMediaItemConverter(), new DefaultControlDispatcher());
|
||||
}
|
||||
|
||||
/**
|
||||
@ -124,19 +109,8 @@ public final class SessionPlayerConnector extends SessionPlayer {
|
||||
taskHandler = new PlayerHandler(player.getApplicationLooper());
|
||||
taskHandlerExecutor = taskHandler::postOrRun;
|
||||
ExoPlayerWrapperListener playerListener = new ExoPlayerWrapperListener();
|
||||
PlayerWrapper playerWrapper =
|
||||
new PlayerWrapper(playerListener, player, mediaItemConverter, controlDispatcher);
|
||||
this.player = playerWrapper;
|
||||
this.player = new PlayerWrapper(playerListener, player, mediaItemConverter, controlDispatcher);
|
||||
playerCommandQueue = new PlayerCommandQueue(this.player, taskHandler);
|
||||
|
||||
@SuppressWarnings("assignment.type.incompatible")
|
||||
@Initialized
|
||||
SessionPlayerConnector initializedThis = this;
|
||||
initializedThis.<Void>runPlayerCallableBlocking(
|
||||
/* callable= */ () -> {
|
||||
playerWrapper.reset();
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -251,17 +225,27 @@ public final class SessionPlayerConnector extends SessionPlayer {
|
||||
return runPlayerCallableBlockingWithNullOnException(/* callable= */ player::getAudioAttributes);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*
|
||||
* <p>{@link FileMediaItem} and {@link CallbackMediaItem} are not supported.
|
||||
*/
|
||||
@Override
|
||||
public ListenableFuture<PlayerResult> setMediaItem(MediaItem item) {
|
||||
Assertions.checkNotNull(item);
|
||||
Assertions.checkArgument(!(item instanceof FileMediaItem));
|
||||
Assertions.checkArgument(!(item instanceof CallbackMediaItem));
|
||||
ListenableFuture<PlayerResult> result =
|
||||
playerCommandQueue.addCommand(
|
||||
PlayerCommandQueue.COMMAND_CODE_PLAYER_SET_MEDIA_ITEM, () -> player.setMediaItem(item));
|
||||
result.addListener(this::handlePlaylistChangedOnHandler, taskHandlerExecutor);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*
|
||||
* <p>{@link FileMediaItem} and {@link CallbackMediaItem} are not supported.
|
||||
*/
|
||||
@Override
|
||||
public ListenableFuture<PlayerResult> setPlaylist(
|
||||
final List<MediaItem> playlist, @Nullable MediaMetadata metadata) {
|
||||
@ -271,6 +255,7 @@ public final class SessionPlayerConnector extends SessionPlayer {
|
||||
MediaItem item = playlist.get(i);
|
||||
Assertions.checkNotNull(item);
|
||||
Assertions.checkArgument(!(item instanceof FileMediaItem));
|
||||
Assertions.checkArgument(!(item instanceof CallbackMediaItem));
|
||||
for (int j = 0; j < i; j++) {
|
||||
Assertions.checkArgument(
|
||||
item != playlist.get(j),
|
||||
@ -281,20 +266,24 @@ public final class SessionPlayerConnector extends SessionPlayer {
|
||||
playerCommandQueue.addCommand(
|
||||
PlayerCommandQueue.COMMAND_CODE_PLAYER_SET_PLAYLIST,
|
||||
/* command= */ () -> player.setPlaylist(playlist, metadata));
|
||||
result.addListener(this::handlePlaylistChangedOnHandler, taskHandlerExecutor);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*
|
||||
* <p>{@link FileMediaItem} and {@link CallbackMediaItem} are not supported.
|
||||
*/
|
||||
@Override
|
||||
public ListenableFuture<PlayerResult> addPlaylistItem(int index, MediaItem item) {
|
||||
Assertions.checkArgument(index >= 0);
|
||||
Assertions.checkNotNull(item);
|
||||
Assertions.checkArgument(!(item instanceof FileMediaItem));
|
||||
Assertions.checkArgument(!(item instanceof CallbackMediaItem));
|
||||
ListenableFuture<PlayerResult> result =
|
||||
playerCommandQueue.addCommand(
|
||||
PlayerCommandQueue.COMMAND_CODE_PLAYER_ADD_PLAYLIST_ITEM,
|
||||
/* command= */ () -> player.addPlaylistItem(index, item));
|
||||
result.addListener(this::handlePlaylistChangedOnHandler, taskHandlerExecutor);
|
||||
return result;
|
||||
}
|
||||
|
||||
@ -305,20 +294,24 @@ public final class SessionPlayerConnector extends SessionPlayer {
|
||||
playerCommandQueue.addCommand(
|
||||
PlayerCommandQueue.COMMAND_CODE_PLAYER_REMOVE_PLAYLIST_ITEM,
|
||||
/* command= */ () -> player.removePlaylistItem(index));
|
||||
result.addListener(this::handlePlaylistChangedOnHandler, taskHandlerExecutor);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*
|
||||
* <p>{@link FileMediaItem} and {@link CallbackMediaItem} are not supported.
|
||||
*/
|
||||
@Override
|
||||
public ListenableFuture<PlayerResult> replacePlaylistItem(int index, MediaItem item) {
|
||||
Assertions.checkArgument(index >= 0);
|
||||
Assertions.checkNotNull(item);
|
||||
Assertions.checkArgument(!(item instanceof FileMediaItem));
|
||||
Assertions.checkArgument(!(item instanceof CallbackMediaItem));
|
||||
ListenableFuture<PlayerResult> result =
|
||||
playerCommandQueue.addCommand(
|
||||
PlayerCommandQueue.COMMAND_CODE_PLAYER_REPLACE_PLAYLIST_ITEM,
|
||||
/* command= */ () -> player.replacePlaylistItem(index, item));
|
||||
result.addListener(this::handlePlaylistChangedOnHandler, taskHandlerExecutor);
|
||||
return result;
|
||||
}
|
||||
|
||||
@ -385,7 +378,7 @@ public final class SessionPlayerConnector extends SessionPlayer {
|
||||
@Override
|
||||
@Nullable
|
||||
public List<MediaItem> getPlaylist() {
|
||||
return runPlayerCallableBlockingWithNullOnException(/* callable= */ player::getCachedPlaylist);
|
||||
return runPlayerCallableBlockingWithNullOnException(/* callable= */ player::getPlaylist);
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -447,7 +440,7 @@ public final class SessionPlayerConnector extends SessionPlayer {
|
||||
}
|
||||
reset();
|
||||
|
||||
this.<Void>runPlayerCallableBlockingInternal(
|
||||
this.<Void>runPlayerCallableBlocking(
|
||||
/* callable= */ () -> {
|
||||
player.close();
|
||||
return null;
|
||||
@ -511,7 +504,7 @@ public final class SessionPlayerConnector extends SessionPlayer {
|
||||
state = PLAYER_STATE_IDLE;
|
||||
mediaItemToBuffState.clear();
|
||||
}
|
||||
this.<Void>runPlayerCallableBlockingInternal(
|
||||
this.<Void>runPlayerCallableBlocking(
|
||||
/* callable= */ () -> {
|
||||
player.reset();
|
||||
return null;
|
||||
@ -558,25 +551,18 @@ public final class SessionPlayerConnector extends SessionPlayer {
|
||||
}
|
||||
|
||||
private void handlePlaylistChangedOnHandler() {
|
||||
List<MediaItem> currentPlaylist = player.getCachedPlaylist();
|
||||
boolean notifyCurrentPlaylist = !ObjectsCompat.equals(this.currentPlaylist, currentPlaylist);
|
||||
this.currentPlaylist = currentPlaylist;
|
||||
List<MediaItem> currentPlaylist = player.getPlaylist();
|
||||
MediaMetadata playlistMetadata = player.getPlaylistMetadata();
|
||||
|
||||
MediaItem currentMediaItem = player.getCurrentMediaItem();
|
||||
boolean notifyCurrentMediaItem = !ObjectsCompat.equals(this.currentMediaItem, currentMediaItem);
|
||||
this.currentMediaItem = currentMediaItem;
|
||||
|
||||
if (!notifyCurrentMediaItem && !notifyCurrentPlaylist) {
|
||||
return;
|
||||
}
|
||||
long currentPosition = getCurrentPosition();
|
||||
notifySessionPlayerCallback(
|
||||
callback -> {
|
||||
if (notifyCurrentPlaylist) {
|
||||
callback.onPlaylistChanged(
|
||||
SessionPlayerConnector.this, currentPlaylist, playlistMetadata);
|
||||
}
|
||||
callback.onPlaylistChanged(
|
||||
SessionPlayerConnector.this, currentPlaylist, playlistMetadata);
|
||||
if (notifyCurrentMediaItem) {
|
||||
Assertions.checkNotNull(
|
||||
currentMediaItem, "PlaylistManager#currentMediaItem() cannot be changed to null");
|
||||
@ -610,13 +596,6 @@ public final class SessionPlayerConnector extends SessionPlayer {
|
||||
}
|
||||
|
||||
private <T> T runPlayerCallableBlocking(Callable<T> callable) {
|
||||
synchronized (stateLock) {
|
||||
Assertions.checkState(!closed);
|
||||
}
|
||||
return runPlayerCallableBlockingInternal(callable);
|
||||
}
|
||||
|
||||
private <T> T runPlayerCallableBlockingInternal(Callable<T> callable) {
|
||||
SettableFuture<T> future = SettableFuture.create();
|
||||
boolean success =
|
||||
taskHandler.postOrRun(
|
||||
|
@ -38,6 +38,7 @@ import com.google.android.exoplayer2.ControlDispatcher;
|
||||
import com.google.android.exoplayer2.DefaultControlDispatcher;
|
||||
import com.google.android.exoplayer2.ExoPlaybackException;
|
||||
import com.google.android.exoplayer2.ExoPlayerLibraryInfo;
|
||||
import com.google.android.exoplayer2.PlaybackParameters;
|
||||
import com.google.android.exoplayer2.Player;
|
||||
import com.google.android.exoplayer2.Timeline;
|
||||
import com.google.android.exoplayer2.util.Assertions;
|
||||
@ -127,8 +128,8 @@ public final class MediaSessionConnector {
|
||||
@PlaybackActions public static final long DEFAULT_PLAYBACK_ACTIONS = ALL_PLAYBACK_ACTIONS;
|
||||
|
||||
/**
|
||||
* The name of the {@link PlaybackStateCompat} float extra with the value of {@link
|
||||
* Player#getPlaybackSpeed()}.
|
||||
* The name of the {@link PlaybackStateCompat} float extra with the value of {@code
|
||||
* Player.getPlaybackParameters().speed}.
|
||||
*/
|
||||
public static final String EXTRAS_SPEED = "EXO_SPEED";
|
||||
|
||||
@ -765,7 +766,7 @@ public final class MediaSessionConnector {
|
||||
queueNavigator != null
|
||||
? queueNavigator.getActiveQueueItemId(player)
|
||||
: MediaSessionCompat.QueueItem.UNKNOWN_ID;
|
||||
float playbackSpeed = player.getPlaybackSpeed();
|
||||
float playbackSpeed = player.getPlaybackParameters().speed;
|
||||
extras.putFloat(EXTRAS_SPEED, playbackSpeed);
|
||||
float sessionPlaybackSpeed = player.isPlaying() ? playbackSpeed : 0f;
|
||||
builder
|
||||
@ -1134,7 +1135,7 @@ public final class MediaSessionConnector {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPlaybackSpeedChanged(float playbackSpeed) {
|
||||
public void onPlaybackParametersChanged(PlaybackParameters playbackParameters) {
|
||||
invalidateMediaSessionPlaybackState();
|
||||
}
|
||||
|
||||
|
@ -592,37 +592,7 @@ public final class Format implements Parcelable {
|
||||
// Build.
|
||||
|
||||
public Format build() {
|
||||
return new Format(
|
||||
id,
|
||||
label,
|
||||
language,
|
||||
selectionFlags,
|
||||
roleFlags,
|
||||
averageBitrate,
|
||||
peakBitrate,
|
||||
codecs,
|
||||
metadata,
|
||||
containerMimeType,
|
||||
sampleMimeType,
|
||||
maxInputSize,
|
||||
initializationData,
|
||||
drmInitData,
|
||||
subsampleOffsetUs,
|
||||
width,
|
||||
height,
|
||||
frameRate,
|
||||
rotationDegrees,
|
||||
pixelWidthHeightRatio,
|
||||
projectionData,
|
||||
stereoMode,
|
||||
colorInfo,
|
||||
channelCount,
|
||||
sampleRate,
|
||||
pcmEncoding,
|
||||
encoderDelay,
|
||||
encoderPadding,
|
||||
accessibilityChannel,
|
||||
exoMediaCryptoType);
|
||||
return new Format(/* builder= */ this);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1211,86 +1181,51 @@ public final class Format implements Parcelable {
|
||||
return new Builder().setId(id).setSampleMimeType(sampleMimeType).build();
|
||||
}
|
||||
|
||||
// Some fields are deprecated but they're still assigned below.
|
||||
/* package */ Format(
|
||||
@Nullable String id,
|
||||
@Nullable String label,
|
||||
@Nullable String language,
|
||||
@C.SelectionFlags int selectionFlags,
|
||||
@C.RoleFlags int roleFlags,
|
||||
int averageBitrate,
|
||||
int peakBitrate,
|
||||
@Nullable String codecs,
|
||||
@Nullable Metadata metadata,
|
||||
// Container specific.
|
||||
@Nullable String containerMimeType,
|
||||
// Sample specific.
|
||||
@Nullable String sampleMimeType,
|
||||
int maxInputSize,
|
||||
@Nullable List<byte[]> initializationData,
|
||||
@Nullable DrmInitData drmInitData,
|
||||
long subsampleOffsetUs,
|
||||
// Video specific.
|
||||
int width,
|
||||
int height,
|
||||
float frameRate,
|
||||
int rotationDegrees,
|
||||
float pixelWidthHeightRatio,
|
||||
@Nullable byte[] projectionData,
|
||||
@C.StereoMode int stereoMode,
|
||||
@Nullable ColorInfo colorInfo,
|
||||
// Audio specific.
|
||||
int channelCount,
|
||||
int sampleRate,
|
||||
@C.PcmEncoding int pcmEncoding,
|
||||
int encoderDelay,
|
||||
int encoderPadding,
|
||||
// Text specific.
|
||||
int accessibilityChannel,
|
||||
// Provided by source.
|
||||
@Nullable Class<? extends ExoMediaCrypto> exoMediaCryptoType) {
|
||||
this.id = id;
|
||||
this.label = label;
|
||||
this.language = Util.normalizeLanguageCode(language);
|
||||
this.selectionFlags = selectionFlags;
|
||||
this.roleFlags = roleFlags;
|
||||
this.averageBitrate = averageBitrate;
|
||||
this.peakBitrate = peakBitrate;
|
||||
this.bitrate = peakBitrate != NO_VALUE ? peakBitrate : averageBitrate;
|
||||
this.codecs = codecs;
|
||||
this.metadata = metadata;
|
||||
private Format(Builder builder) {
|
||||
id = builder.id;
|
||||
label = builder.label;
|
||||
language = Util.normalizeLanguageCode(builder.language);
|
||||
selectionFlags = builder.selectionFlags;
|
||||
roleFlags = builder.roleFlags;
|
||||
averageBitrate = builder.averageBitrate;
|
||||
peakBitrate = builder.peakBitrate;
|
||||
bitrate = peakBitrate != NO_VALUE ? peakBitrate : averageBitrate;
|
||||
codecs = builder.codecs;
|
||||
metadata = builder.metadata;
|
||||
// Container specific.
|
||||
this.containerMimeType = containerMimeType;
|
||||
containerMimeType = builder.containerMimeType;
|
||||
// Sample specific.
|
||||
this.sampleMimeType = sampleMimeType;
|
||||
this.maxInputSize = maxInputSize;
|
||||
this.initializationData =
|
||||
initializationData == null ? Collections.emptyList() : initializationData;
|
||||
this.drmInitData = drmInitData;
|
||||
this.subsampleOffsetUs = subsampleOffsetUs;
|
||||
sampleMimeType = builder.sampleMimeType;
|
||||
maxInputSize = builder.maxInputSize;
|
||||
initializationData =
|
||||
builder.initializationData == null ? Collections.emptyList() : builder.initializationData;
|
||||
drmInitData = builder.drmInitData;
|
||||
subsampleOffsetUs = builder.subsampleOffsetUs;
|
||||
// Video specific.
|
||||
this.width = width;
|
||||
this.height = height;
|
||||
this.frameRate = frameRate;
|
||||
this.rotationDegrees = rotationDegrees == NO_VALUE ? 0 : rotationDegrees;
|
||||
this.pixelWidthHeightRatio = pixelWidthHeightRatio == NO_VALUE ? 1 : pixelWidthHeightRatio;
|
||||
this.projectionData = projectionData;
|
||||
this.stereoMode = stereoMode;
|
||||
this.colorInfo = colorInfo;
|
||||
width = builder.width;
|
||||
height = builder.height;
|
||||
frameRate = builder.frameRate;
|
||||
rotationDegrees = builder.rotationDegrees == NO_VALUE ? 0 : builder.rotationDegrees;
|
||||
pixelWidthHeightRatio =
|
||||
builder.pixelWidthHeightRatio == NO_VALUE ? 1 : builder.pixelWidthHeightRatio;
|
||||
projectionData = builder.projectionData;
|
||||
stereoMode = builder.stereoMode;
|
||||
colorInfo = builder.colorInfo;
|
||||
// Audio specific.
|
||||
this.channelCount = channelCount;
|
||||
this.sampleRate = sampleRate;
|
||||
this.pcmEncoding = pcmEncoding;
|
||||
this.encoderDelay = encoderDelay == NO_VALUE ? 0 : encoderDelay;
|
||||
this.encoderPadding = encoderPadding == NO_VALUE ? 0 : encoderPadding;
|
||||
channelCount = builder.channelCount;
|
||||
sampleRate = builder.sampleRate;
|
||||
pcmEncoding = builder.pcmEncoding;
|
||||
encoderDelay = builder.encoderDelay == NO_VALUE ? 0 : builder.encoderDelay;
|
||||
encoderPadding = builder.encoderPadding == NO_VALUE ? 0 : builder.encoderPadding;
|
||||
// Text specific.
|
||||
this.accessibilityChannel = accessibilityChannel;
|
||||
accessibilityChannel = builder.accessibilityChannel;
|
||||
// Provided by source.
|
||||
if (exoMediaCryptoType == null && drmInitData != null) {
|
||||
if (builder.exoMediaCryptoType == null && drmInitData != null) {
|
||||
// Encrypted content must always have a non-null exoMediaCryptoType.
|
||||
exoMediaCryptoType = UnsupportedMediaCrypto.class;
|
||||
} else {
|
||||
exoMediaCryptoType = builder.exoMediaCryptoType;
|
||||
}
|
||||
this.exoMediaCryptoType = exoMediaCryptoType;
|
||||
}
|
||||
|
||||
// Some fields are deprecated but they're still assigned below.
|
||||
|
@ -15,6 +15,7 @@
|
||||
*/
|
||||
package com.google.android.exoplayer2.util;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.Arrays;
|
||||
|
||||
@ -219,11 +220,12 @@ public final class NalUnitUtil {
|
||||
* Returns whether the NAL unit with the specified header contains supplemental enhancement
|
||||
* information.
|
||||
*
|
||||
* @param mimeType The sample MIME type.
|
||||
* @param mimeType The sample MIME type, or {@code null} if unknown.
|
||||
* @param nalUnitHeaderFirstByte The first byte of nal_unit().
|
||||
* @return Whether the NAL unit with the specified header is an SEI NAL unit.
|
||||
* @return Whether the NAL unit with the specified header is an SEI NAL unit. False is returned if
|
||||
* the {@code MimeType} is {@code null}.
|
||||
*/
|
||||
public static boolean isNalUnitSei(String mimeType, byte nalUnitHeaderFirstByte) {
|
||||
public static boolean isNalUnitSei(@Nullable String mimeType, byte nalUnitHeaderFirstByte) {
|
||||
return (MimeTypes.VIDEO_H264.equals(mimeType)
|
||||
&& (nalUnitHeaderFirstByte & 0x1F) == H264_NAL_UNIT_TYPE_SEI)
|
||||
|| (MimeTypes.VIDEO_H265.equals(mimeType)
|
||||
|
@ -405,6 +405,21 @@ public final class Util {
|
||||
return concatenation;
|
||||
}
|
||||
|
||||
/**
|
||||
* Copies the contents of {@code list} into {@code array}.
|
||||
*
|
||||
* <p>{@code list.size()} must be the same as {@code array.length} to ensure the contents can be
|
||||
* copied into {@code array} without leaving any nulls at the end.
|
||||
*
|
||||
* @param list The list to copy items from.
|
||||
* @param array The array to copy items to.
|
||||
*/
|
||||
@SuppressWarnings("nullness:toArray.nullable.elements.not.newarray")
|
||||
public static <T> void nullSafeListToArray(List<T> list, T[] array) {
|
||||
Assertions.checkState(list.size() == array.length);
|
||||
list.toArray(array);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a {@link Handler} on the current {@link Looper} thread.
|
||||
*
|
||||
@ -2362,7 +2377,7 @@ public final class Util {
|
||||
case TelephonyManager.NETWORK_TYPE_LTE:
|
||||
return C.NETWORK_TYPE_4G;
|
||||
case TelephonyManager.NETWORK_TYPE_NR:
|
||||
return C.NETWORK_TYPE_5G;
|
||||
return SDK_INT >= 29 ? C.NETWORK_TYPE_5G : C.NETWORK_TYPE_UNKNOWN;
|
||||
case TelephonyManager.NETWORK_TYPE_IWLAN:
|
||||
return C.NETWORK_TYPE_WIFI;
|
||||
case TelephonyManager.NETWORK_TYPE_GSM:
|
||||
|
@ -90,37 +90,38 @@ public final class FormatTest {
|
||||
C.COLOR_TRANSFER_SDR,
|
||||
new byte[] {1, 2, 3, 4, 5, 6, 7});
|
||||
|
||||
return new Format(
|
||||
"id",
|
||||
"label",
|
||||
"language",
|
||||
C.SELECTION_FLAG_DEFAULT,
|
||||
C.ROLE_FLAG_MAIN,
|
||||
/* averageBitrate= */ 1024,
|
||||
/* peakBitrate= */ 2048,
|
||||
"codec",
|
||||
metadata,
|
||||
/* containerMimeType= */ MimeTypes.VIDEO_MP4,
|
||||
/* sampleMimeType= */ MimeTypes.VIDEO_H264,
|
||||
/* maxInputSize= */ 5000,
|
||||
initializationData,
|
||||
drmInitData,
|
||||
Format.OFFSET_SAMPLE_RELATIVE,
|
||||
/* width= */ 1920,
|
||||
/* height= */ 1080,
|
||||
/* frameRate= */ 24,
|
||||
/* rotationDegrees= */ 90,
|
||||
/* pixelWidthHeightRatio= */ 4,
|
||||
projectionData,
|
||||
C.STEREO_MODE_TOP_BOTTOM,
|
||||
colorInfo,
|
||||
/* channelCount= */ 6,
|
||||
/* sampleRate= */ 44100,
|
||||
C.ENCODING_PCM_24BIT,
|
||||
/* encoderDelay= */ 1001,
|
||||
/* encoderPadding= */ 1002,
|
||||
/* accessibilityChannel= */ 2,
|
||||
/* exoMediaCryptoType= */ ExoMediaCrypto.class);
|
||||
return new Format.Builder()
|
||||
.setId("id")
|
||||
.setLabel("label")
|
||||
.setLanguage("language")
|
||||
.setSelectionFlags(C.SELECTION_FLAG_DEFAULT)
|
||||
.setRoleFlags(C.ROLE_FLAG_MAIN)
|
||||
.setAverageBitrate(1024)
|
||||
.setPeakBitrate(2048)
|
||||
.setCodecs("codec")
|
||||
.setMetadata(metadata)
|
||||
.setContainerMimeType(MimeTypes.VIDEO_MP4)
|
||||
.setSampleMimeType(MimeTypes.VIDEO_H264)
|
||||
.setMaxInputSize(5000)
|
||||
.setInitializationData(initializationData)
|
||||
.setDrmInitData(drmInitData)
|
||||
.setSubsampleOffsetUs(Format.OFFSET_SAMPLE_RELATIVE)
|
||||
.setWidth(1920)
|
||||
.setHeight(1080)
|
||||
.setFrameRate(24)
|
||||
.setRotationDegrees(90)
|
||||
.setPixelWidthHeightRatio(4)
|
||||
.setProjectionData(projectionData)
|
||||
.setStereoMode(C.STEREO_MODE_TOP_BOTTOM)
|
||||
.setColorInfo(colorInfo)
|
||||
.setChannelCount(6)
|
||||
.setSampleRate(44100)
|
||||
.setPcmEncoding(C.ENCODING_PCM_24BIT)
|
||||
.setEncoderDelay(1001)
|
||||
.setEncoderPadding(1002)
|
||||
.setAccessibilityChannel(2)
|
||||
.setExoMediaCryptoType(ExoMediaCrypto.class)
|
||||
.build();
|
||||
}
|
||||
|
||||
/** Generates an array of random bytes with the specified length. */
|
||||
|
@ -27,20 +27,20 @@ import com.google.android.exoplayer2.util.StandaloneMediaClock;
|
||||
*/
|
||||
/* package */ final class DefaultMediaClock implements MediaClock {
|
||||
|
||||
/** Listener interface to be notified of changes to the active playback speed. */
|
||||
public interface PlaybackSpeedListener {
|
||||
/** Listener interface to be notified of changes to the active playback parameters. */
|
||||
public interface PlaybackParametersListener {
|
||||
|
||||
/**
|
||||
* Called when the active playback speed changed. Will not be called for {@link
|
||||
* #setPlaybackSpeed(float)}.
|
||||
* Called when the active playback parameters changed. Will not be called for {@link
|
||||
* #setPlaybackParameters(PlaybackParameters)}.
|
||||
*
|
||||
* @param newPlaybackSpeed The newly active playback speed.
|
||||
* @param newPlaybackParameters The newly active playback parameters.
|
||||
*/
|
||||
void onPlaybackSpeedChanged(float newPlaybackSpeed);
|
||||
void onPlaybackParametersChanged(PlaybackParameters newPlaybackParameters);
|
||||
}
|
||||
|
||||
private final StandaloneMediaClock standaloneClock;
|
||||
private final PlaybackSpeedListener listener;
|
||||
private final PlaybackParametersListener listener;
|
||||
|
||||
@Nullable private Renderer rendererClockSource;
|
||||
@Nullable private MediaClock rendererClock;
|
||||
@ -48,13 +48,13 @@ import com.google.android.exoplayer2.util.StandaloneMediaClock;
|
||||
private boolean standaloneClockIsStarted;
|
||||
|
||||
/**
|
||||
* Creates a new instance with listener for playback speed changes and a {@link Clock} to use for
|
||||
* the standalone clock implementation.
|
||||
* Creates a new instance with a listener for playback parameters changes and a {@link Clock} to
|
||||
* use for the standalone clock implementation.
|
||||
*
|
||||
* @param listener A {@link PlaybackSpeedListener} to listen for playback speed changes.
|
||||
* @param listener A {@link PlaybackParametersListener} to listen for playback parameters changes.
|
||||
* @param clock A {@link Clock}.
|
||||
*/
|
||||
public DefaultMediaClock(PlaybackSpeedListener listener, Clock clock) {
|
||||
public DefaultMediaClock(PlaybackParametersListener listener, Clock clock) {
|
||||
this.listener = listener;
|
||||
this.standaloneClock = new StandaloneMediaClock(clock);
|
||||
isUsingStandaloneClock = true;
|
||||
@ -102,7 +102,7 @@ import com.google.android.exoplayer2.util.StandaloneMediaClock;
|
||||
}
|
||||
this.rendererClock = rendererMediaClock;
|
||||
this.rendererClockSource = renderer;
|
||||
rendererClock.setPlaybackSpeed(standaloneClock.getPlaybackSpeed());
|
||||
rendererClock.setPlaybackParameters(standaloneClock.getPlaybackParameters());
|
||||
}
|
||||
}
|
||||
|
||||
@ -140,19 +140,19 @@ import com.google.android.exoplayer2.util.StandaloneMediaClock;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setPlaybackSpeed(float playbackSpeed) {
|
||||
public void setPlaybackParameters(PlaybackParameters playbackParameters) {
|
||||
if (rendererClock != null) {
|
||||
rendererClock.setPlaybackSpeed(playbackSpeed);
|
||||
playbackSpeed = rendererClock.getPlaybackSpeed();
|
||||
rendererClock.setPlaybackParameters(playbackParameters);
|
||||
playbackParameters = rendererClock.getPlaybackParameters();
|
||||
}
|
||||
standaloneClock.setPlaybackSpeed(playbackSpeed);
|
||||
standaloneClock.setPlaybackParameters(playbackParameters);
|
||||
}
|
||||
|
||||
@Override
|
||||
public float getPlaybackSpeed() {
|
||||
public PlaybackParameters getPlaybackParameters() {
|
||||
return rendererClock != null
|
||||
? rendererClock.getPlaybackSpeed()
|
||||
: standaloneClock.getPlaybackSpeed();
|
||||
? rendererClock.getPlaybackParameters()
|
||||
: standaloneClock.getPlaybackParameters();
|
||||
}
|
||||
|
||||
private void syncClocks(boolean isReadingAhead) {
|
||||
@ -180,10 +180,10 @@ import com.google.android.exoplayer2.util.StandaloneMediaClock;
|
||||
}
|
||||
// Continuously sync stand-alone clock to renderer clock so that it can take over if needed.
|
||||
standaloneClock.resetPosition(rendererClockPositionUs);
|
||||
float playbackSpeed = rendererClock.getPlaybackSpeed();
|
||||
if (playbackSpeed != standaloneClock.getPlaybackSpeed()) {
|
||||
standaloneClock.setPlaybackSpeed(playbackSpeed);
|
||||
listener.onPlaybackSpeedChanged(playbackSpeed);
|
||||
PlaybackParameters playbackParameters = rendererClock.getPlaybackParameters();
|
||||
if (!playbackParameters.equals(standaloneClock.getPlaybackParameters())) {
|
||||
standaloneClock.setPlaybackParameters(playbackParameters);
|
||||
listener.onPlaybackParametersChanged(playbackParameters);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -619,32 +619,17 @@ import java.util.concurrent.TimeoutException;
|
||||
/* seekProcessed= */ true);
|
||||
}
|
||||
|
||||
/** @deprecated Use {@link #setPlaybackSpeed(float)} instead. */
|
||||
@SuppressWarnings("deprecation")
|
||||
@Deprecated
|
||||
@Override
|
||||
public void setPlaybackParameters(@Nullable PlaybackParameters playbackParameters) {
|
||||
setPlaybackSpeed(
|
||||
playbackParameters != null ? playbackParameters.speed : Player.DEFAULT_PLAYBACK_SPEED);
|
||||
}
|
||||
|
||||
/** @deprecated Use {@link #getPlaybackSpeed()} instead. */
|
||||
@SuppressWarnings("deprecation")
|
||||
@Deprecated
|
||||
@Override
|
||||
public PlaybackParameters getPlaybackParameters() {
|
||||
return new PlaybackParameters(playbackInfo.playbackSpeed);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setPlaybackSpeed(float playbackSpeed) {
|
||||
checkState(playbackSpeed > 0);
|
||||
if (playbackInfo.playbackSpeed == playbackSpeed) {
|
||||
if (playbackParameters == null) {
|
||||
playbackParameters = PlaybackParameters.DEFAULT;
|
||||
}
|
||||
if (playbackInfo.playbackParameters.equals(playbackParameters)) {
|
||||
return;
|
||||
}
|
||||
PlaybackInfo newPlaybackInfo = playbackInfo.copyWithPlaybackSpeed(playbackSpeed);
|
||||
PlaybackInfo newPlaybackInfo = playbackInfo.copyWithPlaybackParameters(playbackParameters);
|
||||
pendingOperationAcks++;
|
||||
internalPlayer.setPlaybackSpeed(playbackSpeed);
|
||||
internalPlayer.setPlaybackParameters(playbackParameters);
|
||||
updatePlaybackInfo(
|
||||
newPlaybackInfo,
|
||||
/* positionDiscontinuity= */ false,
|
||||
@ -655,8 +640,8 @@ import java.util.concurrent.TimeoutException;
|
||||
}
|
||||
|
||||
@Override
|
||||
public float getPlaybackSpeed() {
|
||||
return playbackInfo.playbackSpeed;
|
||||
public PlaybackParameters getPlaybackParameters() {
|
||||
return playbackInfo.playbackParameters;
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -1366,7 +1351,7 @@ import java.util.concurrent.TimeoutException;
|
||||
private final boolean playWhenReadyChanged;
|
||||
private final boolean playbackSuppressionReasonChanged;
|
||||
private final boolean isPlayingChanged;
|
||||
private final boolean playbackSpeedChanged;
|
||||
private final boolean playbackParametersChanged;
|
||||
private final boolean offloadSchedulingEnabledChanged;
|
||||
|
||||
public PlaybackInfoUpdate(
|
||||
@ -1405,7 +1390,8 @@ import java.util.concurrent.TimeoutException;
|
||||
playbackSuppressionReasonChanged =
|
||||
previousPlaybackInfo.playbackSuppressionReason != playbackInfo.playbackSuppressionReason;
|
||||
isPlayingChanged = isPlaying(previousPlaybackInfo) != isPlaying(playbackInfo);
|
||||
playbackSpeedChanged = previousPlaybackInfo.playbackSpeed != playbackInfo.playbackSpeed;
|
||||
playbackParametersChanged =
|
||||
!previousPlaybackInfo.playbackParameters.equals(playbackInfo.playbackParameters);
|
||||
offloadSchedulingEnabledChanged =
|
||||
previousPlaybackInfo.offloadSchedulingEnabled != playbackInfo.offloadSchedulingEnabled;
|
||||
}
|
||||
@ -1473,13 +1459,11 @@ import java.util.concurrent.TimeoutException;
|
||||
invokeAll(
|
||||
listenerSnapshot, listener -> listener.onIsPlayingChanged(isPlaying(playbackInfo)));
|
||||
}
|
||||
if (playbackSpeedChanged) {
|
||||
PlaybackParameters playbackParameters = new PlaybackParameters(playbackInfo.playbackSpeed);
|
||||
if (playbackParametersChanged) {
|
||||
invokeAll(
|
||||
listenerSnapshot,
|
||||
listener -> {
|
||||
listener.onPlaybackSpeedChanged(playbackInfo.playbackSpeed);
|
||||
listener.onPlaybackParametersChanged(playbackParameters);
|
||||
listener.onPlaybackParametersChanged(playbackInfo.playbackParameters);
|
||||
});
|
||||
}
|
||||
if (seekProcessed) {
|
||||
|
@ -27,7 +27,7 @@ import android.os.SystemClock;
|
||||
import android.util.Pair;
|
||||
import androidx.annotation.CheckResult;
|
||||
import androidx.annotation.Nullable;
|
||||
import com.google.android.exoplayer2.DefaultMediaClock.PlaybackSpeedListener;
|
||||
import com.google.android.exoplayer2.DefaultMediaClock.PlaybackParametersListener;
|
||||
import com.google.android.exoplayer2.Player.DiscontinuityReason;
|
||||
import com.google.android.exoplayer2.Player.PlayWhenReadyChangeReason;
|
||||
import com.google.android.exoplayer2.Player.PlaybackSuppressionReason;
|
||||
@ -61,7 +61,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
||||
MediaPeriod.Callback,
|
||||
TrackSelector.InvalidationListener,
|
||||
MediaSourceList.MediaSourceListInfoRefreshListener,
|
||||
PlaybackSpeedListener,
|
||||
PlaybackParametersListener,
|
||||
PlayerMessage.Sender {
|
||||
|
||||
private static final String TAG = "ExoPlayerImplInternal";
|
||||
@ -121,7 +121,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
||||
private static final int MSG_SET_PLAY_WHEN_READY = 1;
|
||||
private static final int MSG_DO_SOME_WORK = 2;
|
||||
private static final int MSG_SEEK_TO = 3;
|
||||
private static final int MSG_SET_PLAYBACK_SPEED = 4;
|
||||
private static final int MSG_SET_PLAYBACK_PARAMETERS = 4;
|
||||
private static final int MSG_SET_SEEK_PARAMETERS = 5;
|
||||
private static final int MSG_STOP = 6;
|
||||
private static final int MSG_RELEASE = 7;
|
||||
@ -133,7 +133,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
||||
private static final int MSG_SET_FOREGROUND_MODE = 13;
|
||||
private static final int MSG_SEND_MESSAGE = 14;
|
||||
private static final int MSG_SEND_MESSAGE_TO_TARGET_THREAD = 15;
|
||||
private static final int MSG_PLAYBACK_SPEED_CHANGED_INTERNAL = 16;
|
||||
private static final int MSG_PLAYBACK_PARAMETERS_CHANGED_INTERNAL = 16;
|
||||
private static final int MSG_SET_MEDIA_SOURCES = 17;
|
||||
private static final int MSG_ADD_MEDIA_SOURCES = 18;
|
||||
private static final int MSG_MOVE_MEDIA_SOURCES = 19;
|
||||
@ -163,6 +163,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
||||
private final BandwidthMeter bandwidthMeter;
|
||||
private final HandlerWrapper handler;
|
||||
private final HandlerThread internalPlaybackThread;
|
||||
private final Looper playbackLooper;
|
||||
private final Timeline.Window window;
|
||||
private final Timeline.Period period;
|
||||
private final long backBufferDurationUs;
|
||||
@ -252,7 +253,8 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
||||
// not normally change to this priority" is incorrect.
|
||||
internalPlaybackThread = new HandlerThread("ExoPlayer:Playback", Process.THREAD_PRIORITY_AUDIO);
|
||||
internalPlaybackThread.start();
|
||||
handler = clock.createHandler(internalPlaybackThread.getLooper(), this);
|
||||
playbackLooper = internalPlaybackThread.getLooper();
|
||||
handler = clock.createHandler(playbackLooper, this);
|
||||
}
|
||||
|
||||
public void experimentalSetReleaseTimeoutMs(long releaseTimeoutMs) {
|
||||
@ -301,8 +303,8 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
||||
.sendToTarget();
|
||||
}
|
||||
|
||||
public void setPlaybackSpeed(float playbackSpeed) {
|
||||
handler.obtainMessage(MSG_SET_PLAYBACK_SPEED, playbackSpeed).sendToTarget();
|
||||
public void setPlaybackParameters(PlaybackParameters playbackParameters) {
|
||||
handler.obtainMessage(MSG_SET_PLAYBACK_PARAMETERS, playbackParameters).sendToTarget();
|
||||
}
|
||||
|
||||
public void setSeekParameters(SeekParameters seekParameters) {
|
||||
@ -403,7 +405,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
||||
}
|
||||
|
||||
public Looper getPlaybackLooper() {
|
||||
return internalPlaybackThread.getLooper();
|
||||
return playbackLooper;
|
||||
}
|
||||
|
||||
// Playlist.PlaylistInfoRefreshListener implementation.
|
||||
@ -432,11 +434,11 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
||||
handler.sendEmptyMessage(MSG_TRACK_SELECTION_INVALIDATED);
|
||||
}
|
||||
|
||||
// DefaultMediaClock.PlaybackSpeedListener implementation.
|
||||
// DefaultMediaClock.PlaybackParametersListener implementation.
|
||||
|
||||
@Override
|
||||
public void onPlaybackSpeedChanged(float playbackSpeed) {
|
||||
sendPlaybackSpeedChangedInternal(playbackSpeed, /* acknowledgeCommand= */ false);
|
||||
public void onPlaybackParametersChanged(PlaybackParameters newPlaybackParameters) {
|
||||
sendPlaybackParametersChangedInternal(newPlaybackParameters, /* acknowledgeCommand= */ false);
|
||||
}
|
||||
|
||||
// Handler.Callback implementation.
|
||||
@ -467,8 +469,8 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
||||
case MSG_SEEK_TO:
|
||||
seekToInternal((SeekPosition) msg.obj);
|
||||
break;
|
||||
case MSG_SET_PLAYBACK_SPEED:
|
||||
setPlaybackSpeedInternal((Float) msg.obj);
|
||||
case MSG_SET_PLAYBACK_PARAMETERS:
|
||||
setPlaybackParametersInternal((PlaybackParameters) msg.obj);
|
||||
break;
|
||||
case MSG_SET_SEEK_PARAMETERS:
|
||||
setSeekParametersInternal((SeekParameters) msg.obj);
|
||||
@ -489,8 +491,9 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
||||
case MSG_TRACK_SELECTION_INVALIDATED:
|
||||
reselectTracksInternal();
|
||||
break;
|
||||
case MSG_PLAYBACK_SPEED_CHANGED_INTERNAL:
|
||||
handlePlaybackSpeed((Float) msg.obj, /* acknowledgeCommand= */ msg.arg1 != 0);
|
||||
case MSG_PLAYBACK_PARAMETERS_CHANGED_INTERNAL:
|
||||
handlePlaybackParameters(
|
||||
(PlaybackParameters) msg.obj, /* acknowledgeCommand= */ msg.arg1 != 0);
|
||||
break;
|
||||
case MSG_SEND_MESSAGE:
|
||||
sendMessageInternal((PlayerMessage) msg.obj);
|
||||
@ -733,11 +736,13 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
||||
private void setPauseAtEndOfWindowInternal(boolean pauseAtEndOfWindow)
|
||||
throws ExoPlaybackException {
|
||||
this.pauseAtEndOfWindow = pauseAtEndOfWindow;
|
||||
if (queue.getReadingPeriod() != queue.getPlayingPeriod()) {
|
||||
seekToCurrentPosition(/* sendDiscontinuity= */ true);
|
||||
}
|
||||
resetPendingPauseAtEndOfPeriod();
|
||||
handleLoadingMediaPeriodChanged(/* loadingTrackSelectionChanged= */ false);
|
||||
if (pendingPauseAtEndOfPeriod && queue.getReadingPeriod() != queue.getPlayingPeriod()) {
|
||||
// When pausing is required, we need to set the streams of the playing period final. If we
|
||||
// already started reading the next period, we need to flush the renderers.
|
||||
seekToCurrentPosition(/* sendDiscontinuity= */ true);
|
||||
handleLoadingMediaPeriodChanged(/* loadingTrackSelectionChanged= */ false);
|
||||
}
|
||||
}
|
||||
|
||||
private void setOffloadSchedulingEnabledInternal(boolean offloadSchedulingEnabled) {
|
||||
@ -1182,9 +1187,10 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
||||
notifyTrackSelectionDiscontinuity();
|
||||
}
|
||||
|
||||
private void setPlaybackSpeedInternal(float playbackSpeed) {
|
||||
mediaClock.setPlaybackSpeed(playbackSpeed);
|
||||
sendPlaybackSpeedChangedInternal(mediaClock.getPlaybackSpeed(), /* acknowledgeCommand= */ true);
|
||||
private void setPlaybackParametersInternal(PlaybackParameters playbackParameters) {
|
||||
mediaClock.setPlaybackParameters(playbackParameters);
|
||||
sendPlaybackParametersChangedInternal(
|
||||
mediaClock.getPlaybackParameters(), /* acknowledgeCommand= */ true);
|
||||
}
|
||||
|
||||
private void setSeekParametersInternal(SeekParameters seekParameters) {
|
||||
@ -1301,7 +1307,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
||||
mediaPeriodId,
|
||||
playbackInfo.playWhenReady,
|
||||
playbackInfo.playbackSuppressionReason,
|
||||
playbackInfo.playbackSpeed,
|
||||
playbackInfo.playbackParameters,
|
||||
startPositionUs,
|
||||
/* totalBufferedDurationUs= */ 0,
|
||||
startPositionUs,
|
||||
@ -1361,7 +1367,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
||||
}
|
||||
|
||||
private void sendMessageToTarget(PlayerMessage message) throws ExoPlaybackException {
|
||||
if (message.getHandler().getLooper() == handler.getLooper()) {
|
||||
if (message.getHandler().getLooper() == playbackLooper) {
|
||||
deliverMessage(message);
|
||||
if (playbackInfo.playbackState == Player.STATE_READY
|
||||
|| playbackInfo.playbackState == Player.STATE_BUFFERING) {
|
||||
@ -1506,7 +1512,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
||||
}
|
||||
|
||||
private void reselectTracksInternal() throws ExoPlaybackException {
|
||||
float playbackSpeed = mediaClock.getPlaybackSpeed();
|
||||
float playbackSpeed = mediaClock.getPlaybackParameters().speed;
|
||||
// Reselect tracks on each period in turn, until the selection changes.
|
||||
MediaPeriodHolder periodHolder = queue.getPlayingPeriod();
|
||||
MediaPeriodHolder readingPeriodHolder = queue.getReadingPeriod();
|
||||
@ -1624,7 +1630,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
||||
boolean bufferedToEnd = loadingHolder.isFullyBuffered() && loadingHolder.info.isFinal;
|
||||
return bufferedToEnd
|
||||
|| loadControl.shouldStartPlayback(
|
||||
getTotalBufferedDurationUs(), mediaClock.getPlaybackSpeed(), rebuffering);
|
||||
getTotalBufferedDurationUs(), mediaClock.getPlaybackParameters().speed, rebuffering);
|
||||
}
|
||||
|
||||
private boolean isTimelineReady() {
|
||||
@ -1960,7 +1966,8 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
||||
return;
|
||||
}
|
||||
MediaPeriodHolder loadingPeriodHolder = queue.getLoadingPeriod();
|
||||
loadingPeriodHolder.handlePrepared(mediaClock.getPlaybackSpeed(), playbackInfo.timeline);
|
||||
loadingPeriodHolder.handlePrepared(
|
||||
mediaClock.getPlaybackParameters().speed, playbackInfo.timeline);
|
||||
updateLoadControlTrackSelection(
|
||||
loadingPeriodHolder.getTrackGroups(), loadingPeriodHolder.getTrackSelectorResult());
|
||||
if (loadingPeriodHolder == queue.getPlayingPeriod()) {
|
||||
@ -1985,14 +1992,15 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
||||
maybeContinueLoading();
|
||||
}
|
||||
|
||||
private void handlePlaybackSpeed(float playbackSpeed, boolean acknowledgeCommand)
|
||||
private void handlePlaybackParameters(
|
||||
PlaybackParameters playbackParameters, boolean acknowledgeCommand)
|
||||
throws ExoPlaybackException {
|
||||
playbackInfoUpdate.incrementPendingOperationAcks(acknowledgeCommand ? 1 : 0);
|
||||
playbackInfo = playbackInfo.copyWithPlaybackSpeed(playbackSpeed);
|
||||
updateTrackSelectionPlaybackSpeed(playbackSpeed);
|
||||
playbackInfo = playbackInfo.copyWithPlaybackParameters(playbackParameters);
|
||||
updateTrackSelectionPlaybackSpeed(playbackParameters.speed);
|
||||
for (Renderer renderer : renderers) {
|
||||
if (renderer != null) {
|
||||
renderer.setOperatingRate(playbackSpeed);
|
||||
renderer.setOperatingRate(playbackParameters.speed);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -2018,7 +2026,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
||||
: loadingPeriodHolder.toPeriodTime(rendererPositionUs)
|
||||
- loadingPeriodHolder.info.startPositionUs;
|
||||
return loadControl.shouldContinueLoading(
|
||||
playbackPositionUs, bufferedDurationUs, mediaClock.getPlaybackSpeed());
|
||||
playbackPositionUs, bufferedDurationUs, mediaClock.getPlaybackParameters().speed);
|
||||
}
|
||||
|
||||
private boolean isLoadingPossible() {
|
||||
@ -2194,10 +2202,14 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
||||
loadControl.onTracksSelected(renderers, trackGroups, trackSelectorResult.selections);
|
||||
}
|
||||
|
||||
private void sendPlaybackSpeedChangedInternal(float playbackSpeed, boolean acknowledgeCommand) {
|
||||
private void sendPlaybackParametersChangedInternal(
|
||||
PlaybackParameters playbackParameters, boolean acknowledgeCommand) {
|
||||
handler
|
||||
.obtainMessage(
|
||||
MSG_PLAYBACK_SPEED_CHANGED_INTERNAL, acknowledgeCommand ? 1 : 0, 0, playbackSpeed)
|
||||
MSG_PLAYBACK_PARAMETERS_CHANGED_INTERNAL,
|
||||
acknowledgeCommand ? 1 : 0,
|
||||
0,
|
||||
playbackParameters)
|
||||
.sendToTarget();
|
||||
}
|
||||
|
||||
|
@ -63,8 +63,8 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult;
|
||||
public final boolean playWhenReady;
|
||||
/** Reason why playback is suppressed even though {@link #playWhenReady} is {@code true}. */
|
||||
@PlaybackSuppressionReason public final int playbackSuppressionReason;
|
||||
/** The playback speed. */
|
||||
public final float playbackSpeed;
|
||||
/** The playback parameters. */
|
||||
public final PlaybackParameters playbackParameters;
|
||||
/** Whether offload scheduling is enabled for the main player loop. */
|
||||
public final boolean offloadSchedulingEnabled;
|
||||
|
||||
@ -105,7 +105,7 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult;
|
||||
PLACEHOLDER_MEDIA_PERIOD_ID,
|
||||
/* playWhenReady= */ false,
|
||||
Player.PLAYBACK_SUPPRESSION_REASON_NONE,
|
||||
Player.DEFAULT_PLAYBACK_SPEED,
|
||||
PlaybackParameters.DEFAULT,
|
||||
/* bufferedPositionUs= */ 0,
|
||||
/* totalBufferedDurationUs= */ 0,
|
||||
/* positionUs= */ 0,
|
||||
@ -119,10 +119,14 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult;
|
||||
* @param periodId See {@link #periodId}.
|
||||
* @param requestedContentPositionUs See {@link #requestedContentPositionUs}.
|
||||
* @param playbackState See {@link #playbackState}.
|
||||
* @param playbackError See {@link #playbackError}.
|
||||
* @param isLoading See {@link #isLoading}.
|
||||
* @param trackGroups See {@link #trackGroups}.
|
||||
* @param trackSelectorResult See {@link #trackSelectorResult}.
|
||||
* @param loadingMediaPeriodId See {@link #loadingMediaPeriodId}.
|
||||
* @param playWhenReady See {@link #playWhenReady}.
|
||||
* @param playbackSuppressionReason See {@link #playbackSuppressionReason}.
|
||||
* @param playbackParameters See {@link #playbackParameters}.
|
||||
* @param bufferedPositionUs See {@link #bufferedPositionUs}.
|
||||
* @param totalBufferedDurationUs See {@link #totalBufferedDurationUs}.
|
||||
* @param positionUs See {@link #positionUs}.
|
||||
@ -140,7 +144,7 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult;
|
||||
MediaPeriodId loadingMediaPeriodId,
|
||||
boolean playWhenReady,
|
||||
@PlaybackSuppressionReason int playbackSuppressionReason,
|
||||
float playbackSpeed,
|
||||
PlaybackParameters playbackParameters,
|
||||
long bufferedPositionUs,
|
||||
long totalBufferedDurationUs,
|
||||
long positionUs,
|
||||
@ -156,7 +160,7 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult;
|
||||
this.loadingMediaPeriodId = loadingMediaPeriodId;
|
||||
this.playWhenReady = playWhenReady;
|
||||
this.playbackSuppressionReason = playbackSuppressionReason;
|
||||
this.playbackSpeed = playbackSpeed;
|
||||
this.playbackParameters = playbackParameters;
|
||||
this.bufferedPositionUs = bufferedPositionUs;
|
||||
this.totalBufferedDurationUs = totalBufferedDurationUs;
|
||||
this.positionUs = positionUs;
|
||||
@ -201,7 +205,7 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult;
|
||||
loadingMediaPeriodId,
|
||||
playWhenReady,
|
||||
playbackSuppressionReason,
|
||||
playbackSpeed,
|
||||
playbackParameters,
|
||||
bufferedPositionUs,
|
||||
totalBufferedDurationUs,
|
||||
positionUs,
|
||||
@ -228,7 +232,7 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult;
|
||||
loadingMediaPeriodId,
|
||||
playWhenReady,
|
||||
playbackSuppressionReason,
|
||||
playbackSpeed,
|
||||
playbackParameters,
|
||||
bufferedPositionUs,
|
||||
totalBufferedDurationUs,
|
||||
positionUs,
|
||||
@ -255,7 +259,7 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult;
|
||||
loadingMediaPeriodId,
|
||||
playWhenReady,
|
||||
playbackSuppressionReason,
|
||||
playbackSpeed,
|
||||
playbackParameters,
|
||||
bufferedPositionUs,
|
||||
totalBufferedDurationUs,
|
||||
positionUs,
|
||||
@ -282,7 +286,7 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult;
|
||||
loadingMediaPeriodId,
|
||||
playWhenReady,
|
||||
playbackSuppressionReason,
|
||||
playbackSpeed,
|
||||
playbackParameters,
|
||||
bufferedPositionUs,
|
||||
totalBufferedDurationUs,
|
||||
positionUs,
|
||||
@ -309,7 +313,7 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult;
|
||||
loadingMediaPeriodId,
|
||||
playWhenReady,
|
||||
playbackSuppressionReason,
|
||||
playbackSpeed,
|
||||
playbackParameters,
|
||||
bufferedPositionUs,
|
||||
totalBufferedDurationUs,
|
||||
positionUs,
|
||||
@ -336,7 +340,7 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult;
|
||||
loadingMediaPeriodId,
|
||||
playWhenReady,
|
||||
playbackSuppressionReason,
|
||||
playbackSpeed,
|
||||
playbackParameters,
|
||||
bufferedPositionUs,
|
||||
totalBufferedDurationUs,
|
||||
positionUs,
|
||||
@ -367,7 +371,7 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult;
|
||||
loadingMediaPeriodId,
|
||||
playWhenReady,
|
||||
playbackSuppressionReason,
|
||||
playbackSpeed,
|
||||
playbackParameters,
|
||||
bufferedPositionUs,
|
||||
totalBufferedDurationUs,
|
||||
positionUs,
|
||||
@ -375,13 +379,13 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult;
|
||||
}
|
||||
|
||||
/**
|
||||
* Copies playback info with new playback speed.
|
||||
* Copies playback info with new playback parameters.
|
||||
*
|
||||
* @param playbackSpeed New playback speed. See {@link #playbackSpeed}.
|
||||
* @return Copied playback info with new playback speed.
|
||||
* @param playbackParameters New playback parameters. See {@link #playbackParameters}.
|
||||
* @return Copied playback info with new playback parameters.
|
||||
*/
|
||||
@CheckResult
|
||||
public PlaybackInfo copyWithPlaybackSpeed(float playbackSpeed) {
|
||||
public PlaybackInfo copyWithPlaybackParameters(PlaybackParameters playbackParameters) {
|
||||
return new PlaybackInfo(
|
||||
timeline,
|
||||
periodId,
|
||||
@ -394,7 +398,7 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult;
|
||||
loadingMediaPeriodId,
|
||||
playWhenReady,
|
||||
playbackSuppressionReason,
|
||||
playbackSpeed,
|
||||
playbackParameters,
|
||||
bufferedPositionUs,
|
||||
totalBufferedDurationUs,
|
||||
positionUs,
|
||||
@ -422,7 +426,7 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult;
|
||||
loadingMediaPeriodId,
|
||||
playWhenReady,
|
||||
playbackSuppressionReason,
|
||||
playbackSpeed,
|
||||
playbackParameters,
|
||||
bufferedPositionUs,
|
||||
totalBufferedDurationUs,
|
||||
positionUs,
|
||||
|
@ -17,13 +17,9 @@ package com.google.android.exoplayer2;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import com.google.android.exoplayer2.util.Assertions;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
|
||||
/**
|
||||
* @deprecated Use {@link Player#setPlaybackSpeed(float)} and {@link
|
||||
* Player.AudioComponent#setSkipSilenceEnabled(boolean)} instead.
|
||||
*/
|
||||
@SuppressWarnings("deprecation")
|
||||
@Deprecated
|
||||
/** Parameters that apply to playback, including speed setting. */
|
||||
public final class PlaybackParameters {
|
||||
|
||||
/** The default playback parameters: real-time playback with no silence skipping. */
|
||||
@ -32,16 +28,34 @@ public final class PlaybackParameters {
|
||||
/** The factor by which playback will be sped up. */
|
||||
public final float speed;
|
||||
|
||||
/** The factor by which pitch will be shifted. */
|
||||
public final float pitch;
|
||||
|
||||
private final int scaledUsPerMs;
|
||||
|
||||
/**
|
||||
* Creates new playback parameters that set the playback speed.
|
||||
* Creates new playback parameters that set the playback speed. The pitch of audio will not be
|
||||
* adjusted, so the effect is to time-stretch the audio.
|
||||
*
|
||||
* @param speed The factor by which playback will be sped up. Must be greater than zero.
|
||||
*/
|
||||
public PlaybackParameters(float speed) {
|
||||
this(speed, /* pitch= */ 1f);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates new playback parameters that set the playback speed/pitch.
|
||||
*
|
||||
* @param speed The factor by which playback will be sped up. Must be greater than zero.
|
||||
* @param pitch The factor by which the pitch of audio will be adjusted. Must be greater than
|
||||
* zero. Useful values are {@code 1} (to time-stretch audio) and the same value as passed in
|
||||
* as the {@code speed} (to resample audio, which is useful for slow-motion videos).
|
||||
*/
|
||||
public PlaybackParameters(float speed, float pitch) {
|
||||
Assertions.checkArgument(speed > 0);
|
||||
Assertions.checkArgument(pitch > 0);
|
||||
this.speed = speed;
|
||||
this.pitch = pitch;
|
||||
scaledUsPerMs = Math.round(speed * 1000f);
|
||||
}
|
||||
|
||||
@ -65,11 +79,19 @@ public final class PlaybackParameters {
|
||||
return false;
|
||||
}
|
||||
PlaybackParameters other = (PlaybackParameters) obj;
|
||||
return this.speed == other.speed;
|
||||
return this.speed == other.speed && this.pitch == other.pitch;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Float.floatToRawIntBits(speed);
|
||||
int result = 17;
|
||||
result = 31 * result + Float.floatToRawIntBits(speed);
|
||||
result = 31 * result + Float.floatToRawIntBits(pitch);
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return Util.formatInvariant("PlaybackParameters(speed=%.2f, pitch=%.2f)", speed, pitch);
|
||||
}
|
||||
}
|
||||
|
@ -583,21 +583,15 @@ public interface Player {
|
||||
default void onPositionDiscontinuity(@DiscontinuityReason int reason) {}
|
||||
|
||||
/**
|
||||
* @deprecated Use {@link #onPlaybackSpeedChanged(float)} and {@link
|
||||
* AudioListener#onSkipSilenceEnabledChanged(boolean)} instead.
|
||||
* Called when the current playback parameters change. The playback parameters may change due to
|
||||
* a call to {@link #setPlaybackParameters(PlaybackParameters)}, or the player itself may change
|
||||
* them (for example, if audio playback switches to passthrough or offload mode, where speed
|
||||
* adjustment is no longer possible).
|
||||
*
|
||||
* @param playbackParameters The playback parameters.
|
||||
*/
|
||||
@SuppressWarnings("deprecation")
|
||||
@Deprecated
|
||||
default void onPlaybackParametersChanged(PlaybackParameters playbackParameters) {}
|
||||
|
||||
/**
|
||||
* Called when the current playback speed changes. The normal playback speed is 1. The speed may
|
||||
* change due to a call to {@link #setPlaybackSpeed(float)}, or the player itself may change it
|
||||
* (for example, if audio playback switches to passthrough mode, where speed adjustment is no
|
||||
* longer possible).
|
||||
*/
|
||||
default void onPlaybackSpeedChanged(float playbackSpeed) {}
|
||||
|
||||
/**
|
||||
* @deprecated Seeks are processed without delay. Listen to {@link
|
||||
* #onPositionDiscontinuity(int)} with reason {@link #DISCONTINUITY_REASON_SEEK} instead.
|
||||
@ -810,9 +804,6 @@ public interface Player {
|
||||
*/
|
||||
int MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED = 3;
|
||||
|
||||
/** The default playback speed. */
|
||||
float DEFAULT_PLAYBACK_SPEED = 1.0f;
|
||||
|
||||
/** Returns the component of this player for audio output, or null if audio is not supported. */
|
||||
@Nullable
|
||||
AudioComponent getAudioComponent();
|
||||
@ -1161,39 +1152,24 @@ public interface Player {
|
||||
void next();
|
||||
|
||||
/**
|
||||
* @deprecated Use {@link #setPlaybackSpeed(float)} or {@link
|
||||
* AudioComponent#setSkipSilenceEnabled(boolean)} instead.
|
||||
* Attempts to set the playback parameters. Passing {@code null} sets the parameters to the
|
||||
* default, {@link PlaybackParameters#DEFAULT}, which means there is no speed or pitch adjustment.
|
||||
*
|
||||
* <p>Playback parameters changes may cause the player to buffer. {@link
|
||||
* EventListener#onPlaybackParametersChanged(PlaybackParameters)} will be called whenever the
|
||||
* currently active playback parameters change.
|
||||
*
|
||||
* @param playbackParameters The playback parameters, or {@code null} to use the defaults.
|
||||
*/
|
||||
@SuppressWarnings("deprecation")
|
||||
@Deprecated
|
||||
void setPlaybackParameters(@Nullable PlaybackParameters playbackParameters);
|
||||
|
||||
/**
|
||||
* @deprecated Use {@link #getPlaybackSpeed()} or {@link AudioComponent#getSkipSilenceEnabled()}
|
||||
* instead.
|
||||
* Returns the currently active playback parameters.
|
||||
*
|
||||
* @see EventListener#onPlaybackParametersChanged(PlaybackParameters)
|
||||
*/
|
||||
@SuppressWarnings("deprecation")
|
||||
@Deprecated
|
||||
PlaybackParameters getPlaybackParameters();
|
||||
|
||||
/**
|
||||
* Attempts to set the playback speed.
|
||||
*
|
||||
* <p>Playback speed changes may cause the player to buffer. {@link
|
||||
* EventListener#onPlaybackSpeedChanged(float)} will be called whenever the currently active
|
||||
* playback speed change.
|
||||
*
|
||||
* @param playbackSpeed The playback speed.
|
||||
*/
|
||||
void setPlaybackSpeed(float playbackSpeed);
|
||||
|
||||
/**
|
||||
* Returns the currently active playback speed.
|
||||
*
|
||||
* @see EventListener#onPlaybackSpeedChanged(float)
|
||||
*/
|
||||
float getPlaybackSpeed();
|
||||
|
||||
/**
|
||||
* Stops playback without resetting the player. Use {@link #pause()} rather than this method if
|
||||
* the intention is to pause playback.
|
||||
|
@ -1030,18 +1030,23 @@ public class SimpleExoPlayer extends BasePlayer
|
||||
this.priorityTaskManager = priorityTaskManager;
|
||||
}
|
||||
|
||||
/** @deprecated Use {@link #setPlaybackSpeed(float)} instead. */
|
||||
/**
|
||||
* Sets the {@link PlaybackParams} governing audio playback.
|
||||
*
|
||||
* @param params The {@link PlaybackParams}, or null to clear any previously set parameters.
|
||||
* @deprecated Use {@link #setPlaybackParameters(PlaybackParameters)}.
|
||||
*/
|
||||
@Deprecated
|
||||
@RequiresApi(23)
|
||||
public void setPlaybackParams(@Nullable PlaybackParams params) {
|
||||
float playbackSpeed;
|
||||
PlaybackParameters playbackParameters;
|
||||
if (params != null) {
|
||||
params.allowDefaults();
|
||||
playbackSpeed = params.getSpeed();
|
||||
playbackParameters = new PlaybackParameters(params.getSpeed(), params.getPitch());
|
||||
} else {
|
||||
playbackSpeed = 1.0f;
|
||||
playbackParameters = null;
|
||||
}
|
||||
setPlaybackSpeed(playbackSpeed);
|
||||
setPlaybackParameters(playbackParameters);
|
||||
}
|
||||
|
||||
/** Returns the video format currently being played, or null if no video is being played. */
|
||||
@ -1623,39 +1628,18 @@ public class SimpleExoPlayer extends BasePlayer
|
||||
player.seekTo(windowIndex, positionMs);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use {@link #setPlaybackSpeed(float)} and {@link #setSkipSilenceEnabled(boolean)}
|
||||
* instead.
|
||||
*/
|
||||
@SuppressWarnings("deprecation")
|
||||
@Deprecated
|
||||
@Override
|
||||
public void setPlaybackParameters(@Nullable PlaybackParameters playbackParameters) {
|
||||
verifyApplicationThread();
|
||||
player.setPlaybackParameters(playbackParameters);
|
||||
}
|
||||
|
||||
/** @deprecated Use {@link #getPlaybackSpeed()} and {@link #getSkipSilenceEnabled()} instead. */
|
||||
@SuppressWarnings("deprecation")
|
||||
@Deprecated
|
||||
@Override
|
||||
public PlaybackParameters getPlaybackParameters() {
|
||||
verifyApplicationThread();
|
||||
return player.getPlaybackParameters();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setPlaybackSpeed(float playbackSpeed) {
|
||||
verifyApplicationThread();
|
||||
player.setPlaybackSpeed(playbackSpeed);
|
||||
}
|
||||
|
||||
@Override
|
||||
public float getPlaybackSpeed() {
|
||||
verifyApplicationThread();
|
||||
return player.getPlaybackSpeed();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setSeekParameters(@Nullable SeekParameters seekParameters) {
|
||||
verifyApplicationThread();
|
||||
@ -2231,6 +2215,13 @@ public class SimpleExoPlayer extends BasePlayer
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAudioPositionAdvancing(long playoutStartSystemTimeMs) {
|
||||
for (AudioRendererEventListener audioDebugListener : audioDebugListeners) {
|
||||
audioDebugListener.onAudioPositionAdvancing(playoutStartSystemTimeMs);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAudioUnderrun(int bufferSize, long bufferSizeMs, long elapsedSinceLastFeedMs) {
|
||||
for (AudioRendererEventListener audioDebugListener : audioDebugListeners) {
|
||||
|
@ -205,6 +205,14 @@ public class AnalyticsCollector
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public final void onAudioPositionAdvancing(long playoutStartSystemTimeMs) {
|
||||
EventTime eventTime = generateReadingMediaPeriodEventTime();
|
||||
for (AnalyticsListener listener : listeners) {
|
||||
listener.onAudioPositionAdvancing(eventTime, playoutStartSystemTimeMs);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public final void onAudioUnderrun(
|
||||
int bufferSize, long bufferSizeMs, long elapsedSinceLastFeedMs) {
|
||||
@ -544,12 +552,6 @@ public class AnalyticsCollector
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use {@link #onPlaybackSpeedChanged(float)} and {@link
|
||||
* #onSkipSilenceEnabledChanged(boolean)} instead.
|
||||
*/
|
||||
@SuppressWarnings("deprecation")
|
||||
@Deprecated
|
||||
@Override
|
||||
public final void onPlaybackParametersChanged(PlaybackParameters playbackParameters) {
|
||||
EventTime eventTime = generateCurrentPlayerMediaPeriodEventTime();
|
||||
@ -558,14 +560,6 @@ public class AnalyticsCollector
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPlaybackSpeedChanged(float playbackSpeed) {
|
||||
EventTime eventTime = generateCurrentPlayerMediaPeriodEventTime();
|
||||
for (AnalyticsListener listener : listeners) {
|
||||
listener.onPlaybackSpeedChanged(eventTime, playbackSpeed);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
@Override
|
||||
public final void onSeekProcessed() {
|
||||
|
@ -279,21 +279,13 @@ public interface AnalyticsListener {
|
||||
default void onSeekProcessed(EventTime eventTime) {}
|
||||
|
||||
/**
|
||||
* @deprecated Use {@link #onPlaybackSpeedChanged(EventTime, float)} and {@link
|
||||
* #onSkipSilenceEnabledChanged(EventTime, boolean)} instead.
|
||||
*/
|
||||
@SuppressWarnings("deprecation")
|
||||
@Deprecated
|
||||
default void onPlaybackParametersChanged(
|
||||
EventTime eventTime, PlaybackParameters playbackParameters) {}
|
||||
|
||||
/**
|
||||
* Called when the playback speed changes.
|
||||
* Called when the playback parameters changed.
|
||||
*
|
||||
* @param eventTime The event time.
|
||||
* @param playbackSpeed The playback speed.
|
||||
* @param playbackParameters The new playback parameters.
|
||||
*/
|
||||
default void onPlaybackSpeedChanged(EventTime eventTime, float playbackSpeed) {}
|
||||
default void onPlaybackParametersChanged(
|
||||
EventTime eventTime, PlaybackParameters playbackParameters) {}
|
||||
|
||||
/**
|
||||
* Called when the repeat mode changed.
|
||||
@ -479,6 +471,16 @@ public interface AnalyticsListener {
|
||||
*/
|
||||
default void onAudioInputFormatChanged(EventTime eventTime, Format format) {}
|
||||
|
||||
/**
|
||||
* Called when the audio position has increased for the first time since the last pause or
|
||||
* position reset.
|
||||
*
|
||||
* @param eventTime The event time.
|
||||
* @param playoutStartSystemTimeMs The approximate derived {@link System#currentTimeMillis()} at
|
||||
* which playout started.
|
||||
*/
|
||||
default void onAudioPositionAdvancing(EventTime eventTime, long playoutStartSystemTimeMs) {}
|
||||
|
||||
/**
|
||||
* Called when an audio underrun occurs.
|
||||
*
|
||||
|
@ -22,6 +22,7 @@ import androidx.annotation.Nullable;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.ExoPlaybackException;
|
||||
import com.google.android.exoplayer2.Format;
|
||||
import com.google.android.exoplayer2.PlaybackParameters;
|
||||
import com.google.android.exoplayer2.Player;
|
||||
import com.google.android.exoplayer2.Timeline;
|
||||
import com.google.android.exoplayer2.Timeline.Period;
|
||||
@ -334,8 +335,9 @@ public final class PlaybackStatsListener
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPlaybackSpeedChanged(EventTime eventTime, float playbackSpeed) {
|
||||
this.playbackSpeed = playbackSpeed;
|
||||
public void onPlaybackParametersChanged(
|
||||
EventTime eventTime, PlaybackParameters playbackParameters) {
|
||||
playbackSpeed = playbackParameters.speed;
|
||||
maybeAddSession(eventTime);
|
||||
for (PlaybackStatsTracker tracker : playbackStatsTrackers.values()) {
|
||||
tracker.onPlaybackSpeedChanged(eventTime, playbackSpeed);
|
||||
|
@ -65,6 +65,15 @@ public interface AudioRendererEventListener {
|
||||
*/
|
||||
default void onAudioInputFormatChanged(Format format) {}
|
||||
|
||||
/**
|
||||
* Called when the audio position has increased for the first time since the last pause or
|
||||
* position reset.
|
||||
*
|
||||
* @param playoutStartSystemTimeMs The approximate derived {@link System#currentTimeMillis()} at
|
||||
* which playout started.
|
||||
*/
|
||||
default void onAudioPositionAdvancing(long playoutStartSystemTimeMs) {}
|
||||
|
||||
/**
|
||||
* Called when an audio underrun occurs.
|
||||
*
|
||||
@ -89,7 +98,7 @@ public interface AudioRendererEventListener {
|
||||
*/
|
||||
default void onSkipSilenceEnabledChanged(boolean skipSilenceEnabled) {}
|
||||
|
||||
/** Dispatches events to a {@link AudioRendererEventListener}. */
|
||||
/** Dispatches events to an {@link AudioRendererEventListener}. */
|
||||
final class EventDispatcher {
|
||||
|
||||
@Nullable private final Handler handler;
|
||||
@ -106,20 +115,16 @@ public interface AudioRendererEventListener {
|
||||
this.listener = listener;
|
||||
}
|
||||
|
||||
/**
|
||||
* Invokes {@link AudioRendererEventListener#onAudioEnabled(DecoderCounters)}.
|
||||
*/
|
||||
public void enabled(final DecoderCounters decoderCounters) {
|
||||
/** Invokes {@link AudioRendererEventListener#onAudioEnabled(DecoderCounters)}. */
|
||||
public void enabled(DecoderCounters decoderCounters) {
|
||||
if (handler != null) {
|
||||
handler.post(() -> castNonNull(listener).onAudioEnabled(decoderCounters));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Invokes {@link AudioRendererEventListener#onAudioDecoderInitialized(String, long, long)}.
|
||||
*/
|
||||
public void decoderInitialized(final String decoderName,
|
||||
final long initializedTimestampMs, final long initializationDurationMs) {
|
||||
/** Invokes {@link AudioRendererEventListener#onAudioDecoderInitialized(String, long, long)}. */
|
||||
public void decoderInitialized(
|
||||
String decoderName, long initializedTimestampMs, long initializationDurationMs) {
|
||||
if (handler != null) {
|
||||
handler.post(
|
||||
() ->
|
||||
@ -129,18 +134,23 @@ public interface AudioRendererEventListener {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Invokes {@link AudioRendererEventListener#onAudioInputFormatChanged(Format)}.
|
||||
*/
|
||||
public void inputFormatChanged(final Format format) {
|
||||
/** Invokes {@link AudioRendererEventListener#onAudioInputFormatChanged(Format)}. */
|
||||
public void inputFormatChanged(Format format) {
|
||||
if (handler != null) {
|
||||
handler.post(() -> castNonNull(listener).onAudioInputFormatChanged(format));
|
||||
}
|
||||
}
|
||||
|
||||
/** Invokes {@link AudioRendererEventListener#onAudioPositionAdvancing(long)}. */
|
||||
public void positionAdvancing(long playoutStartSystemTimeMs) {
|
||||
if (handler != null) {
|
||||
handler.post(
|
||||
() -> castNonNull(listener).onAudioPositionAdvancing(playoutStartSystemTimeMs));
|
||||
}
|
||||
}
|
||||
|
||||
/** Invokes {@link AudioRendererEventListener#onAudioUnderrun(int, long, long)}. */
|
||||
public void underrun(
|
||||
final int bufferSize, final long bufferSizeMs, final long elapsedSinceLastFeedMs) {
|
||||
public void underrun(int bufferSize, long bufferSizeMs, long elapsedSinceLastFeedMs) {
|
||||
if (handler != null) {
|
||||
handler.post(
|
||||
() ->
|
||||
@ -149,10 +159,8 @@ public interface AudioRendererEventListener {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Invokes {@link AudioRendererEventListener#onAudioDisabled(DecoderCounters)}.
|
||||
*/
|
||||
public void disabled(final DecoderCounters counters) {
|
||||
/** Invokes {@link AudioRendererEventListener#onAudioDisabled(DecoderCounters)}. */
|
||||
public void disabled(DecoderCounters counters) {
|
||||
counters.ensureUpdated();
|
||||
if (handler != null) {
|
||||
handler.post(
|
||||
@ -163,17 +171,15 @@ public interface AudioRendererEventListener {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Invokes {@link AudioRendererEventListener#onAudioSessionId(int)}.
|
||||
*/
|
||||
public void audioSessionId(final int audioSessionId) {
|
||||
/** Invokes {@link AudioRendererEventListener#onAudioSessionId(int)}. */
|
||||
public void audioSessionId(int audioSessionId) {
|
||||
if (handler != null) {
|
||||
handler.post(() -> castNonNull(listener).onAudioSessionId(audioSessionId));
|
||||
}
|
||||
}
|
||||
|
||||
/** Invokes {@link AudioRendererEventListener#onSkipSilenceEnabledChanged(boolean)}. */
|
||||
public void skipSilenceEnabledChanged(final boolean skipSilenceEnabled) {
|
||||
public void skipSilenceEnabledChanged(boolean skipSilenceEnabled) {
|
||||
if (handler != null) {
|
||||
handler.post(() -> castNonNull(listener).onSkipSilenceEnabledChanged(skipSilenceEnabled));
|
||||
}
|
||||
|
@ -20,6 +20,7 @@ import androidx.annotation.IntDef;
|
||||
import androidx.annotation.Nullable;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.Format;
|
||||
import com.google.android.exoplayer2.PlaybackParameters;
|
||||
import java.lang.annotation.Documented;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
@ -72,10 +73,19 @@ public interface AudioSink {
|
||||
*/
|
||||
void onPositionDiscontinuity();
|
||||
|
||||
/**
|
||||
* Called when the audio sink's position has increased for the first time since it was last
|
||||
* paused or flushed.
|
||||
*
|
||||
* @param playoutStartSystemTimeMs The approximate derived {@link System#currentTimeMillis()} at
|
||||
* which playout started. Only valid if the audio track has not underrun.
|
||||
*/
|
||||
default void onPositionAdvancing(long playoutStartSystemTimeMs) {}
|
||||
|
||||
/**
|
||||
* Called when the audio sink runs out of data.
|
||||
* <p>
|
||||
* An audio sink implementation may never call this method (for example, if audio data is
|
||||
*
|
||||
* <p>An audio sink implementation may never call this method (for example, if audio data is
|
||||
* consumed in batches rather than based on the sink's own clock).
|
||||
*
|
||||
* @param bufferSize The size of the sink's buffer, in bytes.
|
||||
@ -297,16 +307,21 @@ public interface AudioSink {
|
||||
*/
|
||||
boolean hasPendingData();
|
||||
|
||||
/** Sets the playback speed. */
|
||||
void setPlaybackSpeed(float playbackSpeed);
|
||||
/**
|
||||
* Attempts to set the playback parameters. The audio sink may override these parameters if they
|
||||
* are not supported.
|
||||
*
|
||||
* @param playbackParameters The new playback parameters to attempt to set.
|
||||
*/
|
||||
void setPlaybackParameters(PlaybackParameters playbackParameters);
|
||||
|
||||
/** Gets the playback speed. */
|
||||
float getPlaybackSpeed();
|
||||
/** Returns the active {@link PlaybackParameters}. */
|
||||
PlaybackParameters getPlaybackParameters();
|
||||
|
||||
/** Sets whether silences should be skipped in the audio stream. */
|
||||
void setSkipSilenceEnabled(boolean skipSilenceEnabled);
|
||||
|
||||
/** Gets whether silences are skipped in the audio stream. */
|
||||
/** Returns whether silences are skipped in the audio stream. */
|
||||
boolean getSkipSilenceEnabled();
|
||||
|
||||
/**
|
||||
|
@ -48,6 +48,15 @@ import java.lang.reflect.Method;
|
||||
/** Listener for position tracker events. */
|
||||
public interface Listener {
|
||||
|
||||
/**
|
||||
* Called when the position tracker's position has increased for the first time since it was
|
||||
* last paused or reset.
|
||||
*
|
||||
* @param playoutStartSystemTimeMs The approximate derived {@link System#currentTimeMillis()} at
|
||||
* which playout started.
|
||||
*/
|
||||
void onPositionAdvancing(long playoutStartSystemTimeMs);
|
||||
|
||||
/**
|
||||
* Called when the frame position is too far from the expected frame position.
|
||||
*
|
||||
@ -145,6 +154,7 @@ import java.lang.reflect.Method;
|
||||
private boolean needsPassthroughWorkarounds;
|
||||
private long bufferSizeUs;
|
||||
private float audioTrackPlaybackSpeed;
|
||||
private boolean notifiedPositionIncreasing;
|
||||
|
||||
private long smoothedPlayheadOffsetUs;
|
||||
private long lastPlayheadSampleTimeUs;
|
||||
@ -287,9 +297,21 @@ import java.lang.reflect.Method;
|
||||
positionUs /= 1000;
|
||||
}
|
||||
|
||||
if (!notifiedPositionIncreasing && positionUs > lastPositionUs) {
|
||||
notifiedPositionIncreasing = true;
|
||||
long mediaDurationSinceLastPositionUs = C.usToMs(positionUs - lastPositionUs);
|
||||
long playoutDurationSinceLastPositionUs =
|
||||
Util.getPlayoutDurationForMediaDuration(
|
||||
mediaDurationSinceLastPositionUs, audioTrackPlaybackSpeed);
|
||||
long playoutStartSystemTimeMs =
|
||||
System.currentTimeMillis() - C.usToMs(playoutDurationSinceLastPositionUs);
|
||||
listener.onPositionAdvancing(playoutStartSystemTimeMs);
|
||||
}
|
||||
|
||||
lastSystemTimeUs = systemTimeUs;
|
||||
lastPositionUs = positionUs;
|
||||
lastSampleUsedGetTimestampMode = useGetTimestampMode;
|
||||
|
||||
return positionUs;
|
||||
}
|
||||
|
||||
@ -512,6 +534,7 @@ import java.lang.reflect.Method;
|
||||
lastPlayheadSampleTimeUs = 0;
|
||||
lastSystemTimeUs = 0;
|
||||
previousModeSystemTimeUs = 0;
|
||||
notifiedPositionIncreasing = false;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -29,6 +29,7 @@ import com.google.android.exoplayer2.ExoPlaybackException;
|
||||
import com.google.android.exoplayer2.ExoPlayer;
|
||||
import com.google.android.exoplayer2.Format;
|
||||
import com.google.android.exoplayer2.FormatHolder;
|
||||
import com.google.android.exoplayer2.PlaybackParameters;
|
||||
import com.google.android.exoplayer2.PlayerMessage.Target;
|
||||
import com.google.android.exoplayer2.RendererCapabilities;
|
||||
import com.google.android.exoplayer2.audio.AudioRendererEventListener.EventDispatcher;
|
||||
@ -498,13 +499,13 @@ public abstract class DecoderAudioRenderer<
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setPlaybackSpeed(float playbackSpeed) {
|
||||
audioSink.setPlaybackSpeed(playbackSpeed);
|
||||
public void setPlaybackParameters(PlaybackParameters playbackParameters) {
|
||||
audioSink.setPlaybackParameters(playbackParameters);
|
||||
}
|
||||
|
||||
@Override
|
||||
public float getPlaybackSpeed() {
|
||||
return audioSink.getPlaybackSpeed();
|
||||
public PlaybackParameters getPlaybackParameters() {
|
||||
return audioSink.getPlaybackParameters();
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -708,6 +709,11 @@ public abstract class DecoderAudioRenderer<
|
||||
DecoderAudioRenderer.this.onPositionDiscontinuity();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPositionAdvancing(long playoutStartSystemTimeMs) {
|
||||
eventDispatcher.positionAdvancing(playoutStartSystemTimeMs);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUnderrun(int bufferSize, long bufferSizeMs, long elapsedSinceLastFeedMs) {
|
||||
eventDispatcher.underrun(bufferSize, bufferSizeMs, elapsedSinceLastFeedMs);
|
||||
|
@ -32,6 +32,7 @@ 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.audio.AudioProcessor.UnhandledAudioFormatException;
|
||||
import com.google.android.exoplayer2.util.Assertions;
|
||||
import com.google.android.exoplayer2.util.Log;
|
||||
@ -92,14 +93,14 @@ public final class DefaultAudioSink implements AudioSink {
|
||||
AudioProcessor[] getAudioProcessors();
|
||||
|
||||
/**
|
||||
* Configures audio processors to apply the specified playback speed immediately, returning the
|
||||
* new playback speed, which may differ from the speed passed in. Only called when processors
|
||||
* have no input pending.
|
||||
* Configures audio processors to apply the specified playback parameters immediately, returning
|
||||
* the new playback parameters, which may differ from those passed in. Only called when
|
||||
* processors have no input pending.
|
||||
*
|
||||
* @param playbackSpeed The playback speed to try to apply.
|
||||
* @return The playback speed that was actually applied.
|
||||
* @param playbackParameters The playback parameters to try to apply.
|
||||
* @return The playback parameters that were actually applied.
|
||||
*/
|
||||
float applyPlaybackSpeed(float playbackSpeed);
|
||||
PlaybackParameters applyPlaybackParameters(PlaybackParameters playbackParameters);
|
||||
|
||||
/**
|
||||
* Configures audio processors to apply whether to skip silences immediately, returning the new
|
||||
@ -170,8 +171,10 @@ public final class DefaultAudioSink implements AudioSink {
|
||||
}
|
||||
|
||||
@Override
|
||||
public float applyPlaybackSpeed(float playbackSpeed) {
|
||||
return sonicAudioProcessor.setSpeed(playbackSpeed);
|
||||
public PlaybackParameters applyPlaybackParameters(PlaybackParameters playbackParameters) {
|
||||
float speed = sonicAudioProcessor.setSpeed(playbackParameters.speed);
|
||||
float pitch = sonicAudioProcessor.setPitch(playbackParameters.pitch);
|
||||
return new PlaybackParameters(speed, pitch);
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -197,6 +200,10 @@ public final class DefaultAudioSink implements AudioSink {
|
||||
public static final float MIN_PLAYBACK_SPEED = 0.1f;
|
||||
/** The maximum allowed playback speed. Higher values will be constrained to fall in range. */
|
||||
public static final float MAX_PLAYBACK_SPEED = 8f;
|
||||
/** The minimum allowed pitch factor. Lower values will be constrained to fall in range. */
|
||||
public static final float MIN_PITCH = 0.1f;
|
||||
/** The maximum allowed pitch factor. Higher values will be constrained to fall in range. */
|
||||
public static final float MAX_PITCH = 8f;
|
||||
|
||||
/** The default skip silence flag. */
|
||||
private static final boolean DEFAULT_SKIP_SILENCE = false;
|
||||
@ -296,7 +303,7 @@ public final class DefaultAudioSink implements AudioSink {
|
||||
private AudioAttributes audioAttributes;
|
||||
@Nullable private MediaPositionParameters afterDrainParameters;
|
||||
private MediaPositionParameters mediaPositionParameters;
|
||||
private float audioTrackPlaybackSpeed;
|
||||
private PlaybackParameters audioTrackPlaybackParameters;
|
||||
|
||||
@Nullable private ByteBuffer avSyncHeader;
|
||||
private int bytesUntilNextAvSync;
|
||||
@ -418,11 +425,11 @@ public final class DefaultAudioSink implements AudioSink {
|
||||
auxEffectInfo = new AuxEffectInfo(AuxEffectInfo.NO_AUX_EFFECT_ID, 0f);
|
||||
mediaPositionParameters =
|
||||
new MediaPositionParameters(
|
||||
DEFAULT_PLAYBACK_SPEED,
|
||||
PlaybackParameters.DEFAULT,
|
||||
DEFAULT_SKIP_SILENCE,
|
||||
/* mediaTimeUs= */ 0,
|
||||
/* audioTrackPositionUs= */ 0);
|
||||
audioTrackPlaybackSpeed = 1f;
|
||||
audioTrackPlaybackParameters = PlaybackParameters.DEFAULT;
|
||||
drainingAudioProcessorIndex = C.INDEX_UNSET;
|
||||
activeAudioProcessors = new AudioProcessor[0];
|
||||
outputBuffers = new ByteBuffer[0];
|
||||
@ -707,7 +714,7 @@ public final class DefaultAudioSink implements AudioSink {
|
||||
}
|
||||
}
|
||||
// Re-apply playback parameters.
|
||||
applyAudioProcessorPlaybackSpeedAndSkipSilence(presentationTimeUs);
|
||||
applyAudioProcessorPlaybackParametersAndSkipSilence(presentationTimeUs);
|
||||
}
|
||||
|
||||
if (!isAudioTrackInitialized()) {
|
||||
@ -720,9 +727,9 @@ public final class DefaultAudioSink implements AudioSink {
|
||||
startMediaTimeUsNeedsInit = false;
|
||||
|
||||
if (enableAudioTrackPlaybackParams && Util.SDK_INT >= 23) {
|
||||
setAudioTrackPlaybackSpeedV23(audioTrackPlaybackSpeed);
|
||||
setAudioTrackPlaybackParametersV23(audioTrackPlaybackParameters);
|
||||
}
|
||||
applyAudioProcessorPlaybackSpeedAndSkipSilence(presentationTimeUs);
|
||||
applyAudioProcessorPlaybackParametersAndSkipSilence(presentationTimeUs);
|
||||
|
||||
if (playing) {
|
||||
play();
|
||||
@ -758,7 +765,7 @@ public final class DefaultAudioSink implements AudioSink {
|
||||
// Don't process any more input until draining completes.
|
||||
return false;
|
||||
}
|
||||
applyAudioProcessorPlaybackSpeedAndSkipSilence(presentationTimeUs);
|
||||
applyAudioProcessorPlaybackParametersAndSkipSilence(presentationTimeUs);
|
||||
afterDrainParameters = null;
|
||||
}
|
||||
|
||||
@ -789,7 +796,7 @@ public final class DefaultAudioSink implements AudioSink {
|
||||
startMediaTimeUs += adjustmentUs;
|
||||
startMediaTimeUsNeedsSync = false;
|
||||
// Re-apply playback parameters because the startMediaTimeUs changed.
|
||||
applyAudioProcessorPlaybackSpeedAndSkipSilence(presentationTimeUs);
|
||||
applyAudioProcessorPlaybackParametersAndSkipSilence(presentationTimeUs);
|
||||
if (listener != null && adjustmentUs != 0) {
|
||||
listener.onPositionDiscontinuity();
|
||||
}
|
||||
@ -1011,26 +1018,30 @@ public final class DefaultAudioSink implements AudioSink {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setPlaybackSpeed(float playbackSpeed) {
|
||||
playbackSpeed = Util.constrainValue(playbackSpeed, MIN_PLAYBACK_SPEED, MAX_PLAYBACK_SPEED);
|
||||
public void setPlaybackParameters(PlaybackParameters playbackParameters) {
|
||||
playbackParameters =
|
||||
new PlaybackParameters(
|
||||
Util.constrainValue(playbackParameters.speed, MIN_PLAYBACK_SPEED, MAX_PLAYBACK_SPEED),
|
||||
Util.constrainValue(playbackParameters.pitch, MIN_PITCH, MAX_PITCH));
|
||||
if (enableAudioTrackPlaybackParams && Util.SDK_INT >= 23) {
|
||||
setAudioTrackPlaybackSpeedV23(playbackSpeed);
|
||||
setAudioTrackPlaybackParametersV23(playbackParameters);
|
||||
} else {
|
||||
setAudioProcessorPlaybackSpeedAndSkipSilence(playbackSpeed, getSkipSilenceEnabled());
|
||||
setAudioProcessorPlaybackParametersAndSkipSilence(
|
||||
playbackParameters, getSkipSilenceEnabled());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public float getPlaybackSpeed() {
|
||||
// We use either audio processor speed adjustment or AudioTrack playback parameters, so one of
|
||||
// the operands is always 1f.
|
||||
return getAudioProcessorPlaybackSpeed() * audioTrackPlaybackSpeed;
|
||||
public PlaybackParameters getPlaybackParameters() {
|
||||
return enableAudioTrackPlaybackParams
|
||||
? audioTrackPlaybackParameters
|
||||
: getAudioProcessorPlaybackParameters();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setSkipSilenceEnabled(boolean skipSilenceEnabled) {
|
||||
setAudioProcessorPlaybackSpeedAndSkipSilence(
|
||||
getAudioProcessorPlaybackSpeed(), skipSilenceEnabled);
|
||||
setAudioProcessorPlaybackParametersAndSkipSilence(
|
||||
getAudioProcessorPlaybackParameters(), skipSilenceEnabled);
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -1212,7 +1223,7 @@ public final class DefaultAudioSink implements AudioSink {
|
||||
framesPerEncodedSample = 0;
|
||||
mediaPositionParameters =
|
||||
new MediaPositionParameters(
|
||||
getAudioProcessorPlaybackSpeed(),
|
||||
getAudioProcessorPlaybackParameters(),
|
||||
getSkipSilenceEnabled(),
|
||||
/* mediaTimeUs= */ 0,
|
||||
/* audioTrackPositionUs= */ 0);
|
||||
@ -1249,12 +1260,13 @@ public final class DefaultAudioSink implements AudioSink {
|
||||
}
|
||||
|
||||
@RequiresApi(23)
|
||||
private void setAudioTrackPlaybackSpeedV23(float audioTrackPlaybackSpeed) {
|
||||
private void setAudioTrackPlaybackParametersV23(PlaybackParameters audioTrackPlaybackParameters) {
|
||||
if (isAudioTrackInitialized()) {
|
||||
PlaybackParams playbackParams =
|
||||
new PlaybackParams()
|
||||
.allowDefaults()
|
||||
.setSpeed(audioTrackPlaybackSpeed)
|
||||
.setSpeed(audioTrackPlaybackParameters.speed)
|
||||
.setPitch(audioTrackPlaybackParameters.pitch)
|
||||
.setAudioFallbackMode(PlaybackParams.AUDIO_FALLBACK_MODE_FAIL);
|
||||
try {
|
||||
audioTrack.setPlaybackParams(playbackParams);
|
||||
@ -1262,20 +1274,22 @@ public final class DefaultAudioSink implements AudioSink {
|
||||
Log.w(TAG, "Failed to set playback params", e);
|
||||
}
|
||||
// Update the speed using the actual effective speed from the audio track.
|
||||
audioTrackPlaybackSpeed = audioTrack.getPlaybackParams().getSpeed();
|
||||
audioTrackPositionTracker.setAudioTrackPlaybackSpeed(audioTrackPlaybackSpeed);
|
||||
audioTrackPlaybackParameters =
|
||||
new PlaybackParameters(
|
||||
audioTrack.getPlaybackParams().getSpeed(), audioTrack.getPlaybackParams().getPitch());
|
||||
audioTrackPositionTracker.setAudioTrackPlaybackSpeed(audioTrackPlaybackParameters.speed);
|
||||
}
|
||||
this.audioTrackPlaybackSpeed = audioTrackPlaybackSpeed;
|
||||
this.audioTrackPlaybackParameters = audioTrackPlaybackParameters;
|
||||
}
|
||||
|
||||
private void setAudioProcessorPlaybackSpeedAndSkipSilence(
|
||||
float playbackSpeed, boolean skipSilence) {
|
||||
private void setAudioProcessorPlaybackParametersAndSkipSilence(
|
||||
PlaybackParameters playbackParameters, boolean skipSilence) {
|
||||
MediaPositionParameters currentMediaPositionParameters = getMediaPositionParameters();
|
||||
if (playbackSpeed != currentMediaPositionParameters.playbackSpeed
|
||||
if (!playbackParameters.equals(currentMediaPositionParameters.playbackParameters)
|
||||
|| skipSilence != currentMediaPositionParameters.skipSilence) {
|
||||
MediaPositionParameters mediaPositionParameters =
|
||||
new MediaPositionParameters(
|
||||
playbackSpeed,
|
||||
playbackParameters,
|
||||
skipSilence,
|
||||
/* mediaTimeUs= */ C.TIME_UNSET,
|
||||
/* audioTrackPositionUs= */ C.TIME_UNSET);
|
||||
@ -1291,8 +1305,8 @@ public final class DefaultAudioSink implements AudioSink {
|
||||
}
|
||||
}
|
||||
|
||||
private float getAudioProcessorPlaybackSpeed() {
|
||||
return getMediaPositionParameters().playbackSpeed;
|
||||
private PlaybackParameters getAudioProcessorPlaybackParameters() {
|
||||
return getMediaPositionParameters().playbackParameters;
|
||||
}
|
||||
|
||||
private MediaPositionParameters getMediaPositionParameters() {
|
||||
@ -1304,18 +1318,18 @@ public final class DefaultAudioSink implements AudioSink {
|
||||
: mediaPositionParameters;
|
||||
}
|
||||
|
||||
private void applyAudioProcessorPlaybackSpeedAndSkipSilence(long presentationTimeUs) {
|
||||
float playbackSpeed =
|
||||
private void applyAudioProcessorPlaybackParametersAndSkipSilence(long presentationTimeUs) {
|
||||
PlaybackParameters playbackParameters =
|
||||
configuration.canApplyPlaybackParameters
|
||||
? audioProcessorChain.applyPlaybackSpeed(getAudioProcessorPlaybackSpeed())
|
||||
: DEFAULT_PLAYBACK_SPEED;
|
||||
? audioProcessorChain.applyPlaybackParameters(getAudioProcessorPlaybackParameters())
|
||||
: PlaybackParameters.DEFAULT;
|
||||
boolean skipSilenceEnabled =
|
||||
configuration.canApplyPlaybackParameters
|
||||
? audioProcessorChain.applySkipSilenceEnabled(getSkipSilenceEnabled())
|
||||
: DEFAULT_SKIP_SILENCE;
|
||||
mediaPositionParametersCheckpoints.add(
|
||||
new MediaPositionParameters(
|
||||
playbackSpeed,
|
||||
playbackParameters,
|
||||
skipSilenceEnabled,
|
||||
/* mediaTimeUs= */ max(0, presentationTimeUs),
|
||||
/* audioTrackPositionUs= */ configuration.framesToDurationUs(getWrittenFrames())));
|
||||
@ -1340,7 +1354,7 @@ public final class DefaultAudioSink implements AudioSink {
|
||||
|
||||
long playoutDurationSinceLastCheckpoint =
|
||||
positionUs - mediaPositionParameters.audioTrackPositionUs;
|
||||
if (mediaPositionParameters.playbackSpeed != 1f) {
|
||||
if (!mediaPositionParameters.playbackParameters.equals(PlaybackParameters.DEFAULT)) {
|
||||
if (mediaPositionParametersCheckpoints.isEmpty()) {
|
||||
playoutDurationSinceLastCheckpoint =
|
||||
audioProcessorChain.getMediaDuration(playoutDurationSinceLastCheckpoint);
|
||||
@ -1348,7 +1362,8 @@ public final class DefaultAudioSink implements AudioSink {
|
||||
// Playing data at a previous playback speed, so fall back to multiplying by the speed.
|
||||
playoutDurationSinceLastCheckpoint =
|
||||
Util.getMediaDurationForPlayoutDuration(
|
||||
playoutDurationSinceLastCheckpoint, mediaPositionParameters.playbackSpeed);
|
||||
playoutDurationSinceLastCheckpoint,
|
||||
mediaPositionParameters.playbackParameters.speed);
|
||||
}
|
||||
}
|
||||
return mediaPositionParameters.mediaTimeUs + playoutDurationSinceLastCheckpoint;
|
||||
@ -1692,8 +1707,8 @@ public final class DefaultAudioSink implements AudioSink {
|
||||
/** Stores parameters used to calculate the current media position. */
|
||||
private static final class MediaPositionParameters {
|
||||
|
||||
/** The playback speed. */
|
||||
public final float playbackSpeed;
|
||||
/** The playback parameters. */
|
||||
public final PlaybackParameters playbackParameters;
|
||||
/** Whether to skip silences. */
|
||||
public final boolean skipSilence;
|
||||
/** The media time from which the playback parameters apply, in microseconds. */
|
||||
@ -1702,8 +1717,11 @@ public final class DefaultAudioSink implements AudioSink {
|
||||
public final long audioTrackPositionUs;
|
||||
|
||||
private MediaPositionParameters(
|
||||
float playbackSpeed, boolean skipSilence, long mediaTimeUs, long audioTrackPositionUs) {
|
||||
this.playbackSpeed = playbackSpeed;
|
||||
PlaybackParameters playbackParameters,
|
||||
boolean skipSilence,
|
||||
long mediaTimeUs,
|
||||
long audioTrackPositionUs) {
|
||||
this.playbackParameters = playbackParameters;
|
||||
this.skipSilence = skipSilence;
|
||||
this.mediaTimeUs = mediaTimeUs;
|
||||
this.audioTrackPositionUs = audioTrackPositionUs;
|
||||
@ -1776,6 +1794,13 @@ public final class DefaultAudioSink implements AudioSink {
|
||||
Log.w(TAG, "Ignoring impossibly large audio latency: " + latencyUs);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPositionAdvancing(long playoutStartSystemTimeMs) {
|
||||
if (listener != null) {
|
||||
listener.onPositionAdvancing(playoutStartSystemTimeMs);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUnderrun(int bufferSize, long bufferSizeMs) {
|
||||
if (listener != null) {
|
||||
|
@ -17,6 +17,7 @@ package com.google.android.exoplayer2.audio;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import com.google.android.exoplayer2.Format;
|
||||
import com.google.android.exoplayer2.PlaybackParameters;
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
/** An overridable {@link AudioSink} implementation forwarding all methods to another sink. */
|
||||
@ -88,13 +89,13 @@ public class ForwardingAudioSink implements AudioSink {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setPlaybackSpeed(float playbackSpeed) {
|
||||
sink.setPlaybackSpeed(playbackSpeed);
|
||||
public void setPlaybackParameters(PlaybackParameters playbackParameters) {
|
||||
sink.setPlaybackParameters(playbackParameters);
|
||||
}
|
||||
|
||||
@Override
|
||||
public float getPlaybackSpeed() {
|
||||
return sink.getPlaybackSpeed();
|
||||
public PlaybackParameters getPlaybackParameters() {
|
||||
return sink.getPlaybackParameters();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -33,6 +33,7 @@ import com.google.android.exoplayer2.ExoPlaybackException;
|
||||
import com.google.android.exoplayer2.ExoPlayer;
|
||||
import com.google.android.exoplayer2.Format;
|
||||
import com.google.android.exoplayer2.FormatHolder;
|
||||
import com.google.android.exoplayer2.PlaybackParameters;
|
||||
import com.google.android.exoplayer2.PlayerMessage.Target;
|
||||
import com.google.android.exoplayer2.RendererCapabilities;
|
||||
import com.google.android.exoplayer2.audio.AudioRendererEventListener.EventDispatcher;
|
||||
@ -545,13 +546,13 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setPlaybackSpeed(float playbackSpeed) {
|
||||
audioSink.setPlaybackSpeed(playbackSpeed);
|
||||
public void setPlaybackParameters(PlaybackParameters playbackParameters) {
|
||||
audioSink.setPlaybackParameters(playbackParameters);
|
||||
}
|
||||
|
||||
@Override
|
||||
public float getPlaybackSpeed() {
|
||||
return audioSink.getPlaybackSpeed();
|
||||
public PlaybackParameters getPlaybackParameters() {
|
||||
return audioSink.getPlaybackParameters();
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -828,6 +829,11 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
|
||||
MediaCodecAudioRenderer.this.onPositionDiscontinuity();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPositionAdvancing(long playoutStartSystemTimeMs) {
|
||||
eventDispatcher.positionAdvancing(playoutStartSystemTimeMs);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUnderrun(int bufferSize, long bufferSizeMs, long elapsedSinceLastFeedMs) {
|
||||
eventDispatcher.underrun(bufferSize, bufferSizeMs, elapsedSinceLastFeedMs);
|
||||
|
@ -37,6 +37,7 @@ import java.util.Arrays;
|
||||
private final int inputSampleRateHz;
|
||||
private final int channelCount;
|
||||
private final float speed;
|
||||
private final float pitch;
|
||||
private final float rate;
|
||||
private final int minPeriod;
|
||||
private final int maxPeriod;
|
||||
@ -63,12 +64,15 @@ import java.util.Arrays;
|
||||
* @param inputSampleRateHz The sample rate of input audio, in hertz.
|
||||
* @param channelCount The number of channels in the input audio.
|
||||
* @param speed The speedup factor for output audio.
|
||||
* @param pitch The pitch factor for output audio.
|
||||
* @param outputSampleRateHz The sample rate for output audio, in hertz.
|
||||
*/
|
||||
public Sonic(int inputSampleRateHz, int channelCount, float speed, int outputSampleRateHz) {
|
||||
public Sonic(
|
||||
int inputSampleRateHz, int channelCount, float speed, float pitch, int outputSampleRateHz) {
|
||||
this.inputSampleRateHz = inputSampleRateHz;
|
||||
this.channelCount = channelCount;
|
||||
this.speed = speed;
|
||||
this.pitch = pitch;
|
||||
rate = (float) inputSampleRateHz / outputSampleRateHz;
|
||||
minPeriod = inputSampleRateHz / MAXIMUM_PITCH;
|
||||
maxPeriod = inputSampleRateHz / MINIMUM_PITCH;
|
||||
@ -118,8 +122,10 @@ import java.util.Arrays;
|
||||
*/
|
||||
public void queueEndOfStream() {
|
||||
int remainingFrameCount = inputFrameCount;
|
||||
float s = speed / pitch;
|
||||
float r = rate * pitch;
|
||||
int expectedOutputFrames =
|
||||
outputFrameCount + (int) ((remainingFrameCount / speed + pitchFrameCount) / rate + 0.5f);
|
||||
outputFrameCount + (int) ((remainingFrameCount / s + pitchFrameCount) / r + 0.5f);
|
||||
|
||||
// Add enough silence to flush both input and pitch buffers.
|
||||
inputBuffer =
|
||||
@ -464,14 +470,16 @@ import java.util.Arrays;
|
||||
private void processStreamInput() {
|
||||
// Resample as many pitch periods as we have buffered on the input.
|
||||
int originalOutputFrameCount = outputFrameCount;
|
||||
if (speed > 1.00001 || speed < 0.99999) {
|
||||
changeSpeed(speed);
|
||||
float s = speed / pitch;
|
||||
float r = rate * pitch;
|
||||
if (s > 1.00001 || s < 0.99999) {
|
||||
changeSpeed(s);
|
||||
} else {
|
||||
copyToOutput(inputBuffer, 0, inputFrameCount);
|
||||
inputFrameCount = 0;
|
||||
}
|
||||
if (rate != 1.0f) {
|
||||
adjustRate(rate, originalOutputFrameCount);
|
||||
if (r != 1.0f) {
|
||||
adjustRate(r, originalOutputFrameCount);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -43,6 +43,7 @@ public final class SonicAudioProcessor implements AudioProcessor {
|
||||
|
||||
private int pendingOutputSampleRate;
|
||||
private float speed;
|
||||
private float pitch;
|
||||
|
||||
private AudioFormat pendingInputAudioFormat;
|
||||
private AudioFormat pendingOutputAudioFormat;
|
||||
@ -61,6 +62,7 @@ public final class SonicAudioProcessor implements AudioProcessor {
|
||||
/** Creates a new Sonic audio processor. */
|
||||
public SonicAudioProcessor() {
|
||||
speed = 1f;
|
||||
pitch = 1f;
|
||||
pendingInputAudioFormat = AudioFormat.NOT_SET;
|
||||
pendingOutputAudioFormat = AudioFormat.NOT_SET;
|
||||
inputAudioFormat = AudioFormat.NOT_SET;
|
||||
@ -87,6 +89,22 @@ public final class SonicAudioProcessor implements AudioProcessor {
|
||||
return speed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the playback pitch. This method may only be called after draining data through the
|
||||
* processor. The value returned by {@link #isActive()} may change, and the processor must be
|
||||
* {@link #flush() flushed} before queueing more data.
|
||||
*
|
||||
* @param pitch The requested new pitch.
|
||||
* @return The actual new pitch.
|
||||
*/
|
||||
public float setPitch(float pitch) {
|
||||
if (this.pitch != pitch) {
|
||||
this.pitch = pitch;
|
||||
pendingSonicRecreation = true;
|
||||
}
|
||||
return pitch;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the sample rate for output audio, in Hertz. Pass {@link #SAMPLE_RATE_NO_CHANGE} to output
|
||||
* audio at the same sample rate as the input. After calling this method, call {@link
|
||||
@ -140,6 +158,7 @@ public final class SonicAudioProcessor implements AudioProcessor {
|
||||
public boolean isActive() {
|
||||
return pendingOutputAudioFormat.sampleRate != Format.NO_VALUE
|
||||
&& (Math.abs(speed - 1f) >= CLOSE_THRESHOLD
|
||||
|| Math.abs(pitch - 1f) >= CLOSE_THRESHOLD
|
||||
|| pendingOutputAudioFormat.sampleRate != pendingInputAudioFormat.sampleRate);
|
||||
}
|
||||
|
||||
@ -200,6 +219,7 @@ public final class SonicAudioProcessor implements AudioProcessor {
|
||||
inputAudioFormat.sampleRate,
|
||||
inputAudioFormat.channelCount,
|
||||
speed,
|
||||
pitch,
|
||||
outputAudioFormat.sampleRate);
|
||||
} else if (sonic != null) {
|
||||
sonic.flush();
|
||||
@ -214,6 +234,7 @@ public final class SonicAudioProcessor implements AudioProcessor {
|
||||
@Override
|
||||
public void reset() {
|
||||
speed = 1f;
|
||||
pitch = 1f;
|
||||
pendingInputAudioFormat = AudioFormat.NOT_SET;
|
||||
pendingOutputAudioFormat = AudioFormat.NOT_SET;
|
||||
inputAudioFormat = AudioFormat.NOT_SET;
|
||||
|
@ -409,7 +409,7 @@ 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)}
|
||||
* or {@link #acquirePlaceholderSession} is called.
|
||||
* is called.
|
||||
*
|
||||
* <p>By default, the mode is {@link #MODE_PLAYBACK} and a streaming license is requested when
|
||||
* required.
|
||||
@ -469,34 +469,6 @@ public class DefaultDrmSessionManager implements DrmSessionManager {
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public DrmSession acquirePlaceholderSession(Looper playbackLooper, int trackType) {
|
||||
initPlaybackLooper(playbackLooper);
|
||||
ExoMediaDrm exoMediaDrm = Assertions.checkNotNull(this.exoMediaDrm);
|
||||
boolean avoidPlaceholderDrmSessions =
|
||||
FrameworkMediaCrypto.class.equals(exoMediaDrm.getExoMediaCryptoType())
|
||||
&& FrameworkMediaCrypto.WORKAROUND_DEVICE_NEEDS_KEYS_TO_CONFIGURE_CODEC;
|
||||
// Avoid attaching a session to sparse formats.
|
||||
if (avoidPlaceholderDrmSessions
|
||||
|| Util.linearSearch(useDrmSessionsForClearContentTrackTypes, trackType) == C.INDEX_UNSET
|
||||
|| exoMediaDrm.getExoMediaCryptoType() == null) {
|
||||
return null;
|
||||
}
|
||||
maybeCreateMediaDrmHandler(playbackLooper);
|
||||
if (placeholderDrmSession == null) {
|
||||
DefaultDrmSession placeholderDrmSession =
|
||||
createAndAcquireSessionWithRetry(
|
||||
/* schemeDatas= */ ImmutableList.of(),
|
||||
/* isPlaceholderSession= */ true,
|
||||
/* eventDispatcher= */ null);
|
||||
sessions.add(placeholderDrmSession);
|
||||
this.placeholderDrmSession = placeholderDrmSession;
|
||||
} else {
|
||||
placeholderDrmSession.acquire(/* eventDispatcher= */ null);
|
||||
}
|
||||
return placeholderDrmSession;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DrmSession acquireSession(
|
||||
Looper playbackLooper,
|
||||
@Nullable DrmSessionEventListener.EventDispatcher eventDispatcher,
|
||||
@ -504,6 +476,11 @@ public class DefaultDrmSessionManager implements DrmSessionManager {
|
||||
initPlaybackLooper(playbackLooper);
|
||||
maybeCreateMediaDrmHandler(playbackLooper);
|
||||
|
||||
if (format.drmInitData == null) {
|
||||
// Content is not encrypted.
|
||||
return maybeAcquirePlaceholderSession(MimeTypes.getTrackType(format.sampleMimeType));
|
||||
}
|
||||
|
||||
@Nullable List<SchemeData> schemeDatas = null;
|
||||
if (offlineLicenseKeySetId == null) {
|
||||
schemeDatas = getSchemeDatas(Assertions.checkNotNull(format.drmInitData), uuid, false);
|
||||
@ -565,6 +542,32 @@ public class DefaultDrmSessionManager implements DrmSessionManager {
|
||||
|
||||
// Internal methods.
|
||||
|
||||
@Nullable
|
||||
private DrmSession maybeAcquirePlaceholderSession(int trackType) {
|
||||
ExoMediaDrm exoMediaDrm = Assertions.checkNotNull(this.exoMediaDrm);
|
||||
boolean avoidPlaceholderDrmSessions =
|
||||
FrameworkMediaCrypto.class.equals(exoMediaDrm.getExoMediaCryptoType())
|
||||
&& FrameworkMediaCrypto.WORKAROUND_DEVICE_NEEDS_KEYS_TO_CONFIGURE_CODEC;
|
||||
// Avoid attaching a session to sparse formats.
|
||||
if (avoidPlaceholderDrmSessions
|
||||
|| Util.linearSearch(useDrmSessionsForClearContentTrackTypes, trackType) == C.INDEX_UNSET
|
||||
|| UnsupportedMediaCrypto.class.equals(exoMediaDrm.getExoMediaCryptoType())) {
|
||||
return null;
|
||||
}
|
||||
if (placeholderDrmSession == null) {
|
||||
DefaultDrmSession placeholderDrmSession =
|
||||
createAndAcquireSessionWithRetry(
|
||||
/* schemeDatas= */ ImmutableList.of(),
|
||||
/* isPlaceholderSession= */ true,
|
||||
/* eventDispatcher= */ null);
|
||||
sessions.add(placeholderDrmSession);
|
||||
this.placeholderDrmSession = placeholderDrmSession;
|
||||
} else {
|
||||
placeholderDrmSession.acquire(/* eventDispatcher= */ null);
|
||||
}
|
||||
return placeholderDrmSession;
|
||||
}
|
||||
|
||||
private boolean canAcquireSession(DrmInitData drmInitData) {
|
||||
if (offlineLicenseKeySetId != null) {
|
||||
// An offline license can be restored so a session can always be acquired.
|
||||
@ -585,12 +588,16 @@ public class DefaultDrmSessionManager implements DrmSessionManager {
|
||||
if (schemeType == null || C.CENC_TYPE_cenc.equals(schemeType)) {
|
||||
// If there is no scheme information, assume patternless AES-CTR.
|
||||
return true;
|
||||
} else if (C.CENC_TYPE_cbc1.equals(schemeType)
|
||||
|| C.CENC_TYPE_cbcs.equals(schemeType)
|
||||
|| C.CENC_TYPE_cens.equals(schemeType)) {
|
||||
// API support for AES-CBC and pattern encryption was added in API 24. However, the
|
||||
} else if (C.CENC_TYPE_cbcs.equals(schemeType)) {
|
||||
// Support for cbcs (AES-CBC with pattern encryption) was added in API 24. However, the
|
||||
// implementation was not stable until API 25.
|
||||
return Util.SDK_INT >= 25;
|
||||
} else if (C.CENC_TYPE_cbc1.equals(schemeType) || C.CENC_TYPE_cens.equals(schemeType)) {
|
||||
// Support for cbc1 (AES-CTR with pattern encryption) and cens (AES-CBC without pattern
|
||||
// encryption) was also added in API 24 and made stable from API 25, however support was
|
||||
// removed from API 30. Since the range of API levels for which these modes are usable is too
|
||||
// small to be useful, we don't indicate support on any API level.
|
||||
return false;
|
||||
}
|
||||
// Unknown schemes, assume one of them is supported.
|
||||
return true;
|
||||
|
@ -17,7 +17,6 @@ package com.google.android.exoplayer2.drm;
|
||||
|
||||
import android.os.Looper;
|
||||
import androidx.annotation.Nullable;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.Format;
|
||||
|
||||
/** Manages a DRM session. */
|
||||
@ -33,13 +32,19 @@ public interface DrmSessionManager {
|
||||
new DrmSessionManager() {
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public DrmSession acquireSession(
|
||||
Looper playbackLooper,
|
||||
@Nullable DrmSessionEventListener.EventDispatcher eventDispatcher,
|
||||
Format format) {
|
||||
return new ErrorStateDrmSession(
|
||||
new DrmSession.DrmSessionException(
|
||||
new UnsupportedDrmException(UnsupportedDrmException.REASON_UNSUPPORTED_SCHEME)));
|
||||
if (format.drmInitData == null) {
|
||||
return null;
|
||||
} else {
|
||||
return new ErrorStateDrmSession(
|
||||
new DrmSession.DrmSessionException(
|
||||
new UnsupportedDrmException(
|
||||
UnsupportedDrmException.REASON_UNSUPPORTED_SCHEME)));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -64,39 +69,27 @@ public interface DrmSessionManager {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a {@link DrmSession} that does not execute key requests, with an incremented reference
|
||||
* count. When the caller no longer needs to use the instance, it must call {@link
|
||||
* DrmSession#release(DrmSessionEventListener.EventDispatcher)} to decrement the reference count.
|
||||
*
|
||||
* <p>Placeholder {@link DrmSession DrmSessions} may be used to configure secure decoders for
|
||||
* playback of clear content periods. This can reduce the cost of transitioning between clear and
|
||||
* encrypted content periods.
|
||||
*
|
||||
* @param playbackLooper The looper associated with the media playback thread.
|
||||
* @param trackType The type of the track to acquire a placeholder session for. Must be one of the
|
||||
* {@link C}{@code .TRACK_TYPE_*} constants.
|
||||
* @return The placeholder DRM session, or null if this DRM session manager does not support
|
||||
* placeholder sessions.
|
||||
*/
|
||||
@Nullable
|
||||
default DrmSession acquirePlaceholderSession(Looper playbackLooper, int trackType) {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a {@link DrmSession} for the specified {@link Format}, with an incremented reference
|
||||
* count. When the caller no longer needs to use the instance, it must call {@link
|
||||
* count. May return null if the {@link Format#drmInitData} is null and the DRM session manager is
|
||||
* not configured to attach a {@link DrmSession} to clear content. When the caller no longer needs
|
||||
* to use a returned {@link DrmSession}, it must call {@link
|
||||
* DrmSession#release(DrmSessionEventListener.EventDispatcher)} to decrement the reference count.
|
||||
*
|
||||
* <p>If the provided {@link Format} contains a null {@link Format#drmInitData}, the returned
|
||||
* {@link DrmSession} (if not null) will be a placeholder session which does not execute key
|
||||
* requests, and cannot be used to handle encrypted content. However, a placeholder session may be
|
||||
* 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)}.
|
||||
* @param format The {@link Format} for which to acquire a {@link DrmSession}. Must contain a
|
||||
* non-null {@link Format#drmInitData}.
|
||||
* @return The DRM session.
|
||||
* @param format The {@link Format} for which to acquire a {@link DrmSession}.
|
||||
* @return The DRM session. May be null if the given {@link Format#drmInitData} is null.
|
||||
*/
|
||||
@Nullable
|
||||
DrmSession acquireSession(
|
||||
Looper playbackLooper,
|
||||
@Nullable DrmSessionEventListener.EventDispatcher eventDispatcher,
|
||||
@ -105,16 +98,16 @@ public interface DrmSessionManager {
|
||||
/**
|
||||
* Returns the {@link ExoMediaCrypto} type associated to sessions acquired for the given {@link
|
||||
* Format}. Returns the {@link UnsupportedMediaCrypto} type if this DRM session manager does not
|
||||
* support any of the DRM schemes defined in the given {@link Format}. If the {@link Format}
|
||||
* describes unencrypted content, returns an {@link ExoMediaCrypto} type if this DRM session
|
||||
* manager would associate a {@link #acquirePlaceholderSession placeholder session} to the given
|
||||
* {@link Format}, or null otherwise.
|
||||
* support any of the DRM schemes defined in the given {@link Format}. Returns null if {@link
|
||||
* Format#drmInitData} is null and {@link #acquireSession} would return null for the given {@link
|
||||
* Format}.
|
||||
*
|
||||
* @param format The {@link Format} for which to return the {@link ExoMediaCrypto} type.
|
||||
* @return The {@link ExoMediaCrypto} type associated to sessions acquired using the given
|
||||
* parameters, or the {@link UnsupportedMediaCrypto} type if the provided {@code drmInitData}
|
||||
* is not supported, or {@code null} if {@code drmInitData} is null and no DRM session will be
|
||||
* associated to the given {@code trackType}.
|
||||
* @return The {@link ExoMediaCrypto} type associated to sessions acquired using the given {@link
|
||||
* Format}, or {@link UnsupportedMediaCrypto} if this DRM session manager does not support any
|
||||
* of the DRM schemes defined in the given {@link Format}. May be null if {@link
|
||||
* Format#drmInitData} is null and {@link #acquireSession} would return null for the given
|
||||
* {@link Format}.
|
||||
*/
|
||||
@Nullable
|
||||
Class<? extends ExoMediaCrypto> getExoMediaCryptoType(Format format);
|
||||
|
@ -184,7 +184,8 @@ public final class OfflineLicenseHelper {
|
||||
/**
|
||||
* Downloads an offline license.
|
||||
*
|
||||
* @param format The {@link Format} of the content whose license is to be downloaded.
|
||||
* @param format The {@link Format} of the content whose license is to be downloaded. Must contain
|
||||
* a non-null {@link Format#drmInitData}.
|
||||
* @return The key set id for the downloaded license.
|
||||
* @throws DrmSessionException Thrown when a DRM session error occurs.
|
||||
*/
|
||||
@ -278,13 +279,14 @@ public final class OfflineLicenseHelper {
|
||||
|
||||
private DrmSession openBlockingKeyRequest(
|
||||
@Mode int licenseMode, @Nullable byte[] offlineLicenseKeySetId, Format format) {
|
||||
Assertions.checkNotNull(format.drmInitData);
|
||||
drmSessionManager.setMode(licenseMode, offlineLicenseKeySetId);
|
||||
conditionVariable.close();
|
||||
DrmSession drmSession =
|
||||
drmSessionManager.acquireSession(handlerThread.getLooper(), eventDispatcher, format);
|
||||
// Block current thread until key loading is finished
|
||||
conditionVariable.block();
|
||||
return drmSession;
|
||||
return Assertions.checkNotNull(drmSession);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -15,28 +15,19 @@
|
||||
*/
|
||||
package com.google.android.exoplayer2.source;
|
||||
|
||||
import static com.google.android.exoplayer2.drm.DefaultDrmSessionManager.MODE_PLAYBACK;
|
||||
|
||||
import android.content.Context;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.util.SparseArray;
|
||||
import androidx.annotation.Nullable;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.ExoPlayerLibraryInfo;
|
||||
import com.google.android.exoplayer2.MediaItem;
|
||||
import com.google.android.exoplayer2.MediaItem.DrmConfiguration;
|
||||
import com.google.android.exoplayer2.drm.DefaultDrmSessionManager;
|
||||
import com.google.android.exoplayer2.drm.DrmSessionManager;
|
||||
import com.google.android.exoplayer2.drm.FrameworkMediaDrm;
|
||||
import com.google.android.exoplayer2.drm.HttpMediaDrmCallback;
|
||||
import com.google.android.exoplayer2.drm.MediaDrmCallback;
|
||||
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.upstream.DataSource;
|
||||
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
|
||||
import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory;
|
||||
import com.google.android.exoplayer2.upstream.DefaultLoadErrorHandlingPolicy;
|
||||
import com.google.android.exoplayer2.upstream.HttpDataSource;
|
||||
import com.google.android.exoplayer2.upstream.LoadErrorHandlingPolicy;
|
||||
@ -44,10 +35,8 @@ 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.common.primitives.Ints;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* The default {@link MediaSourceFactory} implementation.
|
||||
@ -79,21 +68,6 @@ import java.util.Map;
|
||||
* the stream.
|
||||
* </ul>
|
||||
*
|
||||
* <h3>DrmSessionManager creation for protected content</h3>
|
||||
*
|
||||
* <p>For a media item with a {@link DrmConfiguration}, a {@link DefaultDrmSessionManager} is
|
||||
* created based on that configuration. The following setter can be used to optionally configure the
|
||||
* creation:
|
||||
*
|
||||
* <ul>
|
||||
* <li>{@link #setDrmHttpDataSourceFactory(HttpDataSource.Factory)}: Sets the data source factory
|
||||
* to be used by the {@link HttpMediaDrmCallback} for network requests (default: {@link
|
||||
* DefaultHttpDataSourceFactory}).
|
||||
* </ul>
|
||||
*
|
||||
* <p>For media items without a {@link DrmConfiguration}, the {@link DrmSessionManager} passed to
|
||||
* {@link #setDrmSessionManager(DrmSessionManager)} will be used.
|
||||
*
|
||||
* <h3>Ad support for media items with ad tag uri</h3>
|
||||
*
|
||||
* <p>For a media item with an ad tag uri, an {@link AdSupportProvider} needs to be passed to {@link
|
||||
@ -167,21 +141,14 @@ public final class DefaultMediaSourceFactory implements MediaSourceFactory {
|
||||
}
|
||||
|
||||
private static final String TAG = "DefaultMediaSourceFactory";
|
||||
private static final String DEFAULT_USER_AGENT =
|
||||
ExoPlayerLibraryInfo.VERSION_SLASHY
|
||||
+ " (Linux;Android "
|
||||
+ Build.VERSION.RELEASE
|
||||
+ ") "
|
||||
+ ExoPlayerLibraryInfo.VERSION_SLASHY;
|
||||
|
||||
private final MediaSourceDrmHelper mediaSourceDrmHelper;
|
||||
private final DataSource.Factory dataSourceFactory;
|
||||
@Nullable private final AdSupportProvider adSupportProvider;
|
||||
private final SparseArray<MediaSourceFactory> mediaSourceFactories;
|
||||
@C.ContentType private final int[] supportedTypes;
|
||||
|
||||
private DrmSessionManager drmSessionManager;
|
||||
@Nullable private HttpDataSource.Factory drmHttpDataSourceFactory;
|
||||
private String userAgent;
|
||||
@Nullable private DrmSessionManager drmSessionManager;
|
||||
@Nullable private List<StreamKey> streamKeys;
|
||||
|
||||
/**
|
||||
@ -196,8 +163,7 @@ public final class DefaultMediaSourceFactory implements MediaSourceFactory {
|
||||
DataSource.Factory dataSourceFactory, @Nullable AdSupportProvider adSupportProvider) {
|
||||
this.dataSourceFactory = dataSourceFactory;
|
||||
this.adSupportProvider = adSupportProvider;
|
||||
drmSessionManager = DrmSessionManager.getDummyDrmSessionManager();
|
||||
userAgent = DEFAULT_USER_AGENT;
|
||||
mediaSourceDrmHelper = new MediaSourceDrmHelper();
|
||||
mediaSourceFactories = loadDelegates(dataSourceFactory);
|
||||
supportedTypes = new int[mediaSourceFactories.size()];
|
||||
for (int i = 0; i < mediaSourceFactories.size(); i++) {
|
||||
@ -205,49 +171,23 @@ public final class DefaultMediaSourceFactory implements MediaSourceFactory {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the {@link HttpDataSource.Factory} to be used for creating {@link HttpMediaDrmCallback
|
||||
* HttpMediaDrmCallbacks} which executes key and provisioning requests over HTTP. If {@code null}
|
||||
* is passed the {@link DefaultHttpDataSourceFactory} is used.
|
||||
*
|
||||
* @param drmHttpDataSourceFactory The HTTP data source factory or {@code null} to use {@link
|
||||
* DefaultHttpDataSourceFactory}.
|
||||
* @return This factory, for convenience.
|
||||
*/
|
||||
@Override
|
||||
public DefaultMediaSourceFactory setDrmHttpDataSourceFactory(
|
||||
@Nullable HttpDataSource.Factory drmHttpDataSourceFactory) {
|
||||
this.drmHttpDataSourceFactory = drmHttpDataSourceFactory;
|
||||
mediaSourceDrmHelper.setDrmHttpDataSourceFactory(drmHttpDataSourceFactory);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the optional user agent to be used for DRM requests.
|
||||
*
|
||||
* <p>In case a factory has been set by {@link
|
||||
* #setDrmHttpDataSourceFactory(HttpDataSource.Factory)}, this user agent is ignored.
|
||||
*
|
||||
* @param userAgent The user agent to be used for DRM requests.
|
||||
* @return This factory, for convenience.
|
||||
*/
|
||||
@Override
|
||||
public DefaultMediaSourceFactory setDrmUserAgent(@Nullable String userAgent) {
|
||||
this.userAgent = userAgent != null ? userAgent : DEFAULT_USER_AGENT;
|
||||
mediaSourceDrmHelper.setDrmUserAgent(userAgent);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the {@link DrmSessionManager} to use for media items that do not specify a {@link
|
||||
* DrmConfiguration}. The default value is {@link DrmSessionManager#DUMMY}.
|
||||
*
|
||||
* @param drmSessionManager The {@link DrmSessionManager}.
|
||||
* @return This factory, for convenience.
|
||||
*/
|
||||
@Override
|
||||
public DefaultMediaSourceFactory setDrmSessionManager(
|
||||
@Nullable DrmSessionManager drmSessionManager) {
|
||||
this.drmSessionManager =
|
||||
drmSessionManager != null
|
||||
? drmSessionManager
|
||||
: DrmSessionManager.getDummyDrmSessionManager();
|
||||
this.drmSessionManager = drmSessionManager;
|
||||
return this;
|
||||
}
|
||||
|
||||
@ -292,7 +232,8 @@ public final class DefaultMediaSourceFactory implements MediaSourceFactory {
|
||||
@Nullable MediaSourceFactory mediaSourceFactory = mediaSourceFactories.get(type);
|
||||
Assertions.checkNotNull(
|
||||
mediaSourceFactory, "No suitable media source factory found for content type: " + type);
|
||||
mediaSourceFactory.setDrmSessionManager(createDrmSessionManager(mediaItem));
|
||||
mediaSourceFactory.setDrmSessionManager(
|
||||
drmSessionManager != null ? drmSessionManager : mediaSourceDrmHelper.create(mediaItem));
|
||||
mediaSourceFactory.setStreamKeys(
|
||||
!mediaItem.playbackProperties.streamKeys.isEmpty()
|
||||
? mediaItem.playbackProperties.streamKeys
|
||||
@ -318,46 +259,6 @@ public final class DefaultMediaSourceFactory implements MediaSourceFactory {
|
||||
|
||||
// internal methods
|
||||
|
||||
private DrmSessionManager createDrmSessionManager(MediaItem mediaItem) {
|
||||
Assertions.checkNotNull(mediaItem.playbackProperties);
|
||||
if (mediaItem.playbackProperties.drmConfiguration == null
|
||||
|| mediaItem.playbackProperties.drmConfiguration.licenseUri == null
|
||||
|| Util.SDK_INT < 18) {
|
||||
return drmSessionManager;
|
||||
}
|
||||
DefaultDrmSessionManager drmSessionManager =
|
||||
new DefaultDrmSessionManager.Builder()
|
||||
.setUuidAndExoMediaDrmProvider(
|
||||
mediaItem.playbackProperties.drmConfiguration.uuid,
|
||||
FrameworkMediaDrm.DEFAULT_PROVIDER)
|
||||
.setMultiSession(mediaItem.playbackProperties.drmConfiguration.multiSession)
|
||||
.setPlayClearSamplesWithoutKeys(
|
||||
mediaItem.playbackProperties.drmConfiguration.playClearContentWithoutKey)
|
||||
.setUseDrmSessionsForClearContent(
|
||||
Ints.toArray(mediaItem.playbackProperties.drmConfiguration.sessionForClearTypes))
|
||||
.build(createHttpMediaDrmCallback(mediaItem.playbackProperties.drmConfiguration));
|
||||
|
||||
drmSessionManager.setMode(
|
||||
MODE_PLAYBACK, mediaItem.playbackProperties.drmConfiguration.getKeySetId());
|
||||
|
||||
return drmSessionManager;
|
||||
}
|
||||
|
||||
private MediaDrmCallback createHttpMediaDrmCallback(MediaItem.DrmConfiguration drmConfiguration) {
|
||||
Assertions.checkNotNull(drmConfiguration.licenseUri);
|
||||
HttpMediaDrmCallback drmCallback =
|
||||
new HttpMediaDrmCallback(
|
||||
drmConfiguration.licenseUri.toString(),
|
||||
drmConfiguration.forceDefaultLicenseUri,
|
||||
drmHttpDataSourceFactory != null
|
||||
? drmHttpDataSourceFactory
|
||||
: new DefaultHttpDataSourceFactory(userAgent));
|
||||
for (Map.Entry<String, String> entry : drmConfiguration.requestHeaders.entrySet()) {
|
||||
drmCallback.setKeyRequestProperty(entry.getKey(), entry.getValue());
|
||||
}
|
||||
return drmCallback;
|
||||
}
|
||||
|
||||
private static MediaSource maybeClipMediaSource(MediaItem mediaItem, MediaSource mediaSource) {
|
||||
if (mediaItem.clippingProperties.startPositionMs == 0
|
||||
&& mediaItem.clippingProperties.endPositionMs == C.TIME_END_OF_SOURCE
|
||||
|
@ -29,6 +29,7 @@ import com.google.android.exoplayer2.extractor.ExtractorsFactory;
|
||||
import com.google.android.exoplayer2.upstream.Allocator;
|
||||
import com.google.android.exoplayer2.upstream.DataSource;
|
||||
import com.google.android.exoplayer2.upstream.DefaultLoadErrorHandlingPolicy;
|
||||
import com.google.android.exoplayer2.upstream.HttpDataSource;
|
||||
import com.google.android.exoplayer2.upstream.LoadErrorHandlingPolicy;
|
||||
import com.google.android.exoplayer2.upstream.TransferListener;
|
||||
import com.google.android.exoplayer2.util.Assertions;
|
||||
@ -168,6 +169,23 @@ public final class ExtractorMediaSource extends CompositeMediaSource<Void> {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use {@link ProgressiveMediaSource.Factory#setDrmHttpDataSourceFactory} instead.
|
||||
*/
|
||||
@Deprecated
|
||||
@Override
|
||||
public MediaSourceFactory setDrmHttpDataSourceFactory(
|
||||
@Nullable HttpDataSource.Factory drmHttpDataSourceFactory) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
/** @deprecated Use {@link ProgressiveMediaSource.Factory#setDrmUserAgent} instead. */
|
||||
@Deprecated
|
||||
@Override
|
||||
public MediaSourceFactory setDrmUserAgent(@Nullable String userAgent) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
/** @deprecated Use {@link #createMediaSource(MediaItem)} instead. */
|
||||
@SuppressWarnings("deprecation")
|
||||
@Deprecated
|
||||
|
@ -0,0 +1,105 @@
|
||||
/*
|
||||
* 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.source;
|
||||
|
||||
import static com.google.android.exoplayer2.drm.DefaultDrmSessionManager.MODE_PLAYBACK;
|
||||
import static com.google.android.exoplayer2.util.Util.castNonNull;
|
||||
|
||||
import android.os.Build;
|
||||
import androidx.annotation.Nullable;
|
||||
import com.google.android.exoplayer2.ExoPlayerLibraryInfo;
|
||||
import com.google.android.exoplayer2.MediaItem;
|
||||
import com.google.android.exoplayer2.drm.DefaultDrmSessionManager;
|
||||
import com.google.android.exoplayer2.drm.DrmSessionManager;
|
||||
import com.google.android.exoplayer2.drm.FrameworkMediaDrm;
|
||||
import com.google.android.exoplayer2.drm.HttpMediaDrmCallback;
|
||||
import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory;
|
||||
import com.google.android.exoplayer2.upstream.HttpDataSource;
|
||||
import com.google.android.exoplayer2.util.Assertions;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
import com.google.common.primitives.Ints;
|
||||
import java.util.Map;
|
||||
|
||||
/** A helper to create a {@link DrmSessionManager} from a {@link MediaItem}. */
|
||||
public final class MediaSourceDrmHelper {
|
||||
|
||||
private static final String DEFAULT_USER_AGENT =
|
||||
ExoPlayerLibraryInfo.VERSION_SLASHY
|
||||
+ " (Linux;Android "
|
||||
+ Build.VERSION.RELEASE
|
||||
+ ") "
|
||||
+ ExoPlayerLibraryInfo.VERSION_SLASHY;
|
||||
|
||||
@Nullable private HttpDataSource.Factory drmHttpDataSourceFactory;
|
||||
@Nullable private String userAgent;
|
||||
|
||||
/**
|
||||
* Sets the {@link HttpDataSource.Factory} to be used for creating {@link HttpMediaDrmCallback
|
||||
* HttpMediaDrmCallbacks} which executes key and provisioning requests over HTTP. If {@code null}
|
||||
* is passed the {@link DefaultHttpDataSourceFactory} is used.
|
||||
*
|
||||
* @param drmHttpDataSourceFactory The HTTP data source factory or {@code null} to use {@link
|
||||
* DefaultHttpDataSourceFactory}.
|
||||
*/
|
||||
public void setDrmHttpDataSourceFactory(
|
||||
@Nullable HttpDataSource.Factory drmHttpDataSourceFactory) {
|
||||
this.drmHttpDataSourceFactory = drmHttpDataSourceFactory;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the optional user agent to be used for DRM requests.
|
||||
*
|
||||
* <p>In case a factory has been set by {@link
|
||||
* #setDrmHttpDataSourceFactory(HttpDataSource.Factory)}, this user agent is ignored.
|
||||
*
|
||||
* @param userAgent The user agent to be used for DRM requests.
|
||||
*/
|
||||
public void setDrmUserAgent(@Nullable String userAgent) {
|
||||
this.userAgent = userAgent;
|
||||
}
|
||||
|
||||
/** Creates a {@link DrmSessionManager} for the given media item. */
|
||||
public DrmSessionManager create(MediaItem mediaItem) {
|
||||
Assertions.checkNotNull(mediaItem.playbackProperties);
|
||||
@Nullable
|
||||
MediaItem.DrmConfiguration drmConfiguration = mediaItem.playbackProperties.drmConfiguration;
|
||||
if (drmConfiguration == null || drmConfiguration.licenseUri == null || Util.SDK_INT < 18) {
|
||||
return DrmSessionManager.getDummyDrmSessionManager();
|
||||
}
|
||||
HttpDataSource.Factory dataSourceFactory =
|
||||
drmHttpDataSourceFactory != null
|
||||
? drmHttpDataSourceFactory
|
||||
: new DefaultHttpDataSourceFactory(userAgent != null ? userAgent : DEFAULT_USER_AGENT);
|
||||
HttpMediaDrmCallback httpDrmCallback =
|
||||
new HttpMediaDrmCallback(
|
||||
castNonNull(drmConfiguration.licenseUri).toString(),
|
||||
drmConfiguration.forceDefaultLicenseUri,
|
||||
dataSourceFactory);
|
||||
for (Map.Entry<String, String> entry : drmConfiguration.requestHeaders.entrySet()) {
|
||||
httpDrmCallback.setKeyRequestProperty(entry.getKey(), entry.getValue());
|
||||
}
|
||||
DefaultDrmSessionManager drmSessionManager =
|
||||
new DefaultDrmSessionManager.Builder()
|
||||
.setUuidAndExoMediaDrmProvider(
|
||||
drmConfiguration.uuid, FrameworkMediaDrm.DEFAULT_PROVIDER)
|
||||
.setMultiSession(drmConfiguration.multiSession)
|
||||
.setPlayClearSamplesWithoutKeys(drmConfiguration.playClearContentWithoutKey)
|
||||
.setUseDrmSessionsForClearContent(Ints.toArray(drmConfiguration.sessionForClearTypes))
|
||||
.build(httpDrmCallback);
|
||||
drmSessionManager.setMode(MODE_PLAYBACK, drmConfiguration.getKeySetId());
|
||||
return drmSessionManager;
|
||||
}
|
||||
}
|
@ -19,13 +19,34 @@ import android.net.Uri;
|
||||
import androidx.annotation.Nullable;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.MediaItem;
|
||||
import com.google.android.exoplayer2.drm.DrmSession;
|
||||
import com.google.android.exoplayer2.drm.DefaultDrmSessionManager;
|
||||
import com.google.android.exoplayer2.drm.DrmSessionManager;
|
||||
import com.google.android.exoplayer2.drm.HttpMediaDrmCallback;
|
||||
import com.google.android.exoplayer2.offline.StreamKey;
|
||||
import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory;
|
||||
import com.google.android.exoplayer2.upstream.HttpDataSource;
|
||||
import com.google.android.exoplayer2.upstream.LoadErrorHandlingPolicy;
|
||||
import java.util.List;
|
||||
|
||||
/** Factory for creating {@link MediaSource}s from URIs. */
|
||||
/**
|
||||
* Factory for creating {@link MediaSource}s from URIs.
|
||||
*
|
||||
* <h3>DrmSessionManager creation for protected content</h3>
|
||||
*
|
||||
* <p>In case a {@link DrmSessionManager} is passed to {@link
|
||||
* #setDrmSessionManager(DrmSessionManager)}, it will be used regardless of the drm configuration of
|
||||
* the media item.
|
||||
*
|
||||
* <p>For a media item with a {@link MediaItem.DrmConfiguration}, a {@link DefaultDrmSessionManager}
|
||||
* is created based on that configuration. The following setter can be used to optionally configure
|
||||
* the creation:
|
||||
*
|
||||
* <ul>
|
||||
* <li>{@link #setDrmHttpDataSourceFactory(HttpDataSource.Factory)}: Sets the data source factory
|
||||
* to be used by the {@link HttpMediaDrmCallback} for network requests (default: {@link
|
||||
* DefaultHttpDataSourceFactory}).
|
||||
* </ul>
|
||||
*/
|
||||
public interface MediaSourceFactory {
|
||||
|
||||
/** @deprecated Use {@link MediaItem.PlaybackProperties#streamKeys} instead. */
|
||||
@ -35,13 +56,40 @@ public interface MediaSourceFactory {
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the {@link DrmSessionManager} to use for acquiring {@link DrmSession DrmSessions}.
|
||||
* Sets the {@link DrmSessionManager} to use for all media items regardless of their {@link
|
||||
* MediaItem.DrmConfiguration}.
|
||||
*
|
||||
* @param drmSessionManager The {@link DrmSessionManager}.
|
||||
* @return This factory, for convenience.
|
||||
*/
|
||||
MediaSourceFactory setDrmSessionManager(@Nullable DrmSessionManager drmSessionManager);
|
||||
|
||||
/**
|
||||
* Sets the {@link HttpDataSource.Factory} to be used for creating {@link HttpMediaDrmCallback
|
||||
* HttpMediaDrmCallbacks} to execute key and provisioning requests over HTTP.
|
||||
*
|
||||
* <p>In case a {@link DrmSessionManager} has been set by {@link
|
||||
* #setDrmSessionManager(DrmSessionManager)}, this data source factory is ignored.
|
||||
*
|
||||
* @param drmHttpDataSourceFactory The HTTP data source factory, or {@code null} to use {@link
|
||||
* DefaultHttpDataSourceFactory}.
|
||||
* @return This factory, for convenience.
|
||||
*/
|
||||
MediaSourceFactory setDrmHttpDataSourceFactory(
|
||||
@Nullable HttpDataSource.Factory drmHttpDataSourceFactory);
|
||||
|
||||
/**
|
||||
* Sets the optional user agent to be used for DRM requests.
|
||||
*
|
||||
* <p>In case a factory has been set by {@link
|
||||
* #setDrmHttpDataSourceFactory(HttpDataSource.Factory)} or a {@link DrmSessionManager} has been
|
||||
* set by {@link #setDrmSessionManager(DrmSessionManager)}, this user agent is ignored.
|
||||
*
|
||||
* @param userAgent The user agent to be used for DRM requests.
|
||||
* @return This factory, for convenience.
|
||||
*/
|
||||
MediaSourceFactory setDrmUserAgent(@Nullable String userAgent);
|
||||
|
||||
/**
|
||||
* Sets an optional {@link LoadErrorHandlingPolicy}.
|
||||
*
|
||||
|
@ -22,7 +22,6 @@ import androidx.annotation.Nullable;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.MediaItem;
|
||||
import com.google.android.exoplayer2.Timeline;
|
||||
import com.google.android.exoplayer2.drm.DrmSession;
|
||||
import com.google.android.exoplayer2.drm.DrmSessionManager;
|
||||
import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory;
|
||||
import com.google.android.exoplayer2.extractor.Extractor;
|
||||
@ -30,6 +29,7 @@ import com.google.android.exoplayer2.extractor.ExtractorsFactory;
|
||||
import com.google.android.exoplayer2.upstream.Allocator;
|
||||
import com.google.android.exoplayer2.upstream.DataSource;
|
||||
import com.google.android.exoplayer2.upstream.DefaultLoadErrorHandlingPolicy;
|
||||
import com.google.android.exoplayer2.upstream.HttpDataSource;
|
||||
import com.google.android.exoplayer2.upstream.LoadErrorHandlingPolicy;
|
||||
import com.google.android.exoplayer2.upstream.TransferListener;
|
||||
|
||||
@ -51,9 +51,10 @@ public final class ProgressiveMediaSource extends BaseMediaSource
|
||||
public static final class Factory implements MediaSourceFactory {
|
||||
|
||||
private final DataSource.Factory dataSourceFactory;
|
||||
private final MediaSourceDrmHelper mediaSourceDrmHelper;
|
||||
|
||||
private ExtractorsFactory extractorsFactory;
|
||||
private DrmSessionManager drmSessionManager;
|
||||
@Nullable private DrmSessionManager drmSessionManager;
|
||||
private LoadErrorHandlingPolicy loadErrorHandlingPolicy;
|
||||
private int continueLoadingCheckIntervalBytes;
|
||||
@Nullable private String customCacheKey;
|
||||
@ -78,7 +79,7 @@ public final class ProgressiveMediaSource extends BaseMediaSource
|
||||
public Factory(DataSource.Factory dataSourceFactory, ExtractorsFactory extractorsFactory) {
|
||||
this.dataSourceFactory = dataSourceFactory;
|
||||
this.extractorsFactory = extractorsFactory;
|
||||
drmSessionManager = DrmSessionManager.getDummyDrmSessionManager();
|
||||
mediaSourceDrmHelper = new MediaSourceDrmHelper();
|
||||
loadErrorHandlingPolicy = new DefaultLoadErrorHandlingPolicy();
|
||||
continueLoadingCheckIntervalBytes = DEFAULT_LOADING_CHECK_INTERVAL_BYTES;
|
||||
}
|
||||
@ -146,19 +147,22 @@ public final class ProgressiveMediaSource extends BaseMediaSource
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the {@link DrmSessionManager} to use for acquiring {@link DrmSession DrmSessions}. The
|
||||
* default value is {@link DrmSessionManager#DUMMY}.
|
||||
*
|
||||
* @param drmSessionManager The {@link DrmSessionManager}.
|
||||
* @return This factory, for convenience.
|
||||
*/
|
||||
@Override
|
||||
public Factory setDrmSessionManager(@Nullable DrmSessionManager drmSessionManager) {
|
||||
this.drmSessionManager =
|
||||
drmSessionManager != null
|
||||
? drmSessionManager
|
||||
: DrmSessionManager.getDummyDrmSessionManager();
|
||||
this.drmSessionManager = drmSessionManager;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Factory setDrmHttpDataSourceFactory(
|
||||
@Nullable HttpDataSource.Factory drmHttpDataSourceFactory) {
|
||||
mediaSourceDrmHelper.setDrmHttpDataSourceFactory(drmHttpDataSourceFactory);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Factory setDrmUserAgent(@Nullable String userAgent) {
|
||||
mediaSourceDrmHelper.setDrmUserAgent(userAgent);
|
||||
return this;
|
||||
}
|
||||
|
||||
@ -194,7 +198,7 @@ public final class ProgressiveMediaSource extends BaseMediaSource
|
||||
mediaItem,
|
||||
dataSourceFactory,
|
||||
extractorsFactory,
|
||||
drmSessionManager,
|
||||
drmSessionManager != null ? drmSessionManager : mediaSourceDrmHelper.create(mediaItem),
|
||||
loadErrorHandlingPolicy,
|
||||
continueLoadingCheckIntervalBytes);
|
||||
}
|
||||
|
@ -327,13 +327,7 @@ public class SampleQueue implements TrackOutput {
|
||||
* Attempts to read from the queue.
|
||||
*
|
||||
* <p>{@link Format Formats} read from this method may be associated to a {@link DrmSession}
|
||||
* through {@link FormatHolder#drmSession}, which is populated in two scenarios:
|
||||
*
|
||||
* <ul>
|
||||
* <li>The {@link Format} has a non-null {@link Format#drmInitData}.
|
||||
* <li>The {@link DrmSessionManager} provides placeholder sessions for this queue's track type.
|
||||
* See {@link DrmSessionManager#acquirePlaceholderSession(Looper, int)}.
|
||||
* </ul>
|
||||
* through {@link FormatHolder#drmSession}.
|
||||
*
|
||||
* @param formatHolder A {@link FormatHolder} to populate in the case of reading a format.
|
||||
* @param buffer A {@link DecoderInputBuffer} to populate in the case of reading a sample or the
|
||||
@ -842,10 +836,7 @@ public class SampleQueue implements TrackOutput {
|
||||
// is being used for both DrmInitData.
|
||||
@Nullable DrmSession previousSession = currentDrmSession;
|
||||
currentDrmSession =
|
||||
newDrmInitData != null
|
||||
? drmSessionManager.acquireSession(playbackLooper, drmEventDispatcher, newFormat)
|
||||
: drmSessionManager.acquirePlaceholderSession(
|
||||
playbackLooper, MimeTypes.getTrackType(newFormat.sampleMimeType));
|
||||
drmSessionManager.acquireSession(playbackLooper, drmEventDispatcher, newFormat);
|
||||
outputFormatHolder.drmSession = currentDrmSession;
|
||||
|
||||
if (previousSession != null) {
|
||||
|
@ -16,6 +16,7 @@
|
||||
package com.google.android.exoplayer2.upstream.cache;
|
||||
|
||||
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 java.lang.Math.max;
|
||||
import static java.lang.Math.min;
|
||||
@ -219,9 +220,9 @@ import java.util.TreeSet;
|
||||
public SimpleCacheSpan setLastTouchTimestamp(
|
||||
SimpleCacheSpan cacheSpan, long lastTouchTimestamp, boolean updateFile) {
|
||||
checkState(cachedSpans.remove(cacheSpan));
|
||||
File file = cacheSpan.file;
|
||||
File file = checkNotNull(cacheSpan.file);
|
||||
if (updateFile) {
|
||||
File directory = file.getParentFile();
|
||||
File directory = checkNotNull(file.getParentFile());
|
||||
long position = cacheSpan.position;
|
||||
File newFile = SimpleCacheSpan.getCacheFile(directory, id, position, lastTouchTimestamp);
|
||||
if (file.renameTo(newFile)) {
|
||||
@ -244,7 +245,9 @@ import java.util.TreeSet;
|
||||
/** Removes the given span from cache. */
|
||||
public boolean removeSpan(CacheSpan span) {
|
||||
if (cachedSpans.remove(span)) {
|
||||
span.file.delete();
|
||||
if (span.file != null) {
|
||||
span.file.delete();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
|
@ -15,6 +15,9 @@
|
||||
*/
|
||||
package com.google.android.exoplayer2.upstream.cache;
|
||||
|
||||
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;
|
||||
import static java.lang.Math.min;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
@ -61,6 +64,7 @@ import javax.crypto.NoSuchPaddingException;
|
||||
import javax.crypto.spec.IvParameterSpec;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
import org.checkerframework.checker.nullness.compatqual.NullableType;
|
||||
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
||||
|
||||
/** Maintains the index of cached content. */
|
||||
/* package */ class CachedContentIndex {
|
||||
@ -155,13 +159,15 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
|
||||
@Nullable byte[] legacyStorageSecretKey,
|
||||
boolean legacyStorageEncrypt,
|
||||
boolean preferLegacyStorage) {
|
||||
Assertions.checkState(databaseProvider != null || legacyStorageDir != null);
|
||||
checkState(databaseProvider != null || legacyStorageDir != null);
|
||||
keyToContent = new HashMap<>();
|
||||
idToKey = new SparseArray<>();
|
||||
removedIds = new SparseBooleanArray();
|
||||
newIds = new SparseBooleanArray();
|
||||
@Nullable
|
||||
Storage databaseStorage =
|
||||
databaseProvider != null ? new DatabaseStorage(databaseProvider) : null;
|
||||
@Nullable
|
||||
Storage legacyStorage =
|
||||
legacyStorageDir != null
|
||||
? new LegacyStorage(
|
||||
@ -170,7 +176,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
|
||||
legacyStorageEncrypt)
|
||||
: null;
|
||||
if (databaseStorage == null || (legacyStorage != null && preferLegacyStorage)) {
|
||||
storage = legacyStorage;
|
||||
storage = castNonNull(legacyStorage);
|
||||
previousStorage = databaseStorage;
|
||||
} else {
|
||||
storage = databaseStorage;
|
||||
@ -325,7 +331,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
|
||||
|
||||
/** Returns a {@link ContentMetadata} for the given key. */
|
||||
public ContentMetadata getContentMetadata(String key) {
|
||||
CachedContent cachedContent = get(key);
|
||||
@Nullable CachedContent cachedContent = get(key);
|
||||
return cachedContent != null ? cachedContent.getMetadata() : DefaultContentMetadata.EMPTY;
|
||||
}
|
||||
|
||||
@ -358,7 +364,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
|
||||
* returns the smallest unused non-negative integer.
|
||||
*/
|
||||
@VisibleForTesting
|
||||
/* package */ static int getNewId(SparseArray<String> idToKey) {
|
||||
/* package */ static int getNewId(SparseArray<@NullableType String> idToKey) {
|
||||
int size = idToKey.size();
|
||||
int id = size == 0 ? 0 : (idToKey.keyAt(size - 1) + 1);
|
||||
if (id < 0) { // In case if we pass max int value.
|
||||
@ -512,8 +518,9 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
|
||||
@Nullable private ReusableBufferedOutputStream bufferedOutputStream;
|
||||
|
||||
public LegacyStorage(File file, @Nullable byte[] secretKey, boolean encrypt) {
|
||||
Cipher cipher = null;
|
||||
SecretKeySpec secretKeySpec = null;
|
||||
checkState(secretKey != null || !encrypt);
|
||||
@Nullable Cipher cipher = null;
|
||||
@Nullable SecretKeySpec secretKeySpec = null;
|
||||
if (secretKey != null) {
|
||||
Assertions.checkArgument(secretKey.length == 16);
|
||||
try {
|
||||
@ -550,7 +557,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
|
||||
@Override
|
||||
public void load(
|
||||
HashMap<String, CachedContent> content, SparseArray<@NullableType String> idToKey) {
|
||||
Assertions.checkState(!changed);
|
||||
checkState(!changed);
|
||||
if (!readFile(content, idToKey)) {
|
||||
content.clear();
|
||||
idToKey.clear();
|
||||
@ -588,7 +595,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
|
||||
return true;
|
||||
}
|
||||
|
||||
DataInputStream input = null;
|
||||
@Nullable DataInputStream input = null;
|
||||
try {
|
||||
InputStream inputStream = new BufferedInputStream(atomicFile.openRead());
|
||||
input = new DataInputStream(inputStream);
|
||||
@ -606,7 +613,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
|
||||
input.readFully(initializationVector);
|
||||
IvParameterSpec ivParameterSpec = new IvParameterSpec(initializationVector);
|
||||
try {
|
||||
cipher.init(Cipher.DECRYPT_MODE, secretKeySpec, ivParameterSpec);
|
||||
cipher.init(Cipher.DECRYPT_MODE, castNonNull(secretKeySpec), ivParameterSpec);
|
||||
} catch (InvalidKeyException | InvalidAlgorithmParameterException e) {
|
||||
throw new IllegalStateException(e);
|
||||
}
|
||||
@ -647,6 +654,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
|
||||
} else {
|
||||
bufferedOutputStream.reset(outputStream);
|
||||
}
|
||||
ReusableBufferedOutputStream bufferedOutputStream = this.bufferedOutputStream;
|
||||
output = new DataOutputStream(bufferedOutputStream);
|
||||
output.writeInt(VERSION);
|
||||
|
||||
@ -655,11 +663,12 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
|
||||
|
||||
if (encrypt) {
|
||||
byte[] initializationVector = new byte[16];
|
||||
random.nextBytes(initializationVector);
|
||||
castNonNull(random).nextBytes(initializationVector);
|
||||
output.write(initializationVector);
|
||||
IvParameterSpec ivParameterSpec = new IvParameterSpec(initializationVector);
|
||||
try {
|
||||
cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec, ivParameterSpec);
|
||||
castNonNull(cipher)
|
||||
.init(Cipher.ENCRYPT_MODE, castNonNull(secretKeySpec), ivParameterSpec);
|
||||
} catch (InvalidKeyException | InvalidAlgorithmParameterException e) {
|
||||
throw new IllegalStateException(e); // Should never happen.
|
||||
}
|
||||
@ -762,16 +771,17 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
|
||||
+ " BLOB NOT NULL)";
|
||||
|
||||
private final DatabaseProvider databaseProvider;
|
||||
private final SparseArray<CachedContent> pendingUpdates;
|
||||
private final SparseArray<@NullableType CachedContent> pendingUpdates;
|
||||
|
||||
private String hexUid;
|
||||
private String tableName;
|
||||
private @MonotonicNonNull String hexUid;
|
||||
private @MonotonicNonNull String tableName;
|
||||
|
||||
public static void delete(DatabaseProvider databaseProvider, long uid)
|
||||
throws DatabaseIOException {
|
||||
delete(databaseProvider, Long.toHexString(uid));
|
||||
}
|
||||
|
||||
@SuppressWarnings("nullness:initialization.fields.uninitialized")
|
||||
public DatabaseStorage(DatabaseProvider databaseProvider) {
|
||||
this.databaseProvider = databaseProvider;
|
||||
pendingUpdates = new SparseArray<>();
|
||||
@ -788,26 +798,26 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
|
||||
return VersionTable.getVersion(
|
||||
databaseProvider.getReadableDatabase(),
|
||||
VersionTable.FEATURE_CACHE_CONTENT_METADATA,
|
||||
hexUid)
|
||||
checkNotNull(hexUid))
|
||||
!= VersionTable.VERSION_UNSET;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void delete() throws DatabaseIOException {
|
||||
delete(databaseProvider, hexUid);
|
||||
delete(databaseProvider, checkNotNull(hexUid));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void load(
|
||||
HashMap<String, CachedContent> content, SparseArray<@NullableType String> idToKey)
|
||||
throws IOException {
|
||||
Assertions.checkState(pendingUpdates.size() == 0);
|
||||
checkState(pendingUpdates.size() == 0);
|
||||
try {
|
||||
int version =
|
||||
VersionTable.getVersion(
|
||||
databaseProvider.getReadableDatabase(),
|
||||
VersionTable.FEATURE_CACHE_CONTENT_METADATA,
|
||||
hexUid);
|
||||
checkNotNull(hexUid));
|
||||
if (version != TABLE_VERSION) {
|
||||
SQLiteDatabase writableDatabase = databaseProvider.getWritableDatabase();
|
||||
writableDatabase.beginTransactionNonExclusive();
|
||||
@ -871,7 +881,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
|
||||
writableDatabase.beginTransactionNonExclusive();
|
||||
try {
|
||||
for (int i = 0; i < pendingUpdates.size(); i++) {
|
||||
CachedContent cachedContent = pendingUpdates.valueAt(i);
|
||||
@Nullable CachedContent cachedContent = pendingUpdates.valueAt(i);
|
||||
if (cachedContent == null) {
|
||||
deleteRow(writableDatabase, pendingUpdates.keyAt(i));
|
||||
} else {
|
||||
@ -906,7 +916,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
|
||||
return databaseProvider
|
||||
.getReadableDatabase()
|
||||
.query(
|
||||
tableName,
|
||||
checkNotNull(tableName),
|
||||
COLUMNS,
|
||||
/* selection= */ null,
|
||||
/* selectionArgs= */ null,
|
||||
@ -917,13 +927,17 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
|
||||
|
||||
private void initializeTable(SQLiteDatabase writableDatabase) throws DatabaseIOException {
|
||||
VersionTable.setVersion(
|
||||
writableDatabase, VersionTable.FEATURE_CACHE_CONTENT_METADATA, hexUid, TABLE_VERSION);
|
||||
dropTable(writableDatabase, tableName);
|
||||
writableDatabase,
|
||||
VersionTable.FEATURE_CACHE_CONTENT_METADATA,
|
||||
checkNotNull(hexUid),
|
||||
TABLE_VERSION);
|
||||
dropTable(writableDatabase, checkNotNull(tableName));
|
||||
writableDatabase.execSQL("CREATE TABLE " + tableName + " " + TABLE_SCHEMA);
|
||||
}
|
||||
|
||||
private void deleteRow(SQLiteDatabase writableDatabase, int key) {
|
||||
writableDatabase.delete(tableName, WHERE_ID_EQUALS, new String[] {Integer.toString(key)});
|
||||
writableDatabase.delete(
|
||||
checkNotNull(tableName), WHERE_ID_EQUALS, new String[] {Integer.toString(key)});
|
||||
}
|
||||
|
||||
private void addOrUpdateRow(SQLiteDatabase writableDatabase, CachedContent cachedContent)
|
||||
@ -936,7 +950,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
|
||||
values.put(COLUMN_ID, cachedContent.id);
|
||||
values.put(COLUMN_KEY, cachedContent.key);
|
||||
values.put(COLUMN_METADATA, data);
|
||||
writableDatabase.replaceOrThrow(tableName, /* nullColumnHack= */ null, values);
|
||||
writableDatabase.replaceOrThrow(checkNotNull(tableName), /* nullColumnHack= */ null, values);
|
||||
}
|
||||
|
||||
private static void delete(DatabaseProvider databaseProvider, String hexUid)
|
||||
|
@ -25,6 +25,7 @@ import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.ExoPlaybackException;
|
||||
import com.google.android.exoplayer2.Format;
|
||||
import com.google.android.exoplayer2.MediaItem;
|
||||
import com.google.android.exoplayer2.PlaybackParameters;
|
||||
import com.google.android.exoplayer2.Player;
|
||||
import com.google.android.exoplayer2.Player.PlaybackSuppressionReason;
|
||||
import com.google.android.exoplayer2.RendererCapabilities;
|
||||
@ -147,8 +148,9 @@ public class EventLogger implements AnalyticsListener {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPlaybackSpeedChanged(EventTime eventTime, float playbackSpeed) {
|
||||
logd(eventTime, "playbackSpeed", Float.toString(playbackSpeed));
|
||||
public void onPlaybackParametersChanged(
|
||||
EventTime eventTime, PlaybackParameters playbackParameters) {
|
||||
logd(eventTime, "playbackParameters", playbackParameters.toString());
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -316,13 +318,19 @@ public class EventLogger implements AnalyticsListener {
|
||||
logd(eventTime, "audioInputFormat", Format.toLogString(format));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAudioPositionAdvancing(EventTime eventTime, long playoutStartSystemTimeMs) {
|
||||
long timeSincePlayoutStartMs = System.currentTimeMillis() - playoutStartSystemTimeMs;
|
||||
logd(eventTime, "audioPositionAdvancing", "timeSincePlayoutStartMs=" + timeSincePlayoutStartMs);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAudioUnderrun(
|
||||
EventTime eventTime, int bufferSize, long bufferSizeMs, long elapsedSinceLastFeedMs) {
|
||||
loge(
|
||||
eventTime,
|
||||
"audioTrackUnderrun",
|
||||
bufferSize + ", " + bufferSizeMs + ", " + elapsedSinceLastFeedMs + "]",
|
||||
bufferSize + ", " + bufferSizeMs + ", " + elapsedSinceLastFeedMs,
|
||||
/* throwable= */ null);
|
||||
}
|
||||
|
||||
|
@ -15,6 +15,8 @@
|
||||
*/
|
||||
package com.google.android.exoplayer2.util;
|
||||
|
||||
import com.google.android.exoplayer2.PlaybackParameters;
|
||||
|
||||
/**
|
||||
* Tracks the progression of media time.
|
||||
*/
|
||||
@ -26,13 +28,13 @@ public interface MediaClock {
|
||||
long getPositionUs();
|
||||
|
||||
/**
|
||||
* Attempts to set the playback speed. The media clock may override the speed if changing the
|
||||
* speed is not supported.
|
||||
* Attempts to set the playback parameters. The media clock may override the speed if changing the
|
||||
* playback parameters is not supported.
|
||||
*
|
||||
* @param playbackSpeed The playback speed to attempt to set.
|
||||
* @param playbackParameters The playback parameters to attempt to set.
|
||||
*/
|
||||
void setPlaybackSpeed(float playbackSpeed);
|
||||
void setPlaybackParameters(PlaybackParameters playbackParameters);
|
||||
|
||||
/** Returns the active playback speed. */
|
||||
float getPlaybackSpeed();
|
||||
/** Returns the active playback parameters. */
|
||||
PlaybackParameters getPlaybackParameters();
|
||||
}
|
||||
|
@ -16,7 +16,7 @@
|
||||
package com.google.android.exoplayer2.util;
|
||||
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.Player;
|
||||
import com.google.android.exoplayer2.PlaybackParameters;
|
||||
|
||||
/**
|
||||
* A {@link MediaClock} whose position advances with real time based on the playback parameters when
|
||||
@ -29,8 +29,7 @@ public final class StandaloneMediaClock implements MediaClock {
|
||||
private boolean started;
|
||||
private long baseUs;
|
||||
private long baseElapsedMs;
|
||||
private float playbackSpeed;
|
||||
private int scaledUsPerMs;
|
||||
private PlaybackParameters playbackParameters;
|
||||
|
||||
/**
|
||||
* Creates a new standalone media clock using the given {@link Clock} implementation.
|
||||
@ -39,8 +38,7 @@ public final class StandaloneMediaClock implements MediaClock {
|
||||
*/
|
||||
public StandaloneMediaClock(Clock clock) {
|
||||
this.clock = clock;
|
||||
playbackSpeed = Player.DEFAULT_PLAYBACK_SPEED;
|
||||
scaledUsPerMs = getScaledUsPerMs(playbackSpeed);
|
||||
playbackParameters = PlaybackParameters.DEFAULT;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -80,33 +78,29 @@ public final class StandaloneMediaClock implements MediaClock {
|
||||
long positionUs = baseUs;
|
||||
if (started) {
|
||||
long elapsedSinceBaseMs = clock.elapsedRealtime() - baseElapsedMs;
|
||||
if (playbackSpeed == 1f) {
|
||||
if (playbackParameters.speed == 1f) {
|
||||
positionUs += C.msToUs(elapsedSinceBaseMs);
|
||||
} else {
|
||||
// Add the media time in microseconds that will elapse in elapsedSinceBaseMs milliseconds of
|
||||
// wallclock time
|
||||
positionUs += elapsedSinceBaseMs * scaledUsPerMs;
|
||||
positionUs += playbackParameters.getMediaTimeUsForPlayoutTimeMs(elapsedSinceBaseMs);
|
||||
}
|
||||
}
|
||||
return positionUs;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setPlaybackSpeed(float playbackSpeed) {
|
||||
public void setPlaybackParameters(PlaybackParameters playbackParameters) {
|
||||
// Store the current position as the new base, in case the playback speed has changed.
|
||||
if (started) {
|
||||
resetPosition(getPositionUs());
|
||||
}
|
||||
this.playbackSpeed = playbackSpeed;
|
||||
scaledUsPerMs = getScaledUsPerMs(playbackSpeed);
|
||||
this.playbackParameters = playbackParameters;
|
||||
}
|
||||
|
||||
@Override
|
||||
public float getPlaybackSpeed() {
|
||||
return playbackSpeed;
|
||||
public PlaybackParameters getPlaybackParameters() {
|
||||
return playbackParameters;
|
||||
}
|
||||
|
||||
private static int getScaledUsPerMs(float playbackSpeed) {
|
||||
return Math.round(playbackSpeed * 1000f);
|
||||
}
|
||||
}
|
||||
|
@ -1611,6 +1611,8 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
|
||||
case "ELUGA_Prim":
|
||||
case "ELUGA_Ray_X":
|
||||
case "EverStar_S":
|
||||
case "F02H":
|
||||
case "F03H":
|
||||
case "F3111":
|
||||
case "F3113":
|
||||
case "F3116":
|
||||
|
@ -22,7 +22,7 @@ import static org.mockito.Mockito.verifyNoMoreInteractions;
|
||||
import static org.mockito.MockitoAnnotations.initMocks;
|
||||
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
import com.google.android.exoplayer2.DefaultMediaClock.PlaybackSpeedListener;
|
||||
import com.google.android.exoplayer2.DefaultMediaClock.PlaybackParametersListener;
|
||||
import com.google.android.exoplayer2.testutil.FakeClock;
|
||||
import com.google.android.exoplayer2.testutil.FakeMediaClockRenderer;
|
||||
import org.junit.Before;
|
||||
@ -36,9 +36,10 @@ public class DefaultMediaClockTest {
|
||||
|
||||
private static final long TEST_POSITION_US = 123456789012345678L;
|
||||
private static final long SLEEP_TIME_MS = 1_000;
|
||||
private static final float TEST_PLAYBACK_SPEED = 2f;
|
||||
private static final PlaybackParameters TEST_PLAYBACK_PARAMETERS =
|
||||
new PlaybackParameters(/* speed= */ 2f);
|
||||
|
||||
@Mock private PlaybackSpeedListener listener;
|
||||
@Mock private PlaybackParametersListener listener;
|
||||
private FakeClock fakeClock;
|
||||
private DefaultMediaClock mediaClock;
|
||||
|
||||
@ -109,44 +110,44 @@ public class DefaultMediaClockTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void standaloneGetPlaybackSpeed_initializedWithDefaultPlaybackSpeed() {
|
||||
assertThat(mediaClock.getPlaybackSpeed()).isEqualTo(Player.DEFAULT_PLAYBACK_SPEED);
|
||||
public void standaloneGetPlaybackParameters_initializedWithDefaultPlaybackParameters() {
|
||||
assertThat(mediaClock.getPlaybackParameters()).isEqualTo(PlaybackParameters.DEFAULT);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void standaloneSetPlaybackSpeed_getPlaybackSpeedShouldReturnSameValue() {
|
||||
mediaClock.setPlaybackSpeed(TEST_PLAYBACK_SPEED);
|
||||
assertThat(mediaClock.getPlaybackSpeed()).isEqualTo(TEST_PLAYBACK_SPEED);
|
||||
public void standaloneSetPlaybackParameters_getPlaybackParametersShouldReturnSameValue() {
|
||||
mediaClock.setPlaybackParameters(TEST_PLAYBACK_PARAMETERS);
|
||||
assertThat(mediaClock.getPlaybackParameters()).isEqualTo(TEST_PLAYBACK_PARAMETERS);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void standaloneSetPlaybackSpeed_shouldNotTriggerCallback() {
|
||||
mediaClock.setPlaybackSpeed(TEST_PLAYBACK_SPEED);
|
||||
public void standaloneSetPlaybackParameters_shouldNotTriggerCallback() {
|
||||
mediaClock.setPlaybackParameters(TEST_PLAYBACK_PARAMETERS);
|
||||
verifyNoMoreInteractions(listener);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void standaloneSetPlaybackSpeed_shouldApplyNewPlaybackSpeed() {
|
||||
mediaClock.setPlaybackSpeed(TEST_PLAYBACK_SPEED);
|
||||
public void standaloneSetPlaybackParameters_shouldApplyNewPlaybackParameters() {
|
||||
mediaClock.setPlaybackParameters(TEST_PLAYBACK_PARAMETERS);
|
||||
mediaClock.start();
|
||||
// Asserts that clock is running with speed declared in getPlaybackSpeed().
|
||||
// Asserts that clock is running with speed declared in getPlaybackParameters().
|
||||
assertClockIsRunning(/* isReadingAhead= */ false);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void standaloneSetOtherPlaybackSpeed_getPlaybackSpeedShouldReturnSameValue() {
|
||||
mediaClock.setPlaybackSpeed(TEST_PLAYBACK_SPEED);
|
||||
mediaClock.setPlaybackSpeed(Player.DEFAULT_PLAYBACK_SPEED);
|
||||
assertThat(mediaClock.getPlaybackSpeed()).isEqualTo(Player.DEFAULT_PLAYBACK_SPEED);
|
||||
public void standaloneSetOtherPlaybackParameters_getPlaybackParametersShouldReturnSameValue() {
|
||||
mediaClock.setPlaybackParameters(TEST_PLAYBACK_PARAMETERS);
|
||||
mediaClock.setPlaybackParameters(PlaybackParameters.DEFAULT);
|
||||
assertThat(mediaClock.getPlaybackParameters()).isEqualTo(PlaybackParameters.DEFAULT);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void enableRendererMediaClock_shouldOverwriteRendererPlaybackSpeedIfPossible()
|
||||
public void enableRendererMediaClock_shouldOverwriteRendererPlaybackParametersIfPossible()
|
||||
throws ExoPlaybackException {
|
||||
FakeMediaClockRenderer mediaClockRenderer =
|
||||
new MediaClockRenderer(TEST_PLAYBACK_SPEED, /* playbackSpeedIsMutable= */ true);
|
||||
new MediaClockRenderer(TEST_PLAYBACK_PARAMETERS, /* playbackParametersAreMutable= */ true);
|
||||
mediaClock.onRendererEnabled(mediaClockRenderer);
|
||||
assertThat(mediaClock.getPlaybackSpeed()).isEqualTo(Player.DEFAULT_PLAYBACK_SPEED);
|
||||
assertThat(mediaClock.getPlaybackParameters()).isEqualTo(PlaybackParameters.DEFAULT);
|
||||
verifyNoMoreInteractions(listener);
|
||||
}
|
||||
|
||||
@ -154,26 +155,27 @@ public class DefaultMediaClockTest {
|
||||
public void enableRendererMediaClockWithFixedPlaybackSpeed_usesRendererPlaybackSpeed()
|
||||
throws ExoPlaybackException {
|
||||
FakeMediaClockRenderer mediaClockRenderer =
|
||||
new MediaClockRenderer(TEST_PLAYBACK_SPEED, /* playbackSpeedIsMutable= */ false);
|
||||
new MediaClockRenderer(TEST_PLAYBACK_PARAMETERS, /* playbackParametersAreMutable= */ false);
|
||||
mediaClock.onRendererEnabled(mediaClockRenderer);
|
||||
assertThat(mediaClock.getPlaybackSpeed()).isEqualTo(TEST_PLAYBACK_SPEED);
|
||||
assertThat(mediaClock.getPlaybackParameters()).isEqualTo(TEST_PLAYBACK_PARAMETERS);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void enableRendererMediaClockWithFixedPlaybackSpeed_shouldTriggerCallback()
|
||||
throws ExoPlaybackException {
|
||||
FakeMediaClockRenderer mediaClockRenderer =
|
||||
new MediaClockRenderer(TEST_PLAYBACK_SPEED, /* playbackSpeedIsMutable= */ false);
|
||||
new MediaClockRenderer(TEST_PLAYBACK_PARAMETERS, /* playbackParametersAreMutable= */ false);
|
||||
mediaClock.onRendererEnabled(mediaClockRenderer);
|
||||
mediaClock.syncAndGetPositionUs(/* isReadingAhead= */ false);
|
||||
verify(listener).onPlaybackSpeedChanged(TEST_PLAYBACK_SPEED);
|
||||
verify(listener).onPlaybackParametersChanged(TEST_PLAYBACK_PARAMETERS);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void enableRendererMediaClockWithFixedButSamePlaybackSpeed_shouldNotTriggerCallback()
|
||||
throws ExoPlaybackException {
|
||||
FakeMediaClockRenderer mediaClockRenderer =
|
||||
new MediaClockRenderer(Player.DEFAULT_PLAYBACK_SPEED, /* playbackSpeedIsMutable= */ false);
|
||||
new MediaClockRenderer(
|
||||
PlaybackParameters.DEFAULT, /* playbackParametersAreMutable= */ false);
|
||||
mediaClock.onRendererEnabled(mediaClockRenderer);
|
||||
mediaClock.syncAndGetPositionUs(/* isReadingAhead= */ false);
|
||||
verifyNoMoreInteractions(listener);
|
||||
@ -182,44 +184,47 @@ public class DefaultMediaClockTest {
|
||||
@Test
|
||||
public void disableRendererMediaClock_shouldKeepPlaybackSpeed() throws ExoPlaybackException {
|
||||
FakeMediaClockRenderer mediaClockRenderer =
|
||||
new MediaClockRenderer(TEST_PLAYBACK_SPEED, /* playbackSpeedIsMutable= */ false);
|
||||
new MediaClockRenderer(TEST_PLAYBACK_PARAMETERS, /* playbackParametersAreMutable= */ false);
|
||||
mediaClock.onRendererEnabled(mediaClockRenderer);
|
||||
mediaClock.syncAndGetPositionUs(/* isReadingAhead= */ false);
|
||||
mediaClock.onRendererDisabled(mediaClockRenderer);
|
||||
mediaClock.syncAndGetPositionUs(/* isReadingAhead= */ false);
|
||||
assertThat(mediaClock.getPlaybackSpeed()).isEqualTo(TEST_PLAYBACK_SPEED);
|
||||
assertThat(mediaClock.getPlaybackParameters()).isEqualTo(TEST_PLAYBACK_PARAMETERS);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void rendererClockSetPlaybackSpeed_getPlaybackSpeedShouldReturnSameValue()
|
||||
public void rendererClockSetPlaybackSpeed_getPlaybackParametersShouldReturnSameValue()
|
||||
throws ExoPlaybackException {
|
||||
FakeMediaClockRenderer mediaClockRenderer =
|
||||
new MediaClockRenderer(Player.DEFAULT_PLAYBACK_SPEED, /* playbackSpeedIsMutable= */ true);
|
||||
new MediaClockRenderer(
|
||||
PlaybackParameters.DEFAULT, /* playbackParametersAreMutable= */ true);
|
||||
mediaClock.onRendererEnabled(mediaClockRenderer);
|
||||
mediaClock.syncAndGetPositionUs(/* isReadingAhead= */ false);
|
||||
mediaClock.setPlaybackSpeed(TEST_PLAYBACK_SPEED);
|
||||
assertThat(mediaClock.getPlaybackSpeed()).isEqualTo(TEST_PLAYBACK_SPEED);
|
||||
mediaClock.setPlaybackParameters(TEST_PLAYBACK_PARAMETERS);
|
||||
assertThat(mediaClock.getPlaybackParameters()).isEqualTo(TEST_PLAYBACK_PARAMETERS);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void rendererClockSetPlaybackSpeed_shouldNotTriggerCallback() throws ExoPlaybackException {
|
||||
FakeMediaClockRenderer mediaClockRenderer =
|
||||
new MediaClockRenderer(Player.DEFAULT_PLAYBACK_SPEED, /* playbackSpeedIsMutable= */ true);
|
||||
new MediaClockRenderer(
|
||||
PlaybackParameters.DEFAULT, /* playbackParametersAreMutable= */ true);
|
||||
mediaClock.onRendererEnabled(mediaClockRenderer);
|
||||
mediaClock.syncAndGetPositionUs(/* isReadingAhead= */ false);
|
||||
mediaClock.setPlaybackSpeed(TEST_PLAYBACK_SPEED);
|
||||
mediaClock.setPlaybackParameters(TEST_PLAYBACK_PARAMETERS);
|
||||
verifyNoMoreInteractions(listener);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void rendererClockSetPlaybackSpeedOverwrite_getPlaybackSpeedShouldReturnSameValue()
|
||||
public void rendererClockSetPlaybackSpeedOverwrite_getPlaybackParametersShouldReturnSameValue()
|
||||
throws ExoPlaybackException {
|
||||
FakeMediaClockRenderer mediaClockRenderer =
|
||||
new MediaClockRenderer(Player.DEFAULT_PLAYBACK_SPEED, /* playbackSpeedIsMutable= */ false);
|
||||
new MediaClockRenderer(
|
||||
PlaybackParameters.DEFAULT, /* playbackParametersAreMutable= */ false);
|
||||
mediaClock.onRendererEnabled(mediaClockRenderer);
|
||||
mediaClock.syncAndGetPositionUs(/* isReadingAhead= */ false);
|
||||
mediaClock.setPlaybackSpeed(TEST_PLAYBACK_SPEED);
|
||||
assertThat(mediaClock.getPlaybackSpeed()).isEqualTo(Player.DEFAULT_PLAYBACK_SPEED);
|
||||
mediaClock.setPlaybackParameters(TEST_PLAYBACK_PARAMETERS);
|
||||
assertThat(mediaClock.getPlaybackParameters()).isEqualTo(PlaybackParameters.DEFAULT);
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -266,12 +271,13 @@ public class DefaultMediaClockTest {
|
||||
public void getPositionWithPlaybackSpeedChange_shouldTriggerCallback()
|
||||
throws ExoPlaybackException {
|
||||
MediaClockRenderer mediaClockRenderer =
|
||||
new MediaClockRenderer(Player.DEFAULT_PLAYBACK_SPEED, /* playbackSpeedIsMutable= */ true);
|
||||
new MediaClockRenderer(
|
||||
PlaybackParameters.DEFAULT, /* playbackParametersAreMutable= */ true);
|
||||
mediaClock.onRendererEnabled(mediaClockRenderer);
|
||||
// Silently change playback speed of renderer clock.
|
||||
mediaClockRenderer.playbackSpeed = TEST_PLAYBACK_SPEED;
|
||||
mediaClockRenderer.playbackParameters = TEST_PLAYBACK_PARAMETERS;
|
||||
mediaClock.syncAndGetPositionUs(/* isReadingAhead= */ false);
|
||||
verify(listener).onPlaybackSpeedChanged(TEST_PLAYBACK_SPEED);
|
||||
verify(listener).onPlaybackParametersChanged(TEST_PLAYBACK_PARAMETERS);
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -356,7 +362,7 @@ public class DefaultMediaClockTest {
|
||||
private void assertClockIsRunning(boolean isReadingAhead) {
|
||||
long clockStartUs = mediaClock.syncAndGetPositionUs(isReadingAhead);
|
||||
fakeClock.advanceTime(SLEEP_TIME_MS);
|
||||
int scaledUsPerMs = Math.round(mediaClock.getPlaybackSpeed() * 1000f);
|
||||
int scaledUsPerMs = Math.round(mediaClock.getPlaybackParameters().speed * 1000f);
|
||||
assertThat(mediaClock.syncAndGetPositionUs(isReadingAhead))
|
||||
.isEqualTo(clockStartUs + (SLEEP_TIME_MS * scaledUsPerMs));
|
||||
}
|
||||
@ -371,37 +377,53 @@ public class DefaultMediaClockTest {
|
||||
@SuppressWarnings("HidingField")
|
||||
private static class MediaClockRenderer extends FakeMediaClockRenderer {
|
||||
|
||||
private final boolean playbackSpeedIsMutable;
|
||||
private final boolean playbackParametersAreMutable;
|
||||
private final boolean isReady;
|
||||
private final boolean isEnded;
|
||||
|
||||
public float playbackSpeed;
|
||||
public PlaybackParameters playbackParameters;
|
||||
public long positionUs;
|
||||
|
||||
public MediaClockRenderer() throws ExoPlaybackException {
|
||||
this(Player.DEFAULT_PLAYBACK_SPEED, false, true, false, false);
|
||||
this(
|
||||
PlaybackParameters.DEFAULT,
|
||||
/* playbackParametersAreMutable= */ false,
|
||||
/* isReady= */ true,
|
||||
/* isEnded= */ false,
|
||||
/* hasReadStreamToEnd= */ false);
|
||||
}
|
||||
|
||||
public MediaClockRenderer(float playbackSpeed, boolean playbackSpeedIsMutable)
|
||||
public MediaClockRenderer(
|
||||
PlaybackParameters playbackParameters, boolean playbackParametersAreMutable)
|
||||
throws ExoPlaybackException {
|
||||
this(playbackSpeed, playbackSpeedIsMutable, true, false, false);
|
||||
this(
|
||||
playbackParameters,
|
||||
playbackParametersAreMutable,
|
||||
/* isReady= */ true,
|
||||
/* isEnded= */ false,
|
||||
/* hasReadStreamToEnd= */ false);
|
||||
}
|
||||
|
||||
public MediaClockRenderer(boolean isReady, boolean isEnded, boolean hasReadStreamToEnd)
|
||||
throws ExoPlaybackException {
|
||||
this(Player.DEFAULT_PLAYBACK_SPEED, false, isReady, isEnded, hasReadStreamToEnd);
|
||||
this(
|
||||
PlaybackParameters.DEFAULT,
|
||||
/* playbackParametersAreMutable= */ false,
|
||||
isReady,
|
||||
isEnded,
|
||||
hasReadStreamToEnd);
|
||||
}
|
||||
|
||||
private MediaClockRenderer(
|
||||
float playbackSpeed,
|
||||
boolean playbackSpeedIsMutable,
|
||||
PlaybackParameters playbackParameters,
|
||||
boolean playbackParametersAreMutable,
|
||||
boolean isReady,
|
||||
boolean isEnded,
|
||||
boolean hasReadStreamToEnd)
|
||||
throws ExoPlaybackException {
|
||||
super(C.TRACK_TYPE_UNKNOWN);
|
||||
this.playbackSpeed = playbackSpeed;
|
||||
this.playbackSpeedIsMutable = playbackSpeedIsMutable;
|
||||
this.playbackParameters = playbackParameters;
|
||||
this.playbackParametersAreMutable = playbackParametersAreMutable;
|
||||
this.isReady = isReady;
|
||||
this.isEnded = isEnded;
|
||||
this.positionUs = TEST_POSITION_US;
|
||||
@ -416,15 +438,15 @@ public class DefaultMediaClockTest {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setPlaybackSpeed(float playbackSpeed) {
|
||||
if (playbackSpeedIsMutable) {
|
||||
this.playbackSpeed = playbackSpeed;
|
||||
public void setPlaybackParameters(PlaybackParameters playbackParameters) {
|
||||
if (playbackParametersAreMutable) {
|
||||
this.playbackParameters = playbackParameters;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public float getPlaybackSpeed() {
|
||||
return playbackSpeed;
|
||||
public PlaybackParameters getPlaybackParameters() {
|
||||
return playbackParameters;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -328,11 +328,11 @@ public final class ExoPlayerTest {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setPlaybackSpeed(float playbackSpeed) {}
|
||||
public void setPlaybackParameters(PlaybackParameters playbackParameters) {}
|
||||
|
||||
@Override
|
||||
public float getPlaybackSpeed() {
|
||||
return Player.DEFAULT_PLAYBACK_SPEED;
|
||||
public PlaybackParameters getPlaybackParameters() {
|
||||
return PlaybackParameters.DEFAULT;
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -1010,7 +1010,7 @@ public final class ExoPlayerTest {
|
||||
}
|
||||
})
|
||||
// Set playback speed (while the fake media period is not yet prepared).
|
||||
.setPlaybackSpeed(2f)
|
||||
.setPlaybackParameters(new PlaybackParameters(/* speed= */ 2f))
|
||||
// Complete preparation of the fake media period.
|
||||
.executeRunnable(() -> fakeMediaPeriodHolder[0].setPreparationComplete())
|
||||
.build();
|
||||
@ -3378,18 +3378,18 @@ public final class ExoPlayerTest {
|
||||
SimpleExoPlayer player,
|
||||
DefaultTrackSelector trackSelector,
|
||||
@Nullable Surface surface) {
|
||||
maskedPlaybackSpeeds.add(player.getPlaybackSpeed());
|
||||
maskedPlaybackSpeeds.add(player.getPlaybackParameters().speed);
|
||||
}
|
||||
};
|
||||
ActionSchedule actionSchedule =
|
||||
new ActionSchedule.Builder(TAG)
|
||||
.pause()
|
||||
.waitForPlaybackState(Player.STATE_READY)
|
||||
.setPlaybackSpeed(1.1f)
|
||||
.setPlaybackParameters(new PlaybackParameters(/* speed= */ 1.1f))
|
||||
.apply(getPlaybackSpeedAction)
|
||||
.setPlaybackSpeed(1.2f)
|
||||
.setPlaybackParameters(new PlaybackParameters(/* speed= */ 1.2f))
|
||||
.apply(getPlaybackSpeedAction)
|
||||
.setPlaybackSpeed(1.3f)
|
||||
.setPlaybackParameters(new PlaybackParameters(/* speed= */ 1.3f))
|
||||
.apply(getPlaybackSpeedAction)
|
||||
.play()
|
||||
.build();
|
||||
@ -3397,8 +3397,8 @@ public final class ExoPlayerTest {
|
||||
EventListener listener =
|
||||
new EventListener() {
|
||||
@Override
|
||||
public void onPlaybackSpeedChanged(float playbackSpeed) {
|
||||
reportedPlaybackSpeeds.add(playbackSpeed);
|
||||
public void onPlaybackParametersChanged(PlaybackParameters playbackParameters) {
|
||||
reportedPlaybackSpeeds.add(playbackParameters.speed);
|
||||
}
|
||||
};
|
||||
new ExoPlayerTestRunner.Builder(context)
|
||||
@ -3424,28 +3424,28 @@ public final class ExoPlayerTest {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setPlaybackSpeed(float playbackSpeed) {}
|
||||
public void setPlaybackParameters(PlaybackParameters playbackParameters) {}
|
||||
|
||||
@Override
|
||||
public float getPlaybackSpeed() {
|
||||
return Player.DEFAULT_PLAYBACK_SPEED;
|
||||
public PlaybackParameters getPlaybackParameters() {
|
||||
return PlaybackParameters.DEFAULT;
|
||||
}
|
||||
};
|
||||
ActionSchedule actionSchedule =
|
||||
new ActionSchedule.Builder(TAG)
|
||||
.pause()
|
||||
.waitForPlaybackState(Player.STATE_READY)
|
||||
.setPlaybackSpeed(1.1f)
|
||||
.setPlaybackSpeed(1.2f)
|
||||
.setPlaybackSpeed(1.3f)
|
||||
.setPlaybackParameters(new PlaybackParameters(/* speed= */ 1.1f))
|
||||
.setPlaybackParameters(new PlaybackParameters(/* speed= */ 1.2f))
|
||||
.setPlaybackParameters(new PlaybackParameters(/* speed= */ 1.3f))
|
||||
.play()
|
||||
.build();
|
||||
List<Float> reportedPlaybackParameters = new ArrayList<>();
|
||||
List<PlaybackParameters> reportedPlaybackParameters = new ArrayList<>();
|
||||
EventListener listener =
|
||||
new EventListener() {
|
||||
@Override
|
||||
public void onPlaybackSpeedChanged(float playbackSpeed) {
|
||||
reportedPlaybackParameters.add(playbackSpeed);
|
||||
public void onPlaybackParametersChanged(PlaybackParameters playbackParameters) {
|
||||
reportedPlaybackParameters.add(playbackParameters);
|
||||
}
|
||||
};
|
||||
new ExoPlayerTestRunner.Builder(context)
|
||||
@ -3458,7 +3458,11 @@ public final class ExoPlayerTest {
|
||||
.blockUntilEnded(TIMEOUT_MS);
|
||||
|
||||
assertThat(reportedPlaybackParameters)
|
||||
.containsExactly(1.1f, 1.2f, 1.3f, Player.DEFAULT_PLAYBACK_SPEED)
|
||||
.containsExactly(
|
||||
new PlaybackParameters(/* speed= */ 1.1f),
|
||||
new PlaybackParameters(/* speed= */ 1.2f),
|
||||
new PlaybackParameters(/* speed= */ 1.3f),
|
||||
PlaybackParameters.DEFAULT)
|
||||
.inOrder();
|
||||
}
|
||||
|
||||
|
@ -435,7 +435,7 @@ public final class MediaPeriodQueueTest {
|
||||
/* loadingMediaPeriodId= */ null,
|
||||
/* playWhenReady= */ false,
|
||||
Player.PLAYBACK_SUPPRESSION_REASON_NONE,
|
||||
/* playbackSpeed= */ Player.DEFAULT_PLAYBACK_SPEED,
|
||||
/* playbackParameters= */ PlaybackParameters.DEFAULT,
|
||||
/* bufferedPositionUs= */ 0,
|
||||
/* totalBufferedDurationUs= */ 0,
|
||||
/* positionUs= */ 0,
|
||||
|
@ -26,6 +26,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.ExoPlaybackException;
|
||||
import com.google.android.exoplayer2.Format;
|
||||
import com.google.android.exoplayer2.PlaybackParameters;
|
||||
import com.google.android.exoplayer2.Player;
|
||||
import com.google.android.exoplayer2.Renderer;
|
||||
import com.google.android.exoplayer2.RenderersFactory;
|
||||
@ -82,7 +83,7 @@ public final class AnalyticsCollectorTest {
|
||||
private static final int EVENT_POSITION_DISCONTINUITY = 2;
|
||||
private static final int EVENT_SEEK_STARTED = 3;
|
||||
private static final int EVENT_SEEK_PROCESSED = 4;
|
||||
private static final int EVENT_PLAYBACK_SPEED_CHANGED = 5;
|
||||
private static final int EVENT_PLAYBACK_PARAMETERS_CHANGED = 5;
|
||||
private static final int EVENT_REPEAT_MODE_CHANGED = 6;
|
||||
private static final int EVENT_SHUFFLE_MODE_CHANGED = 7;
|
||||
private static final int EVENT_LOADING_CHANGED = 8;
|
||||
@ -106,21 +107,22 @@ public final class AnalyticsCollectorTest {
|
||||
private static final int EVENT_AUDIO_INPUT_FORMAT_CHANGED = 26;
|
||||
private static final int EVENT_AUDIO_DISABLED = 27;
|
||||
private static final int EVENT_AUDIO_SESSION_ID = 28;
|
||||
private static final int EVENT_AUDIO_UNDERRUN = 29;
|
||||
private static final int EVENT_VIDEO_ENABLED = 30;
|
||||
private static final int EVENT_VIDEO_DECODER_INIT = 31;
|
||||
private static final int EVENT_VIDEO_INPUT_FORMAT_CHANGED = 32;
|
||||
private static final int EVENT_DROPPED_FRAMES = 33;
|
||||
private static final int EVENT_VIDEO_DISABLED = 34;
|
||||
private static final int EVENT_RENDERED_FIRST_FRAME = 35;
|
||||
private static final int EVENT_VIDEO_FRAME_PROCESSING_OFFSET = 36;
|
||||
private static final int EVENT_VIDEO_SIZE_CHANGED = 37;
|
||||
private static final int EVENT_DRM_KEYS_LOADED = 38;
|
||||
private static final int EVENT_DRM_ERROR = 39;
|
||||
private static final int EVENT_DRM_KEYS_RESTORED = 40;
|
||||
private static final int EVENT_DRM_KEYS_REMOVED = 41;
|
||||
private static final int EVENT_DRM_SESSION_ACQUIRED = 42;
|
||||
private static final int EVENT_DRM_SESSION_RELEASED = 43;
|
||||
private static final int EVENT_AUDIO_POSITION_ADVANCING = 29;
|
||||
private static final int EVENT_AUDIO_UNDERRUN = 30;
|
||||
private static final int EVENT_VIDEO_ENABLED = 31;
|
||||
private static final int EVENT_VIDEO_DECODER_INIT = 32;
|
||||
private static final int EVENT_VIDEO_INPUT_FORMAT_CHANGED = 33;
|
||||
private static final int EVENT_DROPPED_FRAMES = 34;
|
||||
private static final int EVENT_VIDEO_DISABLED = 35;
|
||||
private static final int EVENT_RENDERED_FIRST_FRAME = 36;
|
||||
private static final int EVENT_VIDEO_FRAME_PROCESSING_OFFSET = 37;
|
||||
private static final int EVENT_VIDEO_SIZE_CHANGED = 38;
|
||||
private static final int EVENT_DRM_KEYS_LOADED = 39;
|
||||
private static final int EVENT_DRM_ERROR = 40;
|
||||
private static final int EVENT_DRM_KEYS_RESTORED = 41;
|
||||
private static final int EVENT_DRM_KEYS_REMOVED = 42;
|
||||
private static final int EVENT_DRM_SESSION_ACQUIRED = 43;
|
||||
private static final int EVENT_DRM_SESSION_RELEASED = 44;
|
||||
|
||||
private static final UUID DRM_SCHEME_UUID =
|
||||
UUID.nameUUIDFromBytes(TestUtil.createByteArray(7, 8, 9));
|
||||
@ -226,6 +228,7 @@ public final class AnalyticsCollectorTest {
|
||||
assertThat(listener.getEvents(EVENT_AUDIO_DECODER_INIT)).containsExactly(period0);
|
||||
assertThat(listener.getEvents(EVENT_AUDIO_INPUT_FORMAT_CHANGED)).containsExactly(period0);
|
||||
assertThat(listener.getEvents(EVENT_AUDIO_SESSION_ID)).containsExactly(period0);
|
||||
assertThat(listener.getEvents(EVENT_AUDIO_POSITION_ADVANCING)).containsExactly(period0);
|
||||
assertThat(listener.getEvents(EVENT_VIDEO_ENABLED)).containsExactly(period0);
|
||||
assertThat(listener.getEvents(EVENT_VIDEO_DECODER_INIT)).containsExactly(period0);
|
||||
assertThat(listener.getEvents(EVENT_VIDEO_INPUT_FORMAT_CHANGED)).containsExactly(period0);
|
||||
@ -305,6 +308,7 @@ public final class AnalyticsCollectorTest {
|
||||
.containsExactly(period0, period1)
|
||||
.inOrder();
|
||||
assertThat(listener.getEvents(EVENT_AUDIO_SESSION_ID)).containsExactly(period0);
|
||||
assertThat(listener.getEvents(EVENT_AUDIO_POSITION_ADVANCING)).containsExactly(period0);
|
||||
assertThat(listener.getEvents(EVENT_VIDEO_ENABLED)).containsExactly(period0);
|
||||
assertThat(listener.getEvents(EVENT_VIDEO_DECODER_INIT))
|
||||
.containsExactly(period0, period1)
|
||||
@ -380,6 +384,7 @@ public final class AnalyticsCollectorTest {
|
||||
assertThat(listener.getEvents(EVENT_AUDIO_DECODER_INIT)).containsExactly(period1);
|
||||
assertThat(listener.getEvents(EVENT_AUDIO_INPUT_FORMAT_CHANGED)).containsExactly(period1);
|
||||
assertThat(listener.getEvents(EVENT_AUDIO_SESSION_ID)).containsExactly(period1);
|
||||
assertThat(listener.getEvents(EVENT_AUDIO_POSITION_ADVANCING)).containsExactly(period1);
|
||||
assertThat(listener.getEvents(EVENT_VIDEO_ENABLED)).containsExactly(period0);
|
||||
assertThat(listener.getEvents(EVENT_VIDEO_DECODER_INIT)).containsExactly(period0);
|
||||
assertThat(listener.getEvents(EVENT_VIDEO_INPUT_FORMAT_CHANGED)).containsExactly(period0);
|
||||
@ -476,6 +481,9 @@ public final class AnalyticsCollectorTest {
|
||||
assertThat(listener.getEvents(EVENT_AUDIO_SESSION_ID))
|
||||
.containsExactly(period0, period1)
|
||||
.inOrder();
|
||||
assertThat(listener.getEvents(EVENT_AUDIO_POSITION_ADVANCING))
|
||||
.containsExactly(period0, period1)
|
||||
.inOrder();
|
||||
assertThat(listener.getEvents(EVENT_AUDIO_DISABLED)).containsExactly(period0);
|
||||
assertThat(listener.getEvents(EVENT_VIDEO_ENABLED)).containsExactly(period0);
|
||||
assertThat(listener.getEvents(EVENT_VIDEO_DECODER_INIT)).containsExactly(period0);
|
||||
@ -576,6 +584,9 @@ public final class AnalyticsCollectorTest {
|
||||
assertThat(listener.getEvents(EVENT_AUDIO_SESSION_ID))
|
||||
.containsExactly(period1Seq1, period1Seq2)
|
||||
.inOrder();
|
||||
assertThat(listener.getEvents(EVENT_AUDIO_POSITION_ADVANCING))
|
||||
.containsExactly(period1Seq1, period1Seq2)
|
||||
.inOrder();
|
||||
assertThat(listener.getEvents(EVENT_AUDIO_DISABLED)).containsExactly(period0);
|
||||
assertThat(listener.getEvents(EVENT_VIDEO_ENABLED)).containsExactly(period0, period0);
|
||||
assertThat(listener.getEvents(EVENT_VIDEO_DECODER_INIT))
|
||||
@ -1785,8 +1796,9 @@ public final class AnalyticsCollectorTest {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPlaybackSpeedChanged(EventTime eventTime, float playbackSpeed) {
|
||||
reportedEvents.add(new ReportedEvent(EVENT_PLAYBACK_SPEED_CHANGED, eventTime));
|
||||
public void onPlaybackParametersChanged(
|
||||
EventTime eventTime, PlaybackParameters playbackParameters) {
|
||||
reportedEvents.add(new ReportedEvent(EVENT_PLAYBACK_PARAMETERS_CHANGED, eventTime));
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -1922,6 +1934,11 @@ public final class AnalyticsCollectorTest {
|
||||
reportedEvents.add(new ReportedEvent(EVENT_AUDIO_SESSION_ID, eventTime));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAudioPositionAdvancing(EventTime eventTime, long playoutStartSystemTimeMs) {
|
||||
reportedEvents.add(new ReportedEvent(EVENT_AUDIO_POSITION_ADVANCING, eventTime));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAudioUnderrun(
|
||||
EventTime eventTime, int bufferSize, long bufferSizeMs, long elapsedSinceLastFeedMs) {
|
||||
|
@ -26,6 +26,7 @@ import static org.mockito.Mockito.verify;
|
||||
import android.os.SystemClock;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
import com.google.android.exoplayer2.PlaybackParameters;
|
||||
import com.google.android.exoplayer2.Player;
|
||||
import com.google.android.exoplayer2.Timeline;
|
||||
import com.google.android.exoplayer2.source.MediaSource;
|
||||
@ -75,8 +76,8 @@ public final class PlaybackStatsListenerTest {
|
||||
|
||||
playbackStatsListener.onPositionDiscontinuity(
|
||||
EMPTY_TIMELINE_EVENT_TIME, Player.DISCONTINUITY_REASON_SEEK);
|
||||
playbackStatsListener.onPlaybackSpeedChanged(
|
||||
EMPTY_TIMELINE_EVENT_TIME, /* playbackSpeed= */ 2.0f);
|
||||
playbackStatsListener.onPlaybackParametersChanged(
|
||||
EMPTY_TIMELINE_EVENT_TIME, new PlaybackParameters(/* speed= */ 2.0f));
|
||||
playbackStatsListener.onPlayWhenReadyChanged(
|
||||
EMPTY_TIMELINE_EVENT_TIME,
|
||||
/* playWhenReady= */ true,
|
||||
|
@ -25,6 +25,7 @@ import static org.robolectric.annotation.Config.TARGET_SDK;
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.Format;
|
||||
import com.google.android.exoplayer2.PlaybackParameters;
|
||||
import com.google.android.exoplayer2.util.MimeTypes;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
@ -89,7 +90,7 @@ public final class DefaultAudioSinkTest {
|
||||
|
||||
@Test
|
||||
public void handlesBufferAfterReset_withPlaybackSpeed() throws Exception {
|
||||
defaultAudioSink.setPlaybackSpeed(/* playbackSpeed= */ 1.5f);
|
||||
defaultAudioSink.setPlaybackParameters(new PlaybackParameters(/* speed= */ 1.5f));
|
||||
configureDefaultAudioSink(CHANNEL_COUNT_STEREO);
|
||||
defaultAudioSink.handleBuffer(
|
||||
createDefaultSilenceBuffer(), /* presentationTimeUs= */ 0, /* encodedAccessUnitCount= */ 1);
|
||||
@ -99,7 +100,8 @@ public final class DefaultAudioSinkTest {
|
||||
configureDefaultAudioSink(CHANNEL_COUNT_STEREO);
|
||||
defaultAudioSink.handleBuffer(
|
||||
createDefaultSilenceBuffer(), /* presentationTimeUs= */ 0, /* encodedAccessUnitCount= */ 1);
|
||||
assertThat(defaultAudioSink.getPlaybackSpeed()).isEqualTo(1.5f);
|
||||
assertThat(defaultAudioSink.getPlaybackParameters())
|
||||
.isEqualTo(new PlaybackParameters(/* speed= */ 1.5f));
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -117,7 +119,7 @@ public final class DefaultAudioSinkTest {
|
||||
|
||||
@Test
|
||||
public void handlesBufferAfterReset_withFormatChangeAndPlaybackSpeed() throws Exception {
|
||||
defaultAudioSink.setPlaybackSpeed(/* playbackSpeed= */ 1.5f);
|
||||
defaultAudioSink.setPlaybackParameters(new PlaybackParameters(/* speed= */ 1.5f));
|
||||
configureDefaultAudioSink(CHANNEL_COUNT_STEREO);
|
||||
defaultAudioSink.handleBuffer(
|
||||
createDefaultSilenceBuffer(), /* presentationTimeUs= */ 0, /* encodedAccessUnitCount= */ 1);
|
||||
@ -127,7 +129,8 @@ public final class DefaultAudioSinkTest {
|
||||
configureDefaultAudioSink(CHANNEL_COUNT_MONO);
|
||||
defaultAudioSink.handleBuffer(
|
||||
createDefaultSilenceBuffer(), /* presentationTimeUs= */ 0, /* encodedAccessUnitCount= */ 1);
|
||||
assertThat(defaultAudioSink.getPlaybackSpeed()).isEqualTo(1.5f);
|
||||
assertThat(defaultAudioSink.getPlaybackParameters())
|
||||
.isEqualTo(new PlaybackParameters(/* speed= */ 1.5f));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -49,8 +49,10 @@ import org.junit.runner.RunWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.MockitoJUnit;
|
||||
import org.mockito.junit.MockitoRule;
|
||||
import org.robolectric.annotation.Config;
|
||||
|
||||
/** Unit tests for {@link MediaCodecAudioRenderer} */
|
||||
@Config(sdk = 29)
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public class MediaCodecAudioRendererTest {
|
||||
@Rule public final MockitoRule mockito = MockitoJUnit.rule();
|
||||
|
@ -0,0 +1,64 @@
|
||||
/*
|
||||
* 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.e2etest;
|
||||
|
||||
import android.graphics.SurfaceTexture;
|
||||
import android.view.Surface;
|
||||
import androidx.test.core.app.ApplicationProvider;
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
import com.google.android.exoplayer2.MediaItem;
|
||||
import com.google.android.exoplayer2.Player;
|
||||
import com.google.android.exoplayer2.SimpleExoPlayer;
|
||||
import com.google.android.exoplayer2.testutil.AutoAdvancingFakeClock;
|
||||
import com.google.android.exoplayer2.testutil.DumpFileAsserts;
|
||||
import com.google.android.exoplayer2.testutil.PlaybackOutput;
|
||||
import com.google.android.exoplayer2.testutil.ShadowMediaCodecConfig;
|
||||
import com.google.android.exoplayer2.testutil.TestExoPlayer;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.robolectric.annotation.Config;
|
||||
|
||||
/** End-to-end tests using MP4 samples. */
|
||||
// TODO(b/143232359): Remove once https://issuetracker.google.com/143232359 is resolved.
|
||||
@Config(sdk = 29)
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public class Mp4PlaybackTest {
|
||||
@Rule
|
||||
public ShadowMediaCodecConfig mediaCodecConfig =
|
||||
ShadowMediaCodecConfig.forAllSupportedMimeTypes();
|
||||
|
||||
@Test
|
||||
public void h264VideoAacAudio() throws Exception {
|
||||
SimpleExoPlayer player =
|
||||
new SimpleExoPlayer.Builder(ApplicationProvider.getApplicationContext())
|
||||
.setClock(new AutoAdvancingFakeClock())
|
||||
.build();
|
||||
player.setVideoSurface(new Surface(new SurfaceTexture(/* texName= */ 1)));
|
||||
PlaybackOutput playbackOutput = PlaybackOutput.register(player, mediaCodecConfig);
|
||||
|
||||
player.setMediaItem(MediaItem.fromUri("asset:///media/mp4/sample.mp4"));
|
||||
player.prepare();
|
||||
player.play();
|
||||
TestExoPlayer.runUntilPlaybackState(player, Player.STATE_ENDED);
|
||||
|
||||
DumpFileAsserts.assertOutput(
|
||||
ApplicationProvider.getApplicationContext(),
|
||||
playbackOutput,
|
||||
"playbackdumps/mp4/sample.mp4.dump");
|
||||
}
|
||||
}
|
@ -0,0 +1,64 @@
|
||||
/*
|
||||
* 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.e2etest;
|
||||
|
||||
import android.graphics.SurfaceTexture;
|
||||
import android.view.Surface;
|
||||
import androidx.test.core.app.ApplicationProvider;
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
import com.google.android.exoplayer2.MediaItem;
|
||||
import com.google.android.exoplayer2.Player;
|
||||
import com.google.android.exoplayer2.SimpleExoPlayer;
|
||||
import com.google.android.exoplayer2.testutil.AutoAdvancingFakeClock;
|
||||
import com.google.android.exoplayer2.testutil.DumpFileAsserts;
|
||||
import com.google.android.exoplayer2.testutil.PlaybackOutput;
|
||||
import com.google.android.exoplayer2.testutil.ShadowMediaCodecConfig;
|
||||
import com.google.android.exoplayer2.testutil.TestExoPlayer;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.robolectric.annotation.Config;
|
||||
|
||||
/** End-to-end tests using TS samples. */
|
||||
// TODO(b/143232359): Remove once https://issuetracker.google.com/143232359 is resolved.
|
||||
@Config(sdk = 29)
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public class TsPlaybackTest {
|
||||
|
||||
@Rule
|
||||
public ShadowMediaCodecConfig mediaCodecConfig =
|
||||
ShadowMediaCodecConfig.forAllSupportedMimeTypes();
|
||||
|
||||
@Test
|
||||
public void mpegVideoMpegAudioScte35() throws Exception {
|
||||
SimpleExoPlayer player =
|
||||
new SimpleExoPlayer.Builder(ApplicationProvider.getApplicationContext())
|
||||
.setClock(new AutoAdvancingFakeClock())
|
||||
.build();
|
||||
player.setVideoSurface(new Surface(new SurfaceTexture(/* texName= */ 1)));
|
||||
PlaybackOutput playbackOutput = PlaybackOutput.register(player, mediaCodecConfig);
|
||||
|
||||
player.setMediaItem(MediaItem.fromUri("asset:///media/ts/sample_scte35.ts"));
|
||||
player.prepare();
|
||||
player.play();
|
||||
TestExoPlayer.runUntilPlaybackState(player, Player.STATE_ENDED);
|
||||
|
||||
DumpFileAsserts.assertOutput(
|
||||
ApplicationProvider.getApplicationContext(),
|
||||
playbackOutput,
|
||||
"playbackdumps/ts/sample_scte35.ts.dump");
|
||||
}
|
||||
}
|
@ -0,0 +1,53 @@
|
||||
/*
|
||||
* 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.source;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
|
||||
import android.net.Uri;
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.MediaItem;
|
||||
import com.google.android.exoplayer2.drm.DrmSessionManager;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
/** Unit tests for {@link MediaSourceDrmHelper}. */
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public class MediaSourceDrmHelperTest {
|
||||
|
||||
@Test
|
||||
public void create_noDrmProperties_createsNoopManager() {
|
||||
DrmSessionManager drmSessionManager =
|
||||
new MediaSourceDrmHelper().create(MediaItem.fromUri(Uri.EMPTY));
|
||||
|
||||
assertThat(drmSessionManager).isEqualTo(DrmSessionManager.DUMMY);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void create_createsManager() {
|
||||
MediaItem mediaItem =
|
||||
new MediaItem.Builder()
|
||||
.setUri(Uri.EMPTY)
|
||||
.setDrmLicenseUri(Uri.EMPTY)
|
||||
.setDrmUuid(C.WIDEVINE_UUID)
|
||||
.build();
|
||||
|
||||
DrmSessionManager drmSessionManager = new MediaSourceDrmHelper().create(mediaItem);
|
||||
|
||||
assertThat(drmSessionManager).isNotEqualTo(DrmSessionManager.DUMMY);
|
||||
}
|
||||
}
|
@ -38,6 +38,7 @@ import com.google.android.exoplayer2.drm.DrmInitData;
|
||||
import com.google.android.exoplayer2.drm.DrmSession;
|
||||
import com.google.android.exoplayer2.drm.DrmSessionEventListener;
|
||||
import com.google.android.exoplayer2.drm.DrmSessionManager;
|
||||
import com.google.android.exoplayer2.drm.ExoMediaCrypto;
|
||||
import com.google.android.exoplayer2.extractor.TrackOutput;
|
||||
import com.google.android.exoplayer2.testutil.TestUtil;
|
||||
import com.google.android.exoplayer2.upstream.Allocator;
|
||||
@ -54,7 +55,6 @@ import org.junit.Assert;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.ArgumentMatchers;
|
||||
import org.mockito.Mockito;
|
||||
|
||||
/** Test for {@link SampleQueue}. */
|
||||
@ -69,6 +69,8 @@ public final class SampleQueueTest {
|
||||
private static final Format FORMAT_SPLICED = buildFormat(/* id= */ "spliced");
|
||||
private static final Format FORMAT_ENCRYPTED =
|
||||
new Format.Builder().setId(/* id= */ "encrypted").setDrmInitData(new DrmInitData()).build();
|
||||
private static final Format FORMAT_ENCRYPTED_WITH_EXO_MEDIA_CRYPTO_TYPE =
|
||||
FORMAT_ENCRYPTED.copyWithExoMediaCryptoType(MockExoMediaCrypto.class);
|
||||
private static final byte[] DATA = TestUtil.buildTestData(ALLOCATION_SIZE * 10);
|
||||
|
||||
/*
|
||||
@ -128,7 +130,7 @@ public final class SampleQueueTest {
|
||||
new TrackOutput.CryptoData(C.CRYPTO_MODE_AES_CTR, new byte[16], 0, 0);
|
||||
|
||||
private Allocator allocator;
|
||||
private DrmSessionManager mockDrmSessionManager;
|
||||
private MockDrmSessionManager mockDrmSessionManager;
|
||||
private DrmSession mockDrmSession;
|
||||
private DrmSessionEventListener.EventDispatcher eventDispatcher;
|
||||
private SampleQueue sampleQueue;
|
||||
@ -138,11 +140,8 @@ public final class SampleQueueTest {
|
||||
@Before
|
||||
public void setUp() {
|
||||
allocator = new DefaultAllocator(false, ALLOCATION_SIZE);
|
||||
mockDrmSessionManager = Mockito.mock(DrmSessionManager.class);
|
||||
mockDrmSession = Mockito.mock(DrmSession.class);
|
||||
when(mockDrmSessionManager.acquireSession(
|
||||
ArgumentMatchers.any(), ArgumentMatchers.any(), ArgumentMatchers.any()))
|
||||
.thenReturn(mockDrmSession);
|
||||
mockDrmSessionManager = new MockDrmSessionManager(mockDrmSession);
|
||||
eventDispatcher = new DrmSessionEventListener.EventDispatcher();
|
||||
sampleQueue =
|
||||
new SampleQueue(
|
||||
@ -399,7 +398,7 @@ public final class SampleQueueTest {
|
||||
@Test
|
||||
public void isReadyReturnsTrueForValidDrmSession() {
|
||||
writeTestDataWithEncryptedSections();
|
||||
assertReadFormat(/* formatRequired= */ false, FORMAT_ENCRYPTED);
|
||||
assertReadFormat(/* formatRequired= */ false, FORMAT_ENCRYPTED_WITH_EXO_MEDIA_CRYPTO_TYPE);
|
||||
assertThat(sampleQueue.isReady(/* loadingFinished= */ false)).isFalse();
|
||||
when(mockDrmSession.getState()).thenReturn(DrmSession.STATE_OPENED_WITH_KEYS);
|
||||
assertThat(sampleQueue.isReady(/* loadingFinished= */ false)).isTrue();
|
||||
@ -424,7 +423,7 @@ public final class SampleQueueTest {
|
||||
when(mockDrmSession.getState()).thenReturn(DrmSession.STATE_OPENED);
|
||||
writeTestDataWithEncryptedSections();
|
||||
|
||||
assertReadFormat(/* formatRequired= */ false, FORMAT_ENCRYPTED);
|
||||
assertReadFormat(/* formatRequired= */ false, FORMAT_ENCRYPTED_WITH_EXO_MEDIA_CRYPTO_TYPE);
|
||||
assertReadNothing(/* formatRequired= */ false);
|
||||
assertThat(inputBuffer.waitingForKeys).isTrue();
|
||||
when(mockDrmSession.getState()).thenReturn(DrmSession.STATE_OPENED_WITH_KEYS);
|
||||
@ -464,9 +463,7 @@ public final class SampleQueueTest {
|
||||
when(mockDrmSession.getState()).thenReturn(DrmSession.STATE_OPENED_WITH_KEYS);
|
||||
DrmSession mockPlaceholderDrmSession = Mockito.mock(DrmSession.class);
|
||||
when(mockPlaceholderDrmSession.getState()).thenReturn(DrmSession.STATE_OPENED_WITH_KEYS);
|
||||
when(mockDrmSessionManager.acquirePlaceholderSession(
|
||||
ArgumentMatchers.any(), ArgumentMatchers.anyInt()))
|
||||
.thenReturn(mockPlaceholderDrmSession);
|
||||
mockDrmSessionManager.mockPlaceholderDrmSession = mockPlaceholderDrmSession;
|
||||
writeTestDataWithEncryptedSections();
|
||||
|
||||
int result =
|
||||
@ -497,9 +494,7 @@ public final class SampleQueueTest {
|
||||
when(mockDrmSession.getState()).thenReturn(DrmSession.STATE_OPENED_WITH_KEYS);
|
||||
DrmSession mockPlaceholderDrmSession = Mockito.mock(DrmSession.class);
|
||||
when(mockPlaceholderDrmSession.getState()).thenReturn(DrmSession.STATE_OPENED_WITH_KEYS);
|
||||
when(mockDrmSessionManager.acquirePlaceholderSession(
|
||||
ArgumentMatchers.any(), ArgumentMatchers.anyInt()))
|
||||
.thenReturn(mockPlaceholderDrmSession);
|
||||
mockDrmSessionManager.mockPlaceholderDrmSession = mockPlaceholderDrmSession;
|
||||
|
||||
writeFormat(ENCRYPTED_SAMPLE_FORMATS[0]);
|
||||
byte[] sampleData = new byte[] {0, 1, 2};
|
||||
@ -540,7 +535,7 @@ public final class SampleQueueTest {
|
||||
when(mockDrmSession.getState()).thenReturn(DrmSession.STATE_OPENED);
|
||||
writeTestDataWithEncryptedSections();
|
||||
|
||||
assertReadFormat(/* formatRequired= */ false, FORMAT_ENCRYPTED);
|
||||
assertReadFormat(/* formatRequired= */ false, FORMAT_ENCRYPTED_WITH_EXO_MEDIA_CRYPTO_TYPE);
|
||||
assertReadNothing(/* formatRequired= */ false);
|
||||
sampleQueue.maybeThrowError();
|
||||
when(mockDrmSession.getState()).thenReturn(DrmSession.STATE_ERROR);
|
||||
@ -569,7 +564,7 @@ public final class SampleQueueTest {
|
||||
when(mockDrmSession.getState()).thenReturn(DrmSession.STATE_OPENED);
|
||||
writeTestDataWithEncryptedSections();
|
||||
|
||||
assertReadFormat(/* formatRequired= */ false, FORMAT_ENCRYPTED);
|
||||
assertReadFormat(/* formatRequired= */ false, FORMAT_ENCRYPTED_WITH_EXO_MEDIA_CRYPTO_TYPE);
|
||||
assertReadEncryptedSample(/* sampleIndex= */ 0);
|
||||
}
|
||||
|
||||
@ -1497,4 +1492,33 @@ public final class SampleQueueTest {
|
||||
private static Format copyWithLabel(Format format, String label) {
|
||||
return format.buildUpon().setLabel(label).build();
|
||||
}
|
||||
|
||||
private static final class MockExoMediaCrypto implements ExoMediaCrypto {}
|
||||
|
||||
private static final class MockDrmSessionManager implements DrmSessionManager {
|
||||
|
||||
private final DrmSession mockDrmSession;
|
||||
@Nullable private DrmSession mockPlaceholderDrmSession;
|
||||
|
||||
private MockDrmSessionManager(DrmSession mockDrmSession) {
|
||||
this.mockDrmSession = mockDrmSession;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public DrmSession acquireSession(
|
||||
Looper playbackLooper,
|
||||
@Nullable DrmSessionEventListener.EventDispatcher eventDispatcher,
|
||||
Format format) {
|
||||
return format.drmInitData != null ? mockDrmSession : mockPlaceholderDrmSession;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public Class<? extends ExoMediaCrypto> getExoMediaCryptoType(Format format) {
|
||||
return mockPlaceholderDrmSession != null || format.drmInitData != null
|
||||
? MockExoMediaCrypto.class
|
||||
: null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -30,7 +30,6 @@ import com.google.android.exoplayer2.ExoPlayerLibraryInfo;
|
||||
import com.google.android.exoplayer2.MediaItem;
|
||||
import com.google.android.exoplayer2.ParserException;
|
||||
import com.google.android.exoplayer2.Timeline;
|
||||
import com.google.android.exoplayer2.drm.DrmSession;
|
||||
import com.google.android.exoplayer2.drm.DrmSessionEventListener;
|
||||
import com.google.android.exoplayer2.drm.DrmSessionManager;
|
||||
import com.google.android.exoplayer2.offline.FilteringManifestParser;
|
||||
@ -42,6 +41,7 @@ import com.google.android.exoplayer2.source.LoadEventInfo;
|
||||
import com.google.android.exoplayer2.source.MediaLoadData;
|
||||
import com.google.android.exoplayer2.source.MediaPeriod;
|
||||
import com.google.android.exoplayer2.source.MediaSource;
|
||||
import com.google.android.exoplayer2.source.MediaSourceDrmHelper;
|
||||
import com.google.android.exoplayer2.source.MediaSourceEventListener;
|
||||
import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher;
|
||||
import com.google.android.exoplayer2.source.MediaSourceFactory;
|
||||
@ -54,6 +54,7 @@ import com.google.android.exoplayer2.source.dash.manifest.UtcTimingElement;
|
||||
import com.google.android.exoplayer2.upstream.Allocator;
|
||||
import com.google.android.exoplayer2.upstream.DataSource;
|
||||
import com.google.android.exoplayer2.upstream.DefaultLoadErrorHandlingPolicy;
|
||||
import com.google.android.exoplayer2.upstream.HttpDataSource;
|
||||
import com.google.android.exoplayer2.upstream.LoadErrorHandlingPolicy;
|
||||
import com.google.android.exoplayer2.upstream.LoadErrorHandlingPolicy.LoadErrorInfo;
|
||||
import com.google.android.exoplayer2.upstream.Loader;
|
||||
@ -91,9 +92,10 @@ public final class DashMediaSource extends BaseMediaSource {
|
||||
public static final class Factory implements MediaSourceFactory {
|
||||
|
||||
private final DashChunkSource.Factory chunkSourceFactory;
|
||||
private final MediaSourceDrmHelper mediaSourceDrmHelper;
|
||||
@Nullable private final DataSource.Factory manifestDataSourceFactory;
|
||||
|
||||
private DrmSessionManager drmSessionManager;
|
||||
@Nullable private DrmSessionManager drmSessionManager;
|
||||
private CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory;
|
||||
private LoadErrorHandlingPolicy loadErrorHandlingPolicy;
|
||||
private long livePresentationDelayMs;
|
||||
@ -126,7 +128,7 @@ public final class DashMediaSource extends BaseMediaSource {
|
||||
@Nullable DataSource.Factory manifestDataSourceFactory) {
|
||||
this.chunkSourceFactory = checkNotNull(chunkSourceFactory);
|
||||
this.manifestDataSourceFactory = manifestDataSourceFactory;
|
||||
drmSessionManager = DrmSessionManager.getDummyDrmSessionManager();
|
||||
mediaSourceDrmHelper = new MediaSourceDrmHelper();
|
||||
loadErrorHandlingPolicy = new DefaultLoadErrorHandlingPolicy();
|
||||
livePresentationDelayMs = DEFAULT_LIVE_PRESENTATION_DELAY_MS;
|
||||
compositeSequenceableLoaderFactory = new DefaultCompositeSequenceableLoaderFactory();
|
||||
@ -155,19 +157,22 @@ public final class DashMediaSource extends BaseMediaSource {
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the {@link DrmSessionManager} to use for acquiring {@link DrmSession DrmSessions}. The
|
||||
* default value is {@link DrmSessionManager#DUMMY}.
|
||||
*
|
||||
* @param drmSessionManager The {@link DrmSessionManager}.
|
||||
* @return This factory, for convenience.
|
||||
*/
|
||||
@Override
|
||||
public Factory setDrmSessionManager(@Nullable DrmSessionManager drmSessionManager) {
|
||||
this.drmSessionManager =
|
||||
drmSessionManager != null
|
||||
? drmSessionManager
|
||||
: DrmSessionManager.getDummyDrmSessionManager();
|
||||
this.drmSessionManager = drmSessionManager;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Factory setDrmHttpDataSourceFactory(
|
||||
@Nullable HttpDataSource.Factory drmHttpDataSourceFactory) {
|
||||
mediaSourceDrmHelper.setDrmHttpDataSourceFactory(drmHttpDataSourceFactory);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Factory setDrmUserAgent(@Nullable String userAgent) {
|
||||
mediaSourceDrmHelper.setDrmUserAgent(userAgent);
|
||||
return this;
|
||||
}
|
||||
|
||||
@ -312,7 +317,7 @@ public final class DashMediaSource extends BaseMediaSource {
|
||||
/* manifestParser= */ null,
|
||||
chunkSourceFactory,
|
||||
compositeSequenceableLoaderFactory,
|
||||
drmSessionManager,
|
||||
drmSessionManager != null ? drmSessionManager : mediaSourceDrmHelper.create(mediaItem),
|
||||
loadErrorHandlingPolicy,
|
||||
livePresentationDelayMs,
|
||||
livePresentationDelayOverridesManifest);
|
||||
@ -403,7 +408,7 @@ public final class DashMediaSource extends BaseMediaSource {
|
||||
manifestParser,
|
||||
chunkSourceFactory,
|
||||
compositeSequenceableLoaderFactory,
|
||||
drmSessionManager,
|
||||
drmSessionManager != null ? drmSessionManager : mediaSourceDrmHelper.create(mediaItem),
|
||||
loadErrorHandlingPolicy,
|
||||
livePresentationDelayMs,
|
||||
livePresentationDelayOverridesManifest);
|
||||
|
@ -20,10 +20,34 @@ package com.google.android.exoplayer2.extractor;
|
||||
*/
|
||||
public interface ExtractorOutput {
|
||||
|
||||
/**
|
||||
* Placeholder {@link ExtractorOutput} implementation throwing an {@link
|
||||
* UnsupportedOperationException} in each method.
|
||||
*/
|
||||
ExtractorOutput PLACEHOLDER =
|
||||
new ExtractorOutput() {
|
||||
|
||||
@Override
|
||||
public TrackOutput track(int id, int type) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void endTracks() {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void seekMap(SeekMap seekMap) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Called by the {@link Extractor} to get the {@link TrackOutput} for a specific track.
|
||||
* <p>
|
||||
* The same {@link TrackOutput} is returned if multiple calls are made with the same {@code id}.
|
||||
*
|
||||
* <p>The same {@link TrackOutput} is returned if multiple calls are made with the same {@code
|
||||
* id}.
|
||||
*
|
||||
* @param id A track identifier.
|
||||
* @param type The type of the track. Typically one of the {@link com.google.android.exoplayer2.C}
|
||||
|
@ -48,6 +48,7 @@ import com.google.android.exoplayer2.util.ParsableByteArray;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
import com.google.android.exoplayer2.video.AvcConfig;
|
||||
import com.google.android.exoplayer2.video.ColorInfo;
|
||||
import com.google.android.exoplayer2.video.DolbyVisionConfig;
|
||||
import com.google.android.exoplayer2.video.HevcConfig;
|
||||
import java.io.IOException;
|
||||
import java.lang.annotation.Documented;
|
||||
@ -170,6 +171,9 @@ public class MatroskaExtractor implements Extractor {
|
||||
private static final int ID_FLAG_FORCED = 0x55AA;
|
||||
private static final int ID_DEFAULT_DURATION = 0x23E383;
|
||||
private static final int ID_MAX_BLOCK_ADDITION_ID = 0x55EE;
|
||||
private static final int ID_BLOCK_ADDITION_MAPPING = 0x41E4;
|
||||
private static final int ID_BLOCK_ADD_ID_TYPE = 0x41E7;
|
||||
private static final int ID_BLOCK_ADD_ID_EXTRA_DATA = 0x41ED;
|
||||
private static final int ID_NAME = 0x536E;
|
||||
private static final int ID_CODEC_ID = 0x86;
|
||||
private static final int ID_CODEC_PRIVATE = 0x63A2;
|
||||
@ -234,6 +238,17 @@ public class MatroskaExtractor implements Extractor {
|
||||
*/
|
||||
private static final int BLOCK_ADDITIONAL_ID_VP9_ITU_T_35 = 4;
|
||||
|
||||
/**
|
||||
* BlockAddIdType value for Dolby Vision configuration with profile <= 7. See also
|
||||
* https://www.matroska.org/technical/codec_specs.html.
|
||||
*/
|
||||
private static final int BLOCK_ADD_ID_TYPE_DVCC = 0x64766343;
|
||||
/**
|
||||
* BlockAddIdType value for Dolby Vision configuration with profile > 7. See also
|
||||
* https://www.matroska.org/technical/codec_specs.html.
|
||||
*/
|
||||
private static final int BLOCK_ADD_ID_TYPE_DVVC = 0x64767643;
|
||||
|
||||
private static final int LACING_NONE = 0;
|
||||
private static final int LACING_XIPH = 1;
|
||||
private static final int LACING_FIXED_SIZE = 2;
|
||||
@ -501,6 +516,7 @@ public class MatroskaExtractor implements Extractor {
|
||||
case ID_CLUSTER:
|
||||
case ID_TRACKS:
|
||||
case ID_TRACK_ENTRY:
|
||||
case ID_BLOCK_ADDITION_MAPPING:
|
||||
case ID_AUDIO:
|
||||
case ID_VIDEO:
|
||||
case ID_CONTENT_ENCODINGS:
|
||||
@ -535,6 +551,7 @@ public class MatroskaExtractor implements Extractor {
|
||||
case ID_FLAG_FORCED:
|
||||
case ID_DEFAULT_DURATION:
|
||||
case ID_MAX_BLOCK_ADDITION_ID:
|
||||
case ID_BLOCK_ADD_ID_TYPE:
|
||||
case ID_CODEC_DELAY:
|
||||
case ID_SEEK_PRE_ROLL:
|
||||
case ID_CHANNELS:
|
||||
@ -562,6 +579,7 @@ public class MatroskaExtractor implements Extractor {
|
||||
case ID_LANGUAGE:
|
||||
return EbmlProcessor.ELEMENT_TYPE_STRING;
|
||||
case ID_SEEK_ID:
|
||||
case ID_BLOCK_ADD_ID_EXTRA_DATA:
|
||||
case ID_CONTENT_COMPRESSION_SETTINGS:
|
||||
case ID_CONTENT_ENCRYPTION_KEY_ID:
|
||||
case ID_SIMPLE_BLOCK:
|
||||
@ -814,6 +832,9 @@ public class MatroskaExtractor implements Extractor {
|
||||
case ID_MAX_BLOCK_ADDITION_ID:
|
||||
currentTrack.maxBlockAdditionId = (int) value;
|
||||
break;
|
||||
case ID_BLOCK_ADD_ID_TYPE:
|
||||
currentTrack.blockAddIdType = (int) value;
|
||||
break;
|
||||
case ID_CODEC_DELAY:
|
||||
currentTrack.codecDelayNs = value;
|
||||
break;
|
||||
@ -1076,6 +1097,9 @@ public class MatroskaExtractor implements Extractor {
|
||||
seekEntryIdBytes.setPosition(0);
|
||||
seekEntryId = (int) seekEntryIdBytes.readUnsignedInt();
|
||||
break;
|
||||
case ID_BLOCK_ADD_ID_EXTRA_DATA:
|
||||
handleBlockAddIDExtraData(currentTrack, input, contentSize);
|
||||
break;
|
||||
case ID_CODEC_PRIVATE:
|
||||
currentTrack.codecPrivate = new byte[contentSize];
|
||||
input.readFully(currentTrack.codecPrivate, 0, contentSize);
|
||||
@ -1244,6 +1268,18 @@ public class MatroskaExtractor implements Extractor {
|
||||
}
|
||||
}
|
||||
|
||||
protected void handleBlockAddIDExtraData(Track track, ExtractorInput input, int contentSize)
|
||||
throws IOException {
|
||||
if (track.blockAddIdType == BLOCK_ADD_ID_TYPE_DVVC
|
||||
|| track.blockAddIdType == BLOCK_ADD_ID_TYPE_DVCC) {
|
||||
track.dolbyVisionConfigBytes = new byte[contentSize];
|
||||
input.readFully(track.dolbyVisionConfigBytes, 0, contentSize);
|
||||
} else {
|
||||
// Unhandled BlockAddIDExtraData.
|
||||
input.skipFully(contentSize);
|
||||
}
|
||||
}
|
||||
|
||||
protected void handleBlockAdditionalData(
|
||||
Track track, int blockAdditionalId, ExtractorInput input, int contentSize)
|
||||
throws IOException {
|
||||
@ -1883,6 +1919,7 @@ public class MatroskaExtractor implements Extractor {
|
||||
public int type;
|
||||
public int defaultSampleDurationNs;
|
||||
public int maxBlockAdditionId;
|
||||
private int blockAddIdType;
|
||||
public boolean hasContentEncryption;
|
||||
public byte[] sampleStrippedBytes;
|
||||
public TrackOutput.CryptoData cryptoData;
|
||||
@ -1921,6 +1958,7 @@ public class MatroskaExtractor implements Extractor {
|
||||
public float whitePointChromaticityY = Format.NO_VALUE;
|
||||
public float maxMasteringLuminance = Format.NO_VALUE;
|
||||
public float minMasteringLuminance = Format.NO_VALUE;
|
||||
@Nullable public byte[] dolbyVisionConfigBytes;
|
||||
|
||||
// Audio elements. Initially set to their default values.
|
||||
public int channelCount = 1;
|
||||
@ -2091,6 +2129,16 @@ public class MatroskaExtractor implements Extractor {
|
||||
throw new ParserException("Unrecognized codec identifier.");
|
||||
}
|
||||
|
||||
if (dolbyVisionConfigBytes != null) {
|
||||
@Nullable
|
||||
DolbyVisionConfig dolbyVisionConfig =
|
||||
DolbyVisionConfig.parse(new ParsableByteArray(this.dolbyVisionConfigBytes));
|
||||
if (dolbyVisionConfig != null) {
|
||||
codecs = dolbyVisionConfig.codecs;
|
||||
mimeType = MimeTypes.VIDEO_DOLBY_VISION;
|
||||
}
|
||||
}
|
||||
|
||||
@C.SelectionFlags int selectionFlags = 0;
|
||||
selectionFlags |= flagDefault ? C.SELECTION_FLAG_DEFAULT : 0;
|
||||
selectionFlags |= flagForced ? C.SELECTION_FLAG_FORCED : 0;
|
||||
|
@ -16,6 +16,10 @@
|
||||
package com.google.android.exoplayer2.extractor.mp4;
|
||||
|
||||
import static com.google.android.exoplayer2.extractor.mp4.AtomParsers.parseTraks;
|
||||
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;
|
||||
import static com.google.android.exoplayer2.util.Util.nullSafeArrayCopy;
|
||||
import static java.lang.Math.max;
|
||||
|
||||
import android.util.Pair;
|
||||
@ -42,7 +46,6 @@ import com.google.android.exoplayer2.extractor.mp4.Atom.ContainerAtom;
|
||||
import com.google.android.exoplayer2.extractor.mp4.Atom.LeafAtom;
|
||||
import com.google.android.exoplayer2.metadata.emsg.EventMessage;
|
||||
import com.google.android.exoplayer2.metadata.emsg.EventMessageEncoder;
|
||||
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.NalUnitUtil;
|
||||
@ -59,7 +62,6 @@ import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
||||
|
||||
/** Extracts data from the FMP4 container format. */
|
||||
@SuppressWarnings("ConstantField")
|
||||
@ -175,7 +177,7 @@ public class FragmentedMp4Extractor implements Extractor {
|
||||
private boolean processSeiNalUnitPayload;
|
||||
|
||||
// Outputs.
|
||||
private @MonotonicNonNull ExtractorOutput extractorOutput;
|
||||
private ExtractorOutput extractorOutput;
|
||||
private TrackOutput[] emsgTrackOutputs;
|
||||
private TrackOutput[] ceaTrackOutputs;
|
||||
|
||||
@ -270,9 +272,9 @@ public class FragmentedMp4Extractor implements Extractor {
|
||||
durationUs = C.TIME_UNSET;
|
||||
pendingSeekTimeUs = C.TIME_UNSET;
|
||||
segmentIndexEarliestPresentationTimeUs = C.TIME_UNSET;
|
||||
extractorOutput = ExtractorOutput.PLACEHOLDER;
|
||||
emsgTrackOutputs = new TrackOutput[0];
|
||||
ceaTrackOutputs = new TrackOutput[0];
|
||||
enterReadingAtomHeaderState();
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -283,6 +285,7 @@ public class FragmentedMp4Extractor implements Extractor {
|
||||
@Override
|
||||
public void init(ExtractorOutput output) {
|
||||
extractorOutput = output;
|
||||
enterReadingAtomHeaderState();
|
||||
initExtraTracks();
|
||||
if (sideloadedTrack != null) {
|
||||
TrackBundle bundle =
|
||||
@ -429,8 +432,9 @@ public class FragmentedMp4Extractor implements Extractor {
|
||||
if (atomSize > Integer.MAX_VALUE) {
|
||||
throw new ParserException("Leaf atom with length > 2147483647 (unsupported).");
|
||||
}
|
||||
atomData = new ParsableByteArray((int) atomSize);
|
||||
ParsableByteArray atomData = new ParsableByteArray((int) atomSize);
|
||||
System.arraycopy(atomHeader.getData(), 0, atomData.getData(), 0, Atom.HEADER_SIZE);
|
||||
this.atomData = atomData;
|
||||
parserState = STATE_READING_ATOM_PAYLOAD;
|
||||
} else {
|
||||
if (atomSize > Integer.MAX_VALUE) {
|
||||
@ -445,6 +449,7 @@ public class FragmentedMp4Extractor implements Extractor {
|
||||
|
||||
private void readAtomPayload(ExtractorInput input) throws IOException {
|
||||
int atomPayloadSize = (int) atomSize - atomHeaderBytesRead;
|
||||
@Nullable ParsableByteArray atomData = this.atomData;
|
||||
if (atomData != null) {
|
||||
input.readFully(atomData.getData(), Atom.HEADER_SIZE, atomPayloadSize);
|
||||
onLeafAtomRead(new LeafAtom(atomType, atomData), input.getPosition());
|
||||
@ -485,12 +490,12 @@ public class FragmentedMp4Extractor implements Extractor {
|
||||
}
|
||||
|
||||
private void onMoovContainerAtomRead(ContainerAtom moov) throws ParserException {
|
||||
Assertions.checkState(sideloadedTrack == null, "Unexpected moov box.");
|
||||
checkState(sideloadedTrack == null, "Unexpected moov box.");
|
||||
|
||||
@Nullable DrmInitData drmInitData = getDrmInitDataFromAtoms(moov.leafChildren);
|
||||
|
||||
// Read declaration of track fragments in the Moov box.
|
||||
ContainerAtom mvex = moov.getContainerAtomOfType(Atom.TYPE_mvex);
|
||||
// Read declaration of track fragments in the moov box.
|
||||
ContainerAtom mvex = checkNotNull(moov.getContainerAtomOfType(Atom.TYPE_mvex));
|
||||
SparseArray<DefaultSampleValues> defaultSampleValuesArray = new SparseArray<>();
|
||||
long duration = C.TIME_UNSET;
|
||||
int mvexChildrenSize = mvex.leafChildren.size();
|
||||
@ -531,7 +536,7 @@ public class FragmentedMp4Extractor implements Extractor {
|
||||
}
|
||||
extractorOutput.endTracks();
|
||||
} else {
|
||||
Assertions.checkState(trackBundles.size() == trackCount);
|
||||
checkState(trackBundles.size() == trackCount);
|
||||
for (int i = 0; i < trackCount; i++) {
|
||||
TrackSampleTable sampleTable = sampleTables.get(i);
|
||||
Track track = sampleTable.track;
|
||||
@ -554,7 +559,7 @@ public class FragmentedMp4Extractor implements Extractor {
|
||||
// See https://github.com/google/ExoPlayer/issues/4477.
|
||||
return defaultSampleValuesArray.valueAt(/* index= */ 0);
|
||||
}
|
||||
return Assertions.checkNotNull(defaultSampleValuesArray.get(trackId));
|
||||
return checkNotNull(defaultSampleValuesArray.get(trackId));
|
||||
}
|
||||
|
||||
private void onMoofContainerAtomRead(ContainerAtom moof) throws ParserException {
|
||||
@ -589,7 +594,7 @@ public class FragmentedMp4Extractor implements Extractor {
|
||||
emsgTrackOutputs[emsgTrackOutputCount++] =
|
||||
extractorOutput.track(nextExtraTrackId++, C.TRACK_TYPE_METADATA);
|
||||
}
|
||||
emsgTrackOutputs = Arrays.copyOf(emsgTrackOutputs, emsgTrackOutputCount);
|
||||
emsgTrackOutputs = nullSafeArrayCopy(emsgTrackOutputs, emsgTrackOutputCount);
|
||||
for (TrackOutput eventMessageTrackOutput : emsgTrackOutputs) {
|
||||
eventMessageTrackOutput.format(EMSG_FORMAT);
|
||||
}
|
||||
@ -604,7 +609,7 @@ public class FragmentedMp4Extractor implements Extractor {
|
||||
|
||||
/** Handles an emsg atom (defined in 23009-1). */
|
||||
private void onEmsgLeafAtomRead(ParsableByteArray atom) {
|
||||
if (emsgTrackOutputs == null || emsgTrackOutputs.length == 0) {
|
||||
if (emsgTrackOutputs.length == 0) {
|
||||
return;
|
||||
}
|
||||
atom.setPosition(Atom.HEADER_SIZE);
|
||||
@ -619,8 +624,8 @@ public class FragmentedMp4Extractor implements Extractor {
|
||||
long id;
|
||||
switch (version) {
|
||||
case 0:
|
||||
schemeIdUri = Assertions.checkNotNull(atom.readNullTerminatedString());
|
||||
value = Assertions.checkNotNull(atom.readNullTerminatedString());
|
||||
schemeIdUri = checkNotNull(atom.readNullTerminatedString());
|
||||
value = checkNotNull(atom.readNullTerminatedString());
|
||||
timescale = atom.readUnsignedInt();
|
||||
presentationTimeDeltaUs =
|
||||
Util.scaleLargeTimestamp(atom.readUnsignedInt(), C.MICROS_PER_SECOND, timescale);
|
||||
@ -638,8 +643,8 @@ public class FragmentedMp4Extractor implements Extractor {
|
||||
durationMs =
|
||||
Util.scaleLargeTimestamp(atom.readUnsignedInt(), C.MILLIS_PER_SECOND, timescale);
|
||||
id = atom.readUnsignedInt();
|
||||
schemeIdUri = Assertions.checkNotNull(atom.readNullTerminatedString());
|
||||
value = Assertions.checkNotNull(atom.readNullTerminatedString());
|
||||
schemeIdUri = checkNotNull(atom.readNullTerminatedString());
|
||||
value = checkNotNull(atom.readNullTerminatedString());
|
||||
break;
|
||||
default:
|
||||
Log.w(TAG, "Skipping unsupported emsg version: " + version);
|
||||
@ -717,7 +722,7 @@ public class FragmentedMp4Extractor implements Extractor {
|
||||
*/
|
||||
private static void parseTraf(ContainerAtom traf, SparseArray<TrackBundle> trackBundleArray,
|
||||
@Flags int flags, byte[] extendedTypeScratch) throws ParserException {
|
||||
LeafAtom tfhd = traf.getLeafAtomOfType(Atom.TYPE_tfhd);
|
||||
LeafAtom tfhd = checkNotNull(traf.getLeafAtomOfType(Atom.TYPE_tfhd));
|
||||
@Nullable TrackBundle trackBundle = parseTfhd(tfhd.data, trackBundleArray);
|
||||
if (trackBundle == null) {
|
||||
return;
|
||||
@ -730,7 +735,7 @@ public class FragmentedMp4Extractor implements Extractor {
|
||||
trackBundle.currentlyInFragment = true;
|
||||
@Nullable LeafAtom tfdtAtom = traf.getLeafAtomOfType(Atom.TYPE_tfdt);
|
||||
if (tfdtAtom != null && (flags & FLAG_WORKAROUND_IGNORE_TFDT_BOX) == 0) {
|
||||
fragment.nextFragmentDecodeTime = parseTfdt(traf.getLeafAtomOfType(Atom.TYPE_tfdt).data);
|
||||
fragment.nextFragmentDecodeTime = parseTfdt(tfdtAtom.data);
|
||||
fragment.nextFragmentDecodeTimeIncludesMoov = true;
|
||||
} else {
|
||||
fragment.nextFragmentDecodeTime = fragmentDecodeTime;
|
||||
@ -742,11 +747,11 @@ public class FragmentedMp4Extractor implements Extractor {
|
||||
@Nullable
|
||||
TrackEncryptionBox encryptionBox =
|
||||
trackBundle.moovSampleTable.track.getSampleDescriptionEncryptionBox(
|
||||
fragment.header.sampleDescriptionIndex);
|
||||
checkNotNull(fragment.header).sampleDescriptionIndex);
|
||||
|
||||
@Nullable LeafAtom saiz = traf.getLeafAtomOfType(Atom.TYPE_saiz);
|
||||
if (saiz != null) {
|
||||
parseSaiz(encryptionBox, saiz.data, fragment);
|
||||
parseSaiz(checkNotNull(encryptionBox), saiz.data, fragment);
|
||||
}
|
||||
|
||||
@Nullable LeafAtom saio = traf.getLeafAtomOfType(Atom.TYPE_saio);
|
||||
@ -964,7 +969,7 @@ public class FragmentedMp4Extractor implements Extractor {
|
||||
|
||||
Track track = trackBundle.moovSampleTable.track;
|
||||
TrackFragment fragment = trackBundle.fragment;
|
||||
DefaultSampleValues defaultSampleValues = fragment.header;
|
||||
DefaultSampleValues defaultSampleValues = castNonNull(fragment.header);
|
||||
|
||||
fragment.trunLength[index] = trun.readUnsignedIntToInt();
|
||||
fragment.trunDataPosition[index] = fragment.dataPosition;
|
||||
@ -994,7 +999,7 @@ public class FragmentedMp4Extractor implements Extractor {
|
||||
&& track.editListDurations[0] == 0) {
|
||||
edtsOffsetUs =
|
||||
Util.scaleLargeTimestamp(
|
||||
track.editListMediaTimes[0], C.MICROS_PER_SECOND, track.timescale);
|
||||
castNonNull(track.editListMediaTimes)[0], C.MICROS_PER_SECOND, track.timescale);
|
||||
}
|
||||
|
||||
int[] sampleSizeTable = fragment.sampleSizeTable;
|
||||
@ -1161,7 +1166,7 @@ public class FragmentedMp4Extractor implements Extractor {
|
||||
int perSampleIvSize = sgpd.readUnsignedByte();
|
||||
byte[] keyId = new byte[16];
|
||||
sgpd.readBytes(keyId, 0, keyId.length);
|
||||
byte[] constantIv = null;
|
||||
@Nullable byte[] constantIv = null;
|
||||
if (perSampleIvSize == 0) {
|
||||
int constantIvSize = sgpd.readUnsignedByte();
|
||||
constantIv = new byte[constantIvSize];
|
||||
@ -1238,7 +1243,7 @@ public class FragmentedMp4Extractor implements Extractor {
|
||||
}
|
||||
|
||||
private void readEncryptionData(ExtractorInput input) throws IOException {
|
||||
TrackBundle nextTrackBundle = null;
|
||||
@Nullable TrackBundle nextTrackBundle = null;
|
||||
long nextDataOffset = Long.MAX_VALUE;
|
||||
int trackBundlesSize = trackBundles.size();
|
||||
for (int i = 0; i < trackBundlesSize; i++) {
|
||||
@ -1277,71 +1282,70 @@ public class FragmentedMp4Extractor implements Extractor {
|
||||
* @throws IOException If an error occurs reading from the input.
|
||||
*/
|
||||
private boolean readSample(ExtractorInput input) throws IOException {
|
||||
if (parserState == STATE_READING_SAMPLE_START) {
|
||||
if (currentTrackBundle == null) {
|
||||
@Nullable TrackBundle currentTrackBundle = getNextTrackBundle(trackBundles);
|
||||
if (currentTrackBundle == null) {
|
||||
// We've run out of samples in the current mdat. Discard any trailing data and prepare to
|
||||
// read the header of the next atom.
|
||||
int bytesToSkip = (int) (endOfMdatPosition - input.getPosition());
|
||||
if (bytesToSkip < 0) {
|
||||
throw new ParserException("Offset to end of mdat was negative.");
|
||||
}
|
||||
input.skipFully(bytesToSkip);
|
||||
enterReadingAtomHeaderState();
|
||||
return false;
|
||||
}
|
||||
|
||||
long nextDataPosition = currentTrackBundle.getCurrentSampleOffset();
|
||||
// We skip bytes preceding the next sample to read.
|
||||
int bytesToSkip = (int) (nextDataPosition - input.getPosition());
|
||||
@Nullable TrackBundle trackBundle = currentTrackBundle;
|
||||
if (trackBundle == null) {
|
||||
trackBundle = getNextTrackBundle(trackBundles);
|
||||
if (trackBundle == null) {
|
||||
// We've run out of samples in the current mdat. Discard any trailing data and prepare to
|
||||
// read the header of the next atom.
|
||||
int bytesToSkip = (int) (endOfMdatPosition - input.getPosition());
|
||||
if (bytesToSkip < 0) {
|
||||
// Assume the sample data must be contiguous in the mdat with no preceding data.
|
||||
Log.w(TAG, "Ignoring negative offset to sample data.");
|
||||
bytesToSkip = 0;
|
||||
throw new ParserException("Offset to end of mdat was negative.");
|
||||
}
|
||||
input.skipFully(bytesToSkip);
|
||||
this.currentTrackBundle = currentTrackBundle;
|
||||
enterReadingAtomHeaderState();
|
||||
return false;
|
||||
}
|
||||
|
||||
sampleSize = currentTrackBundle.getCurrentSampleSize();
|
||||
long nextDataPosition = trackBundle.getCurrentSampleOffset();
|
||||
// We skip bytes preceding the next sample to read.
|
||||
int bytesToSkip = (int) (nextDataPosition - input.getPosition());
|
||||
if (bytesToSkip < 0) {
|
||||
// Assume the sample data must be contiguous in the mdat with no preceding data.
|
||||
Log.w(TAG, "Ignoring negative offset to sample data.");
|
||||
bytesToSkip = 0;
|
||||
}
|
||||
input.skipFully(bytesToSkip);
|
||||
currentTrackBundle = trackBundle;
|
||||
}
|
||||
if (parserState == STATE_READING_SAMPLE_START) {
|
||||
sampleSize = trackBundle.getCurrentSampleSize();
|
||||
|
||||
if (currentTrackBundle.currentSampleIndex < currentTrackBundle.firstSampleToOutputIndex) {
|
||||
if (trackBundle.currentSampleIndex < trackBundle.firstSampleToOutputIndex) {
|
||||
input.skipFully(sampleSize);
|
||||
currentTrackBundle.skipSampleEncryptionData();
|
||||
if (!currentTrackBundle.next()) {
|
||||
trackBundle.skipSampleEncryptionData();
|
||||
if (!trackBundle.next()) {
|
||||
currentTrackBundle = null;
|
||||
}
|
||||
parserState = STATE_READING_SAMPLE_START;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (currentTrackBundle.moovSampleTable.track.sampleTransformation
|
||||
if (trackBundle.moovSampleTable.track.sampleTransformation
|
||||
== Track.TRANSFORMATION_CEA608_CDAT) {
|
||||
sampleSize -= Atom.HEADER_SIZE;
|
||||
input.skipFully(Atom.HEADER_SIZE);
|
||||
}
|
||||
|
||||
if (MimeTypes.AUDIO_AC4.equals(
|
||||
currentTrackBundle.moovSampleTable.track.format.sampleMimeType)) {
|
||||
if (MimeTypes.AUDIO_AC4.equals(trackBundle.moovSampleTable.track.format.sampleMimeType)) {
|
||||
// AC4 samples need to be prefixed with a clear sample header.
|
||||
sampleBytesWritten =
|
||||
currentTrackBundle.outputSampleEncryptionData(sampleSize, Ac4Util.SAMPLE_HEADER_SIZE);
|
||||
trackBundle.outputSampleEncryptionData(sampleSize, Ac4Util.SAMPLE_HEADER_SIZE);
|
||||
Ac4Util.getAc4SampleHeader(sampleSize, scratch);
|
||||
currentTrackBundle.output.sampleData(scratch, Ac4Util.SAMPLE_HEADER_SIZE);
|
||||
trackBundle.output.sampleData(scratch, Ac4Util.SAMPLE_HEADER_SIZE);
|
||||
sampleBytesWritten += Ac4Util.SAMPLE_HEADER_SIZE;
|
||||
} else {
|
||||
sampleBytesWritten =
|
||||
currentTrackBundle.outputSampleEncryptionData(sampleSize, /* clearHeaderSize= */ 0);
|
||||
trackBundle.outputSampleEncryptionData(sampleSize, /* clearHeaderSize= */ 0);
|
||||
}
|
||||
sampleSize += sampleBytesWritten;
|
||||
parserState = STATE_READING_SAMPLE_CONTINUE;
|
||||
sampleCurrentNalBytesRemaining = 0;
|
||||
}
|
||||
|
||||
Track track = currentTrackBundle.moovSampleTable.track;
|
||||
TrackOutput output = currentTrackBundle.output;
|
||||
long sampleTimeUs = currentTrackBundle.getCurrentSamplePresentationTimeUs();
|
||||
Track track = trackBundle.moovSampleTable.track;
|
||||
TrackOutput output = trackBundle.output;
|
||||
long sampleTimeUs = trackBundle.getCurrentSamplePresentationTimeUs();
|
||||
if (timestampAdjuster != null) {
|
||||
sampleTimeUs = timestampAdjuster.adjustSampleTimestamp(sampleTimeUs);
|
||||
}
|
||||
@ -1407,11 +1411,11 @@ public class FragmentedMp4Extractor implements Extractor {
|
||||
}
|
||||
}
|
||||
|
||||
@C.BufferFlags int sampleFlags = currentTrackBundle.getCurrentSampleFlags();
|
||||
@C.BufferFlags int sampleFlags = trackBundle.getCurrentSampleFlags();
|
||||
|
||||
// Encryption data.
|
||||
TrackOutput.CryptoData cryptoData = null;
|
||||
TrackEncryptionBox encryptionBox = currentTrackBundle.getEncryptionBoxIfEncrypted();
|
||||
@Nullable TrackOutput.CryptoData cryptoData = null;
|
||||
@Nullable TrackEncryptionBox encryptionBox = trackBundle.getEncryptionBoxIfEncrypted();
|
||||
if (encryptionBox != null) {
|
||||
cryptoData = encryptionBox.cryptoData;
|
||||
}
|
||||
@ -1420,7 +1424,7 @@ public class FragmentedMp4Extractor implements Extractor {
|
||||
|
||||
// After we have the sampleTimeUs, we can commit all the pending metadata samples
|
||||
outputPendingMetadataSamples(sampleTimeUs);
|
||||
if (!currentTrackBundle.next()) {
|
||||
if (!trackBundle.next()) {
|
||||
currentTrackBundle = null;
|
||||
}
|
||||
parserState = STATE_READING_SAMPLE_START;
|
||||
@ -1452,7 +1456,7 @@ public class FragmentedMp4Extractor implements Extractor {
|
||||
*/
|
||||
@Nullable
|
||||
private static TrackBundle getNextTrackBundle(SparseArray<TrackBundle> trackBundles) {
|
||||
TrackBundle nextTrackBundle = null;
|
||||
@Nullable TrackBundle nextTrackBundle = null;
|
||||
long nextSampleOffset = Long.MAX_VALUE;
|
||||
|
||||
int trackBundlesSize = trackBundles.size();
|
||||
@ -1579,6 +1583,8 @@ public class FragmentedMp4Extractor implements Extractor {
|
||||
TrackSampleTable moovSampleTable,
|
||||
DefaultSampleValues defaultSampleValues) {
|
||||
this.output = output;
|
||||
this.moovSampleTable = moovSampleTable;
|
||||
this.defaultSampleValues = defaultSampleValues;
|
||||
fragment = new TrackFragment();
|
||||
scratch = new ParsableByteArray();
|
||||
encryptionSignalByte = new ParsableByteArray(1);
|
||||
@ -1587,9 +1593,8 @@ public class FragmentedMp4Extractor implements Extractor {
|
||||
}
|
||||
|
||||
public void reset(TrackSampleTable moovSampleTable, DefaultSampleValues defaultSampleValues) {
|
||||
Assertions.checkNotNull(moovSampleTable.track);
|
||||
this.moovSampleTable = moovSampleTable;
|
||||
this.defaultSampleValues = Assertions.checkNotNull(defaultSampleValues);
|
||||
this.defaultSampleValues = defaultSampleValues;
|
||||
output.format(moovSampleTable.track.format);
|
||||
resetFragmentInfo();
|
||||
}
|
||||
@ -1598,7 +1603,7 @@ public class FragmentedMp4Extractor implements Extractor {
|
||||
@Nullable
|
||||
TrackEncryptionBox encryptionBox =
|
||||
moovSampleTable.track.getSampleDescriptionEncryptionBox(
|
||||
fragment.header.sampleDescriptionIndex);
|
||||
castNonNull(fragment.header).sampleDescriptionIndex);
|
||||
@Nullable String schemeType = encryptionBox != null ? encryptionBox.schemeType : null;
|
||||
DrmInitData updatedDrmInitData = drmInitData.copyWithSchemeType(schemeType);
|
||||
Format format =
|
||||
@ -1706,7 +1711,7 @@ public class FragmentedMp4Extractor implements Extractor {
|
||||
* @return The number of written bytes.
|
||||
*/
|
||||
public int outputSampleEncryptionData(int sampleSize, int clearHeaderSize) {
|
||||
TrackEncryptionBox encryptionBox = getEncryptionBoxIfEncrypted();
|
||||
@Nullable TrackEncryptionBox encryptionBox = getEncryptionBoxIfEncrypted();
|
||||
if (encryptionBox == null) {
|
||||
return 0;
|
||||
}
|
||||
@ -1718,7 +1723,7 @@ public class FragmentedMp4Extractor implements Extractor {
|
||||
vectorSize = encryptionBox.perSampleIvSize;
|
||||
} else {
|
||||
// The default initialization vector should be used.
|
||||
byte[] initVectorData = encryptionBox.defaultInitializationVector;
|
||||
byte[] initVectorData = castNonNull(encryptionBox.defaultInitializationVector);
|
||||
defaultInitializationVector.reset(initVectorData, initVectorData.length);
|
||||
initializationVectorData = defaultInitializationVector;
|
||||
vectorSize = initVectorData.length;
|
||||
@ -1815,7 +1820,7 @@ public class FragmentedMp4Extractor implements Extractor {
|
||||
// Encryption is not supported yet for samples specified in the sample table.
|
||||
return null;
|
||||
}
|
||||
int sampleDescriptionIndex = fragment.header.sampleDescriptionIndex;
|
||||
int sampleDescriptionIndex = castNonNull(fragment.header).sampleDescriptionIndex;
|
||||
@Nullable
|
||||
TrackEncryptionBox encryptionBox =
|
||||
fragment.trackEncryptionBox != null
|
||||
|
@ -26,7 +26,6 @@ import androidx.annotation.Nullable;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.ExoPlayerLibraryInfo;
|
||||
import com.google.android.exoplayer2.MediaItem;
|
||||
import com.google.android.exoplayer2.drm.DrmSession;
|
||||
import com.google.android.exoplayer2.drm.DrmSessionEventListener;
|
||||
import com.google.android.exoplayer2.drm.DrmSessionManager;
|
||||
import com.google.android.exoplayer2.extractor.Extractor;
|
||||
@ -36,6 +35,7 @@ import com.google.android.exoplayer2.source.CompositeSequenceableLoaderFactory;
|
||||
import com.google.android.exoplayer2.source.DefaultCompositeSequenceableLoaderFactory;
|
||||
import com.google.android.exoplayer2.source.MediaPeriod;
|
||||
import com.google.android.exoplayer2.source.MediaSource;
|
||||
import com.google.android.exoplayer2.source.MediaSourceDrmHelper;
|
||||
import com.google.android.exoplayer2.source.MediaSourceEventListener;
|
||||
import com.google.android.exoplayer2.source.MediaSourceFactory;
|
||||
import com.google.android.exoplayer2.source.SequenceableLoader;
|
||||
@ -49,6 +49,7 @@ import com.google.android.exoplayer2.source.hls.playlist.HlsPlaylistTracker;
|
||||
import com.google.android.exoplayer2.upstream.Allocator;
|
||||
import com.google.android.exoplayer2.upstream.DataSource;
|
||||
import com.google.android.exoplayer2.upstream.DefaultLoadErrorHandlingPolicy;
|
||||
import com.google.android.exoplayer2.upstream.HttpDataSource;
|
||||
import com.google.android.exoplayer2.upstream.LoadErrorHandlingPolicy;
|
||||
import com.google.android.exoplayer2.upstream.TransferListener;
|
||||
import com.google.android.exoplayer2.util.MimeTypes;
|
||||
@ -93,12 +94,13 @@ public final class HlsMediaSource extends BaseMediaSource
|
||||
public static final class Factory implements MediaSourceFactory {
|
||||
|
||||
private final HlsDataSourceFactory hlsDataSourceFactory;
|
||||
private final MediaSourceDrmHelper mediaSourceDrmHelper;
|
||||
|
||||
private HlsExtractorFactory extractorFactory;
|
||||
private HlsPlaylistParserFactory playlistParserFactory;
|
||||
private HlsPlaylistTracker.Factory playlistTrackerFactory;
|
||||
private CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory;
|
||||
private DrmSessionManager drmSessionManager;
|
||||
@Nullable private DrmSessionManager drmSessionManager;
|
||||
private LoadErrorHandlingPolicy loadErrorHandlingPolicy;
|
||||
private boolean allowChunklessPreparation;
|
||||
@MetadataType private int metadataType;
|
||||
@ -125,10 +127,10 @@ public final class HlsMediaSource extends BaseMediaSource
|
||||
*/
|
||||
public Factory(HlsDataSourceFactory hlsDataSourceFactory) {
|
||||
this.hlsDataSourceFactory = checkNotNull(hlsDataSourceFactory);
|
||||
mediaSourceDrmHelper = new MediaSourceDrmHelper();
|
||||
playlistParserFactory = new DefaultHlsPlaylistParserFactory();
|
||||
playlistTrackerFactory = DefaultHlsPlaylistTracker.FACTORY;
|
||||
extractorFactory = HlsExtractorFactory.DEFAULT;
|
||||
drmSessionManager = DrmSessionManager.getDummyDrmSessionManager();
|
||||
loadErrorHandlingPolicy = new DefaultLoadErrorHandlingPolicy();
|
||||
compositeSequenceableLoaderFactory = new DefaultCompositeSequenceableLoaderFactory();
|
||||
metadataType = METADATA_TYPE_ID3;
|
||||
@ -285,19 +287,22 @@ public final class HlsMediaSource extends BaseMediaSource
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the {@link DrmSessionManager} to use for acquiring {@link DrmSession DrmSessions}. The
|
||||
* default value is {@link DrmSessionManager#DUMMY}.
|
||||
*
|
||||
* @param drmSessionManager The {@link DrmSessionManager}.
|
||||
* @return This factory, for convenience.
|
||||
*/
|
||||
@Override
|
||||
public Factory setDrmSessionManager(@Nullable DrmSessionManager drmSessionManager) {
|
||||
this.drmSessionManager =
|
||||
drmSessionManager != null
|
||||
? drmSessionManager
|
||||
: DrmSessionManager.getDummyDrmSessionManager();
|
||||
this.drmSessionManager = drmSessionManager;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Factory setDrmHttpDataSourceFactory(
|
||||
@Nullable HttpDataSource.Factory drmHttpDataSourceFactory) {
|
||||
mediaSourceDrmHelper.setDrmHttpDataSourceFactory(drmHttpDataSourceFactory);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public MediaSourceFactory setDrmUserAgent(@Nullable String userAgent) {
|
||||
mediaSourceDrmHelper.setDrmUserAgent(userAgent);
|
||||
return this;
|
||||
}
|
||||
|
||||
@ -374,7 +379,7 @@ public final class HlsMediaSource extends BaseMediaSource
|
||||
hlsDataSourceFactory,
|
||||
extractorFactory,
|
||||
compositeSequenceableLoaderFactory,
|
||||
drmSessionManager,
|
||||
drmSessionManager != null ? drmSessionManager : mediaSourceDrmHelper.create(mediaItem),
|
||||
loadErrorHandlingPolicy,
|
||||
playlistTrackerFactory.createTracker(
|
||||
hlsDataSourceFactory, loadErrorHandlingPolicy, playlistParserFactory),
|
||||
|
@ -27,7 +27,6 @@ import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.ExoPlayerLibraryInfo;
|
||||
import com.google.android.exoplayer2.MediaItem;
|
||||
import com.google.android.exoplayer2.Timeline;
|
||||
import com.google.android.exoplayer2.drm.DrmSession;
|
||||
import com.google.android.exoplayer2.drm.DrmSessionEventListener;
|
||||
import com.google.android.exoplayer2.drm.DrmSessionManager;
|
||||
import com.google.android.exoplayer2.offline.FilteringManifestParser;
|
||||
@ -39,6 +38,7 @@ import com.google.android.exoplayer2.source.LoadEventInfo;
|
||||
import com.google.android.exoplayer2.source.MediaLoadData;
|
||||
import com.google.android.exoplayer2.source.MediaPeriod;
|
||||
import com.google.android.exoplayer2.source.MediaSource;
|
||||
import com.google.android.exoplayer2.source.MediaSourceDrmHelper;
|
||||
import com.google.android.exoplayer2.source.MediaSourceEventListener;
|
||||
import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher;
|
||||
import com.google.android.exoplayer2.source.MediaSourceFactory;
|
||||
@ -50,6 +50,7 @@ import com.google.android.exoplayer2.source.smoothstreaming.manifest.SsManifestP
|
||||
import com.google.android.exoplayer2.upstream.Allocator;
|
||||
import com.google.android.exoplayer2.upstream.DataSource;
|
||||
import com.google.android.exoplayer2.upstream.DefaultLoadErrorHandlingPolicy;
|
||||
import com.google.android.exoplayer2.upstream.HttpDataSource;
|
||||
import com.google.android.exoplayer2.upstream.LoadErrorHandlingPolicy;
|
||||
import com.google.android.exoplayer2.upstream.LoadErrorHandlingPolicy.LoadErrorInfo;
|
||||
import com.google.android.exoplayer2.upstream.Loader;
|
||||
@ -77,10 +78,11 @@ public final class SsMediaSource extends BaseMediaSource
|
||||
public static final class Factory implements MediaSourceFactory {
|
||||
|
||||
private final SsChunkSource.Factory chunkSourceFactory;
|
||||
private final MediaSourceDrmHelper mediaSourceDrmHelper;
|
||||
@Nullable private final DataSource.Factory manifestDataSourceFactory;
|
||||
|
||||
private CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory;
|
||||
private DrmSessionManager drmSessionManager;
|
||||
@Nullable private DrmSessionManager drmSessionManager;
|
||||
private LoadErrorHandlingPolicy loadErrorHandlingPolicy;
|
||||
private long livePresentationDelayMs;
|
||||
@Nullable private ParsingLoadable.Parser<? extends SsManifest> manifestParser;
|
||||
@ -111,7 +113,7 @@ public final class SsMediaSource extends BaseMediaSource
|
||||
@Nullable DataSource.Factory manifestDataSourceFactory) {
|
||||
this.chunkSourceFactory = checkNotNull(chunkSourceFactory);
|
||||
this.manifestDataSourceFactory = manifestDataSourceFactory;
|
||||
drmSessionManager = DrmSessionManager.getDummyDrmSessionManager();
|
||||
mediaSourceDrmHelper = new MediaSourceDrmHelper();
|
||||
loadErrorHandlingPolicy = new DefaultLoadErrorHandlingPolicy();
|
||||
livePresentationDelayMs = DEFAULT_LIVE_PRESENTATION_DELAY_MS;
|
||||
compositeSequenceableLoaderFactory = new DefaultCompositeSequenceableLoaderFactory();
|
||||
@ -197,19 +199,22 @@ public final class SsMediaSource extends BaseMediaSource
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the {@link DrmSessionManager} to use for acquiring {@link DrmSession DrmSessions}. The
|
||||
* default value is {@link DrmSessionManager#DUMMY}.
|
||||
*
|
||||
* @param drmSessionManager The {@link DrmSessionManager}.
|
||||
* @return This factory, for convenience.
|
||||
*/
|
||||
@Override
|
||||
public Factory setDrmSessionManager(@Nullable DrmSessionManager drmSessionManager) {
|
||||
this.drmSessionManager =
|
||||
drmSessionManager != null
|
||||
? drmSessionManager
|
||||
: DrmSessionManager.getDummyDrmSessionManager();
|
||||
this.drmSessionManager = drmSessionManager;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Factory setDrmHttpDataSourceFactory(
|
||||
@Nullable HttpDataSource.Factory drmHttpDataSourceFactory) {
|
||||
mediaSourceDrmHelper.setDrmHttpDataSourceFactory(drmHttpDataSourceFactory);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Factory setDrmUserAgent(@Nullable String userAgent) {
|
||||
mediaSourceDrmHelper.setDrmUserAgent(userAgent);
|
||||
return this;
|
||||
}
|
||||
|
||||
@ -280,7 +285,7 @@ public final class SsMediaSource extends BaseMediaSource
|
||||
/* manifestParser= */ null,
|
||||
chunkSourceFactory,
|
||||
compositeSequenceableLoaderFactory,
|
||||
drmSessionManager,
|
||||
drmSessionManager != null ? drmSessionManager : mediaSourceDrmHelper.create(mediaItem),
|
||||
loadErrorHandlingPolicy,
|
||||
livePresentationDelayMs);
|
||||
}
|
||||
@ -357,7 +362,7 @@ public final class SsMediaSource extends BaseMediaSource
|
||||
manifestParser,
|
||||
chunkSourceFactory,
|
||||
compositeSequenceableLoaderFactory,
|
||||
drmSessionManager,
|
||||
drmSessionManager != null ? drmSessionManager : mediaSourceDrmHelper.create(mediaItem),
|
||||
loadErrorHandlingPolicy,
|
||||
livePresentationDelayMs);
|
||||
}
|
||||
|
@ -3,7 +3,7 @@
|
||||
# Constructor method accessed via reflection in TrackSelectionDialogBuilder
|
||||
-dontnote androidx.appcompat.app.AlertDialog.Builder
|
||||
-keepclassmembers class androidx.appcompat.app.AlertDialog$Builder {
|
||||
<init>(android.content.Context);
|
||||
<init>(android.content.Context, int);
|
||||
public android.content.Context getContext();
|
||||
public androidx.appcompat.app.AlertDialog$Builder setTitle(java.lang.CharSequence);
|
||||
public androidx.appcompat.app.AlertDialog$Builder setView(android.view.View);
|
||||
|
@ -1085,8 +1085,8 @@ public class PlayerControlView extends FrameLayout {
|
||||
long mediaTimeUntilNextFullSecondMs = 1000 - position % 1000;
|
||||
mediaTimeDelayMs = Math.min(mediaTimeDelayMs, mediaTimeUntilNextFullSecondMs);
|
||||
|
||||
// Calculate the delay until the next update in real time, taking playbackSpeed into account.
|
||||
float playbackSpeed = player.getPlaybackSpeed();
|
||||
// Calculate the delay until the next update in real time, taking playback speed into account.
|
||||
float playbackSpeed = player.getPlaybackParameters().speed;
|
||||
long delayMs =
|
||||
playbackSpeed > 0 ? (long) (mediaTimeDelayMs / playbackSpeed) : MAX_UPDATE_INTERVAL_MS;
|
||||
|
||||
|
@ -37,6 +37,7 @@ import androidx.media.app.NotificationCompat.MediaStyle;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.ControlDispatcher;
|
||||
import com.google.android.exoplayer2.DefaultControlDispatcher;
|
||||
import com.google.android.exoplayer2.PlaybackParameters;
|
||||
import com.google.android.exoplayer2.PlaybackPreparer;
|
||||
import com.google.android.exoplayer2.Player;
|
||||
import com.google.android.exoplayer2.Timeline;
|
||||
@ -923,7 +924,7 @@ public class PlayerNotificationManager {
|
||||
* <li>The media is not {@link Player#isCurrentWindowDynamic() dynamically changing its
|
||||
* duration} (like for example a live stream).
|
||||
* <li>The media is not {@link Player#isPlayingAd() interrupted by an ad}.
|
||||
* <li>The media is played at {@link Player#getPlaybackSpeed() regular speed}.
|
||||
* <li>The media is played at {@link Player#getPlaybackParameters() regular speed}.
|
||||
* <li>The device is running at least API 21 (Lollipop).
|
||||
* </ul>
|
||||
*
|
||||
@ -1086,7 +1087,7 @@ public class PlayerNotificationManager {
|
||||
&& player.isPlaying()
|
||||
&& !player.isPlayingAd()
|
||||
&& !player.isCurrentWindowDynamic()
|
||||
&& player.getPlaybackSpeed() == 1f) {
|
||||
&& player.getPlaybackParameters().speed == 1f) {
|
||||
builder
|
||||
.setWhen(System.currentTimeMillis() - player.getContentPosition())
|
||||
.setShowWhen(true)
|
||||
@ -1336,7 +1337,7 @@ public class PlayerNotificationManager {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPlaybackSpeedChanged(float playbackSpeed) {
|
||||
public void onPlaybackParametersChanged(PlaybackParameters playbackParameters) {
|
||||
postStartOrUpdateNotification();
|
||||
}
|
||||
|
||||
|
@ -42,6 +42,7 @@ import com.google.android.exoplayer2.ControlDispatcher;
|
||||
import com.google.android.exoplayer2.DefaultControlDispatcher;
|
||||
import com.google.android.exoplayer2.ExoPlayerLibraryInfo;
|
||||
import com.google.android.exoplayer2.Format;
|
||||
import com.google.android.exoplayer2.PlaybackParameters;
|
||||
import com.google.android.exoplayer2.PlaybackPreparer;
|
||||
import com.google.android.exoplayer2.Player;
|
||||
import com.google.android.exoplayer2.RendererCapabilities;
|
||||
@ -1408,8 +1409,8 @@ public class StyledPlayerControlView extends FrameLayout {
|
||||
long mediaTimeUntilNextFullSecondMs = 1000 - position % 1000;
|
||||
mediaTimeDelayMs = Math.min(mediaTimeDelayMs, mediaTimeUntilNextFullSecondMs);
|
||||
|
||||
// Calculate the delay until the next update in real time, taking playbackSpeed into account.
|
||||
float playbackSpeed = player.getPlaybackSpeed();
|
||||
// Calculate the delay until the next update in real time, taking playback speed into account.
|
||||
float playbackSpeed = player.getPlaybackParameters().speed;
|
||||
long delayMs =
|
||||
playbackSpeed > 0 ? (long) (mediaTimeDelayMs / playbackSpeed) : MAX_UPDATE_INTERVAL_MS;
|
||||
|
||||
@ -1425,7 +1426,7 @@ public class StyledPlayerControlView extends FrameLayout {
|
||||
if (player == null) {
|
||||
return;
|
||||
}
|
||||
float speed = player.getPlaybackSpeed();
|
||||
float speed = player.getPlaybackParameters().speed;
|
||||
int currentSpeedMultBy100 = Math.round(speed * 100);
|
||||
int indexForCurrentSpeed = playbackSpeedMultBy100List.indexOf(currentSpeedMultBy100);
|
||||
if (indexForCurrentSpeed == UNDEFINED_POSITION) {
|
||||
@ -1481,7 +1482,7 @@ public class StyledPlayerControlView extends FrameLayout {
|
||||
if (player == null) {
|
||||
return;
|
||||
}
|
||||
player.setPlaybackSpeed(speed);
|
||||
player.setPlaybackParameters(new PlaybackParameters(speed));
|
||||
}
|
||||
|
||||
/* package */ void requestPlayPauseFocus() {
|
||||
@ -1771,7 +1772,7 @@ public class StyledPlayerControlView extends FrameLayout {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPlaybackSpeedChanged(float playbackSpeed) {
|
||||
public void onPlaybackParametersChanged(PlaybackParameters playbackParameters) {
|
||||
updateSettingsPlaybackSpeedLists();
|
||||
}
|
||||
|
||||
|
@ -25,6 +25,7 @@ import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import androidx.annotation.Nullable;
|
||||
import com.google.android.exoplayer2.Format;
|
||||
import androidx.annotation.StyleRes;
|
||||
import com.google.android.exoplayer2.source.TrackGroupArray;
|
||||
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
|
||||
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector.SelectionOverride;
|
||||
@ -51,6 +52,7 @@ public final class TrackSelectionDialogBuilder {
|
||||
}
|
||||
|
||||
private final Context context;
|
||||
@StyleRes private int themeResId;
|
||||
private final CharSequence title;
|
||||
private final MappedTrackInfo mappedTrackInfo;
|
||||
private final int rendererIndex;
|
||||
@ -124,6 +126,17 @@ public final class TrackSelectionDialogBuilder {
|
||||
newOverrides.isEmpty() ? null : newOverrides.get(0)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the resource ID of the theme used to inflate this dialog.
|
||||
*
|
||||
* @param themeResId The resource ID of the theme.
|
||||
* @return This builder, for convenience.
|
||||
*/
|
||||
public TrackSelectionDialogBuilder setTheme(@StyleRes int themeResId) {
|
||||
this.themeResId = themeResId;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets whether the selection is initially shown as disabled.
|
||||
*
|
||||
@ -221,7 +234,7 @@ public final class TrackSelectionDialogBuilder {
|
||||
}
|
||||
|
||||
private Dialog buildForPlatform() {
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(context);
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(context, themeResId);
|
||||
|
||||
// Inflate with the builder's context to ensure the correct style is used.
|
||||
LayoutInflater dialogInflater = LayoutInflater.from(builder.getContext());
|
||||
@ -245,8 +258,8 @@ public final class TrackSelectionDialogBuilder {
|
||||
// the APK size even with shrinking. See https://issuetracker.google.com/161514204.
|
||||
// LINT.IfChange
|
||||
Class<?> builderClazz = Class.forName("androidx.appcompat.app.AlertDialog$Builder");
|
||||
Constructor<?> builderConstructor = builderClazz.getConstructor(Context.class);
|
||||
Object builder = builderConstructor.newInstance(context);
|
||||
Constructor<?> builderConstructor = builderClazz.getConstructor(Context.class, int.class);
|
||||
Object builder = builderConstructor.newInstance(context, themeResId);
|
||||
|
||||
// Inflate with the builder's context to ensure the correct style is used.
|
||||
Context builderContext = (Context) builderClazz.getMethod("getContext").invoke(builder);
|
||||
|
@ -134,6 +134,7 @@
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:layout_gravity="bottom|end"
|
||||
android:layout_marginBottom="@dimen/exo_custom_progress_thumb_size"
|
||||
android:visibility="invisible">
|
||||
</LinearLayout>
|
||||
|
||||
|
@ -15,6 +15,8 @@
|
||||
*/
|
||||
package com.google.android.exoplayer2.playbacktests.gts;
|
||||
|
||||
import static com.google.android.exoplayer2.playbacktests.gts.GtsTestUtil.shouldSkipWidevineTest;
|
||||
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
import androidx.test.rule.ActivityTestRule;
|
||||
import com.google.android.exoplayer2.Player;
|
||||
@ -64,7 +66,7 @@ public final class CommonEncryptionDrmTest {
|
||||
|
||||
@Test
|
||||
public void cencSchemeTypeV18() {
|
||||
if (Util.SDK_INT < 18) {
|
||||
if (Util.SDK_INT < 18 || shouldSkipWidevineTest(testRule.getActivity())) {
|
||||
// Pass.
|
||||
return;
|
||||
}
|
||||
@ -74,23 +76,9 @@ public final class CommonEncryptionDrmTest {
|
||||
.run();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void cbc1SchemeTypeV25() {
|
||||
if (Util.SDK_INT < 25) {
|
||||
// cbc1 support was added in API 24, but it is stable from API 25 onwards.
|
||||
// See [internal: b/65634809].
|
||||
// Pass.
|
||||
return;
|
||||
}
|
||||
testRunner
|
||||
.setStreamName("test_widevine_h264_scheme_cbc1")
|
||||
.setManifestUrl(DashTestData.WIDEVINE_SCHEME_CBC1)
|
||||
.run();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void cbcsSchemeTypeV25() {
|
||||
if (Util.SDK_INT < 25) {
|
||||
if (Util.SDK_INT < 25 || shouldSkipWidevineTest(testRule.getActivity())) {
|
||||
// cbcs support was added in API 24, but it is stable from API 25 onwards.
|
||||
// See [internal: b/65634809].
|
||||
// Pass.
|
||||
@ -101,9 +89,4 @@ public final class CommonEncryptionDrmTest {
|
||||
.setManifestUrl(DashTestData.WIDEVINE_SCHEME_CBCS)
|
||||
.run();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void censSchemeTypeV25() {
|
||||
// TODO: Implement once content is available. Track [internal: b/31219813].
|
||||
}
|
||||
}
|
||||
|
@ -15,6 +15,7 @@
|
||||
*/
|
||||
package com.google.android.exoplayer2.playbacktests.gts;
|
||||
|
||||
import static com.google.android.exoplayer2.playbacktests.gts.GtsTestUtil.shouldSkipWidevineTest;
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
@ -356,7 +357,7 @@ public final class DashStreamingTest {
|
||||
|
||||
@Test
|
||||
public void widevineH264FixedV18() throws Exception {
|
||||
if (Util.SDK_INT < 18) {
|
||||
if (Util.SDK_INT < 18 || shouldSkipWidevineTest(testRule.getActivity())) {
|
||||
// Pass.
|
||||
return;
|
||||
}
|
||||
@ -373,7 +374,9 @@ public final class DashStreamingTest {
|
||||
|
||||
@Test
|
||||
public void widevineH264AdaptiveV18() throws Exception {
|
||||
if (Util.SDK_INT < 18 || shouldSkipAdaptiveTest(MimeTypes.VIDEO_H264)) {
|
||||
if (Util.SDK_INT < 18
|
||||
|| shouldSkipAdaptiveTest(MimeTypes.VIDEO_H264)
|
||||
|| shouldSkipWidevineTest(testRule.getActivity())) {
|
||||
// Pass.
|
||||
return;
|
||||
}
|
||||
@ -390,7 +393,9 @@ public final class DashStreamingTest {
|
||||
|
||||
@Test
|
||||
public void widevineH264AdaptiveWithSeekingV18() throws Exception {
|
||||
if (Util.SDK_INT < 18 || shouldSkipAdaptiveTest(MimeTypes.VIDEO_H264)) {
|
||||
if (Util.SDK_INT < 18
|
||||
|| shouldSkipAdaptiveTest(MimeTypes.VIDEO_H264)
|
||||
|| shouldSkipWidevineTest(testRule.getActivity())) {
|
||||
// Pass.
|
||||
return;
|
||||
}
|
||||
@ -408,7 +413,9 @@ public final class DashStreamingTest {
|
||||
|
||||
@Test
|
||||
public void widevineH264AdaptiveWithRendererDisablingV18() throws Exception {
|
||||
if (Util.SDK_INT < 18 || shouldSkipAdaptiveTest(MimeTypes.VIDEO_H264)) {
|
||||
if (Util.SDK_INT < 18
|
||||
|| shouldSkipAdaptiveTest(MimeTypes.VIDEO_H264)
|
||||
|| GtsTestUtil.shouldSkipWidevineTest(testRule.getActivity())) {
|
||||
// Pass.
|
||||
return;
|
||||
}
|
||||
@ -428,7 +435,7 @@ public final class DashStreamingTest {
|
||||
|
||||
@Test
|
||||
public void widevineH265FixedV23() throws Exception {
|
||||
if (Util.SDK_INT < 23) {
|
||||
if (Util.SDK_INT < 23 || GtsTestUtil.shouldSkipWidevineTest(testRule.getActivity())) {
|
||||
// Pass.
|
||||
return;
|
||||
}
|
||||
@ -445,7 +452,7 @@ public final class DashStreamingTest {
|
||||
|
||||
@Test
|
||||
public void widevineH265AdaptiveV24() throws Exception {
|
||||
if (Util.SDK_INT < 24) {
|
||||
if (Util.SDK_INT < 24 || GtsTestUtil.shouldSkipWidevineTest(testRule.getActivity())) {
|
||||
// Pass.
|
||||
return;
|
||||
}
|
||||
@ -462,7 +469,7 @@ public final class DashStreamingTest {
|
||||
|
||||
@Test
|
||||
public void widevineH265AdaptiveWithSeekingV24() throws Exception {
|
||||
if (Util.SDK_INT < 24) {
|
||||
if (Util.SDK_INT < 24 || GtsTestUtil.shouldSkipWidevineTest(testRule.getActivity())) {
|
||||
// Pass.
|
||||
return;
|
||||
}
|
||||
@ -480,7 +487,7 @@ public final class DashStreamingTest {
|
||||
|
||||
@Test
|
||||
public void widevineH265AdaptiveWithRendererDisablingV24() throws Exception {
|
||||
if (Util.SDK_INT < 24) {
|
||||
if (Util.SDK_INT < 24 || GtsTestUtil.shouldSkipWidevineTest(testRule.getActivity())) {
|
||||
// Pass.
|
||||
return;
|
||||
}
|
||||
@ -500,7 +507,7 @@ public final class DashStreamingTest {
|
||||
|
||||
@Test
|
||||
public void widevineVp9Fixed360pV23() throws Exception {
|
||||
if (Util.SDK_INT < 23) {
|
||||
if (Util.SDK_INT < 23 || GtsTestUtil.shouldSkipWidevineTest(testRule.getActivity())) {
|
||||
// Pass.
|
||||
return;
|
||||
}
|
||||
@ -517,7 +524,7 @@ public final class DashStreamingTest {
|
||||
|
||||
@Test
|
||||
public void widevineVp9AdaptiveV24() throws Exception {
|
||||
if (Util.SDK_INT < 24) {
|
||||
if (Util.SDK_INT < 24 || GtsTestUtil.shouldSkipWidevineTest(testRule.getActivity())) {
|
||||
// Pass.
|
||||
return;
|
||||
}
|
||||
@ -534,7 +541,7 @@ public final class DashStreamingTest {
|
||||
|
||||
@Test
|
||||
public void widevineVp9AdaptiveWithSeekingV24() throws Exception {
|
||||
if (Util.SDK_INT < 24) {
|
||||
if (Util.SDK_INT < 24 || GtsTestUtil.shouldSkipWidevineTest(testRule.getActivity())) {
|
||||
// Pass.
|
||||
return;
|
||||
}
|
||||
@ -552,7 +559,7 @@ public final class DashStreamingTest {
|
||||
|
||||
@Test
|
||||
public void widevineVp9AdaptiveWithRendererDisablingV24() throws Exception {
|
||||
if (Util.SDK_INT < 24) {
|
||||
if (Util.SDK_INT < 24 || GtsTestUtil.shouldSkipWidevineTest(testRule.getActivity())) {
|
||||
// Pass.
|
||||
return;
|
||||
}
|
||||
@ -573,7 +580,7 @@ public final class DashStreamingTest {
|
||||
// 23.976 fps.
|
||||
@Test
|
||||
public void widevine23FpsH264FixedV23() throws Exception {
|
||||
if (Util.SDK_INT < 23) {
|
||||
if (Util.SDK_INT < 23 || GtsTestUtil.shouldSkipWidevineTest(testRule.getActivity())) {
|
||||
// Pass.
|
||||
return;
|
||||
}
|
||||
@ -591,7 +598,7 @@ public final class DashStreamingTest {
|
||||
// 24 fps.
|
||||
@Test
|
||||
public void widevine24FpsH264FixedV23() throws Exception {
|
||||
if (Util.SDK_INT < 23) {
|
||||
if (Util.SDK_INT < 23 || GtsTestUtil.shouldSkipWidevineTest(testRule.getActivity())) {
|
||||
// Pass.
|
||||
return;
|
||||
}
|
||||
@ -609,7 +616,7 @@ public final class DashStreamingTest {
|
||||
// 29.97 fps.
|
||||
@Test
|
||||
public void widevine29FpsH264FixedV23() throws Exception {
|
||||
if (Util.SDK_INT < 23) {
|
||||
if (Util.SDK_INT < 23 || GtsTestUtil.shouldSkipWidevineTest(testRule.getActivity())) {
|
||||
// Pass.
|
||||
return;
|
||||
}
|
||||
|
@ -45,8 +45,6 @@ import com.google.android.exoplayer2.util.Util;
|
||||
|
||||
// Widevine encrypted content manifests using different common encryption schemes.
|
||||
public static final String WIDEVINE_SCHEME_CENC = BASE_URL_COMMON_ENCRYPTION + "tears-cenc.mpd";
|
||||
public static final String WIDEVINE_SCHEME_CBC1 =
|
||||
BASE_URL_COMMON_ENCRYPTION + "tears-aes-cbc1.mpd";
|
||||
public static final String WIDEVINE_SCHEME_CBCS =
|
||||
BASE_URL_COMMON_ENCRYPTION + "tears-aes-cbcs.mpd";
|
||||
|
||||
|
@ -100,7 +100,7 @@ public final class DashWidevineOfflineTest {
|
||||
|
||||
@Test
|
||||
public void widevineOfflineLicenseV22() throws Exception {
|
||||
if (Util.SDK_INT < 22) {
|
||||
if (Util.SDK_INT < 22 || GtsTestUtil.shouldSkipWidevineTest(testRule.getActivity())) {
|
||||
return; // Pass.
|
||||
}
|
||||
downloadLicense();
|
||||
@ -113,7 +113,9 @@ public final class DashWidevineOfflineTest {
|
||||
|
||||
@Test
|
||||
public void widevineOfflineReleasedLicenseV22() throws Throwable {
|
||||
if (Util.SDK_INT < 22 || Util.SDK_INT > 28) {
|
||||
if (Util.SDK_INT < 22
|
||||
|| Util.SDK_INT > 28
|
||||
|| GtsTestUtil.shouldSkipWidevineTest(testRule.getActivity())) {
|
||||
return; // Pass.
|
||||
}
|
||||
downloadLicense();
|
||||
@ -136,7 +138,7 @@ public final class DashWidevineOfflineTest {
|
||||
|
||||
@Test
|
||||
public void widevineOfflineReleasedLicenseV29() throws Throwable {
|
||||
if (Util.SDK_INT < 29) {
|
||||
if (Util.SDK_INT < 29 || GtsTestUtil.shouldSkipWidevineTest(testRule.getActivity())) {
|
||||
return; // Pass.
|
||||
}
|
||||
downloadLicense();
|
||||
@ -158,7 +160,7 @@ public final class DashWidevineOfflineTest {
|
||||
|
||||
@Test
|
||||
public void widevineOfflineExpiredLicenseV22() throws Exception {
|
||||
if (Util.SDK_INT < 22) {
|
||||
if (Util.SDK_INT < 22 || GtsTestUtil.shouldSkipWidevineTest(testRule.getActivity())) {
|
||||
return; // Pass.
|
||||
}
|
||||
downloadLicense();
|
||||
@ -188,7 +190,7 @@ public final class DashWidevineOfflineTest {
|
||||
|
||||
@Test
|
||||
public void widevineOfflineLicenseExpiresOnPauseV22() throws Exception {
|
||||
if (Util.SDK_INT < 22) {
|
||||
if (Util.SDK_INT < 22 || GtsTestUtil.shouldSkipWidevineTest(testRule.getActivity())) {
|
||||
return; // Pass.
|
||||
}
|
||||
downloadLicense();
|
||||
@ -198,7 +200,7 @@ public final class DashWidevineOfflineTest {
|
||||
offlineLicenseHelper.getLicenseDurationRemainingSec(offlineLicenseKeySetId);
|
||||
long licenseDuration = licenseDurationRemainingSec.first;
|
||||
assertWithMessage(
|
||||
"License duration should be less than 30 sec. " + "Server settings might have changed.")
|
||||
"License duration should be less than 30 sec. Server settings might have changed.")
|
||||
.that(licenseDuration < 30)
|
||||
.isTrue();
|
||||
ActionSchedule schedule = new ActionSchedule.Builder(TAG)
|
||||
|
@ -24,6 +24,7 @@ import android.media.MediaFormat;
|
||||
import android.os.Handler;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.RequiresApi;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.DefaultRenderersFactory;
|
||||
import com.google.android.exoplayer2.ExoPlaybackException;
|
||||
import com.google.android.exoplayer2.Format;
|
||||
@ -84,13 +85,14 @@ import java.util.ArrayList;
|
||||
|
||||
private final long[] timestampsList;
|
||||
private final ArrayDeque<Long> inputFormatChangeTimesUs;
|
||||
private final boolean enableMediaFormatChangeTimeCheck;
|
||||
|
||||
private boolean skipToPositionBeforeRenderingFirstFrame;
|
||||
private boolean shouldMediaFormatChangeTimesBeChecked;
|
||||
|
||||
private int startIndex;
|
||||
private int queueSize;
|
||||
private int bufferCount;
|
||||
private int minimumInsertIndex;
|
||||
private boolean skipToPositionBeforeRenderingFirstFrame;
|
||||
private boolean inputFormatChanged;
|
||||
private boolean outputMediaFormatChanged;
|
||||
|
||||
@ -112,10 +114,6 @@ import java.util.ArrayList;
|
||||
maxDroppedFrameCountToNotify);
|
||||
timestampsList = new long[ARRAY_SIZE];
|
||||
inputFormatChangeTimesUs = new ArrayDeque<>();
|
||||
|
||||
// As per [Internal ref: b/149818050, b/149751672], MediaFormat changes can occur early for
|
||||
// SDK 29 and 30. Should be fixed for SDK 31 onwards.
|
||||
enableMediaFormatChangeTimeCheck = Util.SDK_INT < 29 || Util.SDK_INT >= 31;
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -137,6 +135,10 @@ import java.util.ArrayList;
|
||||
// frames up to the current playback position [Internal: b/66494991].
|
||||
skipToPositionBeforeRenderingFirstFrame = getState() == Renderer.STATE_STARTED;
|
||||
super.configureCodec(codecInfo, codecAdapter, format, crypto, operatingRate);
|
||||
|
||||
// Output MediaFormat changes are known to occur too early until API 30 (see [internal:
|
||||
// b/149818050, b/149751672]).
|
||||
shouldMediaFormatChangeTimesBeChecked = Util.SDK_INT > 30;
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -247,10 +249,12 @@ import java.util.ArrayList;
|
||||
}
|
||||
|
||||
if (outputMediaFormatChanged) {
|
||||
long inputFormatChangeTimeUs = inputFormatChangeTimesUs.remove();
|
||||
long inputFormatChangeTimeUs =
|
||||
inputFormatChangeTimesUs.isEmpty() ? C.TIME_UNSET : inputFormatChangeTimesUs.remove();
|
||||
outputMediaFormatChanged = false;
|
||||
|
||||
if (enableMediaFormatChangeTimeCheck && presentationTimeUs != inputFormatChangeTimeUs) {
|
||||
if (shouldMediaFormatChangeTimesBeChecked
|
||||
&& presentationTimeUs != inputFormatChangeTimeUs) {
|
||||
throw new IllegalStateException(
|
||||
"Expected output MediaFormat change timestamp ("
|
||||
+ presentationTimeUs
|
||||
|
@ -0,0 +1,50 @@
|
||||
/*
|
||||
* 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.playbacktests.gts;
|
||||
|
||||
import static com.google.android.exoplayer2.C.WIDEVINE_UUID;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.media.MediaDrm;
|
||||
|
||||
/** Utility methods for GTS tests. */
|
||||
public final class GtsTestUtil {
|
||||
|
||||
private GtsTestUtil() {}
|
||||
|
||||
/** Returns true if the device doesn't support Widevine and this is permitted. */
|
||||
public static boolean shouldSkipWidevineTest(Context context) {
|
||||
if (isGmsInstalled(context)) {
|
||||
// GMS devices are required to support Widevine.
|
||||
return false;
|
||||
}
|
||||
// For non-GMS devices Widevine is optional.
|
||||
return !MediaDrm.isCryptoSchemeSupported(WIDEVINE_UUID);
|
||||
}
|
||||
|
||||
private static boolean isGmsInstalled(Context context) {
|
||||
try {
|
||||
context
|
||||
.getPackageManager()
|
||||
.getPackageInfo("com.google.android.gms", PackageManager.GET_SIGNATURES);
|
||||
} catch (PackageManager.NameNotFoundException e) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
81
testdata/src/test/assets/playbackdumps/mp4/sample.mp4.dump
vendored
Normal file
81
testdata/src/test/assets/playbackdumps/mp4/sample.mp4.dump
vendored
Normal file
@ -0,0 +1,81 @@
|
||||
MediaCodec (audio/mp4a-latm):
|
||||
buffers.length = 46
|
||||
buffers[0] = length 23, hash 47DE9131
|
||||
buffers[1] = length 6, hash 31EC5206
|
||||
buffers[2] = length 148, hash 894A176B
|
||||
buffers[3] = length 189, hash CEF235A1
|
||||
buffers[4] = length 205, hash BBF5F7B0
|
||||
buffers[5] = length 210, hash F278B193
|
||||
buffers[6] = length 210, hash 82DA1589
|
||||
buffers[7] = length 207, hash 5BE231DF
|
||||
buffers[8] = length 225, hash 18819EE1
|
||||
buffers[9] = length 215, hash CA7FA67B
|
||||
buffers[10] = length 211, hash 581A1C18
|
||||
buffers[11] = length 216, hash ADB88187
|
||||
buffers[12] = length 229, hash 2E8BA4DC
|
||||
buffers[13] = length 232, hash 22F0C510
|
||||
buffers[14] = length 235, hash 867AD0DC
|
||||
buffers[15] = length 231, hash 84E823A8
|
||||
buffers[16] = length 226, hash 1BEF3A95
|
||||
buffers[17] = length 216, hash EAA345AE
|
||||
buffers[18] = length 229, hash 6957411F
|
||||
buffers[19] = length 219, hash 41275022
|
||||
buffers[20] = length 241, hash 6495DF96
|
||||
buffers[21] = length 228, hash 63D95906
|
||||
buffers[22] = length 238, hash 34F676F9
|
||||
buffers[23] = length 234, hash E5CBC045
|
||||
buffers[24] = length 231, hash 5FC43661
|
||||
buffers[25] = length 217, hash 682708ED
|
||||
buffers[26] = length 239, hash D43780FC
|
||||
buffers[27] = length 243, hash C5E17980
|
||||
buffers[28] = length 231, hash AC5837BA
|
||||
buffers[29] = length 230, hash 169EE895
|
||||
buffers[30] = length 238, hash C48FF3F1
|
||||
buffers[31] = length 225, hash 531E4599
|
||||
buffers[32] = length 232, hash CB3C6B8D
|
||||
buffers[33] = length 243, hash F8C94C7
|
||||
buffers[34] = length 232, hash A646A7D0
|
||||
buffers[35] = length 237, hash E8B787A5
|
||||
buffers[36] = length 228, hash 3FA7A29F
|
||||
buffers[37] = length 235, hash B9B33B0A
|
||||
buffers[38] = length 264, hash 71A4869E
|
||||
buffers[39] = length 257, hash D049B54C
|
||||
buffers[40] = length 227, hash 66757231
|
||||
buffers[41] = length 227, hash BD374F1B
|
||||
buffers[42] = length 235, hash 999477F6
|
||||
buffers[43] = length 229, hash FFF98DF0
|
||||
buffers[44] = length 6, hash 31B22286
|
||||
buffers[45] = length 0, hash 1
|
||||
MediaCodec (video/avc):
|
||||
buffers.length = 31
|
||||
buffers[0] = length 36692, hash D216076E
|
||||
buffers[1] = length 5312, hash D45D3CA0
|
||||
buffers[2] = length 599, hash 1BE7812D
|
||||
buffers[3] = length 7735, hash 4490F110
|
||||
buffers[4] = length 987, hash 560B5036
|
||||
buffers[5] = length 673, hash ED7CD8C7
|
||||
buffers[6] = length 523, hash 3020DF50
|
||||
buffers[7] = length 6061, hash 736C72B2
|
||||
buffers[8] = length 992, hash FE132F23
|
||||
buffers[9] = length 623, hash 5B2C1816
|
||||
buffers[10] = length 421, hash 742E69C1
|
||||
buffers[11] = length 4899, hash F72F86A1
|
||||
buffers[12] = length 568, hash 519A8E50
|
||||
buffers[13] = length 620, hash 3990AA39
|
||||
buffers[14] = length 5450, hash F06EC4AA
|
||||
buffers[15] = length 1051, hash 92DFA63A
|
||||
buffers[16] = length 874, hash 69587FB4
|
||||
buffers[17] = length 781, hash 36BE495B
|
||||
buffers[18] = length 4725, hash AC0C8CD3
|
||||
buffers[19] = length 1022, hash 5D8BFF34
|
||||
buffers[20] = length 790, hash 99413A99
|
||||
buffers[21] = length 610, hash 5E129290
|
||||
buffers[22] = length 2751, hash 769974CB
|
||||
buffers[23] = length 745, hash B78A477A
|
||||
buffers[24] = length 621, hash CF741E7A
|
||||
buffers[25] = length 505, hash 1DB4894E
|
||||
buffers[26] = length 1268, hash C15348DC
|
||||
buffers[27] = length 880, hash C2DE85D0
|
||||
buffers[28] = length 530, hash C98BC6A8
|
||||
buffers[29] = length 568, hash 4FE5C8EA
|
||||
buffers[30] = length 0, hash 1
|
19
testdata/src/test/assets/playbackdumps/ts/sample_scte35.ts.dump
vendored
Normal file
19
testdata/src/test/assets/playbackdumps/ts/sample_scte35.ts.dump
vendored
Normal file
@ -0,0 +1,19 @@
|
||||
MediaCodec (audio/mpeg-L2):
|
||||
buffers.length = 5
|
||||
buffers[0] = length 1253, hash 727FD1C6
|
||||
buffers[1] = length 1254, hash 73FB07B8
|
||||
buffers[2] = length 1254, hash 73FB07B8
|
||||
buffers[3] = length 1254, hash 73FB07B8
|
||||
buffers[4] = length 0, hash 1
|
||||
MediaCodec (video/mpeg2):
|
||||
buffers.length = 3
|
||||
buffers[0] = length 20711, hash 34341E8
|
||||
buffers[1] = length 18112, hash EC44B35B
|
||||
buffers[2] = length 0, hash 1
|
||||
MetadataOutput:
|
||||
Metadata[0]:
|
||||
entry[0] = SpliceInsertCommand
|
||||
Metadata[1]:
|
||||
entry[0] = SpliceInsertCommand
|
||||
Metadata[2]:
|
||||
entry[0] = SpliceInsertCommand
|
@ -24,6 +24,7 @@ dependencies {
|
||||
compileOnly 'org.jetbrains.kotlin:kotlin-annotations-jvm:' + kotlinAnnotationsVersion
|
||||
implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion
|
||||
implementation project(modulePrefix + 'library-core')
|
||||
implementation 'org.robolectric:robolectric:' + robolectricVersion
|
||||
testImplementation 'org.robolectric:robolectric:' + robolectricVersion
|
||||
}
|
||||
|
||||
|
@ -22,6 +22,7 @@ import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.ExoPlaybackException;
|
||||
import com.google.android.exoplayer2.ExoPlayer;
|
||||
import com.google.android.exoplayer2.IllegalSeekPositionException;
|
||||
import com.google.android.exoplayer2.PlaybackParameters;
|
||||
import com.google.android.exoplayer2.Player;
|
||||
import com.google.android.exoplayer2.PlayerMessage;
|
||||
import com.google.android.exoplayer2.PlayerMessage.Target;
|
||||
@ -608,26 +609,26 @@ public abstract class Action {
|
||||
}
|
||||
}
|
||||
|
||||
/** Calls {@link Player#setPlaybackSpeed(float)}. */
|
||||
public static final class SetPlaybackSpeed extends Action {
|
||||
/** Calls {@link Player#setPlaybackParameters(PlaybackParameters)}. */
|
||||
public static final class SetPlaybackParameters extends Action {
|
||||
|
||||
private final float playbackSpeed;
|
||||
private final PlaybackParameters playbackParameters;
|
||||
|
||||
/**
|
||||
* Creates a set playback speed action instance.
|
||||
* Creates a set playback parameters action instance.
|
||||
*
|
||||
* @param tag A tag to use for logging.
|
||||
* @param playbackSpeed The playback speed.
|
||||
* @param playbackParameters The playback parameters.
|
||||
*/
|
||||
public SetPlaybackSpeed(String tag, float playbackSpeed) {
|
||||
super(tag, "SetPlaybackSpeed:" + playbackSpeed);
|
||||
this.playbackSpeed = playbackSpeed;
|
||||
public SetPlaybackParameters(String tag, PlaybackParameters playbackParameters) {
|
||||
super(tag, "SetPlaybackParameters:" + playbackParameters);
|
||||
this.playbackParameters = playbackParameters;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doActionImpl(
|
||||
SimpleExoPlayer player, DefaultTrackSelector trackSelector, @Nullable Surface surface) {
|
||||
player.setPlaybackSpeed(playbackSpeed);
|
||||
player.setPlaybackParameters(playbackParameters);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -20,6 +20,7 @@ import android.view.Surface;
|
||||
import androidx.annotation.Nullable;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.ExoPlaybackException;
|
||||
import com.google.android.exoplayer2.PlaybackParameters;
|
||||
import com.google.android.exoplayer2.Player;
|
||||
import com.google.android.exoplayer2.PlayerMessage;
|
||||
import com.google.android.exoplayer2.PlayerMessage.Target;
|
||||
@ -35,7 +36,7 @@ import com.google.android.exoplayer2.testutil.Action.Seek;
|
||||
import com.google.android.exoplayer2.testutil.Action.SendMessages;
|
||||
import com.google.android.exoplayer2.testutil.Action.SetAudioAttributes;
|
||||
import com.google.android.exoplayer2.testutil.Action.SetPlayWhenReady;
|
||||
import com.google.android.exoplayer2.testutil.Action.SetPlaybackSpeed;
|
||||
import com.google.android.exoplayer2.testutil.Action.SetPlaybackParameters;
|
||||
import com.google.android.exoplayer2.testutil.Action.SetRendererDisabled;
|
||||
import com.google.android.exoplayer2.testutil.Action.SetRepeatMode;
|
||||
import com.google.android.exoplayer2.testutil.Action.SetShuffleModeEnabled;
|
||||
@ -214,14 +215,14 @@ public final class ActionSchedule {
|
||||
}
|
||||
|
||||
/**
|
||||
* Schedules a playback speed setting action.
|
||||
* Schedules a playback parameters setting action.
|
||||
*
|
||||
* @param playbackSpeed The playback speed to set.
|
||||
* @param playbackParameters The playback parameters to set.
|
||||
* @return The builder, for convenience.
|
||||
* @see Player#setPlaybackSpeed(float)
|
||||
* @see Player#setPlaybackParameters(PlaybackParameters)
|
||||
*/
|
||||
public Builder setPlaybackSpeed(float playbackSpeed) {
|
||||
return apply(new SetPlaybackSpeed(tag, playbackSpeed));
|
||||
public Builder setPlaybackParameters(PlaybackParameters playbackParameters) {
|
||||
return apply(new SetPlaybackParameters(tag, playbackParameters));
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -52,7 +52,7 @@ public abstract class ExoHostedTest implements AnalyticsListener, HostedTest {
|
||||
DefaultAudioSink.failOnSpuriousAudioTimestamp = true;
|
||||
}
|
||||
|
||||
public static final long MAX_PLAYING_TIME_DISCREPANCY_MS = 2000;
|
||||
public static final long MAX_PLAYING_TIME_DISCREPANCY_MS = 5000;
|
||||
public static final long EXPECTED_PLAYING_TIME_MEDIA_DURATION_MS = -2;
|
||||
public static final long EXPECTED_PLAYING_TIME_UNSET = -1;
|
||||
|
||||
|
@ -31,6 +31,7 @@ import com.google.android.exoplayer2.upstream.DefaultLoadErrorHandlingPolicy;
|
||||
import com.google.android.exoplayer2.upstream.TransferListener;
|
||||
import com.google.android.exoplayer2.util.Assertions;
|
||||
import com.google.android.exoplayer2.util.MimeTypes;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import org.checkerframework.checker.nullness.compatqual.NullableType;
|
||||
@ -48,7 +49,7 @@ public class FakeAdaptiveMediaPeriod extends FakeMediaPeriod
|
||||
@Nullable private final TransferListener transferListener;
|
||||
private final long durationUs;
|
||||
|
||||
@MonotonicNonNull private Callback callback;
|
||||
private @MonotonicNonNull Callback callback;
|
||||
private ChunkSampleStream<FakeChunkSource>[] sampleStreams;
|
||||
private SequenceableLoader sequenceableLoader;
|
||||
|
||||
@ -99,7 +100,7 @@ public class FakeAdaptiveMediaPeriod extends FakeMediaPeriod
|
||||
}
|
||||
}
|
||||
sampleStreams = newSampleStreamArray(validStreams.size());
|
||||
validStreams.toArray(sampleStreams);
|
||||
Util.nullSafeListToArray(validStreams, sampleStreams);
|
||||
this.sequenceableLoader = new CompositeSequenceableLoader(sampleStreams);
|
||||
return returnPositionUs;
|
||||
}
|
||||
|
@ -30,6 +30,7 @@ public class FakeAudioRenderer extends FakeRenderer {
|
||||
private final AudioRendererEventListener.EventDispatcher eventDispatcher;
|
||||
private final DecoderCounters decoderCounters;
|
||||
private boolean notifiedAudioSessionId;
|
||||
private boolean notifiedPositionAdvancing;
|
||||
|
||||
public FakeAudioRenderer(Handler handler, AudioRendererEventListener eventListener) {
|
||||
super(C.TRACK_TYPE_AUDIO);
|
||||
@ -43,6 +44,7 @@ public class FakeAudioRenderer extends FakeRenderer {
|
||||
super.onEnabled(joining, mayRenderStartOfStream);
|
||||
eventDispatcher.enabled(decoderCounters);
|
||||
notifiedAudioSessionId = false;
|
||||
notifiedPositionAdvancing = false;
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -67,6 +69,10 @@ public class FakeAudioRenderer extends FakeRenderer {
|
||||
eventDispatcher.audioSessionId(/* audioSessionId= */ 1);
|
||||
notifiedAudioSessionId = true;
|
||||
}
|
||||
if (shouldProcess && !notifiedPositionAdvancing) {
|
||||
eventDispatcher.positionAdvancing(System.currentTimeMillis());
|
||||
notifiedPositionAdvancing = true;
|
||||
}
|
||||
return shouldProcess;
|
||||
}
|
||||
}
|
||||
|
@ -217,8 +217,7 @@ public class FakeDataSource extends BaseDataSource {
|
||||
* this method.
|
||||
*/
|
||||
public final DataSpec[] getAndClearOpenedDataSpecs() {
|
||||
DataSpec[] dataSpecs = new DataSpec[openedDataSpecs.size()];
|
||||
openedDataSpecs.toArray(dataSpecs);
|
||||
DataSpec[] dataSpecs = openedDataSpecs.toArray(new DataSpec[0]);
|
||||
openedDataSpecs.clear();
|
||||
return dataSpecs;
|
||||
}
|
||||
|
@ -30,7 +30,6 @@ import com.google.android.exoplayer2.drm.DrmSessionManager;
|
||||
import com.google.android.exoplayer2.source.MediaSourceEventListener;
|
||||
import com.google.android.exoplayer2.source.SampleStream;
|
||||
import com.google.android.exoplayer2.util.Assertions;
|
||||
import com.google.android.exoplayer2.util.MimeTypes;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
import com.google.common.collect.Iterables;
|
||||
import java.io.IOException;
|
||||
@ -274,10 +273,7 @@ public class FakeSampleStream implements SampleStream {
|
||||
@Nullable DrmSession previousSession = currentDrmSession;
|
||||
Looper playbackLooper = Assertions.checkNotNull(Looper.myLooper());
|
||||
currentDrmSession =
|
||||
newDrmInitData != null
|
||||
? drmSessionManager.acquireSession(playbackLooper, drmEventDispatcher, newFormat)
|
||||
: drmSessionManager.acquirePlaceholderSession(
|
||||
playbackLooper, MimeTypes.getTrackType(newFormat.sampleMimeType));
|
||||
drmSessionManager.acquireSession(playbackLooper, drmEventDispatcher, newFormat);
|
||||
outputFormatHolder.drmSession = currentDrmSession;
|
||||
|
||||
if (previousSession != null) {
|
||||
|
@ -0,0 +1,93 @@
|
||||
/*
|
||||
* 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.testutil;
|
||||
|
||||
import com.google.android.exoplayer2.SimpleExoPlayer;
|
||||
import com.google.android.exoplayer2.metadata.Metadata;
|
||||
import com.google.android.exoplayer2.util.Assertions;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Class to capture output from a playback test.
|
||||
*
|
||||
* <p>Implements {@link Dumper.Dumpable} so the output can be easily dumped to a string for
|
||||
* comparison against previous test runs.
|
||||
*/
|
||||
public final class PlaybackOutput implements Dumper.Dumpable {
|
||||
|
||||
private final ShadowMediaCodecConfig codecConfig;
|
||||
|
||||
// TODO: Add support for subtitles too
|
||||
private final List<Metadata> metadatas;
|
||||
|
||||
private PlaybackOutput(SimpleExoPlayer player, ShadowMediaCodecConfig codecConfig) {
|
||||
this.codecConfig = codecConfig;
|
||||
|
||||
metadatas = Collections.synchronizedList(new ArrayList<>());
|
||||
// TODO: Consider passing playback position into MetadataOutput and TextOutput. Calling
|
||||
// player.getCurrentPosition() inside onMetadata/Cues will likely be non-deterministic
|
||||
// because renderer-thread != playback-thread.
|
||||
player.addMetadataOutput(metadatas::add);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an instance that captures the metadata and text output from {@code player} and the audio
|
||||
* and video output via the {@link TeeCodec TeeCodecs} exposed by {@code mediaCodecConfig}.
|
||||
*
|
||||
* <p>Must be called <b>before</b> playback to ensure metadata and text output is captured
|
||||
* correctly.
|
||||
*
|
||||
* @param player The {@link SimpleExoPlayer} to capture metadata and text output from.
|
||||
* @param mediaCodecConfig The {@link ShadowMediaCodecConfig} to capture audio and video output
|
||||
* from.
|
||||
* @return A new instance that can be used to dump the playback output.
|
||||
*/
|
||||
public static PlaybackOutput register(
|
||||
SimpleExoPlayer player, ShadowMediaCodecConfig mediaCodecConfig) {
|
||||
return new PlaybackOutput(player, mediaCodecConfig);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dump(Dumper dumper) {
|
||||
ImmutableMap<String, TeeCodec> codecs = codecConfig.getCodecs();
|
||||
ImmutableList<String> mimeTypes = ImmutableList.sortedCopyOf(codecs.keySet());
|
||||
for (String mimeType : mimeTypes) {
|
||||
dumper.add(Assertions.checkNotNull(codecs.get(mimeType)));
|
||||
}
|
||||
|
||||
dumpMetadata(dumper);
|
||||
}
|
||||
|
||||
private void dumpMetadata(Dumper dumper) {
|
||||
if (metadatas.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
dumper.startBlock("MetadataOutput");
|
||||
for (int i = 0; i < metadatas.size(); i++) {
|
||||
dumper.startBlock("Metadata[" + i + "]");
|
||||
Metadata metadata = metadatas.get(i);
|
||||
for (int j = 0; j < metadata.length(); j++) {
|
||||
dumper.add("entry[" + j + "]", metadata.get(j).getClass().getSimpleName());
|
||||
}
|
||||
dumper.endBlock();
|
||||
}
|
||||
dumper.endBlock();
|
||||
}
|
||||
}
|
@ -0,0 +1,133 @@
|
||||
/*
|
||||
* 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.testutil;
|
||||
|
||||
import android.media.MediaCodecInfo;
|
||||
import android.media.MediaFormat;
|
||||
import com.google.android.exoplayer2.util.MimeTypes;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import com.google.common.primitives.Ints;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import org.junit.rules.ExternalResource;
|
||||
import org.robolectric.shadows.MediaCodecInfoBuilder;
|
||||
import org.robolectric.shadows.ShadowMediaCodec;
|
||||
import org.robolectric.shadows.ShadowMediaCodecList;
|
||||
|
||||
/**
|
||||
* A JUnit @Rule to configure Roboelectric's {@link ShadowMediaCodec}.
|
||||
*
|
||||
* <p>Registers a {@link org.robolectric.shadows.ShadowMediaCodec.CodecConfig} for each audio/video
|
||||
* MIME type known by ExoPlayer, and provides access to the bytes passed to these via {@link
|
||||
* TeeCodec}.
|
||||
*/
|
||||
public final class ShadowMediaCodecConfig extends ExternalResource {
|
||||
|
||||
private final Map<String, TeeCodec> codecsByMimeType;
|
||||
|
||||
private ShadowMediaCodecConfig() {
|
||||
this.codecsByMimeType = new HashMap<>();
|
||||
}
|
||||
|
||||
public static ShadowMediaCodecConfig forAllSupportedMimeTypes() {
|
||||
return new ShadowMediaCodecConfig();
|
||||
}
|
||||
|
||||
public ImmutableMap<String, TeeCodec> getCodecs() {
|
||||
return ImmutableMap.copyOf(codecsByMimeType);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void before() throws Throwable {
|
||||
// Video codecs
|
||||
MediaCodecInfo.CodecProfileLevel avcProfileLevel =
|
||||
createProfileLevel(
|
||||
MediaCodecInfo.CodecProfileLevel.AVCProfileHigh,
|
||||
MediaCodecInfo.CodecProfileLevel.AVCLevel62);
|
||||
configureCodec(
|
||||
/* codecName= */ "exotest.video.avc",
|
||||
MimeTypes.VIDEO_H264,
|
||||
ImmutableList.of(avcProfileLevel),
|
||||
ImmutableList.of(MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Flexible));
|
||||
MediaCodecInfo.CodecProfileLevel mpeg2ProfileLevel =
|
||||
createProfileLevel(
|
||||
MediaCodecInfo.CodecProfileLevel.MPEG2ProfileMain,
|
||||
MediaCodecInfo.CodecProfileLevel.MPEG2LevelML);
|
||||
configureCodec(
|
||||
/* codecName= */ "exotest.video.mpeg2",
|
||||
MimeTypes.VIDEO_MPEG2,
|
||||
ImmutableList.of(mpeg2ProfileLevel),
|
||||
ImmutableList.of(MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Flexible));
|
||||
|
||||
// Audio codecs
|
||||
configureCodec("exotest.audio.aac", MimeTypes.AUDIO_AAC);
|
||||
configureCodec("exotest.audio.mpegl2", MimeTypes.AUDIO_MPEG_L2);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void after() {
|
||||
codecsByMimeType.clear();
|
||||
ShadowMediaCodecList.reset();
|
||||
ShadowMediaCodec.clearCodecs();
|
||||
}
|
||||
|
||||
private void configureCodec(String codecName, String mimeType) {
|
||||
configureCodec(
|
||||
codecName,
|
||||
mimeType,
|
||||
/* profileLevels= */ ImmutableList.of(),
|
||||
/* colorFormats= */ ImmutableList.of());
|
||||
}
|
||||
|
||||
private void configureCodec(
|
||||
String codecName,
|
||||
String mimeType,
|
||||
List<MediaCodecInfo.CodecProfileLevel> profileLevels,
|
||||
List<Integer> colorFormats) {
|
||||
MediaFormat mediaFormat = new MediaFormat();
|
||||
mediaFormat.setString(MediaFormat.KEY_MIME, mimeType);
|
||||
MediaCodecInfoBuilder.CodecCapabilitiesBuilder capabilities =
|
||||
MediaCodecInfoBuilder.CodecCapabilitiesBuilder.newBuilder().setMediaFormat(mediaFormat);
|
||||
if (!profileLevels.isEmpty()) {
|
||||
capabilities.setProfileLevels(profileLevels.toArray(new MediaCodecInfo.CodecProfileLevel[0]));
|
||||
}
|
||||
if (!colorFormats.isEmpty()) {
|
||||
capabilities.setColorFormats(Ints.toArray(colorFormats));
|
||||
}
|
||||
ShadowMediaCodecList.addCodec(
|
||||
MediaCodecInfoBuilder.newBuilder()
|
||||
.setName(codecName)
|
||||
.setCapabilities(capabilities.build())
|
||||
.build());
|
||||
// TODO: Update ShadowMediaCodec to consider the MediaFormat.KEY_MAX_INPUT_SIZE value passed
|
||||
// to configure() so we don't have to specify large buffers here.
|
||||
TeeCodec codec = new TeeCodec(mimeType);
|
||||
ShadowMediaCodec.addDecoder(
|
||||
codecName,
|
||||
new ShadowMediaCodec.CodecConfig(
|
||||
/* inputBufferSize= */ 50_000, /* outputBufferSize= */ 50_000, codec));
|
||||
codecsByMimeType.put(mimeType, codec);
|
||||
}
|
||||
|
||||
private static MediaCodecInfo.CodecProfileLevel createProfileLevel(int profile, int level) {
|
||||
MediaCodecInfo.CodecProfileLevel profileLevel = new MediaCodecInfo.CodecProfileLevel();
|
||||
profileLevel.profile = profile;
|
||||
profileLevel.level = level;
|
||||
return profileLevel;
|
||||
}
|
||||
}
|
@ -314,32 +314,16 @@ public abstract class StubExoPlayer extends BasePlayer implements ExoPlayer {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
/** @deprecated Use {@link #setPlaybackSpeed(float)} instead. */
|
||||
@SuppressWarnings("deprecation")
|
||||
@Deprecated
|
||||
@Override
|
||||
public void setPlaybackParameters(@Nullable PlaybackParameters playbackParameters) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
/** @deprecated Use {@link #getPlaybackSpeed()} instead. */
|
||||
@SuppressWarnings("deprecation")
|
||||
@Deprecated
|
||||
@Override
|
||||
public PlaybackParameters getPlaybackParameters() {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setPlaybackSpeed(float playbackSpeed) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public float getPlaybackSpeed() {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setSeekParameters(@Nullable SeekParameters seekParameters) {
|
||||
throw new UnsupportedOperationException();
|
||||
|
@ -0,0 +1,80 @@
|
||||
/*
|
||||
* 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.testutil;
|
||||
|
||||
import com.google.android.exoplayer2.util.MimeTypes;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import org.robolectric.shadows.ShadowMediaCodec;
|
||||
|
||||
/**
|
||||
* A {@link ShadowMediaCodec.CodecConfig.Codec} for Robolectric's {@link ShadowMediaCodec} that
|
||||
* records the contents of buffers passed to it before copying the contents into the output buffer.
|
||||
*
|
||||
* <p>This also implements {@link Dumper.Dumpable} so the recorded buffers can be written out to a
|
||||
* dump file.
|
||||
*/
|
||||
public final class TeeCodec implements ShadowMediaCodec.CodecConfig.Codec, Dumper.Dumpable {
|
||||
|
||||
private final String mimeType;
|
||||
private final List<byte[]> receivedBuffers;
|
||||
|
||||
public TeeCodec(String mimeType) {
|
||||
this.mimeType = mimeType;
|
||||
this.receivedBuffers = Collections.synchronizedList(new ArrayList<>());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void process(ByteBuffer in, ByteBuffer out) {
|
||||
byte[] bytes = new byte[in.remaining()];
|
||||
in.get(bytes);
|
||||
receivedBuffers.add(bytes);
|
||||
|
||||
if (!MimeTypes.isAudio(mimeType)) {
|
||||
// Don't output audio bytes, because ShadowAudioTrack doesn't advance the playback position so
|
||||
// playback never completes.
|
||||
// TODO: Update ShadowAudioTrack to advance the playback position in a realistic way.
|
||||
out.put(bytes);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dump(Dumper dumper) {
|
||||
if (receivedBuffers.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
dumper.startBlock("MediaCodec (" + mimeType + ")");
|
||||
dumper.add("buffers.length", receivedBuffers.size());
|
||||
for (int i = 0; i < receivedBuffers.size(); i++) {
|
||||
dumper.add("buffers[" + i + "]", receivedBuffers.get(i));
|
||||
}
|
||||
|
||||
dumper.endBlock();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the buffers received by this codec.
|
||||
*
|
||||
* <p>The list is sorted in the order the buffers were passed to {@link #process(ByteBuffer,
|
||||
* ByteBuffer)}.
|
||||
*/
|
||||
public ImmutableList<byte[]> getReceivedBuffers() {
|
||||
return ImmutableList.copyOf(receivedBuffers);
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user