Compare commits

..

No commits in common. "release" and "1.5.0" have entirely different histories.

2293 changed files with 18570 additions and 103038 deletions

View File

@ -19,9 +19,6 @@ body:
options:
- Media3 main branch
- Media3 pre-release (alpha, beta or RC not in this list)
- Media3 1.6.1
- Media3 1.6.0
- Media3 1.5.1
- Media3 1.5.0
- Media3 1.4.1
- Media3 1.4.0
@ -45,6 +42,9 @@ body:
- ExoPlayer 2.16.0
- ExoPlayer 2.15.1
- ExoPlayer 2.15.0
- ExoPlayer 2.14.2
- ExoPlayer 2.14.1
- ExoPlayer 2.14.0
- ExoPlayer dev-v2 branch
- Older (unsupported)
validations:

9
.gitignore vendored
View File

@ -4,7 +4,6 @@ gen
libs
obj
lint.xml
.kotlin
# IntelliJ IDEA
.idea
@ -53,7 +52,6 @@ tmp
# External native builds
.externalNativeBuild
.cxx
# VP9 decoder extension
libraries/decoder_vp9/src/main/jni/libvpx
@ -69,7 +67,6 @@ libraries/decoder_opus/src/main/jni/libopus
# FLAC decoder extension
libraries/decoder_flac/src/main/jni/flac
libraries/decoder_flac/src/main/jni/libflac
# FFmpeg decoder extension
libraries/decoder_ffmpeg/src/main/jni/ffmpeg
@ -82,9 +79,3 @@ libraries/datasource_cronet/libs/*
# MIDI decoder extension
libraries/decoder_midi/lib
# IAMF decoder extension
libraries/decoder_iamf/src/main/jni/libiamf
# MPEG-H decoder extension
libraries/decoder_mpegh/src/main/jni/libmpegh

View File

@ -1,466 +1,7 @@
# Release notes
## 1.6
### 1.6.1 (2025-04-14)
This release includes the following changes since the
[1.6.0 release](#160-2025-03-26):
* Common Library:
* Add `PlaybackParameters.withPitch(float)` method for easily copying a
`PlaybackParameters` with a new `pitch` value
([#2257](https://github.com/androidx/media/issues/2257)).
* ExoPlayer:
* Fix issue where media item transition fails due to recoverable renderer
error during initialization of the next media item
([#2229](https://github.com/androidx/media/issues/2229)).
* Fix issue where `ProgressiveMediaPeriod` throws an
`IllegalStateException` as `PreloadMediaSource` attempts to call its
`getBufferedDurationUs()` before it is prepared
([#2315](https://github.com/androidx/media/issues/2315)).
* Fix sending `CmcdData` in manifest requests for DASH, HLS, and
SmoothStreaming ([#2253](https://github.com/androidx/media/pull/2253)).
* Ensure `AdPlaybackState.withAdDurationsUs(long[][])` can be used after
ad groups have been removed. The user still needs to pass in an array of
durations for removed ad groups which can be empty or null
([#2267](https://github.com/androidx/media/issues/2267)).
* Extractors:
* MP4: Parse `alternate_group` from the `tkhd` box and expose it as an
`Mp4AlternateGroupData` entry in each track's `Format.metadata`
([#2242](https://github.com/androidx/media/issues/2242)).
* Audio:
* Fix offload issue where the position might get stuck when playing a
playlist of short content
([#1920](https://github.com/androidx/media/issues/1920)).
* Session:
* Lower aggregation timeout for platform `MediaSession` callbacks from 500
to 100 milliseconds and add an experimental setter to allow apps to
configure this value.
* Fix issue where notifications reappear after they have been dismissed by
the user ([#2302](https://github.com/androidx/media/issues/2302)).
* Fix a bug where the session returned a single-item timeline when the
wrapped player is actually empty. This happened when the wrapped player
doesn't have `COMMAND_GET_TIMELINE` available while
`COMMAND_GET_CURRENT_MEDIA_ITEM` is available and the wrapped player is
empty ([#2320](https://github.com/androidx/media/issues/2320)).
* Fix a bug where calling
`MediaSessionService.setMediaNotificationProvider` is silently ignored
after other interactions with the service like
`setForegroundServiceTimeoutMs`
([#2305](https://github.com/androidx/media/issues/2305)).
* UI:
* Enable `PlayerSurface` to work with `ExoPlayer.setVideoEffects` and
`CompositionPlayer`.
* Fix bug where `PlayerSurface` can't be recomposed with a new `Player`.
* HLS extension:
* Fix issue where chunk duration wasn't set in `CmcdData` for HLS media,
causing an assertion failure when processing encrypted media segments
([#2312](https://github.com/androidx/media/issues/2312)).
* RTSP extension:
* Add support for URI with RTSPT scheme as a way to configure the RTSP
session to use TCP
([#1484](https://github.com/androidx/media/issues/1484)).
* Cast extension:
* Add support for playlist metadata
([#2235](https://github.com/androidx/media/pull/2235)).
### 1.6.0 (2025-03-26)
This release includes the following changes since the
[1.5.1 release](#151-2024-12-19):
* Common Library:
* Add `AudioManagerCompat` and `AudioFocusRequestCompat` to replace the
equivalent classes in `androidx.media`.
* Upgrade Kotlin from 1.9.20 to 2.0.20 and use Compose Compiler Gradle
plugin. Upgrade KotlinX Coroutines library from 1.8.1 to 1.9.0.
* Remove `Format.toBundle(boolean excludeMetadata)` method, use
`Format.toBundle()` instead.
* Fix bug in `SimpleBasePlayer` where setting a new
`currentMediaItemIndex` in `State` after `setPlaylist` with `null`
`MediaMetadata` does not reevaluate the metadata
([#1940](https://github.com/androidx/media/issues/1940)).
* Change `SimpleBasePlayer.State` access from protected to public to make
it easier to handle updates in other classes
([#2128](https://github.com/androidx/media/issues/2128)).
* ExoPlayer:
* Add `MediaExtractorCompat`, a new class that provides equivalent
features to platform `MediaExtractor`.
* Add experimental 'ExoPlayer' pre-warming support for playback using
`MediaCodecVideoRenderer`. You can configure `DefaultRenderersFactory`
through `experimentalSetEnableMediaCodecVideoRendererPrewarming` to
provide a secondary `MediaCodecVideoRenderer` to `ExoPlayer`. If
enabled, `ExoPlayer` pre-processes the video of consecutive media items
during playback to reduce media item transition latency.
* Reduce default values for `bufferForPlaybackMs` and
`bufferForPlaybackAfterRebufferMs` in `DefaultLoadControl` to 1000 and
2000 ms respectively.
* Initialize `DeviceInfo` and device volume asynchronously (if enabled
using `setDeviceVolumeControlEnabled`). These values aren't available
instantly after `ExoPlayer.Builder.build()`, and `Player.Listener`
notifies changes through `onDeviceInfoChanged` and
`onDeviceVolumeChanged`.
* Initial audio session id is no longer immediately available after
creating the player. You can use
`AnalyticsListener.onAudioSessionIdChanged` to listen to the initial
update if required.
* Consider language when selecting a video track. By default, select a
'main' video track that matches the language of the selected audio
track, if available. Explicit video language preferences can be
expressed with
`TrackSelectionParameters.Builder.setPreferredVideoLanguage(s)`.
* Add `selectedAudioLanguage` parameter to
`DefaultTrackSelector.selectVideoTrack()` method.
* Add `retryCount` parameter to `MediaSourceEventListener.onLoadStarted`
and corresponding `MediaSourceEventListener.EventDispatcher` methods.
* Fix bug where playlist items or periods in multi-period DASH streams
with durations that don't match the actual content could cause frame
freezes at the end of the item
([#1698](https://github.com/androidx/media/issues/1698)).
* Move `BasePreloadManager.Listener` to a top-level
`PreloadManagerListener`.
* `RenderersFactory.createSecondaryRenderer` can be implemented to provide
secondary renderers for pre-warming. Pre-warming enables quicker media
item transitions during playback.
* Enable sending `CmcdData` for manifest requests in adaptive streaming
formats DASH, HLS, and SmoothStreaming
([#1951](https://github.com/androidx/media/issues/1951)).
* Provide `MediaCodecInfo` of the codec that will be initialized in
`MediaCodecRenderer.onReadyToInitializeCodec`
([#1963](https://github.com/androidx/media/pull/1963)).
* Change `AdsMediaSource` to allow the `AdPlaybackStates` to grow by
appending ad groups. Invalid modifications are detected and throw an
exception.
* Fix issue where additional decode-only frames may be displayed in quick
succession when transitioning to content media after a mid-roll ad.
* Make `DefaultRenderersFactory` add two `MetadataRenderer` instances to
enable apps to receive two different schemes of metadata by default.
* Reevaluate whether the ongoing load of a chunk should be cancelled when
playback is paused
([#1785](https://github.com/androidx/media/pull/1785)).
* Add option to `ClippingMediaSource` to allow clipping in unseekable
media.
* Fix bug where seeking with pre-warming could block following media item
transition.
* Fix a bug where `ExoPlayer.isLoading()` remains `true` while it has
transitioned to `STATE_IDLE` or `STATE_ENDED`
([#2133](https://github.com/androidx/media/issues/2133)).
* Add `lastRebufferRealtimeMs` to `LoadControl.Parameter`
([#2113](https://github.com/androidx/media/pull/2113)).
* Transformer:
* Add support for transmuxing into alternative backward compatible
formats.
* Add support for transcoding and transmuxing Dolby Vision (profile 8)
format.
* Update parameters of `VideoFrameProcessor.registerInputStream` and
`VideoFrameProcessor.Listener.onInputStreamRegistered` to use `Format`.
* Generate HDR static metadata when using `DefaultEncoderFactory`.
* Enable support for Android platform diagnostics using
`MediaMetricsManager`. Transformer forwards editing events and
performance data to the platform, which helps to provide system
performance and debugging information on the device. This data may also
be collected by Google
[if sharing usage and diagnostics data is enabled](https://support.google.com/accounts/answer/6078260)
by the user of the device. Apps can opt-out of contributing to platform
diagnostics for Transformer with
`Transformer.Builder.setUsePlatformDiagnostics(false)`.
* Split `InAppMuxer` into `InAppMp4Muxer` and `InAppFragmentedMp4Muxer`.
You use `InAppMp4Muxer` to produce a non-fragmented MP4 file, while
`InAppFragmentedMp4Muxer` is for producing a fragmented MP4 file.
* Move `Muxer` interface from `media3-muxer` to `media3-transformer`.
* Add `MediaProjectionAssetLoader`, which provides media from a
`MediaProjection` for screen recording, and add support for screen
recording to the Transformer demo app.
* Add `#getInputFormat()` to `Codec` interface.
* Shift the responsibility to release the `GlObjectsProvider` onto the
caller in `DefaultVideoFrameProcessor` and `DefaultVideoCompositor` when
possible.
* Extractors:
* AVI: Fix handling of files with constant bitrate compressed audio where
the stream header stores the number of bytes instead of the number of
chunks.
* Fix handling of NAL units with lengths expressed in 1 or 2 bytes (rather
than 4).
* Fix `ArrayIndexOutOfBoundsException` in MP4 edit lists when the edit
list starts at a non-sync frame with no preceding sync frame
([#2062](https://github.com/androidx/media/issues/2062)).
* Fix issue where TS streams can get stuck on some devices
([#2069](https://github.com/androidx/media/issues/2069)).
* FLAC: Add support for 32-bit FLAC files. Previously these would fail to
play with `IllegalStateException: Playback stuck buffering and not
loading` ([#2197](https://github.com/androidx/media/issues/2197)).
* Audio:
* Fix `onAudioPositionAdvancing` to be called when playback resumes
(previously it was called when playback was paused).
* Don't bypass `SonicAudioProcessor` when `SpeedChangingAudioProcessor` is
configured with default parameters.
* Fix underflow in `Sonic#getOutputSize()` that could cause
`DefaultAudioSink` to stall.
* Fix `MediaCodecAudioRenderer.getDurationToProgressUs()` and
`DecoderAudioRenderer.getDurationToProgressUs()` so that seeks correctly
reset the provided durations.
* Make `androidx.media3.common.audio.SonicAudioProcessor` final.
* Add support for float PCM to `ChannelMappingAudioProcessor` and
`TrimmingAudioProcessor`.
* Video:
* Change `MediaCodecVideoRenderer.shouldUsePlaceholderSurface` to
protected so that applications can override to block usage of
placeholder surfaces
([#1905](https://github.com/androidx/media/pull/1905)).
* Add experimental `ExoPlayer` AV1 sample dependency parsing to speed up
seeking. Enable it with the new
`DefaultRenderersFactory.experimentalSetParseAv1SampleDependencies` API.
* Add experimental `ExoPlayer` API to drop late `MediaCodecVideoRenderer`
decoder input buffers that are not depended on. Enable it with
`DefaultRenderersFactory.experimentalSetLateThresholdToDropDecoderInputUs`.
* Fix issue where a player without a surface was ready immediately and
very slow decoding any pending frames
([#1973](https://github.com/androidx/media/issues/1973)).
* Exclude Xiaomi and OPPO devices from detached surface mode to avoid
screen flickering
([#2059](https://github.com/androidx/media/issues/2059)).
* Text:
* Add support for VobSub subtitles
([#8260](https://github.com/google/ExoPlayer/issues/8260)).
* Stop eagerly loading all subtitle files configured with
`MediaItem.Builder.setSubtitleConfigurations`, and instead only load one
if it is selected by track selection
([#1721](https://github.com/androidx/media/issues/1721)).
* TTML: Add support for referencing `tts:origin` and `tts:extent` using
`style` ([#2953](https://github.com/google/ExoPlayer/issues/2953)).
* Restrict WebVTT and SubRip timestamps to exactly 3 decimal places.
Previously we incorrectly parsed any number of decimal places but always
assumed the value was in milliseconds, leading to incorrect timestamps
([#1997](https://github.com/androidx/media/issues/1997)).
* Fix playback hanging when a playlist contains clipped items with CEA-608
or CEA-708 captions.
* Fix `IllegalStateException` when an SSA file contains a cue with zero
duration (start and end time equal)
([#2052](https://github.com/androidx/media/issues/2052)).
* Suppress (and log) subtitle parsing errors when subtitles are muxed into
the same container as audio and video
([#2052](https://github.com/androidx/media/issues/2052)).
* Fix handling of multi-byte UTF-8 characters in WebVTT files using CR
line endings ([#2167](https://github.com/androidx/media/issues/2167)).
* DRM:
* Fix `MediaCodec$CryptoException: Operation not supported in this
configuration` error when playing ClearKey content on API < 27 devices
([#1732](https://github.com/androidx/media/issues/1732)).
* Effect:
* Moved the functionality of `OverlaySettings` into
`StaticOverlaySettings`. `OverlaySettings` can be subclassed to allow
dynamic overlay settings.
* Muxers:
* Moved `MuxerException` out of `Muxer` interface to avoid a very long
fully qualified name.
* Renamed `setSampleCopyEnabled()` method to `setSampleCopyingEnabled()`
in both `Mp4Muxer.Builder` and `FragmentedMp4Muxer.Builder`.
* `Mp4Muxer.addTrack()` and `FragmentedMp4Muxer.addTrack()` now return an
`int` track ID instead of a `TrackToken`.
* `Mp4Muxer` and `FragmentedMp4Muxer` no longer implement `Muxer`
interface.
* Disable `Mp4Muxer` sample batching and copying by default.
* Fix a bug in `FragmentedMp4Muxer` that creates a lot of fragments when
only audio track is written.
* Session:
* Keep foreground service state for an additional 10 minutes when playback
pauses, stops or fails. This allows users to resume playback within this
timeout without risking foreground service restrictions on various
devices. Note that simply calling `player.pause()` can no longer be used
to stop the foreground service before `stopSelf()` when overriding
`onTaskRemoved`, use `MediaSessionService.pauseAllPlayersAndStopSelf()`
instead.
* Keep notification visible when playback enters an error or stopped
state. The notification is only removed if the playlist is cleared or
the player is released.
* Improve handling of Android platform MediaSession actions ACTION_PLAY
and ACTION_PAUSE to only set one of them according to the available
commands and also accept if only one of them is set.
* Add `Context` as a parameter to
`MediaButtonReceiver.shouldStartForegroundService`
([#1887](https://github.com/androidx/media/issues/1887)).
* Fix bug where calling a `Player` method on a `MediaController` connected
to a legacy session dropped changes from a pending update.
* Make `MediaSession.setSessionActivity(PendingIntent)` accept null
([#2109](https://github.com/androidx/media/issues/2109)).
* Fix bug where a stale notification stays visible when the playlist is
cleared ([#2211](https://github.com/androidx/media/issues/2211)).
* UI:
* Add state holders and composables to the `media3-ui-compose` module for
`PlayerSurface`, `PresentationState`, `PlayPauseButtonState`,
`NextButtonState`, `PreviousButtonState`, `RepeatButtonState`,
`ShuffleButtonState` and `PlaybackSpeedState`.
* Downloads:
* Fix bug in `CacheWriter` that leaves data sources open and cache areas
locked in case the data source throws an `Exception` other than
`IOException`
([#9760](https://github.com/google/ExoPlayer/issues/9760)).
* HLS extension:
* Add a first version of `HlsInterstitialsAdsLoader`. The ads loader reads
the HLS interstitials of an HLS media playlist and maps them to the
`AdPlaybackState` that is passed to the `AdsMediaSource`. This initial
version only supports HLS VOD streams with `X-ASSET-URI` attributes.
* Add `HlsInterstitialsAdsLoader.AdsMediaSourceFactory`. Apps can use it
to create `AdsMediaSource` instances that use an
`HlsInterstitialsAdsLoader` in a convenient and safe way.
* Parse `SUPPLEMENTAL-CODECS` tag from HLS playlist to detect Dolby Vision
formats ([#1785](https://github.com/androidx/media/pull/1785)).
* Loosen the condition for seeking to sync positions in an HLS stream
([#2209](https://github.com/androidx/media/issues/2209)).
* DASH extension:
* Add AC-4 Level-4 format support for DASH
([#1898](https://github.com/androidx/media/pull/1898)).
* Fix issue when calculating the update interval for ad insertion in
multi-period live streams
([#1698](https://github.com/androidx/media/issues/1698)).
* Parse `scte214:supplementalCodecs` attribute from DASH manifest to
detect Dolby Vision formats
([#1785](https://github.com/androidx/media/pull/1785)).
* Improve handling of period transitions in live streams where the period
contains media samples beyond the declared period duration
([#1698](https://github.com/androidx/media/issues/1698)).
* Fix issue where adaptation sets marked with `adaptation-set-switching`
but different languages or role flags are merged together
([#2222](https://github.com/androidx/media/issues/2222)).
* Decoder extensions (FFmpeg, VP9, AV1, etc.):
* Add the MPEG-H decoder module which uses the native MPEG-H decoder
module to decode MPEG-H audio
([#1826](https://github.com/androidx/media/pull/1826)).
* MIDI extension:
* Plumb custom `AudioSink` and `AudioRendererEventListener` instances into
`MidiRenderer`.
* Cast extension:
* Bump the `play-services-cast-framework` dependency to 21.5.0 to fix a
`FLAG_MUTABLE` crash in apps targeting API 34+ on devices with Google
Play services installed but disabled
([#2178](https://github.com/androidx/media/issues/2178)).
* Demo app:
* Extend `demo-compose` with additional buttons and enhance
`PlayerSurface` integration with scaling and shutter support.
* Remove deprecated symbols:
* Remove deprecated `AudioMixer.create()` method. Use
`DefaultAudioMixer.Factory().create()` instead.
* Remove the following deprecated `Transformer.Builder` methods:
* `setTransformationRequest()`, use `setAudioMimeType()`,
`setVideoMimeType()`, and `setHdrMode()` instead.
* `setAudioProcessors()`, set the audio processor in an
`EditedMediaItem.Builder.setEffects()`, and pass it to
`Transformer.start()` instead.
* `setVideoEffects()`, set video effect in an
`EditedMediaItem.Builder.setEffects()`, and pass it to
`Transformer.start()` instead.
* `setRemoveAudio()`, use `EditedMediaItem.Builder.setRemoveAudio()`
to remove the audio from the `EditedMediaItem` passed to
`Transformer.start()` instead.
* `setRemoveVideo()`, use `EditedMediaItem.Builder.setRemoveVideo()`
to remove the video from the `EditedMediaItem` passed to
`Transformer.start()` instead.
* `setFlattenForSlowMotion()`, use
`EditedMediaItem.Builder.setFlattenForSlowMotion()` to flatten the
`EditedMediaItem` passed to `Transformer.start()` instead.
* `setListener()`, use `addListener()`, `removeListener()` or
`removeAllListeners()` instead.
* Remove the following deprecated `Transformer.Listener` methods:
* `onTransformationCompleted(MediaItem)`, use
`onCompleted(Composition, ExportResult)` instead.
* `onTransformationCompleted(MediaItem, TransformationResult)`, use
`onCompleted(Composition, ExportResult)` instead.
* `onTransformationError(MediaItem, Exception)`, use
`onError(Composition, ExportResult, ExportException)` instead.
* `onTransformationError(MediaItem, TransformationException)`, use
`onError(Composition, ExportResult, ExportException)` instead.
* `onTransformationError(MediaItem, TransformationResult,
TransformationException)`, use `onError(Composition, ExportResult,
ExportException)` instead.
* `onFallbackApplied(MediaItem, TransformationRequest,
TransformationRequest)`, use `onFallbackApplied(Composition,
TransformationRequest, TransformationRequest)` instead.
* Remove deprecated `TransformationResult` class. Use `ExportResult`
instead.
* Remove deprecated `TransformationException` class. Use `ExportException`
instead.
* Remove deprecated `Transformer.PROGRESS_STATE_NO_TRANSFORMATION`. Use
`Transformer.PROGRESS_STATE_NOT_STARTED` instead.
* Remove deprecated `Transformer.setListener()`. Use
`Transformer.addListener()`, `Transformer.removeListener()` or
`Transformer.removeAllListeners()` instead.
* Remove deprecated `Transformer.startTransformation()`. Use
`Transformer.start(MediaItem, String)` instead.
* Remove deprecated `SingleFrameGlShaderProgram`. Use
`BaseGlShaderProgram` instead.
* Remove `Transformer.flattenForSlowMotion`. Use
`EditedMediaItem.flattenForSlowMotion` instead.
* Removed `ExoPlayer.VideoComponent`, `ExoPlayer.AudioComponent`,
`ExoPlayer.TextComponent` and `ExoPlayer.DeviceComponent`.
* Removed `androidx.media3.exoplayer.audio.SonicAudioProcessor`.
* Removed the following deprecated `DownloadHelper` methods:
* Constructor `DownloadHelper(MediaItem, @Nullable MediaSource,
TrackSelectionParameters, RendererCapabilities[])`, use
`DownloadHelper(MediaItem, @Nullable MediaSource,
TrackSelectionParameters, RendererCapabilitiesList)` instead.
* `getRendererCapabilities(RenderersFactory)`, equivalent
functionality can be achieved by creating a
`DefaultRendererCapabilitiesList` with a `RenderersFactory`, and
calling `DefaultRendererCapabilitiesList.getRendererCapabilities()`.
* Removed
`PlayerNotificationManager.setMediaSessionToken(MediaSessionCompat)`
method. Use
`PlayerNotificationManager.setMediaSessionToken(MediaSession.Token)` and
pass in `(MediaSession.Token) compatToken.getToken()` instead.
### 1.6.0-rc02 (2025-03-18)
Use the 1.6.0 [stable version](#160-2025-03-26).
### 1.6.0-rc01 (2025-03-12)
Use the 1.6.0 [stable version](#160-2025-03-26).
### 1.6.0-beta01 (2025-02-26)
Use the 1.6.0 [stable version](#160-2025-03-26).
### 1.6.0-alpha03 (2025-02-06)
Use the 1.6.0 [stable version](#160-2025-03-26).
### 1.6.0-alpha02 (2025-01-30)
Use the 1.6.0 [stable version](#160-2025-03-26).
### 1.6.0-alpha01 (2024-12-20)
Use the 1.6.0 [stable version](#160-2025-03-26).
## 1.5
### 1.5.1 (2024-12-19)
This release includes the following changes since the
[1.5.0 release](#150-2024-11-27):
* ExoPlayer:
* Disable use of asynchronous decryption in MediaCodec to avoid reported
codec timeout issues with this platform API
([#1641](https://github.com/androidx/media/issues/1641)).
* Extractors:
* MP3: Don't stop playback early when a `VBRI` frame's table of contents
doesn't cover all the MP3 data in a file
([#1904](https://github.com/androidx/media/issues/1904)).
* Video:
* Rollback of using `MediaCodecAdapter` supplied pixel aspect ratio values
when provided while processing `onOutputFormatChanged`
([#1371](https://github.com/androidx/media/pull/1371)).
* Text:
* Fix bug in `ReplacingCuesResolver.discardCuesBeforeTimeUs` where the cue
active at `timeUs` (started before but not yet ended) was incorrectly
discarded ([#1939](https://github.com/androidx/media/issues/1939)).
* Metadata:
* Extract disc/track numbering and genre from Vorbis comments into
`MediaMetadata`
([#1958](https://github.com/androidx/media/issues/1958)).
### 1.5.0 (2024-11-27)
This release includes the following changes since the
@ -745,19 +286,19 @@ This release includes the following changes since the
[#184](https://github.com/androidx/media/issues/184)).
* Fix bug where the "None" choice in the text selection is not working if
there are app-defined text track selection preferences.
* DASH extension:
* DASH Extension:
* Add support for periods starting in the middle of a segment
([#1440](https://github.com/androidx/media/issues/1440)).
* Smooth Streaming extension:
* Smooth Streaming Extension:
* Fix a `Bad magic number for Bundle` error when playing SmoothStreaming
streams with text tracks
([#1779](https://github.com/androidx/media/issues/1779)).
* RTSP extension:
* RTSP Extension:
* Fix user info removal for URLs that contain encoded @ characters
([#1138](https://github.com/androidx/media/pull/1138)).
* Fix crashing when parsing of RTP packets with header extensions
([#1225](https://github.com/androidx/media/pull/1225)).
* Decoder extensions (FFmpeg, VP9, AV1, etc.):
* Decoder Extensions (FFmpeg, VP9, AV1, etc.):
* Add the IAMF decoder module, which provides support for playback of MP4
files containing IAMF tracks using the libiamf native library to
synthesize audio.
@ -766,7 +307,7 @@ This release includes the following changes since the
binaural playback support is currently not available.
* Add 16 KB page support for decoder extensions on Android 15
([#1685](https://github.com/androidx/media/issues/1685)).
* Cast extension:
* Cast Extension:
* Stop cleaning the timeline after the CastSession disconnects, which
enables the sender app to resume playback locally after a disconnection.
* Populate CastPlayer's `DeviceInfo` when a `Context` is provided. This
@ -847,7 +388,7 @@ This release includes the following changes since the
`MediaButtonReceiver` when deciding whether to ignore it to avoid a
`ForegroundServiceDidNotStartInTimeException`
([#1581](https://github.com/androidx/media/issues/1581)).
* RTSP extension:
* RTSP Extension:
* Skip invalid Media Descriptions in SDP parsing
([#1087](https://github.com/androidx/media/issues/1472)).
@ -1192,12 +733,12 @@ This release includes the following changes since the
instances, which can eventually result in an app crashing with
`IllegalStateException: Too many receivers, total of 1000, registered
for pid` ([#1224](https://github.com/androidx/media/issues/1224)).
* Cronet extension:
* Cronet Extension:
* Fix `SocketTimeoutException` in `CronetDataSource`. In some versions of
Cronet, the request provided by the callback is not always the same.
This leads to callback not completing and request timing out
(https://issuetracker.google.com/328442628).
* HLS extension:
* HLS Extension:
* Fix bug where pending EMSG samples waiting for a discontinuity were
delegated in `HlsSampleStreamWrapper` with an incorrect offset causing
an `IndexOutOfBoundsException` or an `IllegalArgumentException`
@ -1211,13 +752,13 @@ This release includes the following changes since the
* Fix bug where enabling CMCD for HLS live streams causes
`ArrayIndexOutOfBoundsException`
([#1395](https://github.com/androidx/media/issues/1395)).
* DASH extension:
* DASH Extension:
* Fix bug where re-preparing a multi-period live stream can throw an
`IndexOutOfBoundsException`
([#1329](https://github.com/androidx/media/issues/1329)).
* Add support for `dashif:Laurl` license urls
([#1345](https://github.com/androidx/media/issues/1345)).
* Cast extension:
* Cast Extension:
* Fix bug that converted the album title of the `MediaQueueItem` to the
artist in the Media3 media item
([#1255](https://github.com/androidx/media/pull/1255)).
@ -1365,13 +906,13 @@ This release includes the following changes since the
* Fallback to include audio track language name if `Locale` cannot
identify a display name
([#988](https://github.com/androidx/media/issues/988)).
* DASH extension:
* DASH Extension:
* Populate all `Label` elements from the manifest into `Format.labels`
([#1054](https://github.com/androidx/media/pull/1054)).
* RTSP extension:
* RTSP Extension:
* Skip empty session information values (i-tags) in SDP parsing
([#1087](https://github.com/androidx/media/issues/1087)).
* Decoder extensions (FFmpeg, VP9, AV1, MIDI, etc.):
* Decoder Extensions (FFmpeg, VP9, AV1, MIDI, etc.):
* Disable the MIDI extension as a local dependency by default because it
requires an additional Maven repository to be configured. Users who need
this module from a local dependency
@ -1524,12 +1065,12 @@ This release includes the following changes since the
not transmitted between media controllers and sessions.
* Add constructor to `MediaLibrarySession.Builder` that only takes a
`Context` instead of a `MediaLibraryService`.
* HLS extension:
* HLS Extension:
* Reduce `HlsMediaPeriod` to package-private visibility. This type
shouldn't be directly depended on from outside the HLS package.
* Resolve seeks to beginning of a segment more efficiently
([#1031](https://github.com/androidx/media/pull/1031)).
* Decoder extensions (FFmpeg, VP9, AV1, MIDI, etc.):
* Decoder Extensions (FFmpeg, VP9, AV1, MIDI, etc.):
* MIDI decoder: Ignore SysEx event messages
([#710](https://github.com/androidx/media/pull/710)).
* Test Utilities:
@ -1627,16 +1168,16 @@ This release includes the following changes since the
* Fix issue where the numbers in the fast forward button of the
`PlayerControlView` were misaligned
([#547](https://github.com/androidx/media/issues/547)).
* DASH extension:
* DASH Extension:
* Parse "f800" as channel count of 5 for Dolby in DASH manifest
([#688](https://github.com/androidx/media/issues/688)).
* Decoder extensions (FFmpeg, VP9, AV1, MIDI, etc.):
* Decoder Extensions (FFmpeg, VP9, AV1, MIDI, etc.):
* MIDI: Fix issue where seeking forward skips the Program Change events
([#704](https://github.com/androidx/media/issues/704)).
* Migrate to FFmpeg 6.0 and update supported NDK to `r26b`
([#707](https://github.com/androidx/media/pull/707),
[#867](https://github.com/androidx/media/pull/867)).
* Cast extension:
* Cast Extension:
* Sanitize creation of a `Timeline` to not crash the app when loading
media fails on the cast device
([#708](https://github.com/androidx/media/issues/708)).
@ -1874,11 +1415,11 @@ This release includes the following changes since the
add `dataSync` as `foregroundServiceType` in the manifest and add the
`FOREGROUND_SERVICE_DATA_SYNC` permission
([#11239](https://github.com/google/ExoPlayer/issues/11239)).
* HLS extension:
* HLS Extension:
* Refresh the HLS live playlist with an interval calculated from the last
load start time rather than the last load completed time
([#663](https://github.com/androidx/media/issues/663)).
* DASH extension:
* DASH Extension:
* Allow multiple of the same DASH identifier in segment template url.
* Add experimental support for parsing subtitles during extraction. This
has better support for merging overlapping subtitles, including
@ -1886,7 +1427,7 @@ This release includes the following changes since the
can enable this using
`DashMediaSource.Factory.experimentalParseSubtitlesDuringExtraction()`
([#288](https://github.com/androidx/media/issues/288)).
* RTSP extension:
* RTSP Extension:
* Fix a race condition that could lead to `IndexOutOfBoundsException` when
falling back to TCP, or playback hanging in some situations.
* Check state in RTSP setup when returning loading state of
@ -1897,7 +1438,7 @@ This release includes the following changes since the
* Use RTSP Setup Response timeout value in time interval of sending
keep-alive RTSP Options requests
([#662](https://github.com/androidx/media/issues/662)).
* Decoder extensions (FFmpeg, VP9, AV1, MIDI, etc.):
* Decoder Extensions (FFmpeg, VP9, AV1, MIDI, etc.):
* Release the MIDI decoder module, which provides support for playback of
standard MIDI files using the Jsyn library to synthesize audio.
* Add `DecoderOutputBuffer.shouldBeSkipped` to directly mark output
@ -2174,20 +1715,20 @@ This release contains the following changes since the
* Add Util methods `shouldShowPlayButton` and
`handlePlayPauseButtonAction` to write custom UI elements with a
play/pause button.
* RTSP extension:
* RTSP Extension:
* For MPEG4-LATM, use default profile-level-id value if absent in Describe
Response SDP message
([#302](https://github.com/androidx/media/issues/302)).
* Use base Uri for relative path resolution from the RTSP session if
present in DESCRIBE response header
([#11160](https://github.com/google/ExoPlayer/issues/11160)).
* DASH extension:
* DASH Extension:
* Remove the media time offset from `MediaLoadData.startTimeMs` and
`MediaLoadData.endTimeMs` for multi period DASH streams.
* Fix a bug where re-preparing a multi-period live Dash media source
produced a `IndexOutOfBoundsException`
([#10838](https://github.com/google/ExoPlayer/issues/10838)).
* HLS extension:
* HLS Extension:
* Add
`HlsMediaSource.Factory.setTimestampAdjusterInitializationTimeoutMs(long)`
to set a timeout for the loading thread to wait for the

108
api.txt
View File

@ -464,7 +464,6 @@ package androidx.media3.common {
field @Nullable public final CharSequence description;
field @Nullable public final Integer discNumber;
field @Nullable public final CharSequence displayTitle;
field @Nullable public final Long durationMs;
field @Nullable public final android.os.Bundle extras;
field @Deprecated @Nullable @androidx.media3.common.MediaMetadata.FolderType public final Integer folderType;
field @Nullable public final CharSequence genre;
@ -503,7 +502,6 @@ package androidx.media3.common {
method public androidx.media3.common.MediaMetadata.Builder setDescription(@Nullable CharSequence);
method public androidx.media3.common.MediaMetadata.Builder setDiscNumber(@Nullable Integer);
method public androidx.media3.common.MediaMetadata.Builder setDisplayTitle(@Nullable CharSequence);
method public androidx.media3.common.MediaMetadata.Builder setDurationMs(@Nullable Long);
method public androidx.media3.common.MediaMetadata.Builder setExtras(@Nullable android.os.Bundle);
method @Deprecated public androidx.media3.common.MediaMetadata.Builder setFolderType(@Nullable @androidx.media3.common.MediaMetadata.FolderType Integer);
method public androidx.media3.common.MediaMetadata.Builder setGenre(@Nullable CharSequence);
@ -638,7 +636,6 @@ package androidx.media3.common {
field public static final int ERROR_CODE_DECODING_FAILED = 4003; // 0xfa3
field public static final int ERROR_CODE_DECODING_FORMAT_EXCEEDS_CAPABILITIES = 4004; // 0xfa4
field public static final int ERROR_CODE_DECODING_FORMAT_UNSUPPORTED = 4005; // 0xfa5
field public static final int ERROR_CODE_DECODING_RESOURCES_RECLAIMED = 4006; // 0xfa6
field public static final int ERROR_CODE_DISCONNECTED = -100; // 0xffffff9c
field public static final int ERROR_CODE_DRM_CONTENT_ERROR = 6003; // 0x1773
field public static final int ERROR_CODE_DRM_DEVICE_REVOKED = 6007; // 0x1777
@ -679,7 +676,7 @@ package androidx.media3.common {
field public final long timestampMs;
}
@IntDef(open=true, value={androidx.media3.common.PlaybackException.ERROR_CODE_INVALID_STATE, androidx.media3.common.PlaybackException.ERROR_CODE_BAD_VALUE, androidx.media3.common.PlaybackException.ERROR_CODE_PERMISSION_DENIED, androidx.media3.common.PlaybackException.ERROR_CODE_NOT_SUPPORTED, androidx.media3.common.PlaybackException.ERROR_CODE_DISCONNECTED, androidx.media3.common.PlaybackException.ERROR_CODE_AUTHENTICATION_EXPIRED, androidx.media3.common.PlaybackException.ERROR_CODE_PREMIUM_ACCOUNT_REQUIRED, androidx.media3.common.PlaybackException.ERROR_CODE_CONCURRENT_STREAM_LIMIT, androidx.media3.common.PlaybackException.ERROR_CODE_PARENTAL_CONTROL_RESTRICTED, androidx.media3.common.PlaybackException.ERROR_CODE_NOT_AVAILABLE_IN_REGION, androidx.media3.common.PlaybackException.ERROR_CODE_SKIP_LIMIT_REACHED, androidx.media3.common.PlaybackException.ERROR_CODE_SETUP_REQUIRED, androidx.media3.common.PlaybackException.ERROR_CODE_END_OF_PLAYLIST, androidx.media3.common.PlaybackException.ERROR_CODE_CONTENT_ALREADY_PLAYING, androidx.media3.common.PlaybackException.ERROR_CODE_UNSPECIFIED, androidx.media3.common.PlaybackException.ERROR_CODE_REMOTE_ERROR, androidx.media3.common.PlaybackException.ERROR_CODE_BEHIND_LIVE_WINDOW, androidx.media3.common.PlaybackException.ERROR_CODE_TIMEOUT, androidx.media3.common.PlaybackException.ERROR_CODE_FAILED_RUNTIME_CHECK, androidx.media3.common.PlaybackException.ERROR_CODE_IO_UNSPECIFIED, androidx.media3.common.PlaybackException.ERROR_CODE_IO_NETWORK_CONNECTION_FAILED, androidx.media3.common.PlaybackException.ERROR_CODE_IO_NETWORK_CONNECTION_TIMEOUT, androidx.media3.common.PlaybackException.ERROR_CODE_IO_INVALID_HTTP_CONTENT_TYPE, androidx.media3.common.PlaybackException.ERROR_CODE_IO_BAD_HTTP_STATUS, androidx.media3.common.PlaybackException.ERROR_CODE_IO_FILE_NOT_FOUND, androidx.media3.common.PlaybackException.ERROR_CODE_IO_NO_PERMISSION, androidx.media3.common.PlaybackException.ERROR_CODE_IO_CLEARTEXT_NOT_PERMITTED, androidx.media3.common.PlaybackException.ERROR_CODE_IO_READ_POSITION_OUT_OF_RANGE, androidx.media3.common.PlaybackException.ERROR_CODE_PARSING_CONTAINER_MALFORMED, androidx.media3.common.PlaybackException.ERROR_CODE_PARSING_MANIFEST_MALFORMED, androidx.media3.common.PlaybackException.ERROR_CODE_PARSING_CONTAINER_UNSUPPORTED, androidx.media3.common.PlaybackException.ERROR_CODE_PARSING_MANIFEST_UNSUPPORTED, androidx.media3.common.PlaybackException.ERROR_CODE_DECODER_INIT_FAILED, androidx.media3.common.PlaybackException.ERROR_CODE_DECODER_QUERY_FAILED, androidx.media3.common.PlaybackException.ERROR_CODE_DECODING_FAILED, androidx.media3.common.PlaybackException.ERROR_CODE_DECODING_FORMAT_EXCEEDS_CAPABILITIES, androidx.media3.common.PlaybackException.ERROR_CODE_DECODING_FORMAT_UNSUPPORTED, androidx.media3.common.PlaybackException.ERROR_CODE_DECODING_RESOURCES_RECLAIMED, androidx.media3.common.PlaybackException.ERROR_CODE_AUDIO_TRACK_INIT_FAILED, androidx.media3.common.PlaybackException.ERROR_CODE_AUDIO_TRACK_WRITE_FAILED, androidx.media3.common.PlaybackException.ERROR_CODE_AUDIO_TRACK_OFFLOAD_INIT_FAILED, androidx.media3.common.PlaybackException.ERROR_CODE_AUDIO_TRACK_OFFLOAD_WRITE_FAILED, androidx.media3.common.PlaybackException.ERROR_CODE_DRM_UNSPECIFIED, androidx.media3.common.PlaybackException.ERROR_CODE_DRM_SCHEME_UNSUPPORTED, androidx.media3.common.PlaybackException.ERROR_CODE_DRM_PROVISIONING_FAILED, androidx.media3.common.PlaybackException.ERROR_CODE_DRM_CONTENT_ERROR, androidx.media3.common.PlaybackException.ERROR_CODE_DRM_LICENSE_ACQUISITION_FAILED, androidx.media3.common.PlaybackException.ERROR_CODE_DRM_DISALLOWED_OPERATION, androidx.media3.common.PlaybackException.ERROR_CODE_DRM_SYSTEM_ERROR, androidx.media3.common.PlaybackException.ERROR_CODE_DRM_DEVICE_REVOKED, androidx.media3.common.PlaybackException.ERROR_CODE_DRM_LICENSE_EXPIRED}) @java.lang.annotation.Documented @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.SOURCE) @java.lang.annotation.Target({java.lang.annotation.ElementType.FIELD, java.lang.annotation.ElementType.METHOD, java.lang.annotation.ElementType.PARAMETER, java.lang.annotation.ElementType.LOCAL_VARIABLE, java.lang.annotation.ElementType.TYPE_USE}) public static @interface PlaybackException.ErrorCode {
@IntDef(open=true, value={androidx.media3.common.PlaybackException.ERROR_CODE_INVALID_STATE, androidx.media3.common.PlaybackException.ERROR_CODE_BAD_VALUE, androidx.media3.common.PlaybackException.ERROR_CODE_PERMISSION_DENIED, androidx.media3.common.PlaybackException.ERROR_CODE_NOT_SUPPORTED, androidx.media3.common.PlaybackException.ERROR_CODE_DISCONNECTED, androidx.media3.common.PlaybackException.ERROR_CODE_AUTHENTICATION_EXPIRED, androidx.media3.common.PlaybackException.ERROR_CODE_PREMIUM_ACCOUNT_REQUIRED, androidx.media3.common.PlaybackException.ERROR_CODE_CONCURRENT_STREAM_LIMIT, androidx.media3.common.PlaybackException.ERROR_CODE_PARENTAL_CONTROL_RESTRICTED, androidx.media3.common.PlaybackException.ERROR_CODE_NOT_AVAILABLE_IN_REGION, androidx.media3.common.PlaybackException.ERROR_CODE_SKIP_LIMIT_REACHED, androidx.media3.common.PlaybackException.ERROR_CODE_SETUP_REQUIRED, androidx.media3.common.PlaybackException.ERROR_CODE_END_OF_PLAYLIST, androidx.media3.common.PlaybackException.ERROR_CODE_CONTENT_ALREADY_PLAYING, androidx.media3.common.PlaybackException.ERROR_CODE_UNSPECIFIED, androidx.media3.common.PlaybackException.ERROR_CODE_REMOTE_ERROR, androidx.media3.common.PlaybackException.ERROR_CODE_BEHIND_LIVE_WINDOW, androidx.media3.common.PlaybackException.ERROR_CODE_TIMEOUT, androidx.media3.common.PlaybackException.ERROR_CODE_FAILED_RUNTIME_CHECK, androidx.media3.common.PlaybackException.ERROR_CODE_IO_UNSPECIFIED, androidx.media3.common.PlaybackException.ERROR_CODE_IO_NETWORK_CONNECTION_FAILED, androidx.media3.common.PlaybackException.ERROR_CODE_IO_NETWORK_CONNECTION_TIMEOUT, androidx.media3.common.PlaybackException.ERROR_CODE_IO_INVALID_HTTP_CONTENT_TYPE, androidx.media3.common.PlaybackException.ERROR_CODE_IO_BAD_HTTP_STATUS, androidx.media3.common.PlaybackException.ERROR_CODE_IO_FILE_NOT_FOUND, androidx.media3.common.PlaybackException.ERROR_CODE_IO_NO_PERMISSION, androidx.media3.common.PlaybackException.ERROR_CODE_IO_CLEARTEXT_NOT_PERMITTED, androidx.media3.common.PlaybackException.ERROR_CODE_IO_READ_POSITION_OUT_OF_RANGE, androidx.media3.common.PlaybackException.ERROR_CODE_PARSING_CONTAINER_MALFORMED, androidx.media3.common.PlaybackException.ERROR_CODE_PARSING_MANIFEST_MALFORMED, androidx.media3.common.PlaybackException.ERROR_CODE_PARSING_CONTAINER_UNSUPPORTED, androidx.media3.common.PlaybackException.ERROR_CODE_PARSING_MANIFEST_UNSUPPORTED, androidx.media3.common.PlaybackException.ERROR_CODE_DECODER_INIT_FAILED, androidx.media3.common.PlaybackException.ERROR_CODE_DECODER_QUERY_FAILED, androidx.media3.common.PlaybackException.ERROR_CODE_DECODING_FAILED, androidx.media3.common.PlaybackException.ERROR_CODE_DECODING_FORMAT_EXCEEDS_CAPABILITIES, androidx.media3.common.PlaybackException.ERROR_CODE_DECODING_FORMAT_UNSUPPORTED, androidx.media3.common.PlaybackException.ERROR_CODE_AUDIO_TRACK_INIT_FAILED, androidx.media3.common.PlaybackException.ERROR_CODE_AUDIO_TRACK_WRITE_FAILED, androidx.media3.common.PlaybackException.ERROR_CODE_AUDIO_TRACK_OFFLOAD_INIT_FAILED, androidx.media3.common.PlaybackException.ERROR_CODE_AUDIO_TRACK_OFFLOAD_WRITE_FAILED, androidx.media3.common.PlaybackException.ERROR_CODE_DRM_UNSPECIFIED, androidx.media3.common.PlaybackException.ERROR_CODE_DRM_SCHEME_UNSUPPORTED, androidx.media3.common.PlaybackException.ERROR_CODE_DRM_PROVISIONING_FAILED, androidx.media3.common.PlaybackException.ERROR_CODE_DRM_CONTENT_ERROR, androidx.media3.common.PlaybackException.ERROR_CODE_DRM_LICENSE_ACQUISITION_FAILED, androidx.media3.common.PlaybackException.ERROR_CODE_DRM_DISALLOWED_OPERATION, androidx.media3.common.PlaybackException.ERROR_CODE_DRM_SYSTEM_ERROR, androidx.media3.common.PlaybackException.ERROR_CODE_DRM_DEVICE_REVOKED, androidx.media3.common.PlaybackException.ERROR_CODE_DRM_LICENSE_EXPIRED}) @java.lang.annotation.Documented @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.SOURCE) @java.lang.annotation.Target({java.lang.annotation.ElementType.FIELD, java.lang.annotation.ElementType.METHOD, java.lang.annotation.ElementType.PARAMETER, java.lang.annotation.ElementType.LOCAL_VARIABLE, java.lang.annotation.ElementType.TYPE_USE}) public static @interface PlaybackException.ErrorCode {
}
public final class PlaybackParameters {
@ -1096,13 +1093,12 @@ package androidx.media3.common {
public class TrackSelectionParameters {
method public androidx.media3.common.TrackSelectionParameters.Builder buildUpon();
method public static androidx.media3.common.TrackSelectionParameters fromBundle(android.os.Bundle);
method @Deprecated public static androidx.media3.common.TrackSelectionParameters getDefaults(android.content.Context);
method public static androidx.media3.common.TrackSelectionParameters getDefaults(android.content.Context);
method @CallSuper public android.os.Bundle toBundle();
field public final com.google.common.collect.ImmutableSet<java.lang.Integer> disabledTrackTypes;
field public final boolean forceHighestSupportedBitrate;
field public final boolean forceLowestBitrate;
field @androidx.media3.common.C.SelectionFlags public final int ignoredTextSelectionFlags;
field public final boolean isViewportSizeLimitedByPhysicalDisplaySize;
field public final int maxAudioBitrate;
field public final int maxAudioChannelCount;
field public final int maxVideoBitrate;
@ -1122,15 +1118,13 @@ package androidx.media3.common {
field public final com.google.common.collect.ImmutableList<java.lang.String> preferredVideoMimeTypes;
field @androidx.media3.common.C.RoleFlags public final int preferredVideoRoleFlags;
field public final boolean selectUndeterminedTextLanguage;
field public final boolean usePreferredTextLanguagesAndRoleFlagsFromCaptioningManager;
field public final int viewportHeight;
field public final boolean viewportOrientationMayChange;
field public final int viewportWidth;
}
public static class TrackSelectionParameters.Builder {
ctor public TrackSelectionParameters.Builder();
ctor @Deprecated @com.google.errorprone.annotations.InlineMe(replacement="this()") public TrackSelectionParameters.Builder(android.content.Context);
ctor public TrackSelectionParameters.Builder(android.content.Context);
method public androidx.media3.common.TrackSelectionParameters.Builder addOverride(androidx.media3.common.TrackSelectionOverride);
method public androidx.media3.common.TrackSelectionParameters build();
method public androidx.media3.common.TrackSelectionParameters.Builder clearOverride(androidx.media3.common.TrackGroup);
@ -1157,8 +1151,7 @@ package androidx.media3.common {
method public androidx.media3.common.TrackSelectionParameters.Builder setPreferredAudioMimeTypes(java.lang.String...);
method public androidx.media3.common.TrackSelectionParameters.Builder setPreferredAudioRoleFlags(@androidx.media3.common.C.RoleFlags int);
method public androidx.media3.common.TrackSelectionParameters.Builder setPreferredTextLanguage(@Nullable String);
method public androidx.media3.common.TrackSelectionParameters.Builder setPreferredTextLanguageAndRoleFlagsToCaptioningManagerSettings();
method @Deprecated public androidx.media3.common.TrackSelectionParameters.Builder setPreferredTextLanguageAndRoleFlagsToCaptioningManagerSettings(android.content.Context);
method public androidx.media3.common.TrackSelectionParameters.Builder setPreferredTextLanguageAndRoleFlagsToCaptioningManagerSettings(android.content.Context);
method public androidx.media3.common.TrackSelectionParameters.Builder setPreferredTextLanguages(java.lang.String...);
method public androidx.media3.common.TrackSelectionParameters.Builder setPreferredTextRoleFlags(@androidx.media3.common.C.RoleFlags int);
method public androidx.media3.common.TrackSelectionParameters.Builder setPreferredVideoMimeType(@Nullable String);
@ -1167,8 +1160,7 @@ package androidx.media3.common {
method public androidx.media3.common.TrackSelectionParameters.Builder setSelectUndeterminedTextLanguage(boolean);
method public androidx.media3.common.TrackSelectionParameters.Builder setTrackTypeDisabled(@androidx.media3.common.C.TrackType int, boolean);
method public androidx.media3.common.TrackSelectionParameters.Builder setViewportSize(int, int, boolean);
method @Deprecated public androidx.media3.common.TrackSelectionParameters.Builder setViewportSizeToPhysicalDisplaySize(android.content.Context, boolean);
method public androidx.media3.common.TrackSelectionParameters.Builder setViewportSizeToPhysicalDisplaySize(boolean);
method public androidx.media3.common.TrackSelectionParameters.Builder setViewportSizeToPhysicalDisplaySize(android.content.Context, boolean);
}
public final class Tracks {
@ -1488,82 +1480,7 @@ package androidx.media3.exoplayer.util {
package androidx.media3.session {
public final class CommandButton {
field public static final int ICON_ALBUM = 57369; // 0xe019
field public static final int ICON_ARTIST = 57370; // 0xe01a
field public static final int ICON_BLOCK = 57675; // 0xe14b
field public static final int ICON_BOOKMARK_FILLED = 1042534; // 0xfe866
field public static final int ICON_BOOKMARK_UNFILLED = 59494; // 0xe866
field public static final int ICON_CHECK_CIRCLE_FILLED = 1042540; // 0xfe86c
field public static final int ICON_CHECK_CIRCLE_UNFILLED = 59500; // 0xe86c
field public static final int ICON_CLOSED_CAPTIONS = 57372; // 0xe01c
field public static final int ICON_CLOSED_CAPTIONS_OFF = 61916; // 0xf1dc
field public static final int ICON_FAST_FORWARD = 57375; // 0xe01f
field public static final int ICON_FEED = 57573; // 0xe0e5
field public static final int ICON_FLAG_FILLED = 1040723; // 0xfe153
field public static final int ICON_FLAG_UNFILLED = 57683; // 0xe153
field public static final int ICON_HEART_FILLED = 1042557; // 0xfe87d
field public static final int ICON_HEART_UNFILLED = 59517; // 0xe87d
field public static final int ICON_MINUS = 57691; // 0xe15b
field public static final int ICON_MINUS_CIRCLE_FILLED = 1040712; // 0xfe148
field public static final int ICON_MINUS_CIRCLE_UNFILLED = 1040713; // 0xfe149
field public static final int ICON_NEXT = 57412; // 0xe044
field public static final int ICON_PAUSE = 57396; // 0xe034
field public static final int ICON_PLAY = 57399; // 0xe037
field public static final int ICON_PLAYBACK_SPEED = 57448; // 0xe068
field public static final int ICON_PLAYBACK_SPEED_0_5 = 62690; // 0xf4e2
field public static final int ICON_PLAYBACK_SPEED_0_8 = 1045730; // 0xff4e2
field public static final int ICON_PLAYBACK_SPEED_1_0 = 61389; // 0xefcd
field public static final int ICON_PLAYBACK_SPEED_1_2 = 62689; // 0xf4e1
field public static final int ICON_PLAYBACK_SPEED_1_5 = 62688; // 0xf4e0
field public static final int ICON_PLAYBACK_SPEED_1_8 = 1045728; // 0xff4e0
field public static final int ICON_PLAYBACK_SPEED_2_0 = 62699; // 0xf4eb
field public static final int ICON_PLAYLIST_ADD = 57403; // 0xe03b
field public static final int ICON_PLAYLIST_REMOVE = 60288; // 0xeb80
field public static final int ICON_PLUS = 57669; // 0xe145
field public static final int ICON_PLUS_CIRCLE_FILLED = 1040711; // 0xfe147
field public static final int ICON_PLUS_CIRCLE_UNFILLED = 57671; // 0xe147
field public static final int ICON_PREVIOUS = 57413; // 0xe045
field public static final int ICON_QUALITY = 58409; // 0xe429
field public static final int ICON_QUEUE_ADD = 57436; // 0xe05c
field public static final int ICON_QUEUE_NEXT = 57446; // 0xe066
field public static final int ICON_QUEUE_REMOVE = 57447; // 0xe067
field public static final int ICON_RADIO = 58654; // 0xe51e
field public static final int ICON_REPEAT_ALL = 57408; // 0xe040
field public static final int ICON_REPEAT_OFF = 1040448; // 0xfe040
field public static final int ICON_REPEAT_ONE = 57409; // 0xe041
field public static final int ICON_REWIND = 57376; // 0xe020
field public static final int ICON_SETTINGS = 59576; // 0xe8b8
field public static final int ICON_SHARE = 59405; // 0xe80d
field public static final int ICON_SHUFFLE_OFF = 1040452; // 0xfe044
field public static final int ICON_SHUFFLE_ON = 57411; // 0xe043
field public static final int ICON_SHUFFLE_STAR = 1040451; // 0xfe043
field public static final int ICON_SIGNAL = 61512; // 0xf048
field public static final int ICON_SKIP_BACK = 57410; // 0xe042
field public static final int ICON_SKIP_BACK_10 = 57433; // 0xe059
field public static final int ICON_SKIP_BACK_15 = 1040473; // 0xfe059
field public static final int ICON_SKIP_BACK_30 = 57434; // 0xe05a
field public static final int ICON_SKIP_BACK_5 = 57435; // 0xe05b
field public static final int ICON_SKIP_FORWARD = 63220; // 0xf6f4
field public static final int ICON_SKIP_FORWARD_10 = 57430; // 0xe056
field public static final int ICON_SKIP_FORWARD_15 = 1040470; // 0xfe056
field public static final int ICON_SKIP_FORWARD_30 = 57431; // 0xe057
field public static final int ICON_SKIP_FORWARD_5 = 57432; // 0xe058
field public static final int ICON_STAR_FILLED = 1042488; // 0xfe838
field public static final int ICON_STAR_UNFILLED = 59448; // 0xe838
field public static final int ICON_STOP = 57415; // 0xe047
field public static final int ICON_SUBTITLES = 57416; // 0xe048
field public static final int ICON_SUBTITLES_OFF = 61298; // 0xef72
field public static final int ICON_SYNC = 58919; // 0xe627
field public static final int ICON_THUMB_DOWN_FILLED = 1042651; // 0xfe8db
field public static final int ICON_THUMB_DOWN_UNFILLED = 59611; // 0xe8db
field public static final int ICON_THUMB_UP_FILLED = 1042652; // 0xfe8dc
field public static final int ICON_THUMB_UP_UNFILLED = 59612; // 0xe8dc
field public static final int ICON_UNDEFINED = 0; // 0x0
field public static final int ICON_VOLUME_DOWN = 57421; // 0xe04d
field public static final int ICON_VOLUME_OFF = 57423; // 0xe04f
field public static final int ICON_VOLUME_UP = 57424; // 0xe050
field public final CharSequence displayName;
field @androidx.media3.session.CommandButton.Icon public final int icon;
field @DrawableRes public final int iconResId;
field public final boolean isEnabled;
field @androidx.media3.common.Player.Command public final int playerCommand;
@ -1571,21 +1488,16 @@ package androidx.media3.session {
}
public static final class CommandButton.Builder {
ctor @Deprecated public CommandButton.Builder();
ctor public CommandButton.Builder(@androidx.media3.session.CommandButton.Icon int);
ctor public CommandButton.Builder();
method public androidx.media3.session.CommandButton build();
method public androidx.media3.session.CommandButton.Builder setCustomIconResId(@DrawableRes int);
method public androidx.media3.session.CommandButton.Builder setDisplayName(CharSequence);
method public androidx.media3.session.CommandButton.Builder setEnabled(boolean);
method public androidx.media3.session.CommandButton.Builder setExtras(android.os.Bundle);
method @Deprecated public androidx.media3.session.CommandButton.Builder setIconResId(@DrawableRes int);
method public androidx.media3.session.CommandButton.Builder setIconResId(@DrawableRes int);
method public androidx.media3.session.CommandButton.Builder setPlayerCommand(@androidx.media3.common.Player.Command int);
method public androidx.media3.session.CommandButton.Builder setSessionCommand(androidx.media3.session.SessionCommand);
}
@IntDef({androidx.media3.session.CommandButton.ICON_UNDEFINED, androidx.media3.session.CommandButton.ICON_PLAY, androidx.media3.session.CommandButton.ICON_PAUSE, androidx.media3.session.CommandButton.ICON_STOP, androidx.media3.session.CommandButton.ICON_NEXT, androidx.media3.session.CommandButton.ICON_PREVIOUS, androidx.media3.session.CommandButton.ICON_SKIP_FORWARD, androidx.media3.session.CommandButton.ICON_SKIP_FORWARD_5, androidx.media3.session.CommandButton.ICON_SKIP_FORWARD_10, androidx.media3.session.CommandButton.ICON_SKIP_FORWARD_15, androidx.media3.session.CommandButton.ICON_SKIP_FORWARD_30, androidx.media3.session.CommandButton.ICON_SKIP_BACK, androidx.media3.session.CommandButton.ICON_SKIP_BACK_5, androidx.media3.session.CommandButton.ICON_SKIP_BACK_10, androidx.media3.session.CommandButton.ICON_SKIP_BACK_15, androidx.media3.session.CommandButton.ICON_SKIP_BACK_30, androidx.media3.session.CommandButton.ICON_FAST_FORWARD, androidx.media3.session.CommandButton.ICON_REWIND, androidx.media3.session.CommandButton.ICON_REPEAT_ALL, androidx.media3.session.CommandButton.ICON_REPEAT_ONE, androidx.media3.session.CommandButton.ICON_REPEAT_OFF, androidx.media3.session.CommandButton.ICON_SHUFFLE_ON, androidx.media3.session.CommandButton.ICON_SHUFFLE_OFF, androidx.media3.session.CommandButton.ICON_SHUFFLE_STAR, androidx.media3.session.CommandButton.ICON_HEART_FILLED, androidx.media3.session.CommandButton.ICON_HEART_UNFILLED, androidx.media3.session.CommandButton.ICON_STAR_FILLED, androidx.media3.session.CommandButton.ICON_STAR_UNFILLED, androidx.media3.session.CommandButton.ICON_BOOKMARK_FILLED, androidx.media3.session.CommandButton.ICON_BOOKMARK_UNFILLED, androidx.media3.session.CommandButton.ICON_THUMB_UP_FILLED, androidx.media3.session.CommandButton.ICON_THUMB_UP_UNFILLED, androidx.media3.session.CommandButton.ICON_THUMB_DOWN_FILLED, androidx.media3.session.CommandButton.ICON_THUMB_DOWN_UNFILLED, androidx.media3.session.CommandButton.ICON_FLAG_FILLED, androidx.media3.session.CommandButton.ICON_FLAG_UNFILLED, androidx.media3.session.CommandButton.ICON_PLUS, androidx.media3.session.CommandButton.ICON_MINUS, androidx.media3.session.CommandButton.ICON_PLAYLIST_ADD, androidx.media3.session.CommandButton.ICON_PLAYLIST_REMOVE, androidx.media3.session.CommandButton.ICON_QUEUE_ADD, androidx.media3.session.CommandButton.ICON_QUEUE_NEXT, androidx.media3.session.CommandButton.ICON_QUEUE_REMOVE, androidx.media3.session.CommandButton.ICON_BLOCK, androidx.media3.session.CommandButton.ICON_PLUS_CIRCLE_FILLED, androidx.media3.session.CommandButton.ICON_PLUS_CIRCLE_UNFILLED, androidx.media3.session.CommandButton.ICON_MINUS_CIRCLE_FILLED, androidx.media3.session.CommandButton.ICON_MINUS_CIRCLE_UNFILLED, androidx.media3.session.CommandButton.ICON_CHECK_CIRCLE_FILLED, androidx.media3.session.CommandButton.ICON_CHECK_CIRCLE_UNFILLED, androidx.media3.session.CommandButton.ICON_PLAYBACK_SPEED, androidx.media3.session.CommandButton.ICON_PLAYBACK_SPEED_0_5, androidx.media3.session.CommandButton.ICON_PLAYBACK_SPEED_0_8, androidx.media3.session.CommandButton.ICON_PLAYBACK_SPEED_1_0, androidx.media3.session.CommandButton.ICON_PLAYBACK_SPEED_1_2, androidx.media3.session.CommandButton.ICON_PLAYBACK_SPEED_1_5, androidx.media3.session.CommandButton.ICON_PLAYBACK_SPEED_1_8, androidx.media3.session.CommandButton.ICON_PLAYBACK_SPEED_2_0, androidx.media3.session.CommandButton.ICON_SETTINGS, androidx.media3.session.CommandButton.ICON_QUALITY, androidx.media3.session.CommandButton.ICON_SUBTITLES, androidx.media3.session.CommandButton.ICON_SUBTITLES_OFF, androidx.media3.session.CommandButton.ICON_CLOSED_CAPTIONS, androidx.media3.session.CommandButton.ICON_CLOSED_CAPTIONS_OFF, androidx.media3.session.CommandButton.ICON_SYNC, androidx.media3.session.CommandButton.ICON_SHARE, androidx.media3.session.CommandButton.ICON_VOLUME_UP, androidx.media3.session.CommandButton.ICON_VOLUME_DOWN, androidx.media3.session.CommandButton.ICON_VOLUME_OFF, androidx.media3.session.CommandButton.ICON_ARTIST, androidx.media3.session.CommandButton.ICON_ALBUM, androidx.media3.session.CommandButton.ICON_RADIO, androidx.media3.session.CommandButton.ICON_SIGNAL, androidx.media3.session.CommandButton.ICON_FEED}) @java.lang.annotation.Documented @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.SOURCE) @java.lang.annotation.Target(java.lang.annotation.ElementType.TYPE_USE) public static @interface CommandButton.Icon {
}
public final class LibraryResult<V> {
method public static <V> androidx.media3.session.LibraryResult<V> ofError(@androidx.media3.session.LibraryResult.Code int);
method public static <V> androidx.media3.session.LibraryResult<V> ofError(@androidx.media3.session.LibraryResult.Code int, @Nullable androidx.media3.session.MediaLibraryService.LibraryParams);
@ -1684,7 +1596,6 @@ package androidx.media3.session {
method public final long getCurrentPosition();
method public final androidx.media3.common.Timeline getCurrentTimeline();
method public final androidx.media3.common.Tracks getCurrentTracks();
method public final com.google.common.collect.ImmutableList<androidx.media3.session.CommandButton> getCustomLayout();
method public final androidx.media3.common.DeviceInfo getDeviceInfo();
method @IntRange(from=0) public final int getDeviceVolume();
method public final long getDuration();
@ -1704,7 +1615,6 @@ package androidx.media3.session {
method public final long getSeekBackIncrement();
method public final long getSeekForwardIncrement();
method @Nullable public final android.app.PendingIntent getSessionActivity();
method public final android.os.Bundle getSessionExtras();
method public final boolean getShuffleModeEnabled();
method public final long getTotalBufferedDuration();
method public final androidx.media3.common.TrackSelectionParameters getTrackSelectionParameters();
@ -1787,7 +1697,6 @@ package androidx.media3.session {
public static interface MediaController.Listener {
method public default void onAvailableSessionCommandsChanged(androidx.media3.session.MediaController, androidx.media3.session.SessionCommands);
method public default com.google.common.util.concurrent.ListenableFuture<androidx.media3.session.SessionResult> onCustomCommand(androidx.media3.session.MediaController, androidx.media3.session.SessionCommand, android.os.Bundle);
method public default void onCustomLayoutChanged(androidx.media3.session.MediaController, java.util.List<androidx.media3.session.CommandButton>);
method public default void onDisconnected(androidx.media3.session.MediaController);
method public default void onExtrasChanged(androidx.media3.session.MediaController, android.os.Bundle);
method public default com.google.common.util.concurrent.ListenableFuture<androidx.media3.session.SessionResult> onSetCustomLayout(androidx.media3.session.MediaController, java.util.List<androidx.media3.session.CommandButton>);
@ -1850,7 +1759,6 @@ package androidx.media3.session {
method public final String getId();
method public final androidx.media3.common.Player getPlayer();
method @Nullable public final android.app.PendingIntent getSessionActivity();
method public android.os.Bundle getSessionExtras();
method public final androidx.media3.session.SessionToken getToken();
method public final void release();
method public final com.google.common.util.concurrent.ListenableFuture<androidx.media3.session.SessionResult> sendCustomCommand(androidx.media3.session.MediaSession.ControllerInfo, androidx.media3.session.SessionCommand, android.os.Bundle);
@ -1869,7 +1777,6 @@ package androidx.media3.session {
method public androidx.media3.session.MediaSession.Builder setExtras(android.os.Bundle);
method public androidx.media3.session.MediaSession.Builder setId(String);
method public androidx.media3.session.MediaSession.Builder setSessionActivity(android.app.PendingIntent);
method public androidx.media3.session.MediaSession.Builder setSessionExtras(android.os.Bundle);
}
public static interface MediaSession.Callback {
@ -1980,7 +1887,6 @@ package androidx.media3.session {
public final class SessionToken {
ctor public SessionToken(android.content.Context, android.content.ComponentName);
method public static com.google.common.util.concurrent.ListenableFuture<androidx.media3.session.SessionToken> createSessionToken(android.content.Context, android.media.session.MediaSession.Token);
method public static com.google.common.collect.ImmutableSet<androidx.media3.session.SessionToken> getAllServiceTokens(android.content.Context);
method public android.os.Bundle getExtras();
method public String getPackageName();

View File

@ -19,8 +19,7 @@ buildscript {
dependencies {
classpath 'com.android.tools.build:gradle:8.3.2'
classpath 'com.google.android.gms:strict-version-matcher-plugin:1.2.4'
classpath 'org.jetbrains.kotlin:kotlin-gradle-plugin:2.0.20'
classpath 'org.jetbrains.kotlin:compose-compiler-gradle-plugin:2.0.20'
classpath 'org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.10'
}
}
allprojects {

View File

@ -29,10 +29,6 @@ android {
}
}
lintOptions {
checkTestSources true
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8

View File

@ -12,15 +12,15 @@
// See the License for the specific language governing permissions and
// limitations under the License.
project.ext {
releaseVersion = '1.6.1'
releaseVersionCode = 1_006_001_3_00
releaseVersion = '1.5.0'
releaseVersionCode = 1_005_000_3_00
minSdkVersion = 21
// See https://developer.android.com/training/cars/media/automotive-os#automotive-module
automotiveMinSdkVersion = 28
appTargetSdkVersion = 34
// Upgrading this requires [Internal ref: b/193254928] to be fixed, or some
// additional robolectric config.
targetSdkVersion = 31
targetSdkVersion = 30
compileSdkVersion = 35
dexmakerVersion = '2.28.3'
// Use the same JUnit version as the Android repo:
@ -30,11 +30,10 @@ project.ext {
// https://cs.android.com/android/platform/superproject/main/+/main:external/guava/METADATA
guavaVersion = '33.3.1-android'
glideVersion = '4.14.2'
// Not the same as kotlin version, https://github.com/Kotlin/kotlinx.coroutines/releases
kotlinxCoroutinesVersion = '1.9.0'
kotlinxCoroutinesVersion = '1.8.1'
leakCanaryVersion = '2.10'
mockitoVersion = '3.12.4'
robolectricVersion = '4.14.1'
robolectricVersion = '4.11'
// Keep this in sync with Google's internal Checker Framework version.
checkerframeworkVersion = '3.13.0'
errorProneVersion = '2.18.0'
@ -47,13 +46,11 @@ project.ext {
androidxConstraintLayoutVersion = '2.1.4'
androidxCoreVersion = '1.8.0'
androidxExifInterfaceVersion = '1.3.6'
androidxLifecycleVersion = '2.8.7'
androidxLifecycleVersion = '2.6.0'
androidxMediaVersion = '1.7.0'
androidxRecyclerViewVersion = '1.3.0'
androidxMaterialVersion = '1.8.0'
androidxTestCoreVersion = '1.5.0'
androidxTestUiAutomatorVersion = '2.3.0'
androidxWindowVersion = '1.3.0'
androidxTestEspressoVersion = '3.5.1'
androidxTestJUnitVersion = '1.1.5'
androidxTestRunnerVersion = '1.5.2'

View File

@ -52,8 +52,6 @@ include modulePrefix + 'lib-ui'
project(modulePrefix + 'lib-ui').projectDir = new File(rootDir, 'libraries/ui')
include modulePrefix + 'lib-ui-leanback'
project(modulePrefix + 'lib-ui-leanback').projectDir = new File(rootDir, 'libraries/ui_leanback')
include modulePrefix + 'lib-ui-compose'
project(modulePrefix + 'lib-ui-compose').projectDir = new File(rootDir, 'libraries/ui_compose')
include modulePrefix + 'lib-database'
project(modulePrefix + 'lib-database').projectDir = new File(rootDir, 'libraries/database')
@ -83,8 +81,6 @@ if (gradle.ext.has('androidxMediaEnableMidiModule') && gradle.ext.androidxMediaE
include modulePrefix + 'lib-decoder-midi'
project(modulePrefix + 'lib-decoder-midi').projectDir = new File(rootDir, 'libraries/decoder_midi')
}
include modulePrefix + 'lib-decoder-mpegh'
project(modulePrefix + 'lib-decoder-mpegh').projectDir = new File(rootDir, 'libraries/decoder_mpegh')
include modulePrefix + 'lib-decoder-opus'
project(modulePrefix + 'lib-decoder-opus').projectDir = new File(rootDir, 'libraries/decoder_opus')
include modulePrefix + 'lib-decoder-vp9'

View File

@ -14,7 +14,6 @@
apply from: '../../constants.gradle'
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'org.jetbrains.kotlin.plugin.compose'
android {
namespace 'androidx.media3.demo.compose'
@ -53,8 +52,12 @@ android {
disable 'GoogleAppIndexingWarning','MissingTranslation'
}
buildFeatures {
viewBinding true
compose true
}
composeOptions {
kotlinCompilerExtensionVersion = "1.5.3"
}
testOptions {
unitTests {
@ -64,21 +67,21 @@ android {
}
dependencies {
def composeBom = platform('androidx.compose:compose-bom:2024.12.01')
def composeBom = platform('androidx.compose:compose-bom:2024.05.00')
implementation composeBom
implementation 'androidx.activity:activity-compose:1.9.0'
implementation 'androidx.compose.foundation:foundation'
implementation 'androidx.compose.material3:material3'
implementation 'androidx.compose.material:material-icons-extended'
implementation 'androidx.compose.ui:ui-tooling-preview'
implementation 'androidx.lifecycle:lifecycle-runtime-compose:' + androidxLifecycleVersion
implementation 'androidx.compose.foundation:foundation-android:1.6.7'
implementation 'androidx.compose.material3:material3-android:1.2.1'
implementation 'com.google.android.material:material:' + androidxMaterialVersion
implementation project(modulePrefix + 'lib-exoplayer')
implementation project(modulePrefix + 'lib-ui-compose')
debugImplementation 'androidx.compose.ui:ui-tooling'
// For detecting and debugging leaks only. LeakCanary is not needed for demo app to work.
debugImplementation 'com.squareup.leakcanary:leakcanary-android:' + leakCanaryVersion
testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:' + kotlinxCoroutinesVersion
testImplementation 'org.robolectric:robolectric:' + robolectricVersion
testImplementation project(modulePrefix + 'test-utils')
}

View File

@ -1,20 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright 2024 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
https://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.
-->
<lint>
<issue id="UnsafeOptInUsageError">
<option name="opt-in" value="androidx.media3.common.util.UnstableApi" />
</issue>
</lint>

View File

@ -15,139 +15,49 @@
*/
package androidx.media3.demo.compose
import android.content.Context
import android.os.Build
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Button
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.material3.Surface
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.LifecycleResumeEffect
import androidx.lifecycle.compose.LifecycleStartEffect
import androidx.media3.common.MediaItem
import androidx.media3.common.Player
import androidx.media3.demo.compose.buttons.ExtraControls
import androidx.media3.demo.compose.buttons.MinimalControls
import androidx.media3.demo.compose.data.videos
import androidx.media3.demo.compose.layout.CONTENT_SCALES
import androidx.media3.demo.compose.layout.noRippleClickable
import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.ui.compose.PlayerSurface
import androidx.media3.ui.compose.SURFACE_TYPE_SURFACE_VIEW
import androidx.media3.ui.compose.modifiers.resizeWithContentScale
import androidx.media3.ui.compose.state.rememberPresentationState
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent { ComposeDemoApp() }
}
}
@Composable
fun ComposeDemoApp(modifier: Modifier = Modifier) {
val context = LocalContext.current
var player by remember { mutableStateOf<Player?>(null) }
// See the following resources
// https://developer.android.com/topic/libraries/architecture/lifecycle#onStop-and-savedState
// https://developer.android.com/develop/ui/views/layout/support-multi-window-mode#multi-window_mode_configuration
// https://developer.android.com/develop/ui/compose/layouts/adaptive/support-multi-window-mode#android_9
if (Build.VERSION.SDK_INT > 23) {
// Initialize/release in onStart()/onStop() only because in a multi-window environment multiple
// apps can be visible at the same time. The apps that are out-of-focus are paused, but video
// playback should continue.
LifecycleStartEffect(Unit) {
player = initializePlayer(context)
onStopOrDispose {
player?.apply { release() }
player = null
}
}
} else {
// Call to onStop() is not guaranteed, hence we release the Player in onPause() instead
LifecycleResumeEffect(Unit) {
player = initializePlayer(context)
onPauseOrDispose {
player?.apply { release() }
player = null
}
}
}
player?.let { MediaPlayerScreen(player = it, modifier = modifier.fillMaxSize()) }
}
private fun initializePlayer(context: Context): Player =
ExoPlayer.Builder(context).build().apply {
setMediaItems(videos.map(MediaItem::fromUri))
prepare()
}
@Composable
private fun MediaPlayerScreen(player: Player, modifier: Modifier = Modifier) {
var showControls by remember { mutableStateOf(true) }
var currentContentScaleIndex by remember { mutableIntStateOf(0) }
val contentScale = CONTENT_SCALES[currentContentScaleIndex].second
val presentationState = rememberPresentationState(player)
val scaledModifier = Modifier.resizeWithContentScale(contentScale, presentationState.videoSizeDp)
// Only use MediaPlayerScreen's modifier once for the top level Composable
Box(modifier) {
// Always leave PlayerSurface to be part of the Compose tree because it will be initialised in
// the process. If this composable is guarded by some condition, it might never become visible
// because the Player will not emit the relevant event, e.g. the first frame being ready.
PlayerSurface(
player = player,
surfaceType = SURFACE_TYPE_SURFACE_VIEW,
modifier = scaledModifier.noRippleClickable { showControls = !showControls },
)
if (presentationState.coverSurface) {
// Cover the surface that is being prepared with a shutter
// Do not use scaledModifier here, makes the Box be measured at 0x0
Box(Modifier.matchParentSize().background(Color.Black))
}
if (showControls) {
// drawn on top of a potential shutter
MinimalControls(player, Modifier.align(Alignment.Center))
ExtraControls(
player,
Modifier.fillMaxWidth()
.align(Alignment.BottomCenter)
.background(Color.Gray.copy(alpha = 0.4f))
.navigationBarsPadding(),
)
}
Button(
onClick = { currentContentScaleIndex = currentContentScaleIndex.inc() % CONTENT_SCALES.size },
modifier = Modifier.align(Alignment.TopCenter).padding(top = 48.dp),
setContent {
Surface {
Column(
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally,
) {
Text("ContentScale is ${CONTENT_SCALES[currentContentScaleIndex].first}")
val context = LocalContext.current
val exoPlayer = remember {
ExoPlayer.Builder(context).build().apply {
setMediaItem(MediaItem.fromUri(videos[0]))
prepare()
playWhenReady = true
repeatMode = Player.REPEAT_MODE_ONE
}
}
PlayerSurface(
player = exoPlayer,
surfaceType = SURFACE_TYPE_SURFACE_VIEW,
modifier = Modifier.align(Alignment.CenterHorizontally),
)
}
}
}
}
}

View File

@ -0,0 +1,76 @@
/*
* Copyright 2024 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
*
* https://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 androidx.media3.demo.compose
import android.view.Surface
import android.view.SurfaceView
import android.view.TextureView
import androidx.annotation.IntDef
import androidx.compose.foundation.AndroidEmbeddedExternalSurface
import androidx.compose.foundation.AndroidExternalSurface
import androidx.compose.foundation.AndroidExternalSurfaceScope
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.media3.common.Player
/**
* Provides a dedicated drawing [Surface] for media playbacks using a [Player].
*
* The player's video output is displayed with either a [SurfaceView]/[AndroidExternalSurface] or a
* [TextureView]/[AndroidEmbeddedExternalSurface].
*
* [Player] takes care of attaching the rendered output to the [Surface] and clearing it, when it is
* destroyed.
*
* See
* [Choosing a surface type](https://developer.android.com/media/media3/ui/playerview#surfacetype)
* for more information.
*/
@Composable
fun PlayerSurface(player: Player, surfaceType: @SurfaceType Int, modifier: Modifier = Modifier) {
val onSurfaceCreated: (Surface) -> Unit = { surface -> player.setVideoSurface(surface) }
val onSurfaceDestroyed: () -> Unit = { player.setVideoSurface(null) }
val onSurfaceInitialized: AndroidExternalSurfaceScope.() -> Unit = {
onSurface { surface, _, _ ->
onSurfaceCreated(surface)
surface.onDestroyed { onSurfaceDestroyed() }
}
}
when (surfaceType) {
SURFACE_TYPE_SURFACE_VIEW ->
AndroidExternalSurface(modifier = modifier, onInit = onSurfaceInitialized)
SURFACE_TYPE_TEXTURE_VIEW ->
AndroidEmbeddedExternalSurface(modifier = modifier, onInit = onSurfaceInitialized)
else -> throw IllegalArgumentException("Unrecognized surface type: $surfaceType")
}
}
/**
* The type of surface view used for media playbacks. One of [SURFACE_TYPE_SURFACE_VIEW] or
* [SURFACE_TYPE_TEXTURE_VIEW].
*/
@MustBeDocumented
@Retention(AnnotationRetention.SOURCE)
@Target(AnnotationTarget.CLASS, AnnotationTarget.TYPE, AnnotationTarget.TYPE_PARAMETER)
@IntDef(SURFACE_TYPE_SURFACE_VIEW, SURFACE_TYPE_TEXTURE_VIEW)
annotation class SurfaceType
/** Surface type equivalent to [SurfaceView] . */
const val SURFACE_TYPE_SURFACE_VIEW = 1
/** Surface type equivalent to [TextureView]. */
const val SURFACE_TYPE_TEXTURE_VIEW = 2

View File

@ -1,37 +0,0 @@
/*
* Copyright 2024 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
*
* https://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 androidx.media3.demo.compose.buttons
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.media3.common.Player
@Composable
internal fun ExtraControls(player: Player, modifier: Modifier = Modifier) {
Row(
modifier = modifier,
horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically,
) {
PlaybackSpeedPopUpButton(player)
ShuffleButton(player)
RepeatButton(player)
}
}

View File

@ -1,51 +0,0 @@
/*
* Copyright 2024 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
*
* https://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 androidx.media3.demo.compose.buttons
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import androidx.media3.common.Player
/**
* Minimal playback controls for a [Player].
*
* Includes buttons for seeking to a previous/next items or playing/pausing the playback.
*/
@Composable
internal fun MinimalControls(player: Player, modifier: Modifier = Modifier) {
val graySemiTransparentBackground = Color.Gray.copy(alpha = 0.1f)
val modifierForIconButton =
modifier.size(80.dp).background(graySemiTransparentBackground, CircleShape)
Row(
modifier = modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceEvenly,
verticalAlignment = Alignment.CenterVertically,
) {
PreviousButton(player, modifierForIconButton)
PlayPauseButton(player, modifierForIconButton)
NextButton(player, modifierForIconButton)
}
}

View File

@ -1,40 +0,0 @@
/*
* Copyright 2024 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
*
* https://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 androidx.media3.demo.compose.buttons
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.SkipNext
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.media3.common.Player
import androidx.media3.demo.compose.R
import androidx.media3.ui.compose.state.rememberNextButtonState
@Composable
internal fun NextButton(player: Player, modifier: Modifier = Modifier) {
val state = rememberNextButtonState(player)
IconButton(onClick = state::onClick, modifier = modifier, enabled = state.isEnabled) {
Icon(
Icons.Default.SkipNext,
contentDescription = stringResource(R.string.next_button),
modifier = modifier,
)
}
}

View File

@ -1,41 +0,0 @@
/*
* Copyright 2024 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
*
* https://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 androidx.media3.demo.compose.buttons
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Pause
import androidx.compose.material.icons.filled.PlayArrow
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.media3.common.Player
import androidx.media3.demo.compose.R
import androidx.media3.ui.compose.state.rememberPlayPauseButtonState
@Composable
internal fun PlayPauseButton(player: Player, modifier: Modifier = Modifier) {
val state = rememberPlayPauseButtonState(player)
val icon = if (state.showPlay) Icons.Default.PlayArrow else Icons.Default.Pause
val contentDescription =
if (state.showPlay) stringResource(R.string.playpause_button_play)
else stringResource(R.string.playpause_button_pause)
IconButton(onClick = state::onClick, modifier = modifier, enabled = state.isEnabled) {
Icon(icon, contentDescription = contentDescription, modifier = modifier)
}
}

View File

@ -1,108 +0,0 @@
/*
* Copyright 2025 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
*
* https://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 androidx.media3.demo.compose.buttons
import android.view.Gravity
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.foundation.layout.wrapContentWidth
import androidx.compose.foundation.text.BasicText
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
import androidx.compose.ui.window.DialogWindowProvider
import androidx.media3.common.Player
import androidx.media3.ui.compose.state.rememberPlaybackSpeedState
@Composable
internal fun PlaybackSpeedPopUpButton(
player: Player,
modifier: Modifier = Modifier,
speedSelection: List<Float> = listOf(0.5f, 0.75f, 1.0f, 1.25f, 1.5f, 1.75f, 2.0f),
) {
val state = rememberPlaybackSpeedState(player)
var openDialog by remember { mutableStateOf(false) }
TextButton(onClick = { openDialog = true }, modifier = modifier, enabled = state.isEnabled) {
// TODO: look into TextMeasurer to ensure 1.1 and 2.2 occupy the same space
BasicText("%.1fx".format(state.playbackSpeed))
}
if (openDialog) {
BottomDialogOfChoices(
currentSpeed = state.playbackSpeed,
choices = speedSelection,
onDismissRequest = { openDialog = false },
onSelectChoice = state::updatePlaybackSpeed,
)
}
}
@Composable
private fun BottomDialogOfChoices(
currentSpeed: Float,
choices: List<Float>,
onDismissRequest: () -> Unit,
onSelectChoice: (Float) -> Unit,
) {
Dialog(
onDismissRequest = onDismissRequest,
properties = DialogProperties(usePlatformDefaultWidth = false),
) {
val dialogWindowProvider = LocalView.current.parent as? DialogWindowProvider
dialogWindowProvider?.window?.let { window ->
window.setGravity(Gravity.BOTTOM) // Move down, by default dialogs are in the centre
window.setDimAmount(0f) // Remove dimmed background of ongoing playback
}
Box(modifier = Modifier.wrapContentSize().background(Color.LightGray)) {
Column(
modifier = Modifier.fillMaxWidth().wrapContentWidth(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally,
) {
choices.forEach { speed ->
TextButton(
onClick = {
onSelectChoice(speed)
onDismissRequest()
}
) {
var fontWeight = FontWeight(400)
if (speed == currentSpeed) {
fontWeight = FontWeight(1000)
}
Text("%.1fx".format(speed), fontWeight = fontWeight)
}
}
}
}
}
}

View File

@ -1,40 +0,0 @@
/*
* Copyright 2024 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
*
* https://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 androidx.media3.demo.compose.buttons
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.SkipPrevious
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.media3.common.Player
import androidx.media3.demo.compose.R
import androidx.media3.ui.compose.state.rememberPreviousButtonState
@Composable
internal fun PreviousButton(player: Player, modifier: Modifier = Modifier) {
val state = rememberPreviousButtonState(player)
IconButton(onClick = state::onClick, modifier = modifier, enabled = state.isEnabled) {
Icon(
Icons.Default.SkipPrevious,
contentDescription = stringResource(R.string.previous_button),
modifier = modifier,
)
}
}

View File

@ -1,58 +0,0 @@
/*
* Copyright 2024 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
*
* https://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 androidx.media3.demo.compose.buttons
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Repeat
import androidx.compose.material.icons.filled.RepeatOn
import androidx.compose.material.icons.filled.RepeatOneOn
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.stringResource
import androidx.media3.common.Player
import androidx.media3.demo.compose.R
import androidx.media3.ui.compose.state.rememberRepeatButtonState
@Composable
internal fun RepeatButton(player: Player, modifier: Modifier = Modifier) {
val state = rememberRepeatButtonState(player)
val icon = repeatModeIcon(state.repeatModeState)
val contentDescription = repeatModeContentDescription(state.repeatModeState)
IconButton(onClick = state::onClick, modifier = modifier, enabled = state.isEnabled) {
Icon(icon, contentDescription = contentDescription, modifier = modifier)
}
}
private fun repeatModeIcon(repeatMode: @Player.RepeatMode Int): ImageVector {
return when (repeatMode) {
Player.REPEAT_MODE_OFF -> Icons.Default.Repeat
Player.REPEAT_MODE_ONE -> Icons.Default.RepeatOneOn
else -> Icons.Default.RepeatOn
}
}
@Composable
private fun repeatModeContentDescription(repeatMode: @Player.RepeatMode Int): String {
return when (repeatMode) {
Player.REPEAT_MODE_OFF -> stringResource(R.string.repeat_button_repeat_off_description)
Player.REPEAT_MODE_ONE -> stringResource(R.string.repeat_button_repeat_one_description)
else -> stringResource(R.string.repeat_button_repeat_all_description)
}
}

View File

@ -1,44 +0,0 @@
/*
* Copyright 2024 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
*
* https://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 androidx.media3.demo.compose.buttons
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Shuffle
import androidx.compose.material.icons.filled.ShuffleOn
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.media3.common.Player
import androidx.media3.demo.compose.R
import androidx.media3.ui.compose.state.rememberShuffleButtonState
@Composable
internal fun ShuffleButton(player: Player, modifier: Modifier = Modifier) {
val state = rememberShuffleButtonState(player)
val icon = if (state.shuffleOn) Icons.Default.ShuffleOn else Icons.Default.Shuffle
val contentDescription =
if (state.shuffleOn) {
stringResource(R.string.shuffle_button_shuffle_on_description)
} else {
stringResource(R.string.shuffle_button_shuffle_off_description)
}
IconButton(onClick = state::onClick, modifier = modifier, enabled = state.isEnabled) {
Icon(icon, contentDescription = contentDescription, modifier = modifier)
}
}

View File

@ -18,7 +18,6 @@ package androidx.media3.demo.compose.data
val videos =
listOf(
"https://html5demos.com/assets/dizzy.mp4",
"https://storage.googleapis.com/exoplayer-test-media-0/shortform_2.mp4",
"https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_4x3/gear0/fileSequence0.aac",
"https://storage.googleapis.com/exoplayer-test-media-1/gen-3/screens/dash-vod-single-segment/video-vp9-360.webm",
"https://storage.googleapis.com/exoplayer-test-media-0/shortform_3.mp4",
)

View File

@ -1,32 +0,0 @@
/*
* Copyright 2024 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
*
* https://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 androidx.media3.demo.compose.layout
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
@Composable
internal fun Modifier.noRippleClickable(onClick: () -> Unit): Modifier =
clickable(
interactionSource = remember { MutableInteractionSource() },
indication = null, // to prevent the ripple from the tap
) {
onClick()
}

View File

@ -1,30 +0,0 @@
/*
* Copyright 2024 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
*
* https://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 androidx.media3.demo.compose.layout
import androidx.compose.ui.layout.ContentScale
val CONTENT_SCALES =
listOf(
"Fit" to ContentScale.Fit,
"Crop" to ContentScale.Crop,
"None" to ContentScale.None,
"Inside" to ContentScale.Inside,
"FillBounds" to ContentScale.FillBounds,
"FillHeight" to ContentScale.FillHeight,
"FillWidth" to ContentScale.FillWidth,
)

View File

@ -15,13 +15,12 @@
-->
<resources>
<string name="app_name">Media3 Compose Demo</string>
<string name="playpause_button_play">Play</string>
<string name="playpause_button_pause">Pause</string>
<string name="next_button">Next</string>
<string name="previous_button">Previous</string>
<string name="repeat_button_repeat_off_description">Current mode: Repeat none. Toggle repeat mode.</string>
<string name="repeat_button_repeat_one_description">Current mode: Repeat one. Toggle repeat mode.</string>
<string name="repeat_button_repeat_all_description">Current mode: Repeat all. Toggle repeat mode.</string>
<string name="shuffle_button_shuffle_on_description">Disable shuffle mode.</string>
<string name="shuffle_button_shuffle_off_description">Enable shuffle mode</string>
<string name="current_playlist_name">Current playlist</string>
<string name="open_player_content_description">Click to view your play list</string>
<string name="added_media_item_format">Added %1$s to playlist</string>
<string name="shuffle">Shuffle</string>
<string name="play_button">Play</string>
<string name="waiting_for_metadata">Waiting for playlist to load…</string>
<string name="notification_permission_denied">
"Without notification access the app can't warn about failed background operations"</string>
</resources>

View File

@ -15,8 +15,6 @@
*/
package androidx.media3.demo.composition;
import static android.content.pm.ActivityInfo.COLOR_MODE_HDR;
import static androidx.media3.common.util.Util.SDK_INT;
import static androidx.media3.transformer.Composition.HDR_MODE_EXPERIMENTAL_FORCE_INTERPRET_HDR_AS_SDR;
import static androidx.media3.transformer.Composition.HDR_MODE_KEEP_HDR;
import static androidx.media3.transformer.Composition.HDR_MODE_TONE_MAP_HDR_TO_SDR_USING_MEDIACODEC;
@ -57,8 +55,7 @@ import androidx.media3.transformer.EditedMediaItemSequence;
import androidx.media3.transformer.Effects;
import androidx.media3.transformer.ExportException;
import androidx.media3.transformer.ExportResult;
import androidx.media3.transformer.InAppFragmentedMp4Muxer;
import androidx.media3.transformer.InAppMp4Muxer;
import androidx.media3.transformer.InAppMuxer;
import androidx.media3.transformer.JsonUtil;
import androidx.media3.transformer.Transformer;
import androidx.media3.ui.PlayerView;
@ -114,9 +111,6 @@ public final class CompositionPreviewActivity extends AppCompatActivity {
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (SDK_INT >= 26) {
getWindow().setColorMode(COLOR_MODE_HDR);
}
setContentView(R.layout.composition_preview_activity);
playerView = findViewById(R.id.composition_player_view);
@ -192,18 +186,6 @@ public final class CompositionPreviewActivity extends AppCompatActivity {
exportStopwatch.reset();
}
@SuppressWarnings("MissingSuperCall")
@Override
public void onBackPressed() {
if (compositionPlayer != null) {
compositionPlayer.pause();
}
if (exportStopwatch.isRunning()) {
cancelExport();
exportStopwatch.reset();
}
}
private Composition prepareComposition() {
String[] presetUris = getResources().getStringArray(/* id= */ R.array.preset_uris);
int[] presetDurationsUs = getResources().getIntArray(/* id= */ R.array.preset_durations);
@ -297,6 +279,7 @@ public final class CompositionPreviewActivity extends AppCompatActivity {
Log.e(TAG, "Preview error", error);
}
});
player.setRepeatMode(Player.REPEAT_MODE_ALL);
player.setComposition(composition);
player.prepare();
player.play();
@ -363,20 +346,21 @@ public final class CompositionPreviewActivity extends AppCompatActivity {
enableDebugTracingCheckBox.setOnCheckedChangeListener(
(buttonView, isChecked) -> DebugTraceUtil.enableTracing = isChecked);
CheckBox useMedia3Mp4MuxerCheckBox =
exportSettingsDialogView.findViewById(R.id.use_media3_mp4_muxer_checkbox);
CheckBox useMedia3FragmentedMp4MuxerCheckBox =
exportSettingsDialogView.findViewById(R.id.use_media3_fragmented_mp4_muxer_checkbox);
useMedia3Mp4MuxerCheckBox.setOnCheckedChangeListener(
// Connect producing fragmented MP4 to using Media3 Muxer
CheckBox useMedia3MuxerCheckBox =
exportSettingsDialogView.findViewById(R.id.use_media3_muxer_checkbox);
CheckBox produceFragmentedMp4CheckBox =
exportSettingsDialogView.findViewById(R.id.produce_fragmented_mp4_checkbox);
useMedia3MuxerCheckBox.setOnCheckedChangeListener(
(buttonView, isChecked) -> {
if (isChecked) {
useMedia3FragmentedMp4MuxerCheckBox.setChecked(false);
if (!isChecked) {
produceFragmentedMp4CheckBox.setChecked(false);
}
});
useMedia3FragmentedMp4MuxerCheckBox.setOnCheckedChangeListener(
produceFragmentedMp4CheckBox.setOnCheckedChangeListener(
(buttonView, isChecked) -> {
if (isChecked) {
useMedia3Mp4MuxerCheckBox.setChecked(false);
useMedia3MuxerCheckBox.setChecked(true);
}
});
@ -419,15 +403,15 @@ public final class CompositionPreviewActivity extends AppCompatActivity {
transformerBuilder.setVideoMimeType(selectedVideoMimeType);
}
CheckBox useMedia3Mp4MuxerCheckBox =
exportSettingsDialogView.findViewById(R.id.use_media3_mp4_muxer_checkbox);
CheckBox useMedia3FragmentedMp4MuxerCheckBox =
exportSettingsDialogView.findViewById(R.id.use_media3_fragmented_mp4_muxer_checkbox);
if (useMedia3Mp4MuxerCheckBox.isChecked()) {
transformerBuilder.setMuxerFactory(new InAppMp4Muxer.Factory());
}
if (useMedia3FragmentedMp4MuxerCheckBox.isChecked()) {
transformerBuilder.setMuxerFactory(new InAppFragmentedMp4Muxer.Factory());
CheckBox useMedia3MuxerCheckBox =
exportSettingsDialogView.findViewById(R.id.use_media3_muxer_checkbox);
CheckBox produceFragmentedMp4CheckBox =
exportSettingsDialogView.findViewById(R.id.produce_fragmented_mp4_checkbox);
if (useMedia3MuxerCheckBox.isChecked()) {
transformerBuilder.setMuxerFactory(
new InAppMuxer.Factory.Builder()
.setOutputFragmentedMp4(produceFragmentedMp4CheckBox.isChecked())
.build());
}
transformer =

View File

@ -18,7 +18,6 @@
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:keepScreenOn="true"
android:padding="16dp">
<com.google.android.material.card.MaterialCardView

View File

@ -79,12 +79,12 @@
android:orientation="horizontal"
android:gravity="center_vertical">
<TextView
android:text="@string/use_media3_mp4_muxer"
android:text="@string/use_media3_muxer"
android:layout_height="wrap_content"
android:layout_width="0dp"
android:layout_weight="1" />
<CheckBox
android:id="@+id/use_media3_mp4_muxer_checkbox"
android:id="@+id/use_media3_muxer_checkbox"
android:layout_gravity="end"
android:checked="false"
android:layout_height="wrap_content"
@ -96,12 +96,12 @@
android:orientation="horizontal"
android:gravity="center_vertical">
<TextView
android:text="@string/use_media3_fragmented_mp4_muxer"
android:text="@string/produce_fragmented_mp4"
android:layout_height="wrap_content"
android:layout_width="0dp"
android:layout_weight="1" />
<CheckBox
android:id="@+id/use_media3_fragmented_mp4_muxer_checkbox"
android:id="@+id/produce_fragmented_mp4_checkbox"
android:layout_gravity="end"
android:checked="false"
android:layout_height="wrap_content"

View File

@ -34,6 +34,6 @@
<string name="output_audio_mime_type" translatable="false">Output audio MIME type</string>
<string name="output_video_mime_type" translatable="false">Output video MIME type</string>
<string name="enable_debug_tracing" translatable="false">Enable debug tracing</string>
<string name="use_media3_mp4_muxer" translatable="false">Use Media3 Mp4Muxer</string>
<string name="use_media3_fragmented_mp4_muxer" translatable="false">Use Media3 FragmentedMp4Muxer</string>
<string name="use_media3_muxer" translatable="false">Use Media3 muxer</string>
<string name="produce_fragmented_mp4" translatable="false">Produce fragmented MP4</string>
</resources>

View File

@ -1,10 +0,0 @@
# Effect demo
This app demonstrates how to use the [Effect][] API to modify videos. It uses
`setVideoEffects` method to add different effects to [ExoPlayer][].
See the [demos README](../README.md) for instructions on how to build and run
this demo.
[Effect]: https://github.com/androidx/media/tree/release/libraries/effect
[ExoPlayer]: https://github.com/androidx/media/tree/release/libraries/exoplayer

View File

@ -1,80 +0,0 @@
// Copyright 2024 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
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
apply from: '../../constants.gradle'
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'org.jetbrains.kotlin.plugin.compose'
android {
namespace 'androidx.media3.demo.effect'
compileSdk project.ext.compileSdkVersion
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = '1.8'
}
defaultConfig {
versionName project.ext.releaseVersion
versionCode project.ext.releaseVersionCode
minSdkVersion project.ext.minSdkVersion
targetSdkVersion project.ext.appTargetSdkVersion
}
buildTypes {
release {
shrinkResources true
minifyEnabled true
signingConfig signingConfigs.debug
}
debug {
jniDebuggable = true
}
}
lintOptions {
// The demo app isn't indexed, and doesn't have translations.
disable 'GoogleAppIndexingWarning','MissingTranslation'
}
buildFeatures {
compose true
}
testOptions {
unitTests {
includeAndroidResources = true
}
}
}
dependencies {
def composeBom = platform('androidx.compose:compose-bom:2024.10.00')
implementation composeBom
implementation 'androidx.activity:activity-compose:1.9.0'
implementation 'androidx.compose.foundation:foundation'
implementation 'androidx.compose.material3:material3'
implementation 'com.google.android.material:material:' + androidxMaterialVersion
implementation project(modulePrefix + 'lib-exoplayer')
implementation project(modulePrefix + 'lib-ui')
implementation project(modulePrefix + 'lib-effect')
// For detecting and debugging leaks only. LeakCanary is not needed for demo app to work.
debugImplementation 'com.squareup.leakcanary:leakcanary-android:' + leakCanaryVersion
}

View File

@ -1,20 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright 2024 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
https://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.
-->
<lint>
<issue id="UnsafeOptInUsageError">
<option name="opt-in" value="androidx.media3.common.util.UnstableApi" />
</issue>
</lint>

View File

@ -1,40 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright 2024 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
https://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="androidx.media3.demo.effect">
<uses-sdk/>
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO"/>
<application
android:allowBackup="false"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:theme="@style/Theme.Media3EffectDemo">
<activity
android:name="androidx.media3.demo.effect.EffectActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>

View File

@ -1,27 +0,0 @@
[
{
"name": "Cats -> Dogs",
"playlist": [
{
"uri": "https://html5demos.com/assets/dizzy.mp4"
},
{
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv"
}
]
},
{
"name": "Android Block -> Dogs -> BigBuckBunny",
"playlist": [
{
"uri": "https://storage.googleapis.com/exoplayer-test-media-0/android-block-1080-hevc.mp4"
},
{
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv"
},
{
"uri": "https://storage.googleapis.com/exoplayer-test-media-0/BigBuckBunny_320x180.mp4"
}
]
}
]

View File

@ -1,140 +0,0 @@
/*
* Copyright 2024 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 androidx.media3.demo.effect
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.graphics.PorterDuff
import android.os.Handler
import androidx.media3.common.VideoFrameProcessingException
import androidx.media3.common.util.Size
import androidx.media3.common.util.Util
import androidx.media3.effect.CanvasOverlay
import kotlin.math.abs
import kotlin.random.Random
/** Mimics an emitter of confetti, dropping from the center of the frame. */
internal class ConfettiOverlay : CanvasOverlay(/* useInputFrameSize= */ true) {
private val confettiList = mutableListOf<Confetti>()
private val paint = Paint()
private val handler = Handler(Util.getCurrentOrMainLooper())
private var addConfettiTask: (() -> Unit)? = null
private var width = 0f
private var height = 0f
private var started = false
override fun configure(videoSize: Size) {
super.configure(videoSize)
this.width = videoSize.width.toFloat()
this.height = videoSize.height.toFloat()
}
@Synchronized
override fun onDraw(canvas: Canvas, presentationTimeUs: Long) {
if (!started) {
start()
}
canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR)
confettiList.removeAll { confetti ->
confetti.y > height / 2 || confetti.x <= 0 || confetti.x > width
}
for (confetti in confettiList) {
confetti.draw(canvas, paint)
confetti.update()
}
}
@Throws(VideoFrameProcessingException::class)
override fun release() {
super.release()
handler.post(this::stop)
}
/** Starts the confetti. */
fun start() {
addConfettiTask = this::addConfetti
handler.post(checkNotNull(addConfettiTask))
started = true
}
/** Stops the confetti. */
fun stop() {
handler.removeCallbacks(checkNotNull(addConfettiTask))
confettiList.clear()
started = false
addConfettiTask = null
}
@Synchronized
fun addConfetti() {
repeat(5) {
confettiList.add(
Confetti(
text = CONFETTI_TEXTS[abs(Random.nextInt()) % CONFETTI_TEXTS.size],
x = width / 2f,
y = EMITTER_POSITION_Y.toFloat(),
size = CONFETTI_BASE_SIZE + Random.nextInt(CONFETTI_SIZE_VARIATION),
color = Color.HSVToColor(floatArrayOf(Random.nextInt(360).toFloat(), 0.6f, 0.8f)),
)
)
}
handler.postDelayed(this::addConfetti, /* delayMillis= */ 100)
}
private class Confetti(
private val text: String,
private val size: Int,
private val color: Int,
var x: Float,
var y: Float,
) {
private val speedX = 4 * (Random.nextFloat() * 2 - 1) // Random speed in x direction
private val speedY = 4 * Random.nextFloat() // Random speed in y direction
private val rotationSpeed = (Random.nextFloat() - 0.5f) * 4f // Random rotation speed
private var rotation = Random.nextFloat() * 360f
/** Draws the [Confetti] on the [Canvas]. */
fun draw(canvas: Canvas, paint: Paint) {
canvas.save()
paint.color = color
canvas.translate(x, y)
canvas.rotate(rotation)
paint.textSize = size.toFloat()
canvas.drawText(text, /* x= */ 0f, /* y= */ 0f, paint) // Only draw text
canvas.restore()
}
/** Updates the [Confetti]. */
fun update() {
x += speedX
y += speedY
rotation += rotationSpeed
}
}
private companion object {
val CONFETTI_TEXTS = listOf("", "", "", "✦︎", "♥︎", "☕︎")
const val EMITTER_POSITION_Y = -50
const val CONFETTI_BASE_SIZE = 30
const val CONFETTI_SIZE_VARIATION = 10
}
}

View File

@ -1,588 +0,0 @@
/*
* Copyright 2024 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
*
* https://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 androidx.media3.demo.effect
import android.Manifest
import android.net.Uri
import android.os.Bundle
import android.text.Spannable
import android.text.SpannableString
import android.text.style.ForegroundColorSpan
import androidx.activity.ComponentActivity
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.compose.setContent
import androidx.activity.result.contract.ActivityResultContracts
import androidx.annotation.OptIn
import androidx.compose.animation.animateContentSize
import androidx.compose.animation.core.LinearEasing
import androidx.compose.animation.core.tween
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.selection.selectable
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button
import androidx.compose.material3.Card
import androidx.compose.material3.Checkbox
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExposedDropdownMenuBox
import androidx.compose.material3.ExposedDropdownMenuDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.MenuAnchorType
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.RadioButton
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Slider
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.dimensionResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.viewinterop.AndroidView
import androidx.core.content.ContextCompat
import androidx.lifecycle.lifecycleScope
import androidx.media3.common.Effect
import androidx.media3.common.MediaItem
import androidx.media3.common.util.UnstableApi
import androidx.media3.common.util.Util.SDK_INT
import androidx.media3.effect.Contrast
import androidx.media3.effect.OverlayEffect
import androidx.media3.effect.StaticOverlaySettings
import androidx.media3.effect.TextOverlay
import androidx.media3.effect.TextureOverlay
import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.ui.PlayerView
import com.google.common.collect.ImmutableList
import kotlinx.coroutines.launch
class EffectActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val playlistHolderList = mutableStateOf<List<PlaylistHolder>>(emptyList())
lifecycleScope.launch {
playlistHolderList.value =
loadPlaylistsFromJson(JSON_FILENAME, this@EffectActivity, "EffectActivity")
}
setContent { EffectDemo(playlistHolderList.value) }
}
@OptIn(UnstableApi::class)
@Composable
private fun EffectDemo(playlistHolderList: List<PlaylistHolder>) {
val snackbarHostState = remember { SnackbarHostState() }
val coroutineScope = rememberCoroutineScope()
val context = LocalContext.current
val exoPlayer by remember {
mutableStateOf(ExoPlayer.Builder(context).build().apply { playWhenReady = true })
}
var effectsEnabled by remember { mutableStateOf(false) }
Scaffold(
modifier = Modifier.fillMaxSize(),
snackbarHost = { SnackbarHost(hostState = snackbarHostState) },
) { paddingValues ->
Column(
modifier = Modifier.fillMaxWidth().padding(paddingValues),
verticalArrangement = Arrangement.Top,
horizontalAlignment = Alignment.CenterHorizontally,
) {
InputChooser(
playlistHolderList,
onException = { message ->
coroutineScope.launch { snackbarHostState.showSnackbar(message) }
},
) { mediaItems ->
effectsEnabled = true
exoPlayer.apply {
setMediaItems(mediaItems)
setVideoEffects(emptyList())
prepare()
}
}
PlayerScreen(exoPlayer)
EffectControls(
effectsEnabled,
onApplyEffectsClicked = { videoEffects ->
exoPlayer.apply {
setVideoEffects(videoEffects)
prepare()
}
},
)
}
}
}
@Composable
private fun InputChooser(
playlistHolderList: List<PlaylistHolder>,
onException: (String) -> Unit,
onNewMediaItems: (List<MediaItem>) -> Unit,
) {
var showPresetInputChooser by remember { mutableStateOf(false) }
var showLocalFileChooser by remember { mutableStateOf(false) }
Row(
Modifier.padding(vertical = dimensionResource(id = R.dimen.regular_padding)),
horizontalArrangement = Arrangement.spacedBy(dimensionResource(id = R.dimen.regular_padding)),
) {
Button(onClick = { showPresetInputChooser = true }) {
Text(text = stringResource(id = R.string.choose_preset_input))
}
Button(onClick = { showLocalFileChooser = true }) {
Text(text = stringResource(id = R.string.choose_local_file))
}
}
if (showPresetInputChooser) {
if (playlistHolderList.isNotEmpty()) {
PresetInputChooser(
playlistHolderList,
onDismissRequest = { showPresetInputChooser = false },
) { mediaItems ->
onNewMediaItems(mediaItems)
showPresetInputChooser = false
}
} else {
onException(stringResource(id = R.string.no_loaded_playlists_error))
showPresetInputChooser = false
}
}
if (showLocalFileChooser) {
LocalFileChooser(
onException = { message ->
onException(message)
showLocalFileChooser = false
}
) { mediaItems ->
onNewMediaItems(mediaItems)
showLocalFileChooser = false
}
}
}
@Composable
private fun PresetInputChooser(
playlistHolderList: List<PlaylistHolder>,
onDismissRequest: () -> Unit,
onInputSelected: (List<MediaItem>) -> Unit,
) {
var selectedOption by remember { mutableStateOf(playlistHolderList.first()) }
AlertDialog(
onDismissRequest = onDismissRequest,
title = { Text(stringResource(id = R.string.choose_preset_input)) },
confirmButton = {
Button(onClick = { onInputSelected(selectedOption.mediaItems) }) {
Text(text = stringResource(id = R.string.ok))
}
},
text = {
Column {
playlistHolderList.forEach { playlistHolder ->
Row(
Modifier.fillMaxWidth()
.selectable(
(playlistHolder == selectedOption),
onClick = { selectedOption = playlistHolder },
),
verticalAlignment = Alignment.CenterVertically,
) {
RadioButton(
selected = (playlistHolder == selectedOption),
onClick = { selectedOption = playlistHolder },
)
Text(playlistHolder.title)
}
}
}
},
)
}
@OptIn(UnstableApi::class)
@Composable
private fun LocalFileChooser(
onException: (String) -> Unit,
onFileSelected: (List<MediaItem>) -> Unit,
) {
val context = LocalContext.current
val localFileChooserLauncher =
rememberLauncherForActivityResult(
contract = ActivityResultContracts.OpenDocument(),
onResult = { uri: Uri? ->
if (uri != null) {
onFileSelected(listOf(MediaItem.fromUri(uri)))
} else {
onException(getString(R.string.can_not_open_file_error))
}
},
)
val permissionLauncher =
rememberLauncherForActivityResult(
contract = ActivityResultContracts.RequestPermission(),
onResult = { isGranted: Boolean ->
if (isGranted) {
localFileChooserLauncher.launch(arrayOf("video/*"))
} else {
onException(getString(R.string.permission_not_granted_error))
}
},
)
LaunchedEffect(Unit) {
val permission =
if (SDK_INT >= 33) Manifest.permission.READ_MEDIA_VIDEO
else Manifest.permission.READ_EXTERNAL_STORAGE
val permissionCheck = ContextCompat.checkSelfPermission(context, permission)
if (permissionCheck == android.content.pm.PackageManager.PERMISSION_GRANTED) {
localFileChooserLauncher.launch(arrayOf("video/*"))
} else {
permissionLauncher.launch(permission)
}
}
}
@Composable
private fun PlayerScreen(exoPlayer: ExoPlayer) {
val context = LocalContext.current
AndroidView(
factory = { PlayerView(context).apply { player = exoPlayer } },
modifier =
Modifier.height(dimensionResource(id = R.dimen.android_view_height))
.padding(all = dimensionResource(id = R.dimen.regular_padding)),
)
}
@OptIn(UnstableApi::class)
@Composable
private fun EffectControls(enabled: Boolean, onApplyEffectsClicked: (List<Effect>) -> Unit) {
var effectControlsState by remember { mutableStateOf(EffectControlsState()) }
Button(
enabled = enabled && effectControlsState.effectsChanged,
onClick = {
val effectsList = mutableListOf<Effect>()
if (effectControlsState.contrastValue != 0f) {
effectsList += Contrast(effectControlsState.contrastValue)
}
val overlaysBuilder = ImmutableList.builder<TextureOverlay>()
if (effectControlsState.confettiOverlayChecked) {
overlaysBuilder.add(ConfettiOverlay())
}
val textOverlayText = effectControlsState.textOverlayText
if (effectControlsState.textOverlayChecked && textOverlayText != null) {
val spannableOverlayText = SpannableString(textOverlayText)
spannableOverlayText.setSpan(
ForegroundColorSpan(effectControlsState.textOverlayColor.toArgb()),
/* start= */ 0,
textOverlayText.length,
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE,
)
val staticOverlaySettings =
StaticOverlaySettings.Builder()
.setAlphaScale(effectControlsState.textOverlayAlpha)
.build()
overlaysBuilder.add(
TextOverlay.createStaticTextOverlay(spannableOverlayText, staticOverlaySettings)
)
}
effectsList += OverlayEffect(overlaysBuilder.build())
onApplyEffectsClicked(effectsList)
effectControlsState = effectControlsState.copy(effectsChanged = false)
},
) {
Text(text = stringResource(id = R.string.apply_effects))
}
EffectControlsList(enabled, effectControlsState) { newEffectControlsState ->
effectControlsState = newEffectControlsState
}
}
@Composable
private fun EffectControlsList(
enabled: Boolean,
effectControlsState: EffectControlsState,
onEffectControlsStateChange: (EffectControlsState) -> Unit,
) {
LazyColumn(Modifier.padding(vertical = dimensionResource(id = R.dimen.small_padding))) {
item {
EffectItem(
name = stringResource(id = R.string.contrast),
enabled = enabled,
onCheckedChange = {
onEffectControlsStateChange(
effectControlsState.copy(effectsChanged = true, contrastValue = 0f)
)
},
) {
Row {
Text(
text = "%.2f".format(effectControlsState.contrastValue),
style = MaterialTheme.typography.bodyLarge,
modifier = Modifier.padding(dimensionResource(id = R.dimen.large_padding)).weight(1f),
)
Slider(
value = effectControlsState.contrastValue,
onValueChange = { newContrastValue ->
val newRoundedContrastValue = "%.2f".format(newContrastValue).toFloat()
onEffectControlsStateChange(
effectControlsState.copy(
effectsChanged = true,
contrastValue = newRoundedContrastValue,
)
)
},
valueRange = -1f..1f,
modifier = Modifier.weight(4f),
)
}
}
}
item {
EffectItem(
name = stringResource(R.string.confetti_overlay),
enabled = enabled,
onCheckedChange = { checked ->
onEffectControlsStateChange(
effectControlsState.copy(effectsChanged = true, confettiOverlayChecked = checked)
)
},
)
}
item {
EffectItem(
name = stringResource(R.string.custom_text_overlay),
enabled = enabled,
onCheckedChange = { checked ->
onEffectControlsStateChange(
effectControlsState.copy(effectsChanged = !checked, textOverlayChecked = checked)
)
},
) {
Column {
OutlinedTextField(
value = effectControlsState.textOverlayText ?: "",
onValueChange = { newTextOverlayText ->
onEffectControlsStateChange(
effectControlsState.copy(
effectsChanged = true,
textOverlayText = newTextOverlayText.ifEmpty { null },
)
)
},
label = { Text(stringResource(R.string.text)) },
singleLine = true,
modifier =
Modifier.fillMaxWidth().padding(bottom = dimensionResource(R.dimen.large_padding)),
)
Row {
ColorsDropDownMenu(effectControlsState.textOverlayColor) { color ->
onEffectControlsStateChange(
effectControlsState.copy(
effectsChanged = effectControlsState.textOverlayText != null,
textOverlayColor = color,
)
)
}
}
Row {
Text(
text =
stringResource(R.string.alpha) +
" = %.2f".format(effectControlsState.textOverlayAlpha),
style = MaterialTheme.typography.bodyLarge,
modifier =
Modifier.padding(dimensionResource(id = R.dimen.large_padding)).weight(1f),
)
Slider(
value = effectControlsState.textOverlayAlpha,
onValueChange = { newAlphaValue ->
val newRoundedAlphaValue = "%.2f".format(newAlphaValue).toFloat()
onEffectControlsStateChange(
effectControlsState.copy(
effectsChanged = effectControlsState.textOverlayText != null,
textOverlayAlpha = newRoundedAlphaValue,
)
)
},
valueRange = 0f..1f,
modifier = Modifier.weight(2f),
)
}
}
}
}
}
}
@kotlin.OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ColorsDropDownMenu(color: Color, onItemSelected: (Color) -> Unit) {
var expanded by remember { mutableStateOf(false) }
ExposedDropdownMenuBox(
expanded = expanded,
onExpandedChange = { expanded = it },
modifier = Modifier.fillMaxWidth().padding(bottom = dimensionResource(R.dimen.large_padding)),
) {
OutlinedTextField(
modifier = Modifier.fillMaxWidth().menuAnchor(MenuAnchorType.PrimaryNotEditable),
value = COLOR_NAMES[color] ?: stringResource(R.string.unknown_color),
onValueChange = {},
readOnly = true,
singleLine = true,
label = { Text(stringResource(R.string.text_color)) },
trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded) },
colors = ExposedDropdownMenuDefaults.textFieldColors(),
)
ExposedDropdownMenu(expanded = expanded, onDismissRequest = { expanded = false }) {
for (color in COLORS) {
DropdownMenuItem(
text = {
Text(
COLOR_NAMES[color] ?: stringResource(R.string.unknown_color),
style = MaterialTheme.typography.bodyLarge,
)
},
onClick = {
onItemSelected(color)
expanded = false
},
contentPadding = ExposedDropdownMenuDefaults.ItemContentPadding,
leadingIcon = {
Box(
modifier =
Modifier.size(dimensionResource(R.dimen.color_circle_size))
.background(color, CircleShape)
)
},
)
}
}
}
}
@Composable
fun EffectItem(
name: String,
enabled: Boolean,
onCheckedChange: (Boolean) -> Unit = {},
content: @Composable () -> Unit = {},
) {
var checked by rememberSaveable { mutableStateOf(false) }
Card(
modifier =
Modifier.padding(
vertical = dimensionResource(id = R.dimen.small_padding),
horizontal = dimensionResource(id = R.dimen.regular_padding),
)
.clickable(enabled = enabled && !checked) {
checked = !checked
onCheckedChange(checked)
}
) {
Column(
Modifier.padding(dimensionResource(id = R.dimen.large_padding))
.animateContentSize(animationSpec = tween(durationMillis = 200, easing = LinearEasing))
) {
Row {
Column(Modifier.weight(1f).padding(dimensionResource(id = R.dimen.large_padding))) {
Text(text = name, style = MaterialTheme.typography.bodyLarge)
}
Checkbox(
enabled = enabled,
checked = checked,
onCheckedChange = {
checked = !checked
onCheckedChange(checked)
},
)
}
if (checked) {
content()
}
}
}
}
private data class EffectControlsState(
val effectsChanged: Boolean = false,
val contrastValue: Float = 0f,
val confettiOverlayChecked: Boolean = false,
val textOverlayChecked: Boolean = false,
val textOverlayText: String? = null,
val textOverlayColor: Color = COLORS[0],
val textOverlayAlpha: Float = 1f,
)
private companion object {
const val JSON_FILENAME = "media.playlist.json"
val COLORS =
listOf(
Color.Black,
Color.DarkGray,
Color.Gray,
Color.LightGray,
Color.White,
Color.Red,
Color.Green,
Color.Blue,
Color.Yellow,
Color.Cyan,
Color.Magenta,
)
val COLOR_NAMES =
mapOf(
Color.Black to "Black",
Color.DarkGray to "Dark Gray",
Color.Gray to "Gray",
Color.LightGray to "Light Gray",
Color.White to "White",
Color.Red to "Red",
Color.Green to "Green",
Color.Blue to "Blue",
Color.Yellow to "Yellow",
Color.Cyan to "Cyan",
Color.Magenta to "Magenta",
)
}
}

View File

@ -1,78 +0,0 @@
/*
* Copyright 2024 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
*
* https://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 androidx.media3.demo.effect
import android.content.Context
import android.net.Uri
import android.util.JsonReader
import android.util.Log
import androidx.media3.common.MediaItem
import java.io.IOException
import java.io.InputStreamReader
import java.nio.charset.StandardCharsets
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
internal suspend fun loadPlaylistsFromJson(
jsonFilename: String,
context: Context,
tag: String,
): List<PlaylistHolder> =
withContext(Dispatchers.IO) {
try {
context.assets.open(jsonFilename).use { inputStream ->
val reader = JsonReader(InputStreamReader(inputStream, StandardCharsets.UTF_8))
val playlistHolders = buildList {
reader.beginArray()
while (reader.hasNext()) {
readPlaylist(reader)?.let { add(it) }
}
reader.endArray()
}
playlistHolders
}
} catch (e: IOException) {
Log.e(tag, context.getString(R.string.playlist_loading_error, jsonFilename, e))
emptyList()
}
}
private fun readPlaylist(reader: JsonReader): PlaylistHolder? {
val playlistHolder = PlaylistHolder("", emptyList())
reader.beginObject()
while (reader.hasNext()) {
val name = reader.nextName()
if (name.equals("name")) {
playlistHolder.title = reader.nextString()
} else if (name.equals("playlist")) {
playlistHolder.mediaItems = buildList {
reader.beginArray()
while (reader.hasNext()) {
reader.beginObject()
reader.nextName()
add(MediaItem.fromUri(Uri.parse(reader.nextString())))
reader.endObject()
}
reader.endArray()
}
}
}
reader.endObject()
// Only return the playlistHolder object if it has media items
return if (playlistHolder.mediaItems.isNotEmpty()) playlistHolder else null
}
internal data class PlaylistHolder(var title: String, var mediaItems: List<MediaItem>)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

View File

@ -1,31 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright 2024 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
https://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.
-->
<resources>
<!-- Base application theme. -->
<style name="Theme.Media3EffectDemo" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
<!-- Primary brand color. -->
<item name="colorPrimary">@color/purple_200</item>
<item name="colorPrimaryVariant">@color/purple_700</item>
<item name="colorOnPrimary">@color/black</item>
<!-- Secondary brand color. -->
<item name="colorSecondary">@color/teal_200</item>
<item name="colorSecondaryVariant">@color/teal_200</item>
<item name="colorOnSecondary">@color/black</item>
<!-- Status bar color. -->
<item name="android:statusBarColor">?attr/colorPrimaryVariant</item>
<!-- Customize your theme here. -->
</style>
</resources>

View File

@ -1,24 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright 2024 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
https://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.
-->
<resources>
<color name="purple_200">#FFBB86FC</color>
<color name="purple_500">#FF6200EE</color>
<color name="purple_700">#FF3700B3</color>
<color name="teal_200">#FF03DAC5</color>
<color name="teal_700">#FF018786</color>
<color name="black">#FF000000</color>
<color name="white">#FFFFFFFF</color>
</resources>

View File

@ -1,22 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright 2024 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
https://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.
-->
<resources>
<dimen name="small_padding">4dp</dimen>
<dimen name="regular_padding">8dp</dimen>
<dimen name="large_padding">12dp</dimen>
<dimen name="android_view_height">256dp</dimen>
<dimen name="color_circle_size">40dp</dimen>
</resources>

View File

@ -1,33 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright 2024 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
https://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.
-->
<resources>
<string name="app_name">Effect Demo</string>
<string name="choose_preset_input">Choose preset input</string>
<string name="choose_local_file">Choose local file</string>
<string name="apply_effects">Apply effects</string>
<string name="ok">OK</string>
<string name="playlist_loading_error">Error loading playlist from %1$s: %2$s</string>
<string name="no_loaded_playlists_error">There are no loaded preset inputs.</string>
<string name="can_not_open_file_error">"File couldn't be opened. Please try again."</string>
<string name="permission_not_granted_error">"Permission was not granted."</string>
<string name="contrast">Contrast</string>
<string name="confetti_overlay">Confetti Overlay</string>
<string name="custom_text_overlay">Custom Text Overlay</string>
<string name="text">Text</string>
<string name="text_color">Text color</string>
<string name="unknown_color">Unknown color</string>
<string name="alpha">Alpha</string>
</resources>

View File

@ -1,31 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright 2024 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
https://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.
-->
<resources>
<!-- Base application theme. -->
<style name="Theme.Media3EffectDemo" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
<!-- Primary brand color. -->
<item name="colorPrimary">@color/purple_500</item>
<item name="colorPrimaryVariant">@color/purple_700</item>
<item name="colorOnPrimary">@color/white</item>
<!-- Secondary brand color. -->
<item name="colorSecondary">@color/teal_200</item>
<item name="colorSecondaryVariant">@color/teal_700</item>
<item name="colorOnSecondary">@color/black</item>
<!-- Status bar color. -->
<item name="android:statusBarColor">?attr/colorPrimaryVariant</item>
<!-- Customize your theme here. -->
</style>
</resources>

View File

@ -14,6 +14,7 @@
apply from: '../../constants.gradle'
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
android {
namespace 'androidx.media3.demo.main'
@ -89,7 +90,6 @@ dependencies {
withDecoderExtensionsImplementation project(modulePrefix + 'lib-decoder-iamf')
withDecoderExtensionsImplementation project(modulePrefix + 'lib-decoder-vp9')
withDecoderExtensionsImplementation project(modulePrefix + 'lib-decoder-midi')
withDecoderExtensionsImplementation project(modulePrefix + 'lib-decoder-mpegh')
withDecoderExtensionsImplementation project(modulePrefix + 'lib-datasource-rtmp')
}

View File

@ -257,10 +257,6 @@
{
"name": "Apple media playlist (AAC)",
"uri": "https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_4x3/gear0/prog_index.m3u8"
},
{
"name": "Bitmovin (FMP4)",
"uri": "https://bitdash-a.akamaihd.net/content/MI201109210084_1/m3u8s-fmp4/f08e80da-bf1d-4e3d-8899-f0f6155f6efa.m3u8"
}
]
},
@ -284,25 +280,15 @@
"name": "IMA sample ad tags",
"samples": [
{
"name": "VMAP pre-roll single ad, mid-roll standard pods with 5 ads every 10 seconds for 1:40, post-roll single ad",
"name": "Single inline linear",
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv",
"ad_tag_uri": "https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/ad_rule_samples&ciu_szs=300x250&ad_rule=1&impl=s&gdfp_req=1&env=vp&output=vmap&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ar%3Dpremidpostlongpod&cmsid=496&vid=short_tencue&correlator="
},
{
"name": "VMAP empty midroll",
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv",
"ad_tag_uri": "https://vastsynthesizer.appspot.com/empty-midroll"
"ad_tag_uri": "https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/single_ad_samples&ciu_szs=300x250&impl=s&gdfp_req=1&env=vp&output=vast&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ct%3Dlinear&correlator="
},
{
"name": "Single skippable inline",
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv",
"ad_tag_uri": "https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/single_ad_samples&ciu_szs=300x250&impl=s&gdfp_req=1&env=vp&output=vast&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ct%3Dskippablelinear&correlator="
},
{
"name": "Single inline linear",
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv",
"ad_tag_uri": "https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/single_ad_samples&ciu_szs=300x250&impl=s&gdfp_req=1&env=vp&output=vast&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ct%3Dlinear&correlator="
},
{
"name": "Single redirect linear",
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv",
@ -363,6 +349,16 @@
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv",
"ad_tag_uri": "https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/ad_rule_samples&ciu_szs=300x250&ad_rule=1&impl=s&gdfp_req=1&env=vp&output=vmap&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ar%3Dpremidpostoptimizedpodbumper&cmsid=496&vid=short_onecue&correlator="
},
{
"name": "VMAP pre-roll single ad, mid-roll standard pods with 5 ads every 10 seconds for 1:40, post-roll single ad",
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv",
"ad_tag_uri": "https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/ad_rule_samples&ciu_szs=300x250&ad_rule=1&impl=s&gdfp_req=1&env=vp&output=vmap&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ar%3Dpremidpostlongpod&cmsid=496&vid=short_tencue&correlator="
},
{
"name": "VMAP empty midroll",
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv",
"ad_tag_uri": "https://vastsynthesizer.appspot.com/empty-midroll"
},
{
"name": "VMAP full, empty, full midrolls",
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv",
@ -400,10 +396,6 @@
{
"name": "IMA DAI streams",
"samples": [
{
"name": "DASH VOD: Tears of Steel (11 periods, pre/mid/post), 2/5/2 ads [5/10s]",
"uri": "ssai://dai.google.com/?contentSourceId=2559737&videoId=tos-dash&format=0&adsId=1"
},
{
"name": "HLS VOD: Demo (skippable pre/post), single ads [30 s]",
"uri": "ssai://dai.google.com/?contentSourceId=2483977&videoId=ima-vod-skippable-test&format=2&adsId=1"
@ -416,6 +408,10 @@
"name": "HLS Live: Big Buck Bunny (mid), 3 ads [10/10/10s]",
"uri": "ssai://dai.google.com/?assetKey=sN_IYUG8STe1ZzhIIE_ksA&format=2&adsId=3"
},
{
"name": "DASH VOD: Tears of Steel (11 periods, pre/mid/post), 2/5/2 ads [5/10s]",
"uri": "ssai://dai.google.com/?contentSourceId=2559737&videoId=tos-dash&format=0&adsId=1"
},
{
"name": "DASH live: Tears of Steel (mid), 3 ads each [10 s]",
"uri": "ssai://dai.google.com/?assetKey=jNVjPZwzSkyeGiaNQTPqiQ&format=0&adsId=1"
@ -424,34 +420,6 @@
"name": "DASH live: New Tears of Steel (mid), 3 ads each [10 s]",
"uri": "ssai://dai.google.com/?assetKey=PSzZMzAkSXCmlJOWDmRj8Q&format=0&adsId=12"
},
{
"name": "Playlist: No ads - HLS live: Big Buck Bunny - No ads",
"playlist": [
{
"uri": "https://html5demos.com/assets/dizzy.mp4"
},
{
"uri": "ssai://dai.google.com/?assetKey=sN_IYUG8STe1ZzhIIE_ksA&format=2&adsId=3"
},
{
"uri": "https://html5demos.com/assets/dizzy.mp4"
}
]
},
{
"name": "Playlist: No ads - DASH live: Tears of Steel (mid) - No ads",
"playlist": [
{
"uri": "https://html5demos.com/assets/dizzy.mp4"
},
{
"uri": "ssai://dai.google.com/?assetKey=PSzZMzAkSXCmlJOWDmRj8Q&format=0&adsId=1"
},
{
"uri": "https://html5demos.com/assets/dizzy.mp4"
}
]
},
{
"name": "DASH live: Unencrypted stream with 30s ad breaks every minute",
"uri": "ssai://dai.google.com/?assetKey=0ndl1dJcRmKDUPxTRjvdog&format=0&adsId=21"
@ -512,6 +480,34 @@
"uri": "https://html5demos.com/assets/dizzy.mp4"
}
]
},
{
"name": "Playlist: No ads - DASH live: Tears of Steel (mid) - No ads",
"playlist": [
{
"uri": "https://html5demos.com/assets/dizzy.mp4"
},
{
"uri": "ssai://dai.google.com/?assetKey=PSzZMzAkSXCmlJOWDmRj8Q&format=0&adsId=1"
},
{
"uri": "https://html5demos.com/assets/dizzy.mp4"
}
]
},
{
"name": "Playlist: No ads - HLS live: Big Buck Bunny - No ads",
"playlist": [
{
"uri": "https://html5demos.com/assets/dizzy.mp4"
},
{
"uri": "ssai://dai.google.com/?assetKey=sN_IYUG8STe1ZzhIIE_ksA&format=2&adsId=3"
},
{
"uri": "https://html5demos.com/assets/dizzy.mp4"
}
]
}
]
},
@ -766,10 +762,6 @@
{
"name": "Immersive Audio Format Sample (MP4, IAMF)",
"uri": "https://github.com/AOMediaCodec/libiamf/raw/main/tests/test_000036_s.mp4"
},
{
"name": "MPEG-H HD (MP4, H265)",
"uri": "https://media.githubusercontent.com/media/Fraunhofer-IIS/mpegh-test-content/main/TRI_Fileset_17_514H_D1_D2_D3_O1_24bit1080p50.mp4"
}
]
},

View File

@ -315,7 +315,7 @@ public class DownloadTracker {
TrackSelectionDialog.createForTracksAndParameters(
/* titleId= */ R.string.exo_download_description,
tracks,
DownloadHelper.DEFAULT_TRACK_SELECTOR_PARAMETERS,
DownloadHelper.getDefaultTrackSelectorParameters(context),
/* allowAdaptiveSelections= */ false,
/* allowMultipleOverrides= */ true,
/* onTracksSelectedListener= */ this,

View File

@ -61,6 +61,7 @@ import androidx.media3.datasource.DataSourceUtil;
import androidx.media3.datasource.DataSpec;
import androidx.media3.exoplayer.RenderersFactory;
import androidx.media3.exoplayer.offline.DownloadService;
import com.google.common.base.Objects;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import java.io.IOException;
@ -73,7 +74,6 @@ import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.UUID;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
@ -524,7 +524,7 @@ public class SampleChooserActivity extends AppCompatActivity
private PlaylistGroup getGroup(String groupName, List<PlaylistGroup> groups) {
for (int i = 0; i < groups.size(); i++) {
if (Objects.equals(groupName, groups.get(i).title)) {
if (Objects.equal(groupName, groups.get(i).title)) {
return groups.get(i);
}
}

View File

@ -41,7 +41,8 @@ open class DemoMediaLibrarySessionCallback(context: Context) :
MediaItemTree.initialize(context.assets)
}
private val commandButtons: List<CommandButton> =
@OptIn(UnstableApi::class) // TODO: b/328238954 - Remove once new CommandButton icons are stable.
private val customLayoutCommandButtons: List<CommandButton> =
listOf(
CommandButton.Builder(CommandButton.ICON_SHUFFLE_OFF)
.setDisplayName(context.getString(R.string.exo_controls_shuffle_on_description))
@ -58,7 +59,7 @@ open class DemoMediaLibrarySessionCallback(context: Context) :
MediaSession.ConnectionResult.DEFAULT_SESSION_AND_LIBRARY_COMMANDS.buildUpon()
.also { builder ->
// Put all custom session commands in the list that may be used by the notification.
commandButtons.forEach { commandButton ->
customLayoutCommandButtons.forEach { commandButton ->
commandButton.sessionCommand?.let { builder.add(it) }
}
}
@ -77,13 +78,13 @@ open class DemoMediaLibrarySessionCallback(context: Context) :
session.isAutoCompanionController(controller)
) {
// Select the button to display.
val customButton = commandButtons[if (session.player.shuffleModeEnabled) 1 else 0]
val customLayout = customLayoutCommandButtons[if (session.player.shuffleModeEnabled) 1 else 0]
return MediaSession.ConnectionResult.AcceptedResultBuilder(session)
.setAvailableSessionCommands(mediaNotificationSessionCommands)
.setMediaButtonPreferences(ImmutableList.of(customButton))
.setCustomLayout(ImmutableList.of(customLayout))
.build()
}
// Default commands without media button preferences for common controllers.
// Default commands without custom layout for common controllers.
return MediaSession.ConnectionResult.AcceptedResultBuilder(session).build()
}
@ -97,19 +98,19 @@ open class DemoMediaLibrarySessionCallback(context: Context) :
if (CUSTOM_COMMAND_TOGGLE_SHUFFLE_MODE_ON == customCommand.customAction) {
// Enable shuffling.
session.player.shuffleModeEnabled = true
// Change the media button preferences to contain the `Disable shuffling` button.
session.setMediaButtonPreferences(
// Change the custom layout to contain the `Disable shuffling` command.
session.setCustomLayout(
session.mediaNotificationControllerInfo!!,
ImmutableList.of(commandButtons[1]),
ImmutableList.of(customLayoutCommandButtons[1]),
)
return Futures.immediateFuture(SessionResult(SessionResult.RESULT_SUCCESS))
} else if (CUSTOM_COMMAND_TOGGLE_SHUFFLE_MODE_OFF == customCommand.customAction) {
// Disable shuffling.
session.player.shuffleModeEnabled = false
// Change the media button preferences to contain the `Enable shuffling` button.
session.setMediaButtonPreferences(
// Change the custom layout to contain the `Enable shuffling` command.
session.setCustomLayout(
session.mediaNotificationControllerInfo!!,
ImmutableList.of(commandButtons[0]),
ImmutableList.of(customLayoutCommandButtons[0]),
)
return Futures.immediateFuture(SessionResult(SessionResult.RESULT_SUCCESS))
}

View File

@ -76,7 +76,6 @@ dependencies {
implementation 'androidx.appcompat:appcompat:' + androidxAppCompatVersion
implementation 'androidx.constraintlayout:constraintlayout:' + androidxConstraintLayoutVersion
implementation 'androidx.recyclerview:recyclerview:' + androidxRecyclerViewVersion
implementation 'androidx.window:window:' + androidxWindowVersion
implementation 'com.google.android.material:material:' + androidxMaterialVersion
implementation project(modulePrefix + 'lib-effect')
implementation project(modulePrefix + 'lib-exoplayer')

View File

@ -24,10 +24,6 @@
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO"/>
<!-- For media projection. -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PROJECTION"/>
<application
android:allowBackup="false"
android:icon="@mipmap/ic_launcher"
@ -68,9 +64,5 @@
android:label="@string/app_name"
android:exported="true"
android:theme="@style/Theme.MaterialComponents.DayNight.NoActionBar"/>
<service
android:name=".TransformerActivity$DemoMediaProjectionService"
android:foregroundServiceType="mediaProjection"
android:exported="false"/>
</application>
</manifest>

View File

@ -1,188 +0,0 @@
/*
* Copyright 2024 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 androidx.media3.demo.transformer;
import android.animation.FloatEvaluator;
import android.animation.Keyframe;
import android.animation.PropertyValuesHolder;
import android.animation.TypeEvaluator;
import android.animation.ValueAnimator;
import android.content.Context;
import android.content.pm.PackageManager;
import android.graphics.drawable.Drawable;
import android.os.Handler;
import android.util.Pair;
import android.view.animation.LinearInterpolator;
import androidx.media3.common.OverlaySettings;
import androidx.media3.common.VideoFrameProcessingException;
import androidx.media3.common.util.Util;
import androidx.media3.effect.DrawableOverlay;
import androidx.media3.effect.TextureOverlay;
import java.util.concurrent.CountDownLatch;
/**
* An animated {@link TextureOverlay} using {@link android.animation}.
*
* <p>The rotation is controlled by a simple {@link ValueAnimator}, while the position is controlled
* by key frames.
*/
public class AnimatedLogoOverlay extends DrawableOverlay {
private static final long ROTATION_PERIOD_MS = 2_000;
private static final long POSITION_PERIOD_MS = 5_000;
private static final float POSITION_X_BOUND = 0.8f;
private static final float POSITION_Y_BOUND = 0.7f;
private final Drawable logo;
private final AnimatedOverlaySettings overlaySettings;
public AnimatedLogoOverlay(Context context) {
try {
logo = context.getPackageManager().getApplicationIcon(context.getPackageName());
} catch (PackageManager.NameNotFoundException e) {
throw new IllegalStateException(e);
}
logo.setBounds(
/* left= */ 0, /* top= */ 0, logo.getIntrinsicWidth(), logo.getIntrinsicHeight());
ValueAnimator rotationAnimator = ValueAnimator.ofFloat(0, 360);
rotationAnimator.setRepeatMode(ValueAnimator.RESTART);
rotationAnimator.setRepeatCount(ValueAnimator.INFINITE);
rotationAnimator.setDuration(ROTATION_PERIOD_MS);
// Rotate the logo with a constant angular velocity.
rotationAnimator.setInterpolator(new LinearInterpolator());
Keyframe[] keyFrames = new Keyframe[5];
keyFrames[0] =
Keyframe.ofObject(/* fraction= */ 0f, Pair.create(-POSITION_X_BOUND, -POSITION_Y_BOUND));
keyFrames[2] =
Keyframe.ofObject(/* fraction= */ 0.5f, Pair.create(POSITION_X_BOUND, POSITION_Y_BOUND));
keyFrames[1] =
Keyframe.ofObject(/* fraction= */ 0.25f, Pair.create(-POSITION_X_BOUND, POSITION_Y_BOUND));
keyFrames[3] =
Keyframe.ofObject(/* fraction= */ 0.75f, Pair.create(POSITION_X_BOUND, -POSITION_Y_BOUND));
keyFrames[4] =
Keyframe.ofObject(/* fraction= */ 1f, Pair.create(-POSITION_X_BOUND, -POSITION_Y_BOUND));
PropertyValuesHolder positionValuesHolder =
PropertyValuesHolder.ofKeyframe("position", keyFrames);
ValueAnimator positionAnimator = ValueAnimator.ofPropertyValuesHolder(positionValuesHolder);
// The position can also be animated using separate animators for x and y, the purpose of
// PairEvaluator is to use one animator for both x and y.
positionAnimator.setEvaluator(new AnimatedOverlaySettings.PairEvaluator());
positionAnimator.setRepeatMode(ValueAnimator.RESTART);
positionAnimator.setRepeatCount(ValueAnimator.INFINITE);
positionAnimator.setDuration(POSITION_PERIOD_MS);
overlaySettings = new AnimatedOverlaySettings(rotationAnimator, positionAnimator);
}
@Override
public Drawable getDrawable(long presentationTimeUs) {
return logo;
}
@Override
public OverlaySettings getOverlaySettings(long presentationTimeUs) {
overlaySettings.setCurrentPresentationTimeUs(presentationTimeUs);
return overlaySettings;
}
@Override
public void release() throws VideoFrameProcessingException {
super.release();
overlaySettings.stopAnimation();
}
private static class AnimatedOverlaySettings implements OverlaySettings {
private final ValueAnimator rotationAnimator;
private final ValueAnimator positionAnimator;
private final Handler mainThreadHandler;
private boolean started;
public AnimatedOverlaySettings(ValueAnimator rotationAnimator, ValueAnimator positionAnimator) {
this.rotationAnimator = rotationAnimator;
this.positionAnimator = positionAnimator;
mainThreadHandler = new Handler(Util.getCurrentOrMainLooper());
}
public void setCurrentPresentationTimeUs(long presentationTimeUs) {
// Sets the animation time to the video presentation time, so the animation is presentation
// time based.
rotationAnimator.setCurrentPlayTime(presentationTimeUs / 1000);
positionAnimator.setCurrentPlayTime(presentationTimeUs / 1000);
}
@Override
public float getRotationDegrees() {
maybeStartAnimator();
return (float) rotationAnimator.getAnimatedValue();
}
@Override
public Pair<Float, Float> getBackgroundFrameAnchor() {
maybeStartAnimator();
return (Pair<Float, Float>) positionAnimator.getAnimatedValue();
}
public void stopAnimation() {
mainThreadHandler.post(
() -> {
rotationAnimator.cancel();
positionAnimator.cancel();
});
}
private void maybeStartAnimator() {
if (!started) {
CountDownLatch latch = new CountDownLatch(1);
mainThreadHandler.post(
() -> {
rotationAnimator.start();
positionAnimator.start();
latch.countDown();
});
try {
// Block until the animators are actually started, or they'll return null values.
latch.await();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new IllegalStateException(e);
}
started = true;
}
}
/** An {@link TypeEvaluator} to animate position in the form of {@link Pair} of floats. */
private static class PairEvaluator implements TypeEvaluator<Pair<Float, Float>> {
private final FloatEvaluator floatEvaluator;
private PairEvaluator() {
floatEvaluator = new FloatEvaluator();
}
@Override
public Pair<Float, Float> evaluate(
float fraction, Pair<Float, Float> startValue, Pair<Float, Float> endValue) {
return Pair.create(
floatEvaluator.evaluate(fraction, startValue.first, endValue.first),
floatEvaluator.evaluate(fraction, startValue.second, endValue.second));
}
}
}
}

View File

@ -1,106 +0,0 @@
/*
* Copyright 2024 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 androidx.media3.demo.transformer;
import static java.lang.Math.toRadians;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.PorterDuff;
import android.graphics.RectF;
import androidx.media3.common.OverlaySettings;
import androidx.media3.effect.CanvasOverlay;
import androidx.media3.effect.StaticOverlaySettings;
/* package */ final class ClockOverlay extends CanvasOverlay {
private static final int CLOCK_COLOR = Color.WHITE;
private static final int DIAL_SIZE = 200;
private static final float DIAL_WIDTH = 3.f;
private static final float NEEDLE_WIDTH = 3.f;
private static final int NEEDLE_LENGTH = DIAL_SIZE / 2 - 20;
private static final int CENTRE_X = DIAL_SIZE / 2;
private static final int CENTRE_Y = DIAL_SIZE / 2;
private static final int DIAL_INSET = 5;
private static final RectF DIAL_BOUND =
new RectF(
/* left= */ DIAL_INSET,
/* top= */ DIAL_INSET,
/* right= */ DIAL_SIZE - DIAL_INSET,
/* bottom= */ DIAL_SIZE - DIAL_INSET);
private static final int HUB_SIZE = 5;
private static final float BOTTOM_RIGHT_ANCHOR_X = 1.f;
private static final float BOTTOM_RIGHT_ANCHOR_Y = -1.f;
private static final float ANCHOR_INSET_X = 0.1f;
private static final float ANCHOR_INSET_Y = -0.1f;
private final Paint dialPaint;
private final Paint needlePaint;
private final Paint hubPaint;
public ClockOverlay() {
super(/* useInputFrameSize= */ false);
setCanvasSize(/* width= */ DIAL_SIZE, /* height= */ DIAL_SIZE);
dialPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
dialPaint.setStyle(Paint.Style.STROKE);
dialPaint.setStrokeWidth(DIAL_WIDTH);
dialPaint.setColor(CLOCK_COLOR);
needlePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
needlePaint.setStrokeWidth(NEEDLE_WIDTH);
needlePaint.setColor(CLOCK_COLOR);
hubPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
hubPaint.setColor(CLOCK_COLOR);
}
@Override
public void onDraw(Canvas canvas, long presentationTimeUs) {
// Clears the canvas
canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);
// Draw the dial
canvas.drawArc(
DIAL_BOUND, /* startAngle= */ 0, /* sweepAngle= */ 360, /* useCenter= */ false, dialPaint);
// Draw the needle
float angle = 6 * presentationTimeUs / 1_000_000.f - 90;
double radians = toRadians(angle);
float startX = CENTRE_X - (float) (10 * Math.cos(radians));
float startY = CENTRE_Y - (float) (10 * Math.sin(radians));
float endX = CENTRE_X + (float) (NEEDLE_LENGTH * Math.cos(radians));
float endY = CENTRE_Y + (float) (NEEDLE_LENGTH * Math.sin(radians));
canvas.drawLine(startX, startY, endX, endY, needlePaint);
// Draw a small hub at the center
canvas.drawCircle(CENTRE_X, CENTRE_Y, HUB_SIZE, hubPaint);
}
@Override
public OverlaySettings getOverlaySettings(long presentationTimeUs) {
return new StaticOverlaySettings.Builder()
.setBackgroundFrameAnchor(
BOTTOM_RIGHT_ANCHOR_X - ANCHOR_INSET_X, BOTTOM_RIGHT_ANCHOR_Y - ANCHOR_INSET_Y)
.setOverlayFrameAnchor(BOTTOM_RIGHT_ANCHOR_X, BOTTOM_RIGHT_ANCHOR_Y)
.build();
}
}

View File

@ -1,162 +0,0 @@
/*
* Copyright 2024 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 androidx.media3.demo.transformer;
import static androidx.media3.common.util.Assertions.checkStateNotNull;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.PorterDuff;
import android.os.Handler;
import androidx.annotation.Nullable;
import androidx.media3.common.VideoFrameProcessingException;
import androidx.media3.common.util.Size;
import androidx.media3.common.util.Util;
import androidx.media3.effect.CanvasOverlay;
import com.google.common.collect.ImmutableList;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
/** Mimics an emitter of confetti, dropping from the center of the frame. */
/* package */ final class ConfettiOverlay extends CanvasOverlay {
private static final ImmutableList<String> CONFETTI_TEXTS =
ImmutableList.of("", "", "", "✦︎", "♥︎", "☕︎");
private static final int EMITTER_POSITION_Y = -50;
private static final int CONFETTI_BASE_SIZE = 30;
private static final int CONFETTI_SIZE_VARIATION = 10;
private final List<Confetti> confettiList;
private final Random random;
private final Paint paint;
private final Handler handler;
@Nullable private Runnable runnable;
private int width;
private int height;
private boolean started;
public ConfettiOverlay() {
super(/* useInputFrameSize= */ true);
confettiList = new ArrayList<>();
random = new Random();
paint = new Paint();
paint.setAntiAlias(true);
handler = new Handler(Util.getCurrentOrMainLooper());
}
@Override
public void configure(Size videoSize) {
super.configure(videoSize);
this.width = videoSize.getWidth();
this.height = videoSize.getHeight();
}
@Override
public synchronized void onDraw(Canvas canvas, long presentationTimeUs) {
if (!started) {
start();
}
canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);
for (int i = 0; i < confettiList.size(); i++) {
Confetti confetti = confettiList.get(i);
if (confetti.y > (float) height / 2 || confetti.x <= 0 || confetti.x > width) {
confettiList.remove(confetti);
continue;
}
confetti.draw(canvas, paint);
confetti.update();
}
}
/** Starts the confetti. */
public void start() {
runnable = this::addConfetti;
handler.post(runnable);
started = true;
}
/** Stops the confetti. */
public void stop() {
checkStateNotNull(runnable);
handler.removeCallbacks(runnable);
confettiList.clear();
started = false;
runnable = null;
}
@Override
public void release() throws VideoFrameProcessingException {
super.release();
handler.post(this::stop);
}
private synchronized void addConfetti() {
for (int i = 0; i < 5; i++) {
confettiList.add(
new Confetti(
CONFETTI_TEXTS.get(Math.abs(random.nextInt()) % CONFETTI_TEXTS.size()),
random,
/* x= */ (float) width / 2,
/* y= */ EMITTER_POSITION_Y,
/* size= */ CONFETTI_BASE_SIZE + random.nextInt(CONFETTI_SIZE_VARIATION),
/* color= */ Color.HSVToColor(
new float[] {
/* hue= */ random.nextInt(360), /* saturation= */ 0.6f, /* value= */ 0.8f
})));
}
handler.postDelayed(this::addConfetti, /* delayMillis= */ 100);
}
private static final class Confetti {
private final String text;
private final float speedX;
private final float speedY;
private final int size;
private final int color;
private float x;
private float y;
public Confetti(String text, Random random, float x, float y, int size, int color) {
this.text = text;
this.x = x;
this.y = y;
this.size = size;
this.color = color;
speedX = 4 * (random.nextFloat() * 2 - 1); // Random speed in x direction
speedY = 4 * random.nextFloat(); // Random downward speed
}
/** Draws the {@code Confetti} on the {@link Canvas}. */
public void draw(Canvas canvas, Paint paint) {
canvas.save();
paint.setColor(color);
paint.setTextSize(size);
canvas.drawText(text, x, y, paint);
canvas.restore();
}
/** Updates the {@code Confetti}. */
public void update() {
x += speedX;
y += speedY;
}
}
}

View File

@ -77,8 +77,8 @@ public final class ConfigurationActivity extends AppCompatActivity {
public static final String ENABLE_ANALYZER_MODE = "enable_analyzer_mode";
public static final String ENABLE_DEBUG_PREVIEW = "enable_debug_preview";
public static final String ABORT_SLOW_EXPORT = "abort_slow_export";
public static final String USE_MEDIA3_MP4_MUXER = "use_media3_mp4_muxer";
public static final String USE_MEDIA3_FRAGMENTED_MP4_MUXER = "use_media3_fragmented_mp4_muxer";
public static final String USE_MEDIA3_MUXER = "use_media3_muxer";
public static final String PRODUCE_FRAGMENTED_MP4 = "produce_fragmented_mp4";
public static final String HDR_MODE = "hdr_mode";
public static final String AUDIO_EFFECTS_SELECTIONS = "audio_effects_selections";
public static final String VIDEO_EFFECTS_SELECTIONS = "video_effects_selections";
@ -114,17 +114,13 @@ public final class ConfigurationActivity extends AppCompatActivity {
public static final int OVERLAY_LOGO_AND_TIMER_INDEX = 10;
public static final int BITMAP_OVERLAY_INDEX = 11;
public static final int TEXT_OVERLAY_INDEX = 12;
public static final int CLOCK_OVERLAY_INDEX = 13;
public static final int CONFETTI_OVERLAY_INDEX = 14;
public static final int ANIMATING_LOGO_OVERLAY = 15;
// Audio effect selections.
public static final int HIGH_PITCHED_INDEX = 0;
public static final int SAMPLE_RATE_48K_INDEX = 1;
public static final int SAMPLE_RATE_96K_INDEX = 2;
public static final int SKIP_SILENCE_INDEX = 3;
public static final int CHANNEL_MIXING_INDEX = 4;
public static final int VOLUME_SCALING_INDEX = 5;
public static final int SAMPLE_RATE_INDEX = 1;
public static final int SKIP_SILENCE_INDEX = 2;
public static final int CHANNEL_MIXING_INDEX = 3;
public static final int VOLUME_SCALING_INDEX = 4;
// Color filter options.
public static final int COLOR_FILTER_GRAYSCALE = 0;
@ -177,8 +173,8 @@ public final class ConfigurationActivity extends AppCompatActivity {
private CheckBox enableDebugPreviewCheckBox;
private CheckBox enableDebugTracingCheckBox;
private CheckBox abortSlowExportCheckBox;
private CheckBox useMedia3Mp4Muxer;
private CheckBox useMedia3FragmentedMp4Muxer;
private CheckBox useMedia3Muxer;
private CheckBox produceFragmentedMp4CheckBox;
private Spinner hdrModeSpinner;
private Button selectAudioEffectsButton;
private Button selectVideoEffectsButton;
@ -266,8 +262,7 @@ public final class ConfigurationActivity extends AppCompatActivity {
MimeTypes.VIDEO_H264,
MimeTypes.VIDEO_H265,
MimeTypes.VIDEO_MP4V,
MimeTypes.VIDEO_AV1,
MimeTypes.VIDEO_DOLBY_VISION);
MimeTypes.VIDEO_AV1);
ArrayAdapter<String> resolutionHeightAdapter =
new ArrayAdapter<>(/* context= */ this, R.layout.spinner_item);
@ -304,18 +299,18 @@ public final class ConfigurationActivity extends AppCompatActivity {
(buttonView, isChecked) -> DebugTraceUtil.enableTracing = isChecked);
abortSlowExportCheckBox = findViewById(R.id.abort_slow_export_checkbox);
useMedia3Mp4Muxer = findViewById(R.id.use_media3_mp4_muxer_checkbox);
useMedia3FragmentedMp4Muxer = findViewById(R.id.use_media3_fragmented_mp4_muxer_checkbox);
useMedia3Mp4Muxer.setOnCheckedChangeListener(
useMedia3Muxer = findViewById(R.id.use_media3_muxer_checkbox);
produceFragmentedMp4CheckBox = findViewById(R.id.produce_fragmented_mp4_checkbox);
useMedia3Muxer.setOnCheckedChangeListener(
(buttonView, isChecked) -> {
if (isChecked) {
useMedia3FragmentedMp4Muxer.setChecked(false);
if (!isChecked) {
produceFragmentedMp4CheckBox.setChecked(false);
}
});
useMedia3FragmentedMp4Muxer.setOnCheckedChangeListener(
produceFragmentedMp4CheckBox.setOnCheckedChangeListener(
(buttonView, isChecked) -> {
if (isChecked) {
useMedia3Mp4Muxer.setChecked(false);
useMedia3Muxer.setChecked(true);
}
});
@ -408,8 +403,8 @@ public final class ConfigurationActivity extends AppCompatActivity {
bundle.putBoolean(ENABLE_ANALYZER_MODE, enableAnalyzerModeCheckBox.isChecked());
bundle.putBoolean(ENABLE_DEBUG_PREVIEW, enableDebugPreviewCheckBox.isChecked());
bundle.putBoolean(ABORT_SLOW_EXPORT, abortSlowExportCheckBox.isChecked());
bundle.putBoolean(USE_MEDIA3_MP4_MUXER, useMedia3Mp4Muxer.isChecked());
bundle.putBoolean(USE_MEDIA3_FRAGMENTED_MP4_MUXER, useMedia3FragmentedMp4Muxer.isChecked());
bundle.putBoolean(USE_MEDIA3_MUXER, useMedia3Muxer.isChecked());
bundle.putBoolean(PRODUCE_FRAGMENTED_MP4, produceFragmentedMp4CheckBox.isChecked());
String selectedHdrMode = String.valueOf(hdrModeSpinner.getSelectedItem());
bundle.putInt(HDR_MODE, HDR_MODE_DESCRIPTIONS.get(selectedHdrMode));
bundle.putBooleanArray(AUDIO_EFFECTS_SELECTIONS, audioEffectsSelections);

View File

@ -20,8 +20,7 @@ import android.text.Spannable;
import android.text.SpannableString;
import android.text.style.ForegroundColorSpan;
import androidx.media3.common.C;
import androidx.media3.common.OverlaySettings;
import androidx.media3.effect.StaticOverlaySettings;
import androidx.media3.effect.OverlaySettings;
import androidx.media3.effect.TextOverlay;
import androidx.media3.effect.TextureOverlay;
import java.util.Locale;
@ -32,11 +31,11 @@ import java.util.Locale;
*/
/* package */ final class TimerOverlay extends TextOverlay {
private final StaticOverlaySettings overlaySettings;
private final OverlaySettings overlaySettings;
public TimerOverlay() {
overlaySettings =
new StaticOverlaySettings.Builder()
new OverlaySettings.Builder()
// Place the timer in the bottom left corner of the screen with some padding from the
// edges.
.setOverlayFrameAnchor(/* x= */ -1f, /* y= */ -1f)

View File

@ -15,33 +15,25 @@
*/
package androidx.media3.demo.transformer;
import static android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PROJECTION;
import static android.Manifest.permission.READ_EXTERNAL_STORAGE;
import static android.Manifest.permission.READ_MEDIA_VIDEO;
import static androidx.media3.common.util.Assertions.checkNotNull;
import static androidx.media3.common.util.Assertions.checkState;
import static androidx.media3.common.util.Util.SDK_INT;
import static androidx.media3.exoplayer.DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS;
import static androidx.media3.exoplayer.DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_MS;
import static androidx.media3.transformer.Transformer.PROGRESS_STATE_NOT_STARTED;
import android.app.Activity;
import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.Service;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.PackageManager;
import android.graphics.Bitmap;
import android.graphics.Color;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.media.projection.MediaProjection;
import android.media.projection.MediaProjectionManager;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import android.text.Spannable;
import android.text.SpannableString;
import android.text.style.ForegroundColorSpan;
@ -53,13 +45,10 @@ import android.widget.Button;
import android.widget.ImageView;
import android.widget.TextView;
import android.widget.Toast;
import androidx.activity.result.contract.ActivityResultContracts;
import androidx.annotation.Nullable;
import androidx.annotation.StringRes;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.NotificationCompat;
import androidx.core.content.ContextCompat;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
import androidx.core.app.ActivityCompat;
import androidx.media3.common.C;
import androidx.media3.common.DebugViewProvider;
import androidx.media3.common.Effect;
@ -82,13 +71,13 @@ import androidx.media3.effect.GlShaderProgram;
import androidx.media3.effect.HslAdjustment;
import androidx.media3.effect.LanczosResample;
import androidx.media3.effect.OverlayEffect;
import androidx.media3.effect.OverlaySettings;
import androidx.media3.effect.Presentation;
import androidx.media3.effect.RgbAdjustment;
import androidx.media3.effect.RgbFilter;
import androidx.media3.effect.RgbMatrix;
import androidx.media3.effect.ScaleAndRotateTransformation;
import androidx.media3.effect.SingleColorLut;
import androidx.media3.effect.StaticOverlaySettings;
import androidx.media3.effect.TextOverlay;
import androidx.media3.effect.TextureOverlay;
import androidx.media3.exoplayer.DefaultLoadControl;
@ -103,16 +92,12 @@ import androidx.media3.transformer.Effects;
import androidx.media3.transformer.ExperimentalAnalyzerModeFactory;
import androidx.media3.transformer.ExportException;
import androidx.media3.transformer.ExportResult;
import androidx.media3.transformer.InAppFragmentedMp4Muxer;
import androidx.media3.transformer.InAppMp4Muxer;
import androidx.media3.transformer.InAppMuxer;
import androidx.media3.transformer.JsonUtil;
import androidx.media3.transformer.MediaProjectionAssetLoader;
import androidx.media3.transformer.ProgressHolder;
import androidx.media3.transformer.Transformer;
import androidx.media3.transformer.VideoEncoderSettings;
import androidx.media3.ui.AspectRatioFrameLayout;
import androidx.media3.ui.PlayerView;
import androidx.window.layout.WindowMetricsCalculator;
import com.google.android.material.card.MaterialCardView;
import com.google.android.material.progressindicator.LinearProgressIndicator;
import com.google.common.base.Stopwatch;
@ -124,6 +109,7 @@ import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.lang.reflect.Constructor;
import java.util.Arrays;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
@ -134,13 +120,12 @@ import org.json.JSONObject;
public final class TransformerActivity extends AppCompatActivity {
private static final String TAG = "TransformerActivity";
private static final int IMAGE_DURATION_MS = 5_000;
private static final int DEFAULT_FRAME_RATE_FPS = 30;
private static final int IMAGE_FRAME_RATE_FPS = 30;
private static int LOAD_CONTROL_MIN_BUFFER_MS = 5_000;
private static int LOAD_CONTROL_MAX_BUFFER_MS = 5_000;
private Button displayInputButton;
private MaterialCardView inputCardView;
private MaterialCardView outputCardView;
private TextView inputTextView;
private ImageView inputImageView;
private PlayerView inputPlayerView;
@ -152,13 +137,10 @@ public final class TransformerActivity extends AppCompatActivity {
private LinearProgressIndicator progressIndicator;
private Button pauseButton;
private Button resumeButton;
private Button stopCaptureButton;
private Stopwatch exportStopwatch;
private AspectRatioFrameLayout debugFrame;
@Nullable private DebugTextViewHelper debugTextViewHelper;
@Nullable private Intent screenCaptureToken;
@Nullable private MediaProjection mediaProjection;
@Nullable private ExoPlayer inputPlayer;
@Nullable private ExoPlayer outputPlayer;
@Nullable private Transformer transformer;
@ -171,7 +153,6 @@ public final class TransformerActivity extends AppCompatActivity {
setContentView(R.layout.transformer_activity);
inputCardView = findViewById(R.id.input_card_view);
outputCardView = findViewById(R.id.output_card_view);
inputTextView = findViewById(R.id.input_text_view);
inputImageView = findViewById(R.id.input_image_view);
inputPlayerView = findViewById(R.id.input_player_view);
@ -185,8 +166,6 @@ public final class TransformerActivity extends AppCompatActivity {
pauseButton.setOnClickListener(view -> pauseExport());
resumeButton = findViewById(R.id.resume_button);
resumeButton.setOnClickListener(view -> startExport());
stopCaptureButton = findViewById(R.id.stop_capture_button);
stopCaptureButton.setOnClickListener(view -> mediaProjection.stop());
debugFrame = findViewById(R.id.debug_aspect_ratio_frame_layout);
displayInputButton = findViewById(R.id.display_input_button);
displayInputButton.setOnClickListener(view -> toggleInputVideoDisplay());
@ -205,10 +184,7 @@ public final class TransformerActivity extends AppCompatActivity {
protected void onStart() {
super.onStart();
// Restart exporting, unless this is a capture session which can run in the background.
if (!isUsingMediaProjection()) {
startExport();
}
inputPlayerView.onResume();
outputPlayerView.onResume();
@ -218,72 +194,32 @@ public final class TransformerActivity extends AppCompatActivity {
protected void onStop() {
super.onStop();
if (transformer != null) {
transformer.cancel();
transformer = null;
}
// The stop watch is reset after cancelling the export, in case cancelling causes the stop watch
// to be stopped in a transformer callback.
exportStopwatch.reset();
inputPlayerView.onPause();
outputPlayerView.onPause();
releasePlayer();
// Keep the capture session going to allow capturing other apps while backgrounded.
if (!isUsingMediaProjection()) {
releasePlayers();
cleanUpExport();
outputFile.delete();
outputFile = null;
if (oldOutputFile != null) {
oldOutputFile.delete();
oldOutputFile = null;
}
}
@Override
protected void onDestroy() {
super.onDestroy();
if (isUsingMediaProjection()) {
releasePlayers();
mediaProjection.stop();
mediaProjection = null;
screenCaptureToken = null;
}
cleanUpExport();
}
private void startExport() {
requestReadVideoPermission(/* activity= */ this);
Intent intent = getIntent();
Uri inputUri = checkNotNull(intent.getData());
if (inputUri.toString().equals("transformer_surface_asset:media_projection")
&& screenCaptureToken == null) {
// MediaProjection can only start once the foreground service is running.
MediaProjectionManager mediaProjectionManager =
(MediaProjectionManager) getSystemService(MEDIA_PROJECTION_SERVICE);
Context context = this;
LocalBroadcastManager.getInstance(context)
.registerReceiver(
new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
String action = checkNotNull(intent.getAction());
if (action.equals(DemoMediaProjectionService.ACTION_EVENT_STARTED)) {
LocalBroadcastManager.getInstance(context)
.unregisterReceiver(/* receiver= */ this);
// The service has started so media projection can start.
startExport();
}
}
},
new IntentFilter(DemoMediaProjectionService.ACTION_EVENT_STARTED));
registerForActivityResult(
new ActivityResultContracts.StartActivityForResult(),
activityResult -> {
int resultCode = activityResult.getResultCode();
if (resultCode == RESULT_OK) {
screenCaptureToken = activityResult.getData();
Intent startServiceIntent = new Intent(context, DemoMediaProjectionService.class);
ContextCompat.startForegroundService(context, startServiceIntent);
} else if (resultCode == RESULT_CANCELED) {
finish();
}
})
.launch(mediaProjectionManager.createScreenCaptureIntent());
inputCardView.setVisibility(View.GONE);
outputCardView.setVisibility(View.GONE);
return;
}
try {
outputFile =
createExternalCacheFile("transformer-output-" + Clock.DEFAULT.elapsedRealtime() + ".mp4");
@ -293,7 +229,6 @@ public final class TransformerActivity extends AppCompatActivity {
String outputFilePath = outputFile.getAbsolutePath();
@Nullable Bundle bundle = intent.getExtras();
MediaItem mediaItem = createMediaItem(bundle, inputUri);
Util.maybeRequestReadStoragePermission(/* activity= */ this, mediaItem);
Transformer transformer = createTransformer(bundle, inputUri, outputFilePath);
Composition composition = createComposition(mediaItem, bundle);
exportStopwatch.reset();
@ -310,20 +245,10 @@ public final class TransformerActivity extends AppCompatActivity {
outputVideoTextView.setVisibility(View.GONE);
debugTextView.setVisibility(View.GONE);
informationTextView.setText(R.string.export_started);
outputCardView.setVisibility(View.VISIBLE);
progressViewGroup.setVisibility(View.VISIBLE);
pauseButton.setVisibility(View.VISIBLE);
resumeButton.setVisibility(View.GONE);
progressIndicator.setProgress(0);
if (isUsingMediaProjection()) {
pauseButton.setVisibility(View.GONE);
resumeButton.setVisibility(View.GONE);
stopCaptureButton.setVisibility(View.VISIBLE);
} else {
pauseButton.setVisibility(View.VISIBLE);
resumeButton.setVisibility(View.GONE);
stopCaptureButton.setVisibility(View.GONE);
}
Handler mainHandler = new Handler(getMainLooper());
ProgressHolder progressHolder = new ProgressHolder();
mainHandler.post(
@ -389,16 +314,21 @@ public final class TransformerActivity extends AppCompatActivity {
transformerBuilder.setVideoMimeType(videoMimeType);
}
transformerBuilder.setEncoderFactory(
new DefaultEncoderFactory.Builder(this.getApplicationContext())
.setEnableFallback(bundle.getBoolean(ConfigurationActivity.ENABLE_FALLBACK))
.build());
if (!bundle.getBoolean(ConfigurationActivity.ABORT_SLOW_EXPORT)) {
transformerBuilder.setMaxDelayBetweenMuxerSamplesMs(C.TIME_UNSET);
}
if (bundle.getBoolean(ConfigurationActivity.USE_MEDIA3_MP4_MUXER)) {
transformerBuilder.setMuxerFactory(new InAppMp4Muxer.Factory());
}
if (bundle.getBoolean(ConfigurationActivity.USE_MEDIA3_FRAGMENTED_MP4_MUXER)) {
transformerBuilder.setMuxerFactory(new InAppFragmentedMp4Muxer.Factory());
if (bundle.getBoolean(ConfigurationActivity.USE_MEDIA3_MUXER)) {
transformerBuilder.setMuxerFactory(
new InAppMuxer.Factory.Builder()
.setOutputFragmentedMp4(
bundle.getBoolean(ConfigurationActivity.PRODUCE_FRAGMENTED_MP4))
.build());
}
if (bundle.getBoolean(ConfigurationActivity.ENABLE_DEBUG_PREVIEW)) {
@ -411,32 +341,6 @@ public final class TransformerActivity extends AppCompatActivity {
}
}
VideoEncoderSettings videoEncoderSettings = VideoEncoderSettings.DEFAULT;
if (screenCaptureToken != null) {
MediaProjectionManager mediaProjectionManager =
(MediaProjectionManager) getSystemService(MEDIA_PROJECTION_SERVICE);
MediaProjection mediaProjection =
mediaProjectionManager.getMediaProjection(RESULT_OK, checkNotNull(screenCaptureToken));
Rect bounds =
WindowMetricsCalculator.getOrCreate()
.computeCurrentWindowMetrics(/* activity= */ this)
.getBounds();
int densityDpi = getResources().getConfiguration().densityDpi;
transformerBuilder.setAssetLoaderFactory(
new MediaProjectionAssetLoader.Factory(mediaProjection, bounds, densityDpi));
this.mediaProjection = mediaProjection;
videoEncoderSettings =
videoEncoderSettings
.buildUpon()
.setRepeatPreviousFrameIntervalUs(C.MICROS_PER_SECOND / DEFAULT_FRAME_RATE_FPS)
.build();
}
transformerBuilder.setEncoderFactory(
new DefaultEncoderFactory.Builder(this.getApplicationContext())
.setEnableFallback(bundle.getBoolean(ConfigurationActivity.ENABLE_FALLBACK))
.setRequestedVideoEncoderSettings(videoEncoderSettings)
.build());
return transformerBuilder.build();
}
@ -455,7 +359,7 @@ public final class TransformerActivity extends AppCompatActivity {
private Composition createComposition(MediaItem mediaItem, @Nullable Bundle bundle) {
EditedMediaItem.Builder editedMediaItemBuilder = new EditedMediaItem.Builder(mediaItem);
// For image inputs. Automatically ignored if input is audio/video.
editedMediaItemBuilder.setFrameRate(DEFAULT_FRAME_RATE_FPS);
editedMediaItemBuilder.setFrameRate(IMAGE_FRAME_RATE_FPS);
if (bundle != null) {
ImmutableList<AudioProcessor> audioProcessors = createAudioProcessorsFromBundle(bundle);
ImmutableList<Effect> videoEffects = createVideoEffectsFromBundle(bundle);
@ -490,18 +394,14 @@ public final class TransformerActivity extends AppCompatActivity {
ImmutableList.Builder<AudioProcessor> processors = new ImmutableList.Builder<>();
if (selectedAudioEffects[ConfigurationActivity.HIGH_PITCHED_INDEX]
|| selectedAudioEffects[ConfigurationActivity.SAMPLE_RATE_48K_INDEX]
|| selectedAudioEffects[ConfigurationActivity.SAMPLE_RATE_96K_INDEX]) {
|| selectedAudioEffects[ConfigurationActivity.SAMPLE_RATE_INDEX]) {
SonicAudioProcessor sonicAudioProcessor = new SonicAudioProcessor();
if (selectedAudioEffects[ConfigurationActivity.HIGH_PITCHED_INDEX]) {
sonicAudioProcessor.setPitch(2f);
}
if (selectedAudioEffects[ConfigurationActivity.SAMPLE_RATE_48K_INDEX]) {
if (selectedAudioEffects[ConfigurationActivity.SAMPLE_RATE_INDEX]) {
sonicAudioProcessor.setOutputSampleRateHz(48_000);
}
if (selectedAudioEffects[ConfigurationActivity.SAMPLE_RATE_96K_INDEX]) {
sonicAudioProcessor.setOutputSampleRateHz(96_000);
}
processors.add(sonicAudioProcessor);
}
@ -517,9 +417,20 @@ public final class TransformerActivity extends AppCompatActivity {
if (mixToMono || scaleVolumeToHalf) {
ChannelMixingAudioProcessor mixingAudioProcessor = new ChannelMixingAudioProcessor();
for (int inputChannelCount = 1; inputChannelCount <= 6; inputChannelCount++) {
ChannelMixingMatrix matrix =
ChannelMixingMatrix.createForConstantPower(
inputChannelCount, /* outputChannelCount= */ mixToMono ? 1 : inputChannelCount);
ChannelMixingMatrix matrix;
if (mixToMono) {
float[] mixingCoefficients = new float[inputChannelCount];
// Each channel is equally weighted in the mix to mono.
Arrays.fill(mixingCoefficients, 1f / inputChannelCount);
matrix =
new ChannelMixingMatrix(
inputChannelCount, /* outputChannelCount= */ 1, mixingCoefficients);
} else {
// Identity matrix.
matrix =
ChannelMixingMatrix.create(
inputChannelCount, /* outputChannelCount= */ inputChannelCount);
}
// Apply the volume adjustment.
mixingAudioProcessor.putChannelMixingMatrix(
@ -688,8 +599,8 @@ public final class TransformerActivity extends AppCompatActivity {
private OverlayEffect createOverlayEffectFromBundle(Bundle bundle, boolean[] selectedEffects) {
ImmutableList.Builder<TextureOverlay> overlaysBuilder = new ImmutableList.Builder<>();
if (selectedEffects[ConfigurationActivity.OVERLAY_LOGO_AND_TIMER_INDEX]) {
StaticOverlaySettings logoSettings =
new StaticOverlaySettings.Builder()
OverlaySettings logoSettings =
new OverlaySettings.Builder()
// Place the logo in the bottom left corner of the screen with some padding from the
// edges.
.setOverlayFrameAnchor(/* x= */ -1f, /* y= */ -1f)
@ -708,8 +619,8 @@ public final class TransformerActivity extends AppCompatActivity {
overlaysBuilder.add(logoOverlay, timerOverlay);
}
if (selectedEffects[ConfigurationActivity.BITMAP_OVERLAY_INDEX]) {
StaticOverlaySettings overlaySettings =
new StaticOverlaySettings.Builder()
OverlaySettings overlaySettings =
new OverlaySettings.Builder()
.setAlphaScale(
bundle.getFloat(
ConfigurationActivity.BITMAP_OVERLAY_ALPHA, /* defaultValue= */ 1))
@ -722,8 +633,8 @@ public final class TransformerActivity extends AppCompatActivity {
overlaysBuilder.add(bitmapOverlay);
}
if (selectedEffects[ConfigurationActivity.TEXT_OVERLAY_INDEX]) {
StaticOverlaySettings overlaySettings =
new StaticOverlaySettings.Builder()
OverlaySettings overlaySettings =
new OverlaySettings.Builder()
.setAlphaScale(
bundle.getFloat(ConfigurationActivity.TEXT_OVERLAY_ALPHA, /* defaultValue= */ 1))
.build();
@ -737,15 +648,6 @@ public final class TransformerActivity extends AppCompatActivity {
TextOverlay textOverlay = TextOverlay.createStaticTextOverlay(overlayText, overlaySettings);
overlaysBuilder.add(textOverlay);
}
if (selectedEffects[ConfigurationActivity.CLOCK_OVERLAY_INDEX]) {
overlaysBuilder.add(new ClockOverlay());
}
if (selectedEffects[ConfigurationActivity.CONFETTI_OVERLAY_INDEX]) {
overlaysBuilder.add(new ConfettiOverlay());
}
if (selectedEffects[ConfigurationActivity.ANIMATING_LOGO_OVERLAY]) {
overlaysBuilder.add(new AnimatedLogoOverlay(this.getApplicationContext()));
}
ImmutableList<TextureOverlay> overlays = overlaysBuilder.build();
return overlays.isEmpty() ? null : new OverlayEffect(overlays);
@ -756,9 +658,6 @@ public final class TransformerActivity extends AppCompatActivity {
informationTextView.setText(R.string.export_error);
progressViewGroup.setVisibility(View.GONE);
debugFrame.removeAllViews();
if (isUsingMediaProjection()) {
mediaProjection.stop();
}
Toast.makeText(getApplicationContext(), "Export error: " + exportException, Toast.LENGTH_LONG)
.show();
Log.e(TAG, "Export error", exportException);
@ -802,8 +701,9 @@ public final class TransformerActivity extends AppCompatActivity {
private void playMediaItems(MediaItem inputMediaItem, MediaItem outputMediaItem) {
inputPlayerView.setPlayer(null);
outputPlayerView.setPlayer(null);
releasePlayers();
releasePlayer();
Uri uri = checkNotNull(inputMediaItem.localConfiguration).uri;
ExoPlayer outputPlayer =
new ExoPlayer.Builder(/* context= */ this)
.setLoadControl(
@ -822,7 +722,6 @@ public final class TransformerActivity extends AppCompatActivity {
this.outputPlayer = outputPlayer;
// Only support showing jpg images.
Uri uri = checkNotNull(inputMediaItem.localConfiguration).uri;
if (uri.toString().endsWith("jpg")) {
inputPlayerView.setVisibility(View.GONE);
inputImageView.setVisibility(View.VISIBLE);
@ -836,12 +735,6 @@ public final class TransformerActivity extends AppCompatActivity {
} catch (ExecutionException | InterruptedException e) {
throw new IllegalArgumentException("Failed to load bitmap.", e);
}
} else if (isUsingMediaProjection()) {
inputCardView.setVisibility(View.GONE);
displayInputButton.setVisibility(View.GONE);
Intent stopIntent = new Intent(/* context= */ this, DemoMediaProjectionService.class);
stopIntent.setAction(DemoMediaProjectionService.ACTION_STOP);
ContextCompat.startForegroundService(/* context= */ this, stopIntent);
} else {
inputPlayerView.setVisibility(View.VISIBLE);
inputImageView.setVisibility(View.GONE);
@ -892,7 +785,7 @@ public final class TransformerActivity extends AppCompatActivity {
}
}
private void releasePlayers() {
private void releasePlayer() {
if (debugTextViewHelper != null) {
debugTextViewHelper.stop();
debugTextViewHelper = null;
@ -907,22 +800,12 @@ public final class TransformerActivity extends AppCompatActivity {
}
}
private void cleanUpExport() {
if (transformer != null) {
transformer.cancel();
transformer = null;
private static void requestReadVideoPermission(AppCompatActivity activity) {
String permission = SDK_INT >= 33 ? READ_MEDIA_VIDEO : READ_EXTERNAL_STORAGE;
if (ActivityCompat.checkSelfPermission(activity, permission)
!= PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(activity, new String[] {permission}, /* requestCode= */ 0);
}
if (outputFile != null) {
outputFile.delete();
outputFile = null;
}
if (oldOutputFile != null) {
oldOutputFile.delete();
oldOutputFile = null;
}
// The stop watch is reset after cancelling the export, in case cancelling causes the stop watch
// to be stopped in a transformer callback.
exportStopwatch.reset();
}
private void showToast(@StringRes int messageResource) {
@ -954,54 +837,6 @@ public final class TransformerActivity extends AppCompatActivity {
oldOutputFile = outputFile;
}
private boolean isUsingMediaProjection() {
return mediaProjection != null;
}
/** Foreground service that's required by the media projection APIs. */
public static final class DemoMediaProjectionService extends Service {
private static final String CHANNEL_ID = "DemoMediaProjectionServiceChannel";
private static final String CHANNEL_NAME = "Media projection";
private static final int NOTIFICATION_ID = 1;
private static final String ACTION_EVENT_STARTED = "started";
private static final String ACTION_STOP = "stop";
@Override
@Nullable
public IBinder onBind(Intent intent) {
return null;
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
if (ACTION_STOP.equals(intent.getAction())) {
stopSelf();
} else {
Context context = this;
Notification notification =
new NotificationCompat.Builder(context, CHANNEL_ID)
.setOngoing(true)
.setSmallIcon(R.drawable.exo_icon_play)
.build();
if (Util.SDK_INT >= 26) {
NotificationChannel channel =
new NotificationChannel(
CHANNEL_ID, CHANNEL_NAME, NotificationManager.IMPORTANCE_HIGH);
NotificationManager manager = getSystemService(NotificationManager.class);
manager.createNotificationChannel(channel);
}
if (Util.SDK_INT >= 29) {
startForeground(NOTIFICATION_ID, notification, FOREGROUND_SERVICE_TYPE_MEDIA_PROJECTION);
} else {
startForeground(NOTIFICATION_ID, notification);
}
// Notify that the service is started (and it's now safe to set up media projection).
LocalBroadcastManager.getInstance(context).sendBroadcast(new Intent(ACTION_EVENT_STARTED));
}
return START_STICKY;
}
}
private final class DemoDebugViewProvider implements DebugViewProvider {
@Nullable private SurfaceView surfaceView;

View File

@ -239,18 +239,18 @@
android:layout_weight="1">
<TextView
android:layout_gravity="center_vertical"
android:text="@string/use_media3_mp4_muxer" />
android:text="@string/use_media3_muxer" />
<CheckBox
android:id="@+id/use_media3_mp4_muxer_checkbox"
android:id="@+id/use_media3_muxer_checkbox"
android:layout_gravity="end"/>
</TableRow>
<TableRow
android:layout_weight="1">
<TextView
android:layout_gravity="center_vertical"
android:text="@string/use_media3_fragmented_mp4_muxer" />
android:text="@string/produce_fragmented_mp4" />
<CheckBox
android:id="@+id/use_media3_fragmented_mp4_muxer_checkbox"
android:id="@+id/produce_fragmented_mp4_checkbox"
android:layout_gravity="end"/>
</TableRow>
<TableRow

View File

@ -165,12 +165,6 @@
android:layout_width="match_parent"
android:text="@string/resume"/>
<Button
android:id="@+id/stop_capture_button"
android:layout_height="wrap_content"
android:layout_width="match_parent"
android:text="@string/stop_capture"/>
<androidx.media3.ui.AspectRatioFrameLayout
android:id="@+id/debug_aspect_ratio_frame_layout"
android:layout_width="match_parent"

View File

@ -28,14 +28,10 @@
<item>Overlay logo and timer</item>
<item>Custom Bitmap Overlay</item>
<item>Custom Text Overlay</item>
<item>Clock Overlay</item>
<item>Confetti Overlay</item>
<item>Animated logo</item>
</string-array>
<string-array name="audio_effects_names">
<item>High pitched</item>
<item>Sample rate of 48000Hz</item>
<item>Sample rate of 96000Hz</item>
<item>Skip silence</item>
<item>Mix channels into mono</item>
<item>Scale volume to 50%</item>
@ -58,7 +54,6 @@
<item>HDR (HDR10+) H265 limited range video (encoding may fail)</item>
<item>HDR (HLG) H265 limited range video (encoding may fail)</item>
<item>720p H264 video with no audio (B-frames)</item>
<item>Record screen</item>
</string-array>
<string-array name="preset_uris">
<item>https://storage.googleapis.com/exoplayer-test-media-1/mp4/android-screens-10s.mp4</item>
@ -78,6 +73,5 @@
<item>https://storage.googleapis.com/exoplayer-test-media-1/mp4/samsung-s21-hdr-hdr10.mp4</item>
<item>https://storage.googleapis.com/exoplayer-test-media-1/mp4/Pixel7Pro_HLG_1080P.mp4</item>
<item>https://storage.googleapis.com/exoplayer-test-media-1/mp4/sample_video_track_only.mp4</item>
<item>transformer_surface_asset:media_projection</item>
</string-array>
</resources>

View File

@ -32,8 +32,8 @@
<string name="enable_debug_preview" translatable="false">Enable debug preview</string>
<string name="enable_debug_tracing" translatable="false">Enable debug tracing</string>
<string name="abort_slow_export" translatable="false">Abort slow export</string>
<string name="use_media3_mp4_muxer" translatable="false">Use Media3 Mp4Muxer</string>
<string name="use_media3_fragmented_mp4_muxer" translatable="false">Use Media3 FragmentedMp4Muxer</string>
<string name="use_media3_muxer" translatable="false">Use Media3 muxer</string>
<string name="produce_fragmented_mp4" translatable="false">Produce fragmented MP4</string>
<string name="trim" translatable="false">Trim</string>
<string name="hdr_mode" translatable="false">HDR mode</string>
<string name="select_audio_effects" translatable="false">Add audio effects</string>
@ -44,7 +44,6 @@
<string name="debug_preview" translatable="false">Debug preview:</string>
<string name="pause" translatable="false">Pause</string>
<string name="resume" translatable="false">Resume</string>
<string name="stop_capture" translatable="false">Stop capture</string>
<string name="debug_preview_not_available" translatable="false">No debug preview available.</string>
<string name="export_started" translatable="false">Export started</string>
<string name="export_timer" translatable="false">Export started %d seconds ago.</string>

View File

@ -24,9 +24,9 @@ android {
}
dependencies {
api 'com.google.android.gms:play-services-cast-framework:21.5.0'
api 'com.google.android.gms:play-services-cast-framework:21.3.0'
implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion
api project(modulePrefix + 'lib-common')
implementation project(modulePrefix + 'lib-common')
compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion
compileOnly 'org.jetbrains.kotlin:kotlin-annotations-jvm:' + kotlinAnnotationsVersion
testImplementation project(modulePrefix + 'test-utils')

View File

@ -15,8 +15,8 @@
*/
package androidx.media3.cast;
import static androidx.annotation.VisibleForTesting.PROTECTED;
import static androidx.media3.common.util.Assertions.checkArgument;
import static androidx.media3.common.util.Assertions.checkNotNull;
import static androidx.media3.common.util.Util.SDK_INT;
import static androidx.media3.common.util.Util.castNonNull;
import static java.lang.Math.min;
@ -74,7 +74,6 @@ import com.google.android.gms.common.api.PendingResult;
import com.google.android.gms.common.api.ResultCallback;
import com.google.common.collect.ImmutableList;
import java.util.List;
import java.util.Objects;
import org.checkerframework.checker.nullness.qual.RequiresNonNull;
/**
@ -167,7 +166,6 @@ public final class CastPlayer extends BasePlayer {
private long pendingSeekPositionMs;
@Nullable private PositionInfo pendingMediaItemRemovalPosition;
private MediaMetadata mediaMetadata;
private MediaMetadata playlistMetadata;
private DeviceInfo deviceInfo;
/**
@ -270,7 +268,6 @@ public final class CastPlayer extends BasePlayer {
playbackState = STATE_IDLE;
currentTimeline = CastTimeline.EMPTY_CAST_TIMELINE;
mediaMetadata = MediaMetadata.EMPTY;
playlistMetadata = MediaMetadata.EMPTY;
currentTracks = Tracks.EMPTY;
availableCommands = new Commands.Builder().addAll(PERMANENT_AVAILABLE_COMMANDS).build();
pendingSeekWindowIndex = C.INDEX_UNSET;
@ -470,7 +467,8 @@ public final class CastPlayer extends BasePlayer {
// onPositionDiscontinuity(PositionInfo, PositionInfo, @DiscontinuityReason int).
@SuppressWarnings("deprecation")
@Override
protected void seekTo(
@VisibleForTesting(otherwise = PROTECTED)
public void seekTo(
int mediaItemIndex,
long positionMs,
@Player.Command int seekCommand,
@ -641,7 +639,7 @@ public final class CastPlayer extends BasePlayer {
@Override
public TrackSelectionParameters getTrackSelectionParameters() {
return TrackSelectionParameters.DEFAULT;
return TrackSelectionParameters.DEFAULT_WITHOUT_CONTEXT;
}
@Override
@ -659,19 +657,14 @@ public final class CastPlayer extends BasePlayer {
@Override
public MediaMetadata getPlaylistMetadata() {
return playlistMetadata;
// CastPlayer does not currently support metadata.
return MediaMetadata.EMPTY;
}
/** This method is not supported and does nothing. */
@Override
public void setPlaylistMetadata(MediaMetadata playlistMetadata) {
checkNotNull(playlistMetadata);
if (playlistMetadata.equals(this.playlistMetadata)) {
return;
}
this.playlistMetadata = playlistMetadata;
listeners.sendEvent(
EVENT_PLAYLIST_METADATA_CHANGED,
listener -> listener.onPlaylistMetadataChanged(this.playlistMetadata));
public void setPlaylistMetadata(MediaMetadata mediaMetadata) {
// CastPlayer does not currently support metadata.
}
@Override
@ -918,7 +911,7 @@ public final class CastPlayer extends BasePlayer {
? currentTimeline.getPeriod(currentWindowIndex, period, /* setIds= */ true).uid
: null;
if (!playingPeriodChangedByTimelineChange
&& !Objects.equals(oldPeriodUid, currentPeriodUid)
&& !Util.areEqual(oldPeriodUid, currentPeriodUid)
&& pendingSeekCount == 0) {
// Report discontinuity and media item auto transition.
currentTimeline.getPeriod(oldWindowIndex, period, /* setIds= */ true);

View File

@ -1800,7 +1800,7 @@ public class CastPlayerTest {
}
@Test
public void setMediaItems_doesNotifyOnMediaMetadataChanged() {
public void setMediaItems_doesNotifyOnMetadataChanged() {
when(mockRemoteMediaClient.queueJumpToItem(anyInt(), anyLong(), eq(null)))
.thenReturn(mockPendingResult);
ArgumentCaptor<MediaMetadata> metadataCaptor = ArgumentCaptor.forClass(MediaMetadata.class);
@ -1827,7 +1827,7 @@ public class CastPlayerTest {
.build());
castPlayer.addListener(mockListener);
MediaMetadata initialMetadata = castPlayer.getMediaMetadata();
MediaMetadata intitalMetadata = castPlayer.getMediaMetadata();
castPlayer.setMediaItems(firstPlaylist, /* startIndex= */ 0, /* startPositionMs= */ 2000L);
updateTimeLine(firstPlaylist, /* mediaQueueItemIds= */ new int[] {1}, /* currentItemId= */ 1);
MediaMetadata firstMetadata = castPlayer.getMediaMetadata();
@ -1850,7 +1850,7 @@ public class CastPlayerTest {
secondPlaylist.get(1).mediaMetadata,
secondPlaylist.get(0).mediaMetadata)
.inOrder();
assertThat(initialMetadata).isEqualTo(MediaMetadata.EMPTY);
assertThat(intitalMetadata).isEqualTo(MediaMetadata.EMPTY);
assertThat(ImmutableList.of(firstMetadata, secondMetadata, thirdMetadata))
.containsExactly(
firstPlaylist.get(0).mediaMetadata,
@ -1898,35 +1898,6 @@ public class CastPlayerTest {
verify(mockListener, never()).onMediaMetadataChanged(any());
}
@Test
public void setPlaylistMetadata_doesNotifyOnPlaylistMetadataChanged() {
castPlayer.addListener(mockListener);
MediaMetadata metadata = new MediaMetadata.Builder().setArtist("foo").build();
assertThat(castPlayer.getPlaylistMetadata()).isEqualTo(MediaMetadata.EMPTY);
castPlayer.setPlaylistMetadata(metadata);
assertThat(castPlayer.getPlaylistMetadata()).isEqualTo(metadata);
verify(mockListener).onPlaylistMetadataChanged(metadata);
}
@Test
public void setPlaylistMetadata_equalMetadata_doesNotNotifyOnPlaylistMetadataChanged() {
castPlayer.addListener(mockListener);
MediaMetadata metadata = new MediaMetadata.Builder().setArtist("foo").build();
castPlayer.setPlaylistMetadata(metadata);
castPlayer.setPlaylistMetadata(metadata);
assertThat(castPlayer.getPlaylistMetadata()).isEqualTo(metadata);
verify(mockListener, times(1)).onPlaylistMetadataChanged(metadata);
}
@Test
public void getDeviceInfo_returnsCorrectDeviceInfoWithPlaybackTypeRemote() {
DeviceInfo deviceInfo = castPlayer.getDeviceInfo();

View File

@ -35,14 +35,12 @@ import androidx.annotation.VisibleForTesting;
import androidx.media3.common.util.NullableType;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.common.util.Util;
import com.google.errorprone.annotations.InlineMe;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Objects;
/**
* Represents ad group times and information on the state and URIs of ads within each ad group.
@ -72,7 +70,7 @@ public final class AdPlaybackState {
/**
* The original number of ads in the ad group in case the ad group is only partially available,
* or {@link C#LENGTH_UNSET} if unknown. An ad can be partially available when a server-side
* or {@link C#LENGTH_UNSET} if unknown. An ad can be partially available when a server side
* inserted ad live stream is joined while an ad is already playing and some ad information is
* missing.
*/
@ -92,9 +90,6 @@ public final class AdPlaybackState {
/** The durations of each ad in the ad group, in microseconds. */
public final long[] durationsUs;
/** The optional IDs of the ads. */
public final @NullableType String[] ids;
/**
* The offset in microseconds which should be added to the content stream when resuming playback
* after the ad group.
@ -104,9 +99,6 @@ public final class AdPlaybackState {
/** Whether this ad group is server-side inserted and part of the content stream. */
public final boolean isServerSideInserted;
/** Whether this is an ignorable placeholder that must not be attempted to be played. */
public final boolean isPlaceholder;
/**
* Creates a new ad group with an unspecified number of ads.
*
@ -122,9 +114,7 @@ public final class AdPlaybackState {
/* mediaItems= */ new MediaItem[0],
/* durationsUs= */ new long[0],
/* contentResumeOffsetUs= */ 0,
/* isServerSideInserted= */ false,
/* ids= */ new String[0],
/* isPlaceholder= */ false);
/* isServerSideInserted= */ false);
}
@SuppressWarnings("deprecation") // Intentionally assigning deprecated field
@ -136,9 +126,7 @@ public final class AdPlaybackState {
@NullableType MediaItem[] mediaItems,
long[] durationsUs,
long contentResumeOffsetUs,
boolean isServerSideInserted,
@NullableType String[] ids,
boolean isPlaceholder) {
boolean isServerSideInserted) {
checkArgument(states.length == mediaItems.length);
this.timeUs = timeUs;
this.count = count;
@ -152,8 +140,6 @@ public final class AdPlaybackState {
for (int i = 0; i < uris.length; i++) {
uris[i] = mediaItems[i] == null ? null : checkNotNull(mediaItems[i].localConfiguration).uri;
}
this.ids = ids;
this.isPlaceholder = isPlaceholder;
}
/**
@ -169,7 +155,7 @@ public final class AdPlaybackState {
* lastPlayedAdIndex}, or {@link #count} if no later ads should be played. If no ads have been
* played, pass -1 to get the index of the first ad to play.
*
* <p>Note: {@linkplain #isServerSideInserted server-side inserted ads} are always considered
* <p>Note: {@linkplain #isServerSideInserted Server side inserted ads} are always considered
* playable.
*/
public int getNextAdIndexToPlay(@IntRange(from = -1) int lastPlayedAdIndex) {
@ -205,24 +191,8 @@ public final class AdPlaybackState {
return false;
}
/**
* Returns whether this is a is a placeholder ad group.
*
* @param isServerSideInserted Whether the postroll placeholder must be server-side inserted.
* @return true only if this ad group has a matching {@link #isServerSideInserted} flag.
*/
public boolean isLivePostrollPlaceholder(boolean isServerSideInserted) {
return (this.isServerSideInserted == isServerSideInserted) && isLivePostrollPlaceholder();
}
/**
* Returns whether this is a placeholder ad group. It can be server-side inserted or not. Use
* {@link #isLivePostrollPlaceholder(boolean)} if you want to differentiate.
*
* @return true only if this is a live postroll placeholder.
*/
public boolean isLivePostrollPlaceholder() {
return isPlaceholder && timeUs == C.TIME_END_OF_SOURCE && count == C.LENGTH_UNSET;
private boolean isLivePostrollPlaceholder() {
return isServerSideInserted && timeUs == C.TIME_END_OF_SOURCE && count == C.LENGTH_UNSET;
}
@Override
@ -241,9 +211,7 @@ public final class AdPlaybackState {
&& Arrays.equals(states, adGroup.states)
&& Arrays.equals(durationsUs, adGroup.durationsUs)
&& contentResumeOffsetUs == adGroup.contentResumeOffsetUs
&& isServerSideInserted == adGroup.isServerSideInserted
&& Arrays.equals(ids, adGroup.ids)
&& isPlaceholder == adGroup.isPlaceholder;
&& isServerSideInserted == adGroup.isServerSideInserted;
}
@Override
@ -256,8 +224,6 @@ public final class AdPlaybackState {
result = 31 * result + Arrays.hashCode(durationsUs);
result = 31 * result + (int) (contentResumeOffsetUs ^ (contentResumeOffsetUs >>> 32));
result = 31 * result + (isServerSideInserted ? 1 : 0);
result = 31 * result + Arrays.hashCode(ids);
result = 31 * result + (isPlaceholder ? 1 : 0);
return result;
}
@ -272,9 +238,7 @@ public final class AdPlaybackState {
mediaItems,
durationsUs,
contentResumeOffsetUs,
isServerSideInserted,
ids,
isPlaceholder);
isServerSideInserted);
}
/** Returns a new instance with the ad count set to {@code count}. */
@ -283,7 +247,6 @@ public final class AdPlaybackState {
@AdState int[] states = copyStatesWithSpaceForAdCount(this.states, count);
long[] durationsUs = copyDurationsUsWithSpaceForAdCount(this.durationsUs, count);
@NullableType MediaItem[] mediaItems = Arrays.copyOf(this.mediaItems, count);
@NullableType String[] ids = Arrays.copyOf(this.ids, count);
return new AdGroup(
timeUs,
count,
@ -292,9 +255,7 @@ public final class AdPlaybackState {
mediaItems,
durationsUs,
contentResumeOffsetUs,
isServerSideInserted,
ids,
isPlaceholder);
isServerSideInserted);
}
/**
@ -320,9 +281,6 @@ public final class AdPlaybackState {
@NullableType MediaItem[] mediaItems = Arrays.copyOf(this.mediaItems, states.length);
mediaItems[index] = mediaItem;
states[index] = AD_STATE_AVAILABLE;
@NullableType
String[] ids =
this.ids.length == states.length ? this.ids : Arrays.copyOf(this.ids, states.length);
return new AdGroup(
timeUs,
count,
@ -331,9 +289,7 @@ public final class AdPlaybackState {
mediaItems,
durationsUs,
contentResumeOffsetUs,
isServerSideInserted,
ids,
isPlaceholder);
isServerSideInserted);
}
/**
@ -361,9 +317,6 @@ public final class AdPlaybackState {
this.mediaItems.length == states.length
? this.mediaItems
: Arrays.copyOf(this.mediaItems, states.length);
@NullableType
String[] ids =
this.ids.length == states.length ? this.ids : Arrays.copyOf(this.ids, states.length);
states[index] = state;
return new AdGroup(
timeUs,
@ -373,9 +326,7 @@ public final class AdPlaybackState {
mediaItems,
durationsUs,
contentResumeOffsetUs,
isServerSideInserted,
ids,
isPlaceholder);
isServerSideInserted);
}
/** Returns a new instance with the specified ad durations, in microseconds. */
@ -394,39 +345,7 @@ public final class AdPlaybackState {
mediaItems,
durationsUs,
contentResumeOffsetUs,
isServerSideInserted,
ids,
isPlaceholder);
}
/** Returns a new instance with the specified ID for the given ad index. */
@CheckResult
public AdGroup withAdId(String adId, @IntRange(from = 0) int index) {
@AdState int[] states = copyStatesWithSpaceForAdCount(this.states, index + 1);
long[] durationsUs =
this.durationsUs.length == states.length
? this.durationsUs
: copyDurationsUsWithSpaceForAdCount(this.durationsUs, states.length);
@NullableType
MediaItem[] mediaItems =
this.mediaItems.length == states.length
? this.mediaItems
: Arrays.copyOf(this.mediaItems, states.length);
@NullableType
String[] ids =
this.ids.length == states.length ? this.ids : Arrays.copyOf(this.ids, states.length);
ids[index] = adId;
return new AdGroup(
timeUs,
count,
originalCount,
states,
mediaItems,
durationsUs,
contentResumeOffsetUs,
isServerSideInserted,
ids,
isPlaceholder);
isServerSideInserted);
}
/** Returns an instance with the specified {@link #contentResumeOffsetUs}. */
@ -440,9 +359,7 @@ public final class AdPlaybackState {
mediaItems,
durationsUs,
contentResumeOffsetUs,
isServerSideInserted,
ids,
isPlaceholder);
isServerSideInserted);
}
/** Returns an instance with the specified value for {@link #isServerSideInserted}. */
@ -456,9 +373,7 @@ public final class AdPlaybackState {
mediaItems,
durationsUs,
contentResumeOffsetUs,
isServerSideInserted,
ids,
isPlaceholder);
isServerSideInserted);
}
/** Returns an instance with the specified value for {@link #originalCount}. */
@ -471,9 +386,7 @@ public final class AdPlaybackState {
mediaItems,
durationsUs,
contentResumeOffsetUs,
isServerSideInserted,
ids,
isPlaceholder);
isServerSideInserted);
}
/** Removes the last ad from the ad group. */
@ -485,7 +398,6 @@ public final class AdPlaybackState {
if (durationsUs.length > newCount) {
newDurationsUs = Arrays.copyOf(durationsUs, newCount);
}
@NullableType String[] newIds = Arrays.copyOf(ids, newCount);
return new AdGroup(
timeUs,
newCount,
@ -494,9 +406,7 @@ public final class AdPlaybackState {
newMediaItems,
newDurationsUs,
/* contentResumeOffsetUs= */ Util.sum(newDurationsUs),
isServerSideInserted,
newIds,
isPlaceholder);
isServerSideInserted);
}
/**
@ -514,9 +424,7 @@ public final class AdPlaybackState {
/* mediaItems= */ new MediaItem[0],
/* durationsUs= */ new long[0],
contentResumeOffsetUs,
isServerSideInserted,
ids,
isPlaceholder);
isServerSideInserted);
}
int count = this.states.length;
@AdState int[] states = Arrays.copyOf(this.states, count);
@ -533,9 +441,7 @@ public final class AdPlaybackState {
mediaItems,
durationsUs,
contentResumeOffsetUs,
isServerSideInserted,
ids,
isPlaceholder);
isServerSideInserted);
}
/**
@ -564,36 +470,7 @@ public final class AdPlaybackState {
mediaItems,
durationsUs,
contentResumeOffsetUs,
isServerSideInserted,
ids,
isPlaceholder);
}
private AdGroup withIsPlaceholder(boolean isPlaceholder, boolean isServerSideInserted) {
return new AdGroup(
timeUs,
count,
originalCount,
states,
mediaItems,
durationsUs,
contentResumeOffsetUs,
isServerSideInserted,
ids,
isPlaceholder);
}
/**
* Returns the index of the ad with the given ad ID, or {@link C#INDEX_UNSET} if the ad ID can't
* be found.
*/
public int getIndexOfAdId(String adId) {
for (int i = 0; i < ids.length; i++) {
if (Objects.equals(ids[i], adId)) {
return i;
}
}
return C.INDEX_UNSET;
isServerSideInserted);
}
@CheckResult
@ -623,8 +500,6 @@ public final class AdPlaybackState {
private static final String FIELD_IS_SERVER_SIDE_INSERTED = Util.intToStringMaxRadix(6);
private static final String FIELD_ORIGINAL_COUNT = Util.intToStringMaxRadix(7);
@VisibleForTesting static final String FIELD_MEDIA_ITEMS = Util.intToStringMaxRadix(8);
static final String FIELD_IDS = Util.intToStringMaxRadix(9);
static final String FIELD_IS_PLACEHOLDER = Util.intToStringMaxRadix(10);
// Intentionally assigning deprecated field.
// putParcelableArrayList actually supports null elements.
@ -641,8 +516,6 @@ public final class AdPlaybackState {
bundle.putLongArray(FIELD_DURATIONS_US, durationsUs);
bundle.putLong(FIELD_CONTENT_RESUME_OFFSET_US, contentResumeOffsetUs);
bundle.putBoolean(FIELD_IS_SERVER_SIDE_INSERTED, isServerSideInserted);
bundle.putStringArrayList(FIELD_IDS, new ArrayList<>(Arrays.asList(ids)));
bundle.putBoolean(FIELD_IS_PLACEHOLDER, isPlaceholder);
return bundle;
}
@ -663,8 +536,6 @@ public final class AdPlaybackState {
@Nullable long[] durationsUs = bundle.getLongArray(FIELD_DURATIONS_US);
long contentResumeOffsetUs = bundle.getLong(FIELD_CONTENT_RESUME_OFFSET_US);
boolean isServerSideInserted = bundle.getBoolean(FIELD_IS_SERVER_SIDE_INSERTED);
@Nullable ArrayList<String> ids = bundle.getStringArrayList(FIELD_IDS);
boolean isPlaceholder = bundle.getBoolean(FIELD_IS_PLACEHOLDER);
return new AdGroup(
timeUs,
count,
@ -673,9 +544,7 @@ public final class AdPlaybackState {
getMediaItemsFromBundleArrays(mediaItemBundleList, uriList),
durationsUs == null ? new long[0] : durationsUs,
contentResumeOffsetUs,
isServerSideInserted,
ids == null ? new String[0] : ids.toArray(new String[0]),
isPlaceholder);
isServerSideInserted);
}
private ArrayList<@NullableType Bundle> getMediaItemsArrayBundles() {
@ -975,21 +844,14 @@ public final class AdPlaybackState {
adsId, adGroups, adResumePositionUs, contentDurationUs, removedAdGroupCount);
}
/** Returns an instance with the specified value for {@link #adsId}. */
@CheckResult
public AdPlaybackState withAdsId(Object adsId) {
return new AdPlaybackState(
adsId, adGroups, adResumePositionUs, contentDurationUs, removedAdGroupCount);
}
/**
* Returns an instance with the specified ad marked as {@linkplain #AD_STATE_AVAILABLE available}.
*
* <p>Must not be called with client-side inserted ad groups. Client-side inserted ads should use
* <p>Must not be called with client side inserted ad groups. Client side inserted ads should use
* {@link #withAvailableAdMediaItem}.
*
* @throws IllegalStateException in case this methods is called on an ad group that {@linkplain
* AdGroup#isServerSideInserted is not server-side inserted}.
* AdGroup#isServerSideInserted is not server side inserted}.
*/
@CheckResult
public AdPlaybackState withAvailableAd(
@ -1045,17 +907,6 @@ public final class AdPlaybackState {
adsId, adGroups, adResumePositionUs, contentDurationUs, removedAdGroupCount);
}
/** Returns an instance with the specified ad ID for the given ad. */
@CheckResult
public AdPlaybackState withAdId(
@IntRange(from = 0) int adGroupIndex, @IntRange(from = 0) int adIndexInAdGroup, String adId) {
int adjustedIndex = adGroupIndex - removedAdGroupCount;
AdGroup[] adGroups = Util.nullSafeArrayCopy(this.adGroups, this.adGroups.length);
adGroups[adjustedIndex] = adGroups[adjustedIndex].withAdId(adId, adIndexInAdGroup);
return new AdPlaybackState(
adsId, adGroups, adResumePositionUs, contentDurationUs, removedAdGroupCount);
}
/**
* Returns an instance with all ads in the specified ad group skipped (except for those already
* marked as played or in the error state).
@ -1072,23 +923,14 @@ public final class AdPlaybackState {
/**
* Returns an instance with the specified ad durations, in microseconds.
*
* <p>The number of arrays of durations ({@code adDurations.length}) must always be equal to
* {@link #adGroupCount}. This is required even on an instance created with {@link
* #withRemovedAdGroupCount(int)}. The array of durations at the index of a removed ad group can
* be null or empty.
*
* @throws IllegalArgumentException if {@code adDurations.length != adGroupCount}.
* <p>Must only be used if {@link #removedAdGroupCount} is 0.
*/
@CheckResult
public AdPlaybackState withAdDurationsUs(long[][] adDurationUs) {
checkArgument(adDurationUs.length == adGroupCount);
checkState(removedAdGroupCount == 0);
AdGroup[] adGroups = Util.nullSafeArrayCopy(this.adGroups, this.adGroups.length);
for (int correctedAdGroupIndex = 0;
correctedAdGroupIndex < adGroupCount - removedAdGroupCount;
correctedAdGroupIndex++) {
adGroups[correctedAdGroupIndex] =
adGroups[correctedAdGroupIndex].withAdDurationsUs(
adDurationUs[removedAdGroupCount + correctedAdGroupIndex]);
for (int adGroupIndex = 0; adGroupIndex < adGroupCount; adGroupIndex++) {
adGroups[adGroupIndex] = adGroups[adGroupIndex].withAdDurationsUs(adDurationUs[adGroupIndex]);
}
return new AdPlaybackState(
adsId, adGroups, adResumePositionUs, contentDurationUs, removedAdGroupCount);
@ -1224,56 +1066,25 @@ public final class AdPlaybackState {
adsId, adGroups, adResumePositionUs, contentDurationUs, removedAdGroupCount);
}
/**
* @deprecated Use {@link #withLivePostrollPlaceholderAppended(boolean)} and pass {@code true}
* instead.
*/
@InlineMe(replacement = "this.withLivePostrollPlaceholderAppended(true)")
@Deprecated
public AdPlaybackState withLivePostrollPlaceholderAppended() {
return withLivePostrollPlaceholderAppended(/* isServerSideInserted= */ true);
}
/**
* Appends a live postroll placeholder ad group to the ad playback state.
*
* <p>Adding such a placeholder is only required for periods of live streams. A player is not
* expected to play this placeholder. It is only used to indicate that another ad group with this
* ad group index will be inserted in the future.
* <p>Adding such a placeholder is only required for periods of server side ad insertion live
* streams. A player is not expected to play this placeholder. It is only used to indicate that
* another ad group with this ad group index will be inserted in the future.
*
* <p>See {@link #endsWithLivePostrollPlaceHolder()} and {@link
* #endsWithLivePostrollPlaceHolder(boolean)} also.
* <p>See {@link #endsWithLivePostrollPlaceHolder()} also.
*
* @param isServerSideInserted Whether this is a server-side inserted ad (single stream).
* @return The new ad playback state instance ending with a live postroll placeholder.
*/
public AdPlaybackState withLivePostrollPlaceholderAppended(boolean isServerSideInserted) {
public AdPlaybackState withLivePostrollPlaceholderAppended() {
return withNewAdGroup(adGroupCount, /* adGroupTimeUs= */ C.TIME_END_OF_SOURCE)
.withIsPlaceholder(adGroupCount, /* isPlaceholder= */ true, isServerSideInserted);
}
@VisibleForTesting
/* package */ AdPlaybackState withIsPlaceholder(
int adGroupIndex, boolean isPlaceholder, boolean isServerSideInserted) {
int adjustedIndex = adGroupIndex - removedAdGroupCount;
if (adGroups[adjustedIndex].isPlaceholder == isPlaceholder
&& adGroups[adjustedIndex].isServerSideInserted == isServerSideInserted) {
return this;
}
AdGroup[] adGroups = Util.nullSafeArrayCopy(this.adGroups, this.adGroups.length);
adGroups[adjustedIndex] =
adGroups[adjustedIndex].withIsPlaceholder(isPlaceholder, isServerSideInserted);
return new AdPlaybackState(
adsId, adGroups, adResumePositionUs, contentDurationUs, removedAdGroupCount);
.withIsServerSideInserted(adGroupCount, true);
}
/**
* Returns whether the last ad group is a live postroll placeholder as inserted by {@link
* #withLivePostrollPlaceholderAppended(boolean)}.
*
* <p>Note: That either server-side or client-side inserted placeholders are considered. Use
* {@link #endsWithLivePostrollPlaceHolder(boolean)} if you want to test for one or the other
* only.
* #withLivePostrollPlaceholderAppended()}.
*
* @return Whether the ad playback state ends with a live postroll placeholder.
*/
@ -1283,22 +1094,7 @@ public final class AdPlaybackState {
}
/**
* Returns whether the last ad group is a live postroll placeholder as inserted by {@link
* #withLivePostrollPlaceholderAppended(boolean)} .
*
* @param isServerSideInserted Whether the trailing placeholder is server-side inserted.
* @return Whether the ad playback state ends with a live postroll placeholder.
*/
public boolean endsWithLivePostrollPlaceHolder(boolean isServerSideInserted) {
int adGroupIndex = adGroupCount - 1;
return adGroupIndex >= 0 && isLivePostrollPlaceholder(adGroupIndex, isServerSideInserted);
}
/**
* Returns whether the {@link AdGroup} at the given ad group index is a live postroll placeholder.
*
* <p>Note: That either server-side or client-side inserted placeholders return true. Use {@link
* #isLivePostrollPlaceholder(int, boolean)} if you want to test for one or the other only.
* Whether the {@link AdGroup} at the given ad group index is a live postroll placeholder.
*
* @param adGroupIndex The ad group index.
* @return True if the ad group at the given index is a live postroll placeholder, false if not.
@ -1307,31 +1103,6 @@ public final class AdPlaybackState {
return adGroupIndex == adGroupCount - 1 && getAdGroup(adGroupIndex).isLivePostrollPlaceholder();
}
/**
* Returns whether the {@link AdGroup} at the given ad group index is a live postroll placeholder
* and either server or client-side inserted.
*
* @param adGroupIndex The ad group index.
* @param isServerSideInserted Whether the placeholder is server-side inserted.
* @return True if the ad group at the given index is a live postroll placeholder, false if not.
*/
public boolean isLivePostrollPlaceholder(int adGroupIndex, boolean isServerSideInserted) {
return adGroupIndex == adGroupCount - 1
&& getAdGroup(adGroupIndex).isLivePostrollPlaceholder(isServerSideInserted);
}
/**
* Returns the index of the ad with the given ad ID in the given ad group, or {@link
* C#INDEX_UNSET} if the ad ID can't be found.
*
* @param adGroupIndex The ad group index.
* @param adId The ad ID.
* @return The ad index in the ad group, or {@link C#INDEX_UNSET} if the ad ID is not found.
*/
public int getAdIndexOfAdId(int adGroupIndex, String adId) {
return getAdGroup(adGroupIndex).getIndexOfAdId(adId);
}
/**
* Returns a copy of the ad playback state with the given ads ID.
*
@ -1353,9 +1124,7 @@ public final class AdPlaybackState {
Arrays.copyOf(adGroup.mediaItems, adGroup.mediaItems.length),
Arrays.copyOf(adGroup.durationsUs, adGroup.durationsUs.length),
adGroup.contentResumeOffsetUs,
adGroup.isServerSideInserted,
adGroup.ids,
adGroup.isPlaceholder);
adGroup.isServerSideInserted);
}
return new AdPlaybackState(
adsId,
@ -1374,7 +1143,7 @@ public final class AdPlaybackState {
return false;
}
AdPlaybackState that = (AdPlaybackState) o;
return Objects.equals(adsId, that.adsId)
return Util.areEqual(adsId, that.adsId)
&& adGroupCount == that.adGroupCount
&& adResumePositionUs == that.adResumePositionUs
&& contentDurationUs == that.contentDurationUs
@ -1457,7 +1226,7 @@ public final class AdPlaybackState {
// placeholder in a period of a multi-period live window, or when c) the position actually is
// before the given period duration.
return periodDurationUs == C.TIME_UNSET
|| adGroup.isLivePostrollPlaceholder()
|| (adGroup.isServerSideInserted && adGroup.count == C.LENGTH_UNSET)
|| positionUs < periodDurationUs;
}
return positionUs < adGroupPositionUs;

View File

@ -170,43 +170,6 @@ public final class AudioAttributes {
return audioAttributesV21;
}
/** Returns the {@link C.StreamType} corresponding to these audio attributes. */
@UnstableApi
public @C.StreamType int getStreamType() {
// Flags to stream type mapping
if ((flags & C.FLAG_AUDIBILITY_ENFORCED) == C.FLAG_AUDIBILITY_ENFORCED) {
return C.STREAM_TYPE_SYSTEM;
}
// Usage to stream type mapping
switch (usage) {
case C.USAGE_ASSISTANCE_SONIFICATION:
return C.STREAM_TYPE_SYSTEM;
case C.USAGE_VOICE_COMMUNICATION:
return C.STREAM_TYPE_VOICE_CALL;
case C.USAGE_VOICE_COMMUNICATION_SIGNALLING:
return C.STREAM_TYPE_DTMF;
case C.USAGE_ALARM:
return C.STREAM_TYPE_ALARM;
case C.USAGE_NOTIFICATION_RINGTONE:
return C.STREAM_TYPE_RING;
case C.USAGE_NOTIFICATION:
case C.USAGE_NOTIFICATION_COMMUNICATION_REQUEST:
case C.USAGE_NOTIFICATION_COMMUNICATION_INSTANT:
case C.USAGE_NOTIFICATION_COMMUNICATION_DELAYED:
case C.USAGE_NOTIFICATION_EVENT:
return C.STREAM_TYPE_NOTIFICATION;
case C.USAGE_ASSISTANCE_ACCESSIBILITY:
return C.STREAM_TYPE_ACCESSIBILITY;
case C.USAGE_MEDIA:
case C.USAGE_GAME:
case C.USAGE_ASSISTANCE_NAVIGATION_GUIDANCE:
case C.USAGE_ASSISTANT:
case C.USAGE_UNKNOWN:
default:
return C.STREAM_TYPE_MUSIC;
}
}
@Override
public boolean equals(@Nullable Object obj) {
if (this == obj) {

View File

@ -15,14 +15,15 @@
*/
package androidx.media3.common;
import static androidx.annotation.VisibleForTesting.PROTECTED;
import static java.lang.Math.max;
import static java.lang.Math.min;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.common.util.Util;
import com.google.common.collect.ImmutableList;
import com.google.errorprone.annotations.ForOverride;
import java.util.List;
/** Abstract base {@link Player} which implements common implementation independent methods. */
@ -275,8 +276,8 @@ public abstract class BasePlayer implements Player {
* @param seekCommand The {@link Player.Command} used to trigger the seek.
* @param isRepeatingCurrentItem Whether this seeks repeats the current item.
*/
@ForOverride
protected abstract void seekTo(
@VisibleForTesting(otherwise = PROTECTED)
public abstract void seekTo(
int mediaItemIndex,
long positionMs,
@Player.Command int seekCommand,

View File

@ -29,6 +29,7 @@ import androidx.media3.common.util.Util;
import com.google.common.collect.ImmutableList;
import java.util.List;
// LINT.IfChange(javadoc)
/**
* A {@link Binder} to transfer a list of {@link Bundle Bundles} across processes by splitting the
* list into multiple transactions.

View File

@ -30,7 +30,6 @@ import android.media.MediaCodec;
import android.media.MediaCrypto;
import android.media.MediaFormat;
import android.net.Uri;
import android.opengl.GLES20;
import android.view.Surface;
import androidx.annotation.IntDef;
import androidx.media3.common.util.UnstableApi;
@ -328,8 +327,8 @@ public final class C {
/**
* Stream types for an {@link android.media.AudioTrack}. One of {@link #STREAM_TYPE_ALARM}, {@link
* #STREAM_TYPE_DTMF}, {@link #STREAM_TYPE_MUSIC}, {@link #STREAM_TYPE_NOTIFICATION}, {@link
* #STREAM_TYPE_RING}, {@link #STREAM_TYPE_SYSTEM}, {@link #STREAM_TYPE_VOICE_CALL}, {@link
* #STREAM_TYPE_ACCESSIBILITY} or {@link #STREAM_TYPE_DEFAULT}.
* #STREAM_TYPE_RING}, {@link #STREAM_TYPE_SYSTEM}, {@link #STREAM_TYPE_VOICE_CALL} or {@link
* #STREAM_TYPE_DEFAULT}.
*/
// @Target list includes both 'default' targets and TYPE_USE, to ensure backwards compatibility
// with Kotlin usages from before TYPE_USE was added.
@ -346,7 +345,6 @@ public final class C {
STREAM_TYPE_RING,
STREAM_TYPE_SYSTEM,
STREAM_TYPE_VOICE_CALL,
STREAM_TYPE_ACCESSIBILITY,
STREAM_TYPE_DEFAULT
})
public @interface StreamType {}
@ -372,10 +370,6 @@ public final class C {
/** See {@link AudioManager#STREAM_VOICE_CALL}. */
@UnstableApi public static final int STREAM_TYPE_VOICE_CALL = AudioManager.STREAM_VOICE_CALL;
/** See {@link AudioManager#STREAM_ACCESSIBILITY}. */
@UnstableApi
public static final int STREAM_TYPE_ACCESSIBILITY = AudioManager.STREAM_ACCESSIBILITY;
/** The default stream type used by audio renderers. Equal to {@link #STREAM_TYPE_MUSIC}. */
@UnstableApi public static final int STREAM_TYPE_DEFAULT = STREAM_TYPE_MUSIC;
@ -617,28 +611,6 @@ public final class C {
/** See {@link android.media.AudioAttributes#ALLOW_CAPTURE_BY_SYSTEM}. */
public static final int ALLOW_CAPTURE_BY_SYSTEM = AudioAttributes.ALLOW_CAPTURE_BY_SYSTEM;
/**
* Flags which represent a set of video codecs.
*
* <p>Possible flag values are:
*
* <ul>
* <li>{@link #VIDEO_CODEC_FLAG_H264}
* <li>{@link #VIDEO_CODEC_FLAG_H265}
* </ul>
*/
@UnstableApi
@Documented
@Retention(RetentionPolicy.SOURCE)
@Target(TYPE_USE)
@IntDef(
flag = true,
value = {VIDEO_CODEC_FLAG_H264, VIDEO_CODEC_FLAG_H265})
public @interface VideoCodecFlags {}
@UnstableApi public static final int VIDEO_CODEC_FLAG_H264 = 1;
@UnstableApi public static final int VIDEO_CODEC_FLAG_H265 = 2;
/**
* Flags which can apply to a buffer containing a media sample.
*
@ -698,7 +670,6 @@ public final class C {
/** A non-realtime (as fast as possible) {@linkplain MediaFormat#KEY_PRIORITY codec priority}. */
@UnstableApi public static final int MEDIA_CODEC_PRIORITY_NON_REALTIME = 1;
// LINT.IfChange
/**
* Video decoder output modes. Possible modes are {@link #VIDEO_OUTPUT_MODE_NONE}, {@link
* #VIDEO_OUTPUT_MODE_YUV} and {@link #VIDEO_OUTPUT_MODE_SURFACE_YUV}.
@ -719,11 +690,6 @@ public final class C {
/** Video decoder output mode that renders 4:2:0 YUV planes directly to a surface. */
@UnstableApi public static final int VIDEO_OUTPUT_MODE_SURFACE_YUV = 1;
// LINT.ThenChange(
// ../../../../../../../decoder_av1/src/main/jni/gav1_jni.cc,
// ../../../../../../../decoder_vp9/src/main/jni/vpx_jni.cc
// )
/**
* Video scaling modes for {@link MediaCodec}-based renderers. One of {@link
* #VIDEO_SCALING_MODE_SCALE_TO_FIT}, {@link #VIDEO_SCALING_MODE_SCALE_TO_FIT_WITH_CROPPING} or
@ -815,8 +781,6 @@ public final class C {
*/
public static final int SELECTION_FLAG_AUTOSELECT = 1 << 2; // 4
// LINT.ThenChange("util/Util.java:selection_flags")
/** Represents an undetermined language as an ISO 639-2 language code. */
public static final String LANGUAGE_UNDETERMINED = "und";
@ -1198,11 +1162,6 @@ public final class C {
/** See {@link MediaFormat#COLOR_STANDARD_BT2020}. */
@UnstableApi public static final int COLOR_SPACE_BT2020 = MediaFormat.COLOR_STANDARD_BT2020;
// LINT.ThenChange(
// util/MediaFormatUtil.java:color_space,
// ColorInfo.java:color_space,
// )
// LINT.IfChange(color_transfer)
/**
* Video/image color transfer characteristics. One of {@link Format#NO_VALUE}, {@link
@ -1250,14 +1209,6 @@ public final class C {
/** See {@link MediaFormat#COLOR_TRANSFER_HLG}. */
@UnstableApi public static final int COLOR_TRANSFER_HLG = MediaFormat.COLOR_TRANSFER_HLG;
// LINT.ThenChange(
// util/MediaFormatUtil.java:color_transfer,
// ColorInfo.java:color_transfer,
// ../../../../../../../effect/src/main/assets/shaders/fragment_shader_transformation_sdr_external_es2.glsl:color_transfer,
// ../../../../../../../effect/src/main/assets/shaders/fragment_shader_transformation_external_yuv_es3.glsl:color_transfer,
// ../../../../../../../effect/src/main/assets/shaders/fragment_shader_oetf_es3.glsl:color_transfer,
// )
// LINT.IfChange(color_range)
/**
* Video color range. One of {@link Format#NO_VALUE}, {@link #COLOR_RANGE_LIMITED} or {@link
@ -1276,11 +1227,6 @@ public final class C {
/** See {@link MediaFormat#COLOR_RANGE_FULL}. */
@UnstableApi public static final int COLOR_RANGE_FULL = MediaFormat.COLOR_RANGE_FULL;
// LINT.ThenChange(
// util/MediaFormatUtil.java:color_range,
// ColorInfo.java:color_range,
// )
/** Video projection types. */
@UnstableApi
@Documented
@ -1574,8 +1520,6 @@ public final class C {
*/
public static final int ROLE_FLAG_AUXILIARY = 1 << 15;
// LINT.ThenChange("util/Util.java:role_flags")
/**
* {@linkplain #ROLE_FLAG_AUXILIARY Auxiliary track types}. One of {@link
* #AUXILIARY_TRACK_TYPE_UNDEFINED}, {@link #AUXILIARY_TRACK_TYPE_ORIGINAL}, {@link
@ -1621,8 +1565,6 @@ public final class C {
/** A timed metadata of depth video track. */
@UnstableApi public static final int AUXILIARY_TRACK_TYPE_DEPTH_METADATA = 4;
// LINT.ThenChange("util/Util.java:auxiliary_track_type")
/**
* Level of support for a format. One of {@link #FORMAT_HANDLED}, {@link
* #FORMAT_EXCEEDS_CAPABILITIES}, {@link #FORMAT_UNSUPPORTED_DRM}, {@link
@ -1724,40 +1666,6 @@ public final class C {
/** The first frame was rendered. */
@UnstableApi public static final int FIRST_FRAME_RENDERED = 3;
/**
* Texture filtering algorithm for minification.
*
* <p>Possible values are:
*
* <ul>
* <li>{@link #TEXTURE_MIN_FILTER_LINEAR}
* <li>{@link #TEXTURE_MIN_FILTER_LINEAR_MIPMAP_LINEAR}
* </ul>
*
* <p>The algorithms are ordered by increasing visual quality and computational cost.
*/
@UnstableApi
@Documented
@Retention(RetentionPolicy.SOURCE)
@Target(TYPE_USE)
@IntDef({TEXTURE_MIN_FILTER_LINEAR, TEXTURE_MIN_FILTER_LINEAR_MIPMAP_LINEAR})
public @interface TextureMinFilter {}
/**
* Returns the weighted average of the four texture elements that are closest to the specified
* texture coordinates.
*/
@UnstableApi public static final int TEXTURE_MIN_FILTER_LINEAR = GLES20.GL_LINEAR;
/**
* Chooses the two mipmaps that most closely match the size of the pixel being textured and uses
* the {@link C#TEXTURE_MIN_FILTER_LINEAR} criterion (a weighted average of the texture elements
* that are closest to the specified texture coordinates) to produce a texture value from each
* mipmap. The final texture value is a weighted average of those two values.
*/
@UnstableApi
public static final int TEXTURE_MIN_FILTER_LINEAR_MIPMAP_LINEAR = GLES20.GL_LINEAR_MIPMAP_LINEAR;
/**
* @deprecated Use {@link Util#usToMs(long)}.
*/

View File

@ -203,7 +203,7 @@ public final class ColorInfo {
/**
* Returns the {@link C.ColorSpace} corresponding to the given ISO color primary code, as per
* table A.7.21.1 in Rec. ITU-T T.832 (06/2019), or {@link Format#NO_VALUE} if no mapping can be
* table A.7.21.1 in Rec. ITU-T T.832 (03/2009), or {@link Format#NO_VALUE} if no mapping can be
* made.
*/
@Pure
@ -219,52 +219,13 @@ public final class ColorInfo {
case 9:
return C.COLOR_SPACE_BT2020;
default:
// Remaining color primaries are either reserved or unspecified.
return Format.NO_VALUE;
}
}
/**
* Returns the ISO color primary code corresponding to the given {@link C.ColorSpace}, as per
* table A.7.21.1 in Rec. ITU-T T.832 (06/2019). made.
*/
public static int colorSpaceToIsoColorPrimaries(@C.ColorSpace int colorSpace) {
switch (colorSpace) {
// Default to BT.709 SDR as per the <a
// href="https://www.webmproject.org/vp9/mp4/#optional-fields">recommendation</a>.
case Format.NO_VALUE:
case C.COLOR_SPACE_BT709:
return 1;
case C.COLOR_SPACE_BT601:
return 5;
case C.COLOR_SPACE_BT2020:
return 9;
}
return 1;
}
/**
* Returns the ISO matrix coefficients code corresponding to the given {@link C.ColorSpace}, as
* per table A.7.21.3 in Rec. ITU-T T.832 (06/2019).
*/
public static int colorSpaceToIsoMatrixCoefficients(@C.ColorSpace int colorSpace) {
switch (colorSpace) {
// Default to BT.709 SDR as per the <a
// href="https://www.webmproject.org/vp9/mp4/#optional-fields">recommendation</a>.
case Format.NO_VALUE:
case C.COLOR_SPACE_BT709:
return 1;
case C.COLOR_SPACE_BT601:
return 6;
case C.COLOR_SPACE_BT2020:
return 9;
}
return 1;
}
/**
* Returns the {@link C.ColorTransfer} corresponding to the given ISO transfer characteristics
* code, as per table A.7.21.2 in Rec. ITU-T T.832 (06/2019), or {@link Format#NO_VALUE} if no
* code, as per table A.7.21.2 in Rec. ITU-T T.832 (03/2009), or {@link Format#NO_VALUE} if no
* mapping can be made.
*/
@Pure
@ -288,31 +249,6 @@ public final class ColorInfo {
}
}
/**
* Returns the ISO transfer characteristics code corresponding to the given {@link
* C.ColorTransfer}, as per table A.7.21.2 in Rec. ITU-T T.832 (06/2019).
*/
public static int colorTransferToIsoTransferCharacteristics(@C.ColorTransfer int colorTransfer) {
switch (colorTransfer) {
// Default to BT.709 SDR as per the <a
// href="https://www.webmproject.org/vp9/mp4/#optional-fields">recommendation</a>.
case C.COLOR_TRANSFER_LINEAR:
return 8;
case C.COLOR_TRANSFER_SRGB:
return 13;
case Format.NO_VALUE:
case C.COLOR_TRANSFER_SDR:
return 1;
case C.COLOR_TRANSFER_ST2084:
return 16;
case C.COLOR_TRANSFER_HLG:
return 18;
case C.COLOR_TRANSFER_GAMMA_2_2:
return 4;
}
return 1;
}
/**
* Returns whether the {@code ColorInfo} uses an HDR {@link C.ColorTransfer}.
*
@ -487,7 +423,6 @@ public final class ColorInfo {
default:
return "Undefined color space " + colorSpace;
}
// LINT.ThenChange(C.java:color_space)
}
private static String colorTransferToString(@C.ColorTransfer int colorTransfer) {
@ -510,7 +445,6 @@ public final class ColorInfo {
default:
return "Undefined color transfer " + colorTransfer;
}
// LINT.ThenChange(C.java:color_transfer)
}
private static String colorRangeToString(@C.ColorRange int colorRange) {
@ -525,7 +459,6 @@ public final class ColorInfo {
default:
return "Undefined color range " + colorRange;
}
// LINT.ThenChange(C.java:color_range)
}
private static final String FIELD_COLOR_SPACE = Util.intToStringMaxRadix(0);

View File

@ -30,7 +30,6 @@ import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.Objects;
/** Information about the playback device. */
public final class DeviceInfo {
@ -179,7 +178,7 @@ public final class DeviceInfo {
return playbackType == other.playbackType
&& minVolume == other.minVolume
&& maxVolume == other.maxVolume
&& Objects.equals(routingControllerId, other.routingControllerId);
&& Util.areEqual(routingControllerId, other.routingControllerId);
}
@Override
@ -228,4 +227,5 @@ public final class DeviceInfo {
.setRoutingControllerId(routingControllerId)
.build();
}
;
}

View File

@ -28,7 +28,6 @@ import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
import java.util.Objects;
import java.util.UUID;
/** Initialization data for one or more DRM schemes. */
@ -161,7 +160,7 @@ public final class DrmInitData implements Comparator<SchemeData>, Parcelable {
*/
@CheckResult
public DrmInitData copyWithSchemeType(@Nullable String schemeType) {
if (Objects.equals(this.schemeType, schemeType)) {
if (Util.areEqual(this.schemeType, schemeType)) {
return this;
}
return new DrmInitData(schemeType, false, schemeDatas);
@ -205,7 +204,7 @@ public final class DrmInitData implements Comparator<SchemeData>, Parcelable {
return false;
}
DrmInitData other = (DrmInitData) obj;
return Objects.equals(schemeType, other.schemeType)
return Util.areEqual(schemeType, other.schemeType)
&& Arrays.equals(schemeDatas, other.schemeDatas);
}
@ -353,9 +352,9 @@ public final class DrmInitData implements Comparator<SchemeData>, Parcelable {
return true;
}
SchemeData other = (SchemeData) obj;
return Objects.equals(licenseServerUrl, other.licenseServerUrl)
&& Objects.equals(mimeType, other.mimeType)
&& Objects.equals(uuid, other.uuid)
return Util.areEqual(licenseServerUrl, other.licenseServerUrl)
&& Util.areEqual(mimeType, other.mimeType)
&& Util.areEqual(uuid, other.uuid)
&& Arrays.equals(data, other.data);
}

View File

@ -16,7 +16,6 @@
package androidx.media3.common;
import static androidx.media3.common.util.Assertions.checkState;
import static com.google.common.math.DoubleMath.fuzzyEquals;
import static java.lang.annotation.ElementType.TYPE_USE;
import android.os.Bundle;
@ -103,7 +102,6 @@ import java.util.UUID;
* <li>{@link #projectionData}
* <li>{@link #stereoMode}
* <li>{@link #colorInfo}
* <li>{@link #maxSubLayers}
* </ul>
*
* <h2 id="audio-formats">Fields relevant to audio formats</h2>
@ -180,7 +178,6 @@ public final class Format {
@Nullable private byte[] projectionData;
private @C.StereoMode int stereoMode;
@Nullable private ColorInfo colorInfo;
private int maxSubLayers;
// Audio specific.
@ -219,7 +216,6 @@ public final class Format {
frameRate = NO_VALUE;
pixelWidthHeightRatio = 1.0f;
stereoMode = NO_VALUE;
maxSubLayers = NO_VALUE;
// Audio specific.
channelCount = NO_VALUE;
sampleRate = NO_VALUE;
@ -271,7 +267,6 @@ public final class Format {
this.projectionData = format.projectionData;
this.stereoMode = format.stereoMode;
this.colorInfo = format.colorInfo;
this.maxSubLayers = format.maxSubLayers;
// Audio specific.
this.channelCount = format.channelCount;
this.sampleRate = format.sampleRate;
@ -660,18 +655,6 @@ public final class Format {
return this;
}
/**
* Sets {@link Format#maxSubLayers}. The default value is {@link #NO_VALUE}.
*
* @param maxSubLayers The {@link Format#maxSubLayers}.
* @return The builder.
*/
@CanIgnoreReturnValue
public Builder setMaxSubLayers(int maxSubLayers) {
this.maxSubLayers = maxSubLayers;
return this;
}
// Audio specific.
/**
@ -766,7 +749,7 @@ public final class Format {
/**
* Sets {@link Format#tileCountHorizontal}. The default value is {@link #NO_VALUE}.
*
* @param tileCountHorizontal The {@link Format#tileCountHorizontal}.
* @param tileCountHorizontal The {@link Format#accessibilityChannel}.
* @return The builder.
*/
@CanIgnoreReturnValue
@ -778,7 +761,7 @@ public final class Format {
/**
* Sets {@link Format#tileCountVertical}. The default value is {@link #NO_VALUE}.
*
* @param tileCountVertical The {@link Format#tileCountVertical}.
* @param tileCountVertical The {@link Format#accessibilityChannel}.
* @return The builder.
*/
@CanIgnoreReturnValue
@ -1025,12 +1008,6 @@ public final class Format {
/** The color metadata associated with the video, or null if not applicable. */
@UnstableApi @Nullable public final ColorInfo colorInfo;
/**
* The maximum number of temporal scalable sub-layers in the video bitstream, or {@link #NO_VALUE}
* if not applicable.
*/
@UnstableApi public final int maxSubLayers;
// Audio specific.
/** The number of audio channels, or {@link #NO_VALUE} if unknown or not applicable. */
@ -1039,10 +1016,7 @@ public final class Format {
/** The audio sampling rate in Hz, or {@link #NO_VALUE} if unknown or not applicable. */
public final int sampleRate;
/**
* The {@link C.PcmEncoding} for PCM or losslessly compressed audio. Set to {@link #NO_VALUE} for
* other media types.
*/
/** The {@link C.PcmEncoding} for PCM audio. Set to {@link #NO_VALUE} for other media types. */
@UnstableApi public final @C.PcmEncoding int pcmEncoding;
/**
@ -1152,7 +1126,6 @@ public final class Format {
projectionData = builder.projectionData;
stereoMode = builder.stereoMode;
colorInfo = builder.colorInfo;
maxSubLayers = builder.maxSubLayers;
// Audio specific.
channelCount = builder.channelCount;
sampleRate = builder.sampleRate;
@ -1335,7 +1308,6 @@ public final class Format {
// [Omitted] projectionData.
result = 31 * result + stereoMode;
// [Omitted] colorInfo.
result = 31 * result + maxSubLayers;
// Audio specific.
result = 31 * result + channelCount;
result = 31 * result + sampleRate;
@ -1378,7 +1350,6 @@ public final class Format {
&& height == other.height
&& rotationDegrees == other.rotationDegrees
&& stereoMode == other.stereoMode
&& maxSubLayers == other.maxSubLayers
&& channelCount == other.channelCount
&& sampleRate == other.sampleRate
&& pcmEncoding == other.pcmEncoding
@ -1471,18 +1442,12 @@ public final class Format {
if (format.width != NO_VALUE && format.height != NO_VALUE) {
builder.append(", res=").append(format.width).append("x").append(format.height);
}
if (!fuzzyEquals(format.pixelWidthHeightRatio, 1, 0.001)) {
builder.append(", par=").append(Util.formatInvariant("%.3f", format.pixelWidthHeightRatio));
}
if (format.colorInfo != null && format.colorInfo.isValid()) {
builder.append(", color=").append(format.colorInfo.toLogString());
}
if (format.frameRate != NO_VALUE) {
builder.append(", fps=").append(format.frameRate);
}
if (format.maxSubLayers != NO_VALUE) {
builder.append(", maxSubLayers=").append(format.maxSubLayers);
}
if (format.channelCount != NO_VALUE) {
builder.append(", channels=").append(format.channelCount);
}
@ -1527,8 +1492,7 @@ public final class Format {
private static final String FIELD_AVERAGE_BITRATE = Util.intToStringMaxRadix(5);
private static final String FIELD_PEAK_BITRATE = Util.intToStringMaxRadix(6);
private static final String FIELD_CODECS = Util.intToStringMaxRadix(7);
// Do not reuse this key.
private static final String UNUSED_FIELD_METADATA = Util.intToStringMaxRadix(8);
private static final String FIELD_METADATA = Util.intToStringMaxRadix(8);
private static final String FIELD_CONTAINER_MIME_TYPE = Util.intToStringMaxRadix(9);
private static final String FIELD_SAMPLE_MIME_TYPE = Util.intToStringMaxRadix(10);
private static final String FIELD_MAX_INPUT_SIZE = Util.intToStringMaxRadix(11);
@ -1554,14 +1518,22 @@ public final class Format {
private static final String FIELD_TILE_COUNT_VERTICAL = Util.intToStringMaxRadix(31);
private static final String FIELD_LABELS = Util.intToStringMaxRadix(32);
private static final String FIELD_AUXILIARY_TRACK_TYPE = Util.intToStringMaxRadix(33);
private static final String FIELD_MAX_SUB_LAYERS = Util.intToStringMaxRadix(34);
/**
* @deprecated Use {@link #toBundle(boolean)} instead.
*/
@UnstableApi
@Deprecated
public Bundle toBundle() {
return toBundle(/* excludeMetadata= */ false);
}
/**
* Returns a {@link Bundle} representing the information stored in this object. If {@code
* excludeMetadata} is true, {@linkplain Format#metadata metadata} is excluded.
*/
@UnstableApi
public Bundle toBundle() {
public Bundle toBundle(boolean excludeMetadata) {
Bundle bundle = new Bundle();
bundle.putString(FIELD_ID, id);
bundle.putString(FIELD_LABEL, label);
@ -1576,7 +1548,10 @@ public final class Format {
bundle.putInt(FIELD_AVERAGE_BITRATE, averageBitrate);
bundle.putInt(FIELD_PEAK_BITRATE, peakBitrate);
bundle.putString(FIELD_CODECS, codecs);
// The metadata does not implement toBundle() method, hence can not be added.
if (!excludeMetadata) {
// TODO (internal ref: b/239701618)
bundle.putParcelable(FIELD_METADATA, metadata);
}
// Container specific.
bundle.putString(FIELD_CONTAINER_MIME_TYPE, containerMimeType);
// Sample specific.
@ -1600,7 +1575,6 @@ public final class Format {
if (colorInfo != null) {
bundle.putBundle(FIELD_COLOR_INFO, colorInfo.toBundle());
}
bundle.putInt(FIELD_MAX_SUB_LAYERS, maxSubLayers);
// Audio specific.
bundle.putInt(FIELD_CHANNEL_COUNT, channelCount);
bundle.putInt(FIELD_SAMPLE_RATE, sampleRate);
@ -1640,6 +1614,7 @@ public final class Format {
.setAverageBitrate(bundle.getInt(FIELD_AVERAGE_BITRATE, DEFAULT.averageBitrate))
.setPeakBitrate(bundle.getInt(FIELD_PEAK_BITRATE, DEFAULT.peakBitrate))
.setCodecs(defaultIfNull(bundle.getString(FIELD_CODECS), DEFAULT.codecs))
.setMetadata(defaultIfNull(bundle.getParcelable(FIELD_METADATA), DEFAULT.metadata))
// Container specific.
.setContainerMimeType(
defaultIfNull(bundle.getString(FIELD_CONTAINER_MIME_TYPE), DEFAULT.containerMimeType))
@ -1668,8 +1643,7 @@ public final class Format {
.setPixelWidthHeightRatio(
bundle.getFloat(FIELD_PIXEL_WIDTH_HEIGHT_RATIO, DEFAULT.pixelWidthHeightRatio))
.setProjectionData(bundle.getByteArray(FIELD_PROJECTION_DATA))
.setStereoMode(bundle.getInt(FIELD_STEREO_MODE, DEFAULT.stereoMode))
.setMaxSubLayers(bundle.getInt(FIELD_MAX_SUB_LAYERS, DEFAULT.maxSubLayers));
.setStereoMode(bundle.getInt(FIELD_STEREO_MODE, DEFAULT.stereoMode));
Bundle colorInfoBundle = bundle.getBundle(FIELD_COLOR_INFO);
if (colorInfoBundle != null) {
builder.setColorInfo(ColorInfo.fromBundle(colorInfoBundle));

View File

@ -30,25 +30,6 @@ import java.util.List;
/**
* A {@link Player} that forwards method calls to another {@link Player}. Applications can use this
* class to suppress or modify specific operations, by overriding the respective methods.
*
* <p>Subclasses must ensure they maintain consistency with the {@link Player} interface, including
* interactions with {@link Player.Listener}, which can be quite fiddly. For example, if removing an
* available {@link Player.Command} and disabling the corresponding method, subclasses need to:
*
* <ul>
* <li>Override {@link #isCommandAvailable(int)} and {@link #getAvailableCommands()}
* <li>Override and no-op the method itself
* <li>Override {@link #addListener(Listener)} and wrap the provided {@link Player.Listener} with
* an implementation that drops calls to {@link
* Player.Listener#onAvailableCommandsChanged(Commands)} and {@link
* Player.Listener#onEvents(Player, Events)} if they were only triggered by a change in
* command availability that is 'invisible' after the command removal.
* </ul>
*
* <p>Many customization use-cases are instead better served by {@link ForwardingSimpleBasePlayer},
* which allows subclasses to more concisely modify the behavior of an operation, or disallow a
* {@link Player.Command}. In many cases {@link ForwardingSimpleBasePlayer} should be used in
* preference to {@code ForwardingPlayer}.
*/
@UnstableApi
public class ForwardingPlayer implements Player {

View File

@ -25,6 +25,7 @@ import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import java.util.List;
// LINT.IfChange(javadoc)
/**
* A {@link SimpleBasePlayer} that forwards all calls to another {@link Player} instance.
*
@ -59,7 +60,7 @@ public class ForwardingSimpleBasePlayer extends SimpleBasePlayer {
private final Player player;
private LivePositionSuppliers livePositionSuppliers;
private ForwardingPositionSupplier currentPositionSupplier;
private Metadata lastTimedMetadata;
private @Player.PlayWhenReadyChangeReason int playWhenReadyChangeReason;
private @Player.DiscontinuityReason int pendingDiscontinuityReason;
@ -77,7 +78,7 @@ public class ForwardingSimpleBasePlayer extends SimpleBasePlayer {
this.lastTimedMetadata = new Metadata(/* presentationTimeUs= */ C.TIME_UNSET);
this.playWhenReadyChangeReason = Player.PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST;
this.pendingDiscontinuityReason = Player.DISCONTINUITY_REASON_INTERNAL;
this.livePositionSuppliers = new LivePositionSuppliers(player);
this.currentPositionSupplier = new ForwardingPositionSupplier(player);
player.addListener(
new Listener() {
@Override
@ -98,8 +99,15 @@ public class ForwardingSimpleBasePlayer extends SimpleBasePlayer {
@Player.DiscontinuityReason int reason) {
pendingDiscontinuityReason = reason;
pendingPositionDiscontinuityNewPositionMs = newPosition.positionMs;
livePositionSuppliers.disconnect(oldPosition.positionMs, oldPosition.contentPositionMs);
livePositionSuppliers = new LivePositionSuppliers(player);
// Any previously created State will directly call through to player.getCurrentPosition
// via the existing position supplier. From this point onwards, this is wrong as the
// player had a discontinuity and will now return a new position unrelated to the old
// State. We can disconnect these old State objects from the underlying Player by fixing
// the position to the one before the discontinuity and using a new (live) position
// supplier for future State objects.
currentPositionSupplier.setConstant(
oldPosition.positionMs, oldPosition.contentPositionMs);
currentPositionSupplier = new ForwardingPositionSupplier(player);
}
@Override
@ -124,18 +132,18 @@ public class ForwardingSimpleBasePlayer extends SimpleBasePlayer {
protected State getState() {
// Ordered alphabetically by State.Builder setters.
State.Builder state = new State.Builder();
LivePositionSuppliers positionSuppliers = livePositionSuppliers;
ForwardingPositionSupplier positionSupplier = currentPositionSupplier;
if (player.isCommandAvailable(Player.COMMAND_GET_CURRENT_MEDIA_ITEM)) {
state.setAdBufferedPositionMs(positionSuppliers.bufferedPositionSupplier);
state.setAdPositionMs(positionSuppliers.currentPositionSupplier);
state.setAdBufferedPositionMs(positionSupplier::getBufferedPositionMs);
state.setAdPositionMs(positionSupplier::getCurrentPositionMs);
}
if (player.isCommandAvailable(Player.COMMAND_GET_AUDIO_ATTRIBUTES)) {
state.setAudioAttributes(player.getAudioAttributes());
}
state.setAvailableCommands(player.getAvailableCommands());
if (player.isCommandAvailable(Player.COMMAND_GET_CURRENT_MEDIA_ITEM)) {
state.setContentBufferedPositionMs(positionSuppliers.contentBufferedPositionSupplier);
state.setContentPositionMs(positionSuppliers.contentPositionSupplier);
state.setContentBufferedPositionMs(positionSupplier::getContentBufferedPositionMs);
state.setContentPositionMs(positionSupplier::getContentPositionMs);
if (player.isCommandAvailable(Player.COMMAND_GET_TIMELINE)) {
state.setCurrentAd(player.getCurrentAdGroupIndex(), player.getCurrentAdIndexInAdGroup());
}
@ -186,7 +194,7 @@ public class ForwardingSimpleBasePlayer extends SimpleBasePlayer {
state.setSurfaceSize(player.getSurfaceSize());
state.setTimedMetadata(lastTimedMetadata);
if (player.isCommandAvailable(Player.COMMAND_GET_CURRENT_MEDIA_ITEM)) {
state.setTotalBufferedDurationMs(positionSuppliers.totalBufferedPositionSupplier);
state.setTotalBufferedDurationMs(positionSupplier::getTotalBufferedDurationMs);
}
state.setTrackSelectionParameters(player.getTrackSelectionParameters());
state.setVideoSize(player.getVideoSize());
@ -448,29 +456,44 @@ public class ForwardingSimpleBasePlayer extends SimpleBasePlayer {
* Forwards to the changing position values of the wrapped player until the forwarding is
* deactivated with constant values.
*/
private static final class LivePositionSuppliers {
private static final class ForwardingPositionSupplier {
public final LivePositionSupplier currentPositionSupplier;
public final LivePositionSupplier bufferedPositionSupplier;
public final LivePositionSupplier contentPositionSupplier;
public final LivePositionSupplier contentBufferedPositionSupplier;
public final LivePositionSupplier totalBufferedPositionSupplier;
private final Player player;
public LivePositionSuppliers(Player player) {
currentPositionSupplier = new LivePositionSupplier(player::getCurrentPosition);
bufferedPositionSupplier = new LivePositionSupplier(player::getBufferedPosition);
contentPositionSupplier = new LivePositionSupplier(player::getContentPosition);
contentBufferedPositionSupplier =
new LivePositionSupplier(player::getContentBufferedPosition);
totalBufferedPositionSupplier = new LivePositionSupplier(player::getTotalBufferedDuration);
private long positionsMs;
private long contentPositionMs;
public ForwardingPositionSupplier(Player player) {
this.player = player;
this.positionsMs = C.TIME_UNSET;
this.contentPositionMs = C.TIME_UNSET;
}
public void disconnect(long positionMs, long contentPositionMs) {
currentPositionSupplier.disconnect(positionMs);
bufferedPositionSupplier.disconnect(positionMs);
contentPositionSupplier.disconnect(contentPositionMs);
contentBufferedPositionSupplier.disconnect(contentPositionMs);
totalBufferedPositionSupplier.disconnect(/* finalValue= */ 0);
public void setConstant(long positionMs, long contentPositionMs) {
this.positionsMs = positionMs;
this.contentPositionMs = contentPositionMs;
}
public long getCurrentPositionMs() {
return positionsMs == C.TIME_UNSET ? player.getCurrentPosition() : positionsMs;
}
public long getBufferedPositionMs() {
return positionsMs == C.TIME_UNSET ? player.getBufferedPosition() : positionsMs;
}
public long getContentPositionMs() {
return contentPositionMs == C.TIME_UNSET ? player.getContentPosition() : contentPositionMs;
}
public long getContentBufferedPositionMs() {
return contentPositionMs == C.TIME_UNSET
? player.getContentBufferedPosition()
: contentPositionMs;
}
public long getTotalBufferedDurationMs() {
return positionsMs == C.TIME_UNSET ? player.getTotalBufferedDuration() : 0;
}
}
}

View File

@ -18,34 +18,123 @@ package androidx.media3.common;
import static androidx.media3.common.util.Assertions.checkArgument;
import androidx.media3.common.util.UnstableApi;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
/** Value class specifying information about a decoded video frame. */
@UnstableApi
public class FrameInfo {
/**
* The {@link Format} of the frame.
*
* <p>The {@link Format#colorInfo} must be set, and the {@link Format#width} and {@link
* Format#height} must be greater than 0.
*/
public final Format format;
/** A builder for {@link FrameInfo} instances. */
public static final class Builder {
/** The offset that must be added to the frame presentation timestamp, in microseconds. */
private ColorInfo colorInfo;
private int width;
private int height;
private float pixelWidthHeightRatio;
private long offsetToAddUs;
/**
* Creates an instance with default values.
*
* @param colorInfo The {@link ColorInfo}.
* @param width The frame width, in pixels.
* @param height The frame height, in pixels.
*/
public Builder(ColorInfo colorInfo, int width, int height) {
this.colorInfo = colorInfo;
this.width = width;
this.height = height;
pixelWidthHeightRatio = 1;
}
/** Creates an instance with the values of the provided {@link FrameInfo}. */
public Builder(FrameInfo frameInfo) {
colorInfo = frameInfo.colorInfo;
width = frameInfo.width;
height = frameInfo.height;
pixelWidthHeightRatio = frameInfo.pixelWidthHeightRatio;
offsetToAddUs = frameInfo.offsetToAddUs;
}
/** Sets the {@link ColorInfo}. */
@CanIgnoreReturnValue
public Builder setColorInfo(ColorInfo colorInfo) {
this.colorInfo = colorInfo;
return this;
}
/** Sets the frame width, in pixels. */
@CanIgnoreReturnValue
public Builder setWidth(int width) {
this.width = width;
return this;
}
/** Sets the frame height, in pixels. */
@CanIgnoreReturnValue
public Builder setHeight(int height) {
this.height = height;
return this;
}
/**
* Sets the ratio of width over height for each pixel.
*
* <p>The default value is {@code 1}.
*/
@CanIgnoreReturnValue
public Builder setPixelWidthHeightRatio(float pixelWidthHeightRatio) {
this.pixelWidthHeightRatio = pixelWidthHeightRatio;
return this;
}
/**
* Sets the {@linkplain FrameInfo#offsetToAddUs offset to add} to the frame presentation
* timestamp, in microseconds.
*
* <p>The default value is {@code 0}.
*/
@CanIgnoreReturnValue
public Builder setOffsetToAddUs(long offsetToAddUs) {
this.offsetToAddUs = offsetToAddUs;
return this;
}
/** Builds a {@link FrameInfo} instance. */
public FrameInfo build() {
return new FrameInfo(colorInfo, width, height, pixelWidthHeightRatio, offsetToAddUs);
}
}
/** The {@link ColorInfo} of the frame. */
public final ColorInfo colorInfo;
/** The width of the frame, in pixels. */
public final int width;
/** The height of the frame, in pixels. */
public final int height;
/** The ratio of width over height for each pixel. */
public final float pixelWidthHeightRatio;
/**
* The offset that must be added to the frame presentation timestamp, in microseconds.
*
* <p>This offset is not part of the input timestamps. It is added to the frame timestamps before
* processing, and is retained in the output timestamps.
*/
public final long offsetToAddUs;
/**
* Creates an instance.
*
* @param format See {@link #format}.
* @param offsetToAddUs See {@link #offsetToAddUs}.
*/
public FrameInfo(Format format, long offsetToAddUs) {
checkArgument(format.colorInfo != null, "format colorInfo must be set");
checkArgument(format.width > 0, "format width must be positive, but is: " + format.width);
checkArgument(format.height > 0, "format height must be positive, but is: " + format.height);
private FrameInfo(
ColorInfo colorInfo, int width, int height, float pixelWidthHeightRatio, long offsetToAddUs) {
checkArgument(width > 0, "width must be positive, but is: " + width);
checkArgument(height > 0, "height must be positive, but is: " + height);
this.format = format;
this.colorInfo = colorInfo;
this.width = width;
this.height = height;
this.pixelWidthHeightRatio = pixelWidthHeightRatio;
this.offsetToAddUs = offsetToAddUs;
}
}

View File

@ -21,7 +21,7 @@ import android.os.Bundle;
import androidx.annotation.Nullable;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.common.util.Util;
import java.util.Objects;
import com.google.common.base.Objects;
/**
* A rating expressed as "heart" or "no heart". It can be used to indicate whether the content is a
@ -60,7 +60,7 @@ public final class HeartRating extends Rating {
@Override
public int hashCode() {
return Objects.hash(rated, isHeart);
return Objects.hashCode(rated, isHeart);
}
@Override

View File

@ -21,7 +21,6 @@ import android.os.Bundle;
import androidx.annotation.Nullable;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.common.util.Util;
import java.util.Objects;
/** A label for a {@link Format}. */
@UnstableApi
@ -56,7 +55,7 @@ public class Label {
return false;
}
Label label = (Label) o;
return Objects.equals(language, label.language) && Objects.equals(value, label.value);
return Util.areEqual(language, label.language) && Util.areEqual(value, label.value);
}
@Override

View File

@ -39,7 +39,6 @@ import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.UUID;
/** Representation of a media item. */
@ -916,8 +915,8 @@ public final class MediaItem {
DrmConfiguration other = (DrmConfiguration) obj;
return scheme.equals(other.scheme)
&& Objects.equals(licenseUri, other.licenseUri)
&& Objects.equals(licenseRequestHeaders, other.licenseRequestHeaders)
&& Util.areEqual(licenseUri, other.licenseUri)
&& Util.areEqual(licenseRequestHeaders, other.licenseRequestHeaders)
&& multiSession == other.multiSession
&& forceDefaultLicenseUri == other.forceDefaultLicenseUri
&& playClearContentWithoutKey == other.playClearContentWithoutKey
@ -1091,7 +1090,7 @@ public final class MediaItem {
}
AdsConfiguration other = (AdsConfiguration) obj;
return adTagUri.equals(other.adTagUri) && Objects.equals(adsId, other.adsId);
return adTagUri.equals(other.adTagUri) && Util.areEqual(adsId, other.adsId);
}
@Override
@ -1210,14 +1209,14 @@ public final class MediaItem {
LocalConfiguration other = (LocalConfiguration) obj;
return uri.equals(other.uri)
&& Objects.equals(mimeType, other.mimeType)
&& Objects.equals(drmConfiguration, other.drmConfiguration)
&& Objects.equals(adsConfiguration, other.adsConfiguration)
&& Util.areEqual(mimeType, other.mimeType)
&& Util.areEqual(drmConfiguration, other.drmConfiguration)
&& Util.areEqual(adsConfiguration, other.adsConfiguration)
&& streamKeys.equals(other.streamKeys)
&& Objects.equals(customCacheKey, other.customCacheKey)
&& Util.areEqual(customCacheKey, other.customCacheKey)
&& subtitleConfigurations.equals(other.subtitleConfigurations)
&& Objects.equals(tag, other.tag)
&& imageDurationMs == other.imageDurationMs;
&& Util.areEqual(tag, other.tag)
&& Util.areEqual(imageDurationMs, other.imageDurationMs);
}
@Override
@ -1715,12 +1714,12 @@ public final class MediaItem {
SubtitleConfiguration other = (SubtitleConfiguration) obj;
return uri.equals(other.uri)
&& Objects.equals(mimeType, other.mimeType)
&& Objects.equals(language, other.language)
&& Util.areEqual(mimeType, other.mimeType)
&& Util.areEqual(language, other.language)
&& selectionFlags == other.selectionFlags
&& roleFlags == other.roleFlags
&& Objects.equals(label, other.label)
&& Objects.equals(id, other.id);
&& Util.areEqual(label, other.label)
&& Util.areEqual(id, other.id);
}
@Override
@ -2217,8 +2216,8 @@ public final class MediaItem {
return false;
}
RequestMetadata that = (RequestMetadata) o;
return Objects.equals(mediaUri, that.mediaUri)
&& Objects.equals(searchQuery, that.searchQuery)
return Util.areEqual(mediaUri, that.mediaUri)
&& Util.areEqual(searchQuery, that.searchQuery)
&& ((extras == null) == (that.extras == null));
}
@ -2338,12 +2337,12 @@ public final class MediaItem {
MediaItem other = (MediaItem) obj;
return Objects.equals(mediaId, other.mediaId)
return Util.areEqual(mediaId, other.mediaId)
&& clippingConfiguration.equals(other.clippingConfiguration)
&& Objects.equals(localConfiguration, other.localConfiguration)
&& Objects.equals(liveConfiguration, other.liveConfiguration)
&& Objects.equals(mediaMetadata, other.mediaMetadata)
&& Objects.equals(requestMetadata, other.requestMetadata);
&& Util.areEqual(localConfiguration, other.localConfiguration)
&& Util.areEqual(liveConfiguration, other.liveConfiguration)
&& Util.areEqual(mediaMetadata, other.mediaMetadata)
&& Util.areEqual(requestMetadata, other.requestMetadata);
}
@Override

View File

@ -29,11 +29,11 @@ public final class MediaLibraryInfo {
/** The version of the library expressed as a string, for example "1.2.3" or "1.2.0-beta01". */
// Intentionally hardcoded. Do not derive from other constants (e.g. VERSION_INT) or vice versa.
public static final String VERSION = "1.6.1";
public static final String VERSION = "1.5.0";
/** The version of the library expressed as {@code TAG + "/" + VERSION}. */
// Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa.
public static final String VERSION_SLASHY = "AndroidXMedia3/1.6.1";
public static final String VERSION_SLASHY = "AndroidXMedia3/1.5.0";
/**
* The version of the library expressed as an integer, for example 1002003300.
@ -47,7 +47,7 @@ public final class MediaLibraryInfo {
* (123-045-006-3-00).
*/
// Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa.
public static final int VERSION_INT = 1_006_001_3_00;
public static final int VERSION_INT = 1_005_000_3_00;
/** Whether the library was compiled with {@link Assertions} checks enabled. */
public static final boolean ASSERTIONS_ENABLED = true;

View File

@ -29,6 +29,7 @@ import androidx.annotation.IntRange;
import androidx.annotation.Nullable;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.common.util.Util;
import com.google.common.base.Objects;
import com.google.common.collect.ImmutableList;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
import java.lang.annotation.Documented;
@ -38,7 +39,6 @@ import java.lang.annotation.Target;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
/**
* Metadata of a {@link MediaItem}, playlist, or a combination of multiple sources of {@link
@ -194,6 +194,7 @@ public final class MediaMetadata {
*
* @throws IllegalArgumentException if the duration is negative.
*/
@UnstableApi
@CanIgnoreReturnValue
public Builder setDurationMs(@Nullable Long durationMs) {
checkArgument(durationMs == null || durationMs >= 0);
@ -249,8 +250,8 @@ public final class MediaMetadata {
@CanIgnoreReturnValue
public Builder maybeSetArtworkData(byte[] artworkData, @PictureType int artworkDataType) {
if (this.artworkData == null
|| artworkDataType == PICTURE_TYPE_FRONT_COVER
|| !Objects.equals(this.artworkDataType, PICTURE_TYPE_FRONT_COVER)) {
|| Util.areEqual(artworkDataType, PICTURE_TYPE_FRONT_COVER)
|| !Util.areEqual(this.artworkDataType, PICTURE_TYPE_FRONT_COVER)) {
this.artworkData = artworkData.clone();
this.artworkDataType = artworkDataType;
}
@ -1028,7 +1029,7 @@ public final class MediaMetadata {
* informational purpose only. For retrieving the duration of the media item currently being
* played, use {@link Player#getDuration()} instead.
*/
@Nullable public final Long durationMs;
@UnstableApi @Nullable public final Long durationMs;
/** Optional user {@link Rating}. */
@Nullable public final Rating userRating;
@ -1221,47 +1222,47 @@ public final class MediaMetadata {
return false;
}
MediaMetadata that = (MediaMetadata) obj;
return Objects.equals(title, that.title)
&& Objects.equals(artist, that.artist)
&& Objects.equals(albumTitle, that.albumTitle)
&& Objects.equals(albumArtist, that.albumArtist)
&& Objects.equals(displayTitle, that.displayTitle)
&& Objects.equals(subtitle, that.subtitle)
&& Objects.equals(description, that.description)
&& Objects.equals(durationMs, that.durationMs)
&& Objects.equals(userRating, that.userRating)
&& Objects.equals(overallRating, that.overallRating)
return Util.areEqual(title, that.title)
&& Util.areEqual(artist, that.artist)
&& Util.areEqual(albumTitle, that.albumTitle)
&& Util.areEqual(albumArtist, that.albumArtist)
&& Util.areEqual(displayTitle, that.displayTitle)
&& Util.areEqual(subtitle, that.subtitle)
&& Util.areEqual(description, that.description)
&& Util.areEqual(durationMs, that.durationMs)
&& Util.areEqual(userRating, that.userRating)
&& Util.areEqual(overallRating, that.overallRating)
&& Arrays.equals(artworkData, that.artworkData)
&& Objects.equals(artworkDataType, that.artworkDataType)
&& Objects.equals(artworkUri, that.artworkUri)
&& Objects.equals(trackNumber, that.trackNumber)
&& Objects.equals(totalTrackCount, that.totalTrackCount)
&& Objects.equals(folderType, that.folderType)
&& Objects.equals(isBrowsable, that.isBrowsable)
&& Objects.equals(isPlayable, that.isPlayable)
&& Objects.equals(recordingYear, that.recordingYear)
&& Objects.equals(recordingMonth, that.recordingMonth)
&& Objects.equals(recordingDay, that.recordingDay)
&& Objects.equals(releaseYear, that.releaseYear)
&& Objects.equals(releaseMonth, that.releaseMonth)
&& Objects.equals(releaseDay, that.releaseDay)
&& Objects.equals(writer, that.writer)
&& Objects.equals(composer, that.composer)
&& Objects.equals(conductor, that.conductor)
&& Objects.equals(discNumber, that.discNumber)
&& Objects.equals(totalDiscCount, that.totalDiscCount)
&& Objects.equals(genre, that.genre)
&& Objects.equals(compilation, that.compilation)
&& Objects.equals(station, that.station)
&& Objects.equals(mediaType, that.mediaType)
&& Objects.equals(supportedCommands, that.supportedCommands)
&& Util.areEqual(artworkDataType, that.artworkDataType)
&& Util.areEqual(artworkUri, that.artworkUri)
&& Util.areEqual(trackNumber, that.trackNumber)
&& Util.areEqual(totalTrackCount, that.totalTrackCount)
&& Util.areEqual(folderType, that.folderType)
&& Util.areEqual(isBrowsable, that.isBrowsable)
&& Util.areEqual(isPlayable, that.isPlayable)
&& Util.areEqual(recordingYear, that.recordingYear)
&& Util.areEqual(recordingMonth, that.recordingMonth)
&& Util.areEqual(recordingDay, that.recordingDay)
&& Util.areEqual(releaseYear, that.releaseYear)
&& Util.areEqual(releaseMonth, that.releaseMonth)
&& Util.areEqual(releaseDay, that.releaseDay)
&& Util.areEqual(writer, that.writer)
&& Util.areEqual(composer, that.composer)
&& Util.areEqual(conductor, that.conductor)
&& Util.areEqual(discNumber, that.discNumber)
&& Util.areEqual(totalDiscCount, that.totalDiscCount)
&& Util.areEqual(genre, that.genre)
&& Util.areEqual(compilation, that.compilation)
&& Util.areEqual(station, that.station)
&& Util.areEqual(mediaType, that.mediaType)
&& Util.areEqual(supportedCommands, that.supportedCommands)
&& ((extras == null) == (that.extras == null));
}
@SuppressWarnings("deprecation") // Hashing deprecated fields.
@Override
public int hashCode() {
return Objects.hash(
return Objects.hashCode(
title,
artist,
albumTitle,

View File

@ -15,6 +15,8 @@
*/
package androidx.media3.common;
import android.os.Parcel;
import android.os.Parcelable;
import androidx.annotation.Nullable;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.common.util.Util;
@ -24,10 +26,10 @@ import java.util.List;
/** A collection of metadata entries. */
@UnstableApi
public final class Metadata {
public final class Metadata implements Parcelable {
/** A metadata entry. */
public interface Entry {
public interface Entry extends Parcelable {
/**
* Returns the {@link Format} that can be used to decode the wrapped metadata in {@link
@ -98,6 +100,14 @@ public final class Metadata {
this(presentationTimeUs, entries.toArray(new Entry[0]));
}
/* package */ Metadata(Parcel in) {
entries = new Metadata.Entry[in.readInt()];
for (int i = 0; i < entries.length; i++) {
entries[i] = in.readParcelable(Entry.class.getClassLoader());
}
presentationTimeUs = in.readLong();
}
/** Returns the number of metadata entries. */
public int length() {
return entries.length;
@ -180,4 +190,33 @@ public final class Metadata {
+ Arrays.toString(entries)
+ (presentationTimeUs == C.TIME_UNSET ? "" : ", presentationTimeUs=" + presentationTimeUs);
}
// Parcelable implementation.
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeInt(entries.length);
for (Entry entry : entries) {
dest.writeParcelable(entry, 0);
}
dest.writeLong(presentationTimeUs);
}
public static final Parcelable.Creator<Metadata> CREATOR =
new Parcelable.Creator<Metadata>() {
@Override
public Metadata createFromParcel(Parcel in) {
return new Metadata(in);
}
@Override
public Metadata[] newArray(int size) {
return new Metadata[size];
}
};
}

View File

@ -44,7 +44,6 @@ public final class MimeTypes {
public static final String VIDEO_WEBM = BASE_TYPE_VIDEO + "/webm";
public static final String VIDEO_H263 = BASE_TYPE_VIDEO + "/3gpp";
public static final String VIDEO_H264 = BASE_TYPE_VIDEO + "/avc";
@UnstableApi public static final String VIDEO_APV = BASE_TYPE_VIDEO + "/apv";
public static final String VIDEO_H265 = BASE_TYPE_VIDEO + "/hevc";
@UnstableApi public static final String VIDEO_VP8 = BASE_TYPE_VIDEO + "/x-vnd.on2.vp8";
@UnstableApi public static final String VIDEO_VP9 = BASE_TYPE_VIDEO + "/x-vnd.on2.vp9";
@ -580,36 +579,6 @@ public final class MimeTypes {
}
}
/**
* Returns whether the given {@code codecs} and {@code supplementalCodecs} correspond to a valid
* Dolby Vision codec.
*
* @param codecs An RFC 6381 codecs string for the base codec. may be null.
* @param supplementalCodecs An optional RFC 6381 codecs string for supplemental codecs.
* @return Whether the given {@code codecs} and {@code supplementalCodecs} correspond to a valid
* Dolby Vision codec.
*/
@UnstableApi
public static boolean isDolbyVisionCodec(
@Nullable String codecs, @Nullable String supplementalCodecs) {
if (codecs == null) {
return false;
}
if (codecs.startsWith("dvhe") || codecs.startsWith("dvh1")) {
// profile 5
return true;
}
if (supplementalCodecs == null) {
return false;
}
// profiles 8, 9 and 10
return (supplementalCodecs.startsWith("dvhe") && codecs.startsWith("hev1"))
|| (supplementalCodecs.startsWith("dvh1") && codecs.startsWith("hvc1"))
|| (supplementalCodecs.startsWith("dvav") && codecs.startsWith("avc3"))
|| (supplementalCodecs.startsWith("dva1") && codecs.startsWith("avc1"))
|| (supplementalCodecs.startsWith("dav1") && codecs.startsWith("av01"));
}
/**
* Returns the {@link C.TrackType track type} constant corresponding to a specified MIME type,
* which may be {@link C#TRACK_TYPE_UNKNOWN} if it could not be determined.
@ -716,9 +685,6 @@ public final class MimeTypes {
}
mimeType = Ascii.toLowerCase(mimeType);
switch (mimeType) {
// Normalize uncommon versions of some video MIME types to their standard equivalent.
case BASE_TYPE_VIDEO + "/x-mvhevc":
return VIDEO_MV_HEVC;
// Normalize uncommon versions of some audio MIME types to their standard equivalent.
case BASE_TYPE_AUDIO + "/x-flac":
return AUDIO_FLAC;

View File

@ -1,133 +0,0 @@
/*
* Copyright 2024 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 androidx.media3.common;
import android.util.Pair;
import androidx.media3.common.util.UnstableApi;
/**
* Provides information of how an input texture (for example, a {@code TextureOverlay} or in {@code
* VideoCompositor}) is presented.
*/
@UnstableApi
public interface OverlaySettings {
/** The default alpha scale value of the overlay. */
float DEFAULT_ALPHA_SCALE = 1f;
/** The default coordinates for the anchor point of the overlay within the background frame. */
Pair<Float, Float> DEFAULT_BACKGROUND_FRAME_ANCHOR = Pair.create(0f, 0f);
/** The default coordinates for the anchor point of the overlay frame. */
Pair<Float, Float> DEFAULT_OVERLAY_FRAME_ANCHOR = Pair.create(0f, 0f);
/** The default scaling of the overlay. */
Pair<Float, Float> DEFAULT_SCALE = Pair.create(1f, 1f);
/** The default rotation of the overlay, counter-clockwise. */
float DEFAULT_ROTATION_DEGREES = 0f;
/** The default luminance multiplier of an SDR overlay when overlaid on a HDR frame. */
float DEFAULT_HDR_LUMINANCE_MULTIPLIER = 1f;
/**
* Returns the alpha scale value of the overlay, altering its translucency.
*
* <p>An {@code alphaScale} value of {@code 1} means no change is applied. A value below {@code 1}
* increases translucency, and a value above {@code 1} reduces translucency.
*
* <p>The default value is {@link #DEFAULT_ALPHA_SCALE}.
*/
default float getAlphaScale() {
return DEFAULT_ALPHA_SCALE;
}
/**
* Returns the coordinates for the anchor point of the overlay within the background frame.
*
* <p>The coordinates are specified in Normalised Device Coordinates (NDCs) relative to the
* background frame. The ranges for x and y are from {@code -1} to {@code 1}. The default value is
* {@code (0,0)}, the center of the background frame.
*
* <p>The overlay's {@linkplain #getOverlayFrameAnchor anchor point} will be positioned at the
* anchor point returned from this method. For example, a value of {@code (1,1)} will move the
* {@linkplain #getOverlayFrameAnchor overlay's anchor} to the top right corner. That is, if the
* overlay's anchor is at {@code (1,1)} (the top right corner), the overlay's top right corner
* will be aligned with that of the background frame; whereas if the overlay's anchor is at {@code
* (0,0)} (the center), the overlay's center will be positioned at the top right corner of the
* background frame.
*
* <p>The default value is {@link #DEFAULT_BACKGROUND_FRAME_ANCHOR}.
*/
default Pair<Float, Float> getBackgroundFrameAnchor() {
return DEFAULT_BACKGROUND_FRAME_ANCHOR;
}
/**
* Returns the coordinates for the anchor point within the overlay.
*
* <p>The anchor point is the point inside the overlay that is placed on the {@linkplain
* #getBackgroundFrameAnchor background frame anchor}
*
* <p>The coordinates are specified in Normalised Device Coordinates (NDCs) relative to the
* overlay. The ranges for x and y are from {@code -1} to {@code 1}. The default value is {@code
* (0,0)}, the center of the overlay.
*
* <p>See {@link #getBackgroundFrameAnchor} for examples of how to position an overlay.
*
* <p>The default value is {@link #DEFAULT_OVERLAY_FRAME_ANCHOR}.
*/
default Pair<Float, Float> getOverlayFrameAnchor() {
return DEFAULT_OVERLAY_FRAME_ANCHOR;
}
/**
* Returns the scaling of the overlay.
*
* <p>The default value is {@link #DEFAULT_SCALE}.
*/
default Pair<Float, Float> getScale() {
return DEFAULT_SCALE;
}
/**
* Returns the rotation of the overlay, counter-clockwise.
*
* <p>The overlay is rotated at the center of its frame.
*
* <p>The default value is {@link #DEFAULT_ROTATION_DEGREES}.
*/
default float getRotationDegrees() {
return DEFAULT_ROTATION_DEGREES;
}
/**
* Returns the luminance multiplier of an SDR overlay when overlaid on a HDR frame.
*
* <p>Scales the luminance of the overlay to adjust the output brightness of the overlay on the
* frame. The default value is 1, which scales the overlay colors into the standard HDR luminance
* within the processing pipeline. Use 0.5 to scale the luminance of the overlay to SDR range, so
* that no extra luminance is added.
*
* <p>Currently only supported on text overlays
*
* <p>The default value is {@link #DEFAULT_HDR_LUMINANCE_MULTIPLIER}.
*/
default float getHdrLuminanceMultiplier() {
return DEFAULT_HDR_LUMINANCE_MULTIPLIER;
}
}

View File

@ -109,10 +109,10 @@ public class ParserException extends IOException {
this.dataType = dataType;
}
@Nullable
@Override
public String getMessage() {
String superMessage = super.getMessage();
return (superMessage != null ? superMessage + " " : "")
return super.getMessage()
+ " {contentIsMalformed="
+ contentIsMalformed
+ ", dataType="

View File

@ -22,7 +22,7 @@ import androidx.annotation.FloatRange;
import androidx.annotation.Nullable;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.common.util.Util;
import java.util.Objects;
import com.google.common.base.Objects;
/** A rating expressed as a percentage. */
public final class PercentageRating extends Rating {
@ -59,7 +59,7 @@ public final class PercentageRating extends Rating {
@Override
public int hashCode() {
return Objects.hash(percent);
return Objects.hashCode(percent);
}
@Override

View File

@ -36,7 +36,6 @@ import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.Objects;
/** Thrown when a non locally recoverable playback failure occurs. */
public class PlaybackException extends Exception {
@ -92,7 +91,6 @@ public class PlaybackException extends Exception {
ERROR_CODE_DECODING_FAILED,
ERROR_CODE_DECODING_FORMAT_EXCEEDS_CAPABILITIES,
ERROR_CODE_DECODING_FORMAT_UNSUPPORTED,
ERROR_CODE_DECODING_RESOURCES_RECLAIMED,
ERROR_CODE_AUDIO_TRACK_INIT_FAILED,
ERROR_CODE_AUDIO_TRACK_WRITE_FAILED,
ERROR_CODE_AUDIO_TRACK_OFFLOAD_INIT_FAILED,
@ -274,8 +272,9 @@ public class PlaybackException extends Exception {
/** Caused by trying to decode content whose format is not supported. */
public static final int ERROR_CODE_DECODING_FORMAT_UNSUPPORTED = 4005;
// TODO: b/322943860 - Stabilize error code and add to IntDef
/** Caused by higher priority task reclaiming resources needed for decoding. */
public static final int ERROR_CODE_DECODING_RESOURCES_RECLAIMED = 4006;
@UnstableApi public static final int ERROR_CODE_DECODING_RESOURCES_RECLAIMED = 4006;
// AudioTrack errors (5xxx).
@ -554,17 +553,17 @@ public class PlaybackException extends Exception {
@Nullable Throwable thisCause = getCause();
@Nullable Throwable thatCause = other.getCause();
if (thisCause != null && thatCause != null) {
if (!Objects.equals(thisCause.getMessage(), thatCause.getMessage())) {
if (!Util.areEqual(thisCause.getMessage(), thatCause.getMessage())) {
return false;
}
if (!Objects.equals(thisCause.getClass(), thatCause.getClass())) {
if (!Util.areEqual(thisCause.getClass(), thatCause.getClass())) {
return false;
}
} else if (thisCause != null || thatCause != null) {
return false;
}
return errorCode == other.errorCode
&& Objects.equals(getMessage(), other.getMessage())
&& Util.areEqual(getMessage(), other.getMessage())
&& timestampMs == other.timestampMs;
}

View File

@ -88,18 +88,6 @@ public final class PlaybackParameters {
return new PlaybackParameters(speed, pitch);
}
/**
* Returns a copy with the given pitch.
*
* @param pitch The new pitch. Must be greater than zero.
* @return The copied playback parameters.
*/
@UnstableApi
@CheckResult
public PlaybackParameters withPitch(@FloatRange(from = 0, fromInclusive = false) float pitch) {
return new PlaybackParameters(speed, pitch);
}
@Override
public boolean equals(@Nullable Object obj) {
if (this == obj) {

View File

@ -37,6 +37,7 @@ import androidx.media3.common.text.CueGroup;
import androidx.media3.common.util.Size;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.common.util.Util;
import com.google.common.base.Objects;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
@ -44,7 +45,6 @@ import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
/**
* A media player interface defining high-level functionality, such as the ability to play, pause,
@ -352,13 +352,13 @@ public interface Player {
}
PositionInfo that = (PositionInfo) o;
return equalsForBundling(that)
&& Objects.equals(windowUid, that.windowUid)
&& Objects.equals(periodUid, that.periodUid);
&& Objects.equal(windowUid, that.windowUid)
&& Objects.equal(periodUid, that.periodUid);
}
@Override
public int hashCode() {
return Objects.hash(
return Objects.hashCode(
windowUid,
mediaItemIndex,
mediaItem,
@ -382,7 +382,7 @@ public interface Player {
&& contentPositionMs == other.contentPositionMs
&& adGroupIndex == other.adGroupIndex
&& adIndexInAdGroup == other.adIndexInAdGroup
&& Objects.equals(mediaItem, other.mediaItem);
&& Objects.equal(mediaItem, other.mediaItem);
}
@VisibleForTesting static final String FIELD_MEDIA_ITEM_INDEX = Util.intToStringMaxRadix(0);
@ -1613,7 +1613,7 @@ public interface Player {
/** {@link #getDeviceInfo()} changed. */
int EVENT_DEVICE_INFO_CHANGED = 29;
/** {@link #getDeviceVolume()} or {@link #isDeviceMuted()} changed. */
/** {@link #getDeviceVolume()} changed. */
int EVENT_DEVICE_VOLUME_CHANGED = 30;
/**
@ -2855,6 +2855,7 @@ public interface Player {
*/
TrackSelectionParameters getTrackSelectionParameters();
// LINT.IfChange(set_track_selection_parameters)
/**
* Sets the parameters constraining the track selection.
*
@ -3383,7 +3384,8 @@ public interface Player {
*
* <p>For devices with {@link DeviceInfo#PLAYBACK_TYPE_LOCAL local playback}, the volume returned
* by this method varies according to the current {@link C.StreamType stream type}. The stream
* type is determined by {@link AudioAttributes#getStreamType()}.
* type is determined by {@link AudioAttributes#usage} which can be converted to stream type with
* {@link Util#getStreamTypeForAudioUsage(int)}.
*
* <p>For devices with {@link DeviceInfo#PLAYBACK_TYPE_REMOTE remote playback}, the volume of the
* remote device is returned.
@ -3506,6 +3508,10 @@ public interface Player {
* <p>If tunneling is enabled by the track selector, the specified audio attributes will be
* ignored, but they will take effect if audio is later played without tunneling.
*
* <p>If the device is running a build before platform API version 21, audio attributes cannot be
* set directly on the underlying audio track. In this case, the usage will be mapped onto an
* equivalent stream type using {@link Util#getStreamTypeForAudioUsage(int)}.
*
* <p>If audio focus should be handled, the {@link AudioAttributes#usage} must be {@link
* C#USAGE_MEDIA} or {@link C#USAGE_GAME}. Other usages will throw an {@link
* IllegalArgumentException}.

View File

@ -35,7 +35,6 @@ public interface PreviewingVideoGraph extends VideoGraph {
* @param debugViewProvider A {@link DebugViewProvider}.
* @param listener A {@link Listener}.
* @param listenerExecutor The {@link Executor} on which the {@code listener} is invoked.
* @param videoCompositorSettings The {@link VideoCompositorSettings}.
* @param compositionEffects A list of {@linkplain Effect effects} to apply to the composition.
* @param initialTimestampOffsetUs The timestamp offset for the first frame, in microseconds.
* @return A new instance.
@ -48,16 +47,9 @@ public interface PreviewingVideoGraph extends VideoGraph {
DebugViewProvider debugViewProvider,
Listener listener,
Executor listenerExecutor,
VideoCompositorSettings videoCompositorSettings,
List<Effect> compositionEffects,
long initialTimestampOffsetUs)
throws VideoFrameProcessingException;
/**
* Returns whether the {@link VideoGraph} implementation supports {@linkplain #registerInput
* registering} multiple inputs.
*/
boolean supportsMultipleInputs();
}
/**

View File

@ -15,6 +15,7 @@
*/
package androidx.media3.common;
import static androidx.annotation.VisibleForTesting.PROTECTED;
import static androidx.media3.common.util.Assertions.checkArgument;
import static androidx.media3.common.util.Assertions.checkNotNull;
import static androidx.media3.common.util.Util.castNonNull;
@ -34,6 +35,7 @@ import android.view.TextureView;
import androidx.annotation.FloatRange;
import androidx.annotation.IntRange;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import androidx.media3.common.text.CueGroup;
import androidx.media3.common.util.Clock;
import androidx.media3.common.util.HandlerWrapper;
@ -96,7 +98,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
public abstract class SimpleBasePlayer extends BasePlayer {
/** An immutable state description of the player. */
public static final class State {
protected static final class State {
/** A builder for {@link State} objects. */
public static final class Builder {
@ -159,7 +161,7 @@ public abstract class SimpleBasePlayer extends BasePlayer {
seekForwardIncrementMs = C.DEFAULT_SEEK_FORWARD_INCREMENT_MS;
maxSeekToPreviousPositionMs = C.DEFAULT_MAX_SEEK_TO_PREVIOUS_POSITION_MS;
playbackParameters = PlaybackParameters.DEFAULT;
trackSelectionParameters = TrackSelectionParameters.DEFAULT;
trackSelectionParameters = TrackSelectionParameters.DEFAULT_WITHOUT_CONTEXT;
audioAttributes = AudioAttributes.DEFAULT;
volume = 1f;
videoSize = VideoSize.UNKNOWN;
@ -220,7 +222,7 @@ public abstract class SimpleBasePlayer extends BasePlayer {
this.playlist = ((PlaylistTimeline) state.timeline).playlist;
} else {
this.currentTracks = state.currentTracks;
this.currentMetadata = state.usesDerivedMediaMetadata ? null : state.currentMetadata;
this.currentMetadata = state.currentMetadata;
}
this.playlistMetadata = state.playlistMetadata;
this.currentMediaItemIndex = state.currentMediaItemIndex;
@ -956,12 +958,9 @@ public abstract class SimpleBasePlayer extends BasePlayer {
*/
public final long discontinuityPositionMs;
private final boolean usesDerivedMediaMetadata;
private State(Builder builder) {
Tracks currentTracks = builder.currentTracks;
MediaMetadata currentMetadata = builder.currentMetadata;
boolean usesDerivedMediaMetadata = false;
if (builder.timeline.isEmpty()) {
checkArgument(
builder.playbackState == Player.STATE_IDLE
@ -1017,7 +1016,6 @@ public abstract class SimpleBasePlayer extends BasePlayer {
getCombinedMediaMetadata(
builder.timeline.getWindow(mediaItemIndex, new Timeline.Window()).mediaItem,
checkNotNull(currentTracks));
usesDerivedMediaMetadata = true;
}
}
if (builder.playerError != null) {
@ -1094,7 +1092,6 @@ public abstract class SimpleBasePlayer extends BasePlayer {
this.hasPositionDiscontinuity = builder.hasPositionDiscontinuity;
this.positionDiscontinuityReason = builder.positionDiscontinuityReason;
this.discontinuityPositionMs = builder.discontinuityPositionMs;
this.usesDerivedMediaMetadata = usesDerivedMediaMetadata;
}
/** Returns a {@link Builder} pre-populated with the current state values. */
@ -1793,9 +1790,9 @@ public abstract class SimpleBasePlayer extends BasePlayer {
return this.uid.equals(mediaItemData.uid)
&& this.tracks.equals(mediaItemData.tracks)
&& this.mediaItem.equals(mediaItemData.mediaItem)
&& Objects.equals(this.mediaMetadata, mediaItemData.mediaMetadata)
&& Objects.equals(this.manifest, mediaItemData.manifest)
&& Objects.equals(this.liveConfiguration, mediaItemData.liveConfiguration)
&& Util.areEqual(this.mediaMetadata, mediaItemData.mediaMetadata)
&& Util.areEqual(this.manifest, mediaItemData.manifest)
&& Util.areEqual(this.liveConfiguration, mediaItemData.liveConfiguration)
&& this.presentationStartTimeMs == mediaItemData.presentationStartTimeMs
&& this.windowStartTimeMs == mediaItemData.windowStartTimeMs
&& this.elapsedRealtimeEpochOffsetMs == mediaItemData.elapsedRealtimeEpochOffsetMs
@ -2072,21 +2069,7 @@ public abstract class SimpleBasePlayer extends BasePlayer {
}
}
/**
* A supplier for a position.
*
* <p>Convenience methods and classes for creating position suppliers:
*
* <ul>
* <li>Use {@link #getConstant} for constant or non-moving positions.
* <li>Use {@link #getExtrapolating} for positions advancing with the system clock from a
* provided start time.
* <li>Use {@link LivePositionSupplier} for positions that can be directly obtained from a live
* system. Note that these suppliers should be {@linkplain LivePositionSupplier#disconnect
* disconnected} from the live source as soon as the position is no longer valid, for
* example after a position discontinuity.
* </ul>
*/
/** A supplier for a position. */
protected interface PositionSupplier {
/** An instance returning a constant position of zero. */
@ -2119,48 +2102,6 @@ public abstract class SimpleBasePlayer extends BasePlayer {
long get();
}
/**
* A {@link PositionSupplier} connected to a live provider that returns a new value on each
* invocation until it is {@linkplain #disconnect disconnected} from the live source.
*
* <p>The recommended usage of this class is to create a new instance connected to the live source
* and keep returning this instance as long as the position source is still valid. As soon as the
* position source becomes invalid, for example when handling a position discontinuity, call
* {@link #disconnect} with the final position that will be returned for all future invocations.
*/
protected static final class LivePositionSupplier implements PositionSupplier {
private final PositionSupplier livePosition;
private long finalValue;
/**
* Creates the live position supplier.
*
* @param livePosition The function returning the live position.
*/
public LivePositionSupplier(PositionSupplier livePosition) {
this.livePosition = livePosition;
this.finalValue = C.TIME_UNSET;
}
/**
* Disconnects the position supplier from the live source.
*
* <p>All future invocations of {@link #get()} will return the provided final position.
*
* @param finalValue The final position value.
*/
public void disconnect(long finalValue) {
this.finalValue = finalValue;
}
@Override
public long get() {
return finalValue != C.TIME_UNSET ? finalValue : livePosition.get();
}
}
/**
* Position difference threshold below which we do not automatically report a position
* discontinuity, in milliseconds.
@ -2509,7 +2450,8 @@ public abstract class SimpleBasePlayer extends BasePlayer {
}
@Override
protected final void seekTo(
@VisibleForTesting(otherwise = PROTECTED)
public final void seekTo(
int mediaItemIndex,
long positionMs,
@Player.Command int seekCommand,
@ -3460,7 +3402,7 @@ public abstract class SimpleBasePlayer extends BasePlayer {
* index is in the range {@code fromIndex} &lt; {@code toIndex} &lt;= {@link
* #getMediaItemCount()}.
* @param newIndex The new index of the first moved item. The index is in the range {@code 0}
* &lt;= {@code newIndex} &lt;= {@link #getMediaItemCount() - (toIndex - fromIndex)}.
* &lt;= {@code newIndex} &lt; {@link #getMediaItemCount() - (toIndex - fromIndex)}.
* @return A {@link ListenableFuture} indicating the completion of all immediate {@link State}
* changes caused by this call.
*/
@ -3475,9 +3417,9 @@ public abstract class SimpleBasePlayer extends BasePlayer {
* <p>Will only be called if {@link Player#COMMAND_CHANGE_MEDIA_ITEMS} is available.
*
* @param fromIndex The start index of the items to replace. The index is in the range 0 &lt;=
* {@code fromIndex} &lt;= {@link #getMediaItemCount()}.
* {@code fromIndex} &lt; {@link #getMediaItemCount()}.
* @param toIndex The index of the first item not to be replaced (exclusive). The index is in the
* range {@code fromIndex} &lt;= {@code toIndex} &lt;= {@link #getMediaItemCount()}.
* range {@code fromIndex} &lt; {@code toIndex} &lt;= {@link #getMediaItemCount()}.
* @param mediaItems The media items to replace the specified range with.
* @return A {@link ListenableFuture} indicating the completion of all immediate {@link State}
* changes caused by this call.
@ -3486,9 +3428,6 @@ public abstract class SimpleBasePlayer extends BasePlayer {
protected ListenableFuture<?> handleReplaceMediaItems(
int fromIndex, int toIndex, List<MediaItem> mediaItems) {
ListenableFuture<?> addFuture = handleAddMediaItems(toIndex, mediaItems);
if (fromIndex == toIndex) {
return addFuture;
}
ListenableFuture<?> removeFuture = handleRemoveMediaItems(fromIndex, toIndex);
return Util.transformFutureAsync(addFuture, unused -> removeFuture);
}
@ -3622,7 +3561,7 @@ public abstract class SimpleBasePlayer extends BasePlayer {
Player.EVENT_MEDIA_ITEM_TRANSITION,
listener -> listener.onMediaItemTransition(mediaItem, mediaItemTransitionReason));
}
if (!Objects.equals(previousState.playerError, newState.playerError)) {
if (!Util.areEqual(previousState.playerError, newState.playerError)) {
listeners.queueEvent(
Player.EVENT_PLAYER_ERROR,
listener -> listener.onPlayerErrorChanged(newState.playerError));
@ -4069,11 +4008,15 @@ public abstract class SimpleBasePlayer extends BasePlayer {
}
// Only mark changes within the current item as a transition if we are repeating automatically
// or via a seek to next/previous.
if (positionDiscontinuityReason == DISCONTINUITY_REASON_AUTO_TRANSITION
&& getContentPositionMsInternal(previousState, window)
> getContentPositionMsInternal(newState, window)) {
if (positionDiscontinuityReason == DISCONTINUITY_REASON_AUTO_TRANSITION) {
if ((getContentPositionMsInternal(previousState, window)
> getContentPositionMsInternal(newState, window))
|| (newState.hasPositionDiscontinuity
&& newState.discontinuityPositionMs == C.TIME_UNSET
&& isRepeatingCurrentItem)) {
return MEDIA_ITEM_TRANSITION_REASON_REPEAT;
}
}
if (positionDiscontinuityReason == DISCONTINUITY_REASON_SEEK && isRepeatingCurrentItem) {
return MEDIA_ITEM_TRANSITION_REASON_SEEK;
}

View File

@ -23,7 +23,7 @@ import androidx.annotation.IntRange;
import androidx.annotation.Nullable;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.common.util.Util;
import java.util.Objects;
import com.google.common.base.Objects;
/** A rating expressed as a fractional number of stars. */
public final class StarRating extends Rating {
@ -84,7 +84,7 @@ public final class StarRating extends Rating {
@Override
public int hashCode() {
return Objects.hash(maxStars, starRating);
return Objects.hashCode(maxStars, starRating);
}
@Override

View File

@ -21,7 +21,7 @@ import android.os.Bundle;
import androidx.annotation.Nullable;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.common.util.Util;
import java.util.Objects;
import com.google.common.base.Objects;
/** A rating expressed as "thumbs up" or "thumbs down". */
public final class ThumbRating extends Rating {
@ -57,7 +57,7 @@ public final class ThumbRating extends Rating {
@Override
public int hashCode() {
return Objects.hash(rated, isThumbsUp);
return Objects.hashCode(rated, isThumbsUp);
}
@Override

View File

@ -36,7 +36,6 @@ import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.errorprone.annotations.InlineMe;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import org.checkerframework.checker.nullness.qual.NonNull;
/**
@ -372,10 +371,10 @@ public abstract class Timeline {
return false;
}
Window that = (Window) obj;
return Objects.equals(uid, that.uid)
&& Objects.equals(mediaItem, that.mediaItem)
&& Objects.equals(manifest, that.manifest)
&& Objects.equals(liveConfiguration, that.liveConfiguration)
return Util.areEqual(uid, that.uid)
&& Util.areEqual(mediaItem, that.mediaItem)
&& Util.areEqual(manifest, that.manifest)
&& Util.areEqual(liveConfiguration, that.liveConfiguration)
&& presentationStartTimeMs == that.presentationStartTimeMs
&& windowStartTimeMs == that.windowStartTimeMs
&& elapsedRealtimeEpochOffsetMs == that.elapsedRealtimeEpochOffsetMs
@ -872,13 +871,13 @@ public abstract class Timeline {
return false;
}
Period that = (Period) obj;
return Objects.equals(id, that.id)
&& Objects.equals(uid, that.uid)
return Util.areEqual(id, that.id)
&& Util.areEqual(uid, that.uid)
&& windowIndex == that.windowIndex
&& durationUs == that.durationUs
&& positionInWindowUs == that.positionInWindowUs
&& isPlaceholder == that.isPlaceholder
&& Objects.equals(adPlaybackState, that.adPlaybackState);
&& Util.areEqual(adPlaybackState, that.adPlaybackState);
}
@Override

View File

@ -161,11 +161,6 @@ public final class TrackGroup {
return id.equals(other.id) && Arrays.equals(formats, other.formats);
}
@Override
public String toString() {
return id + ": " + Arrays.toString(formats);
}
private static final String FIELD_FORMATS = Util.intToStringMaxRadix(0);
private static final String FIELD_ID = Util.intToStringMaxRadix(1);
@ -174,7 +169,7 @@ public final class TrackGroup {
Bundle bundle = new Bundle();
ArrayList<Bundle> arrayList = new ArrayList<>(formats.length);
for (Format format : formats) {
arrayList.add(format.toBundle());
arrayList.add(format.toBundle(/* excludeMetadata= */ true));
}
bundle.putParcelableArrayList(FIELD_FORMATS, arrayList);
bundle.putString(FIELD_ID, id);

View File

@ -22,7 +22,9 @@ import static java.lang.annotation.ElementType.TYPE_USE;
import static java.lang.annotation.RetentionPolicy.SOURCE;
import android.content.Context;
import android.graphics.Point;
import android.os.Bundle;
import android.os.Looper;
import android.view.accessibility.CaptioningManager;
import androidx.annotation.CallSuper;
import androidx.annotation.IntDef;
@ -35,7 +37,6 @@ import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.primitives.Ints;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.errorprone.annotations.InlineMe;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
@ -43,10 +44,12 @@ import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Set;
import org.checkerframework.checker.initialization.qual.UnknownInitialization;
import org.checkerframework.checker.nullness.qual.EnsuresNonNull;
// LINT.IfChange(javadoc)
/**
* Parameters for controlling track selection.
*
@ -85,10 +88,8 @@ public class TrackSelectionParameters {
private int minVideoBitrate;
private int viewportWidth;
private int viewportHeight;
private boolean isViewportSizeLimitedByPhysicalDisplaySize;
private boolean viewportOrientationMayChange;
private ImmutableList<String> preferredVideoMimeTypes;
private ImmutableList<String> preferredVideoLanguages;
private @C.RoleFlags int preferredVideoRoleFlags;
// Audio
private ImmutableList<String> preferredAudioLanguages;
@ -100,7 +101,6 @@ public class TrackSelectionParameters {
// Text
private ImmutableList<String> preferredTextLanguages;
private @C.RoleFlags int preferredTextRoleFlags;
private boolean usePreferredTextLanguagesAndRoleFlagsFromCaptioningManager;
private @C.SelectionFlags int ignoredTextSelectionFlags;
private boolean selectUndeterminedTextLanguage;
// Image
@ -111,7 +111,12 @@ public class TrackSelectionParameters {
private HashMap<TrackGroup, TrackSelectionOverride> overrides;
private HashSet<@C.TrackType Integer> disabledTrackTypes;
/** Creates a builder with default initial values. */
/**
* @deprecated {@link Context} constraints will not be set using this constructor. Use {@link
* #Builder(Context)} instead.
*/
@UnstableApi
@Deprecated
public Builder() {
// Video
maxVideoWidth = Integer.MAX_VALUE;
@ -120,10 +125,8 @@ public class TrackSelectionParameters {
maxVideoBitrate = Integer.MAX_VALUE;
viewportWidth = Integer.MAX_VALUE;
viewportHeight = Integer.MAX_VALUE;
isViewportSizeLimitedByPhysicalDisplaySize = true;
viewportOrientationMayChange = true;
preferredVideoMimeTypes = ImmutableList.of();
preferredVideoLanguages = ImmutableList.of();
preferredVideoRoleFlags = 0;
// Audio
preferredAudioLanguages = ImmutableList.of();
@ -135,7 +138,6 @@ public class TrackSelectionParameters {
// Text
preferredTextLanguages = ImmutableList.of();
preferredTextRoleFlags = 0;
usePreferredTextLanguagesAndRoleFlagsFromCaptioningManager = true;
ignoredTextSelectionFlags = 0;
selectUndeterminedTextLanguage = false;
// Image
@ -148,12 +150,15 @@ public class TrackSelectionParameters {
}
/**
* @deprecated Use {@link #Builder()} instead.
* Creates a builder with default initial values.
*
* @param context Any context.
*/
@Deprecated
@InlineMe(replacement = "this()")
@SuppressWarnings({"deprecation", "method.invocation"}) // Methods invoked are setter only.
public Builder(Context context) {
this();
setPreferredTextLanguageAndRoleFlagsToCaptioningManagerSettings(context);
setViewportSizeToPhysicalDisplaySize(context, /* viewportOrientationMayChange= */ true);
}
/** Creates a builder with the initial values specified in {@code initialValues}. */
@ -166,42 +171,44 @@ public class TrackSelectionParameters {
@UnstableApi
protected Builder(Bundle bundle) {
// Video
maxVideoWidth = bundle.getInt(FIELD_MAX_VIDEO_WIDTH, DEFAULT.maxVideoWidth);
maxVideoHeight = bundle.getInt(FIELD_MAX_VIDEO_HEIGHT, DEFAULT.maxVideoHeight);
maxVideoFrameRate = bundle.getInt(FIELD_MAX_VIDEO_FRAMERATE, DEFAULT.maxVideoFrameRate);
maxVideoBitrate = bundle.getInt(FIELD_MAX_VIDEO_BITRATE, DEFAULT.maxVideoBitrate);
minVideoWidth = bundle.getInt(FIELD_MIN_VIDEO_WIDTH, DEFAULT.minVideoWidth);
minVideoHeight = bundle.getInt(FIELD_MIN_VIDEO_HEIGHT, DEFAULT.minVideoHeight);
minVideoFrameRate = bundle.getInt(FIELD_MIN_VIDEO_FRAMERATE, DEFAULT.minVideoFrameRate);
minVideoBitrate = bundle.getInt(FIELD_MIN_VIDEO_BITRATE, DEFAULT.minVideoBitrate);
viewportWidth = bundle.getInt(FIELD_VIEWPORT_WIDTH, DEFAULT.viewportWidth);
viewportHeight = bundle.getInt(FIELD_VIEWPORT_HEIGHT, DEFAULT.viewportHeight);
isViewportSizeLimitedByPhysicalDisplaySize =
viewportWidth == Integer.MAX_VALUE
&& viewportHeight == Integer.MAX_VALUE
&& bundle.getBoolean(
FIELD_IS_VIEWPORT_SIZE_LIMITED_BY_PHYSICAL_DISPLAY_SIZE,
DEFAULT.isViewportSizeLimitedByPhysicalDisplaySize);
maxVideoWidth = bundle.getInt(FIELD_MAX_VIDEO_WIDTH, DEFAULT_WITHOUT_CONTEXT.maxVideoWidth);
maxVideoHeight =
bundle.getInt(FIELD_MAX_VIDEO_HEIGHT, DEFAULT_WITHOUT_CONTEXT.maxVideoHeight);
maxVideoFrameRate =
bundle.getInt(FIELD_MAX_VIDEO_FRAMERATE, DEFAULT_WITHOUT_CONTEXT.maxVideoFrameRate);
maxVideoBitrate =
bundle.getInt(FIELD_MAX_VIDEO_BITRATE, DEFAULT_WITHOUT_CONTEXT.maxVideoBitrate);
minVideoWidth = bundle.getInt(FIELD_MIN_VIDEO_WIDTH, DEFAULT_WITHOUT_CONTEXT.minVideoWidth);
minVideoHeight =
bundle.getInt(FIELD_MIN_VIDEO_HEIGHT, DEFAULT_WITHOUT_CONTEXT.minVideoHeight);
minVideoFrameRate =
bundle.getInt(FIELD_MIN_VIDEO_FRAMERATE, DEFAULT_WITHOUT_CONTEXT.minVideoFrameRate);
minVideoBitrate =
bundle.getInt(FIELD_MIN_VIDEO_BITRATE, DEFAULT_WITHOUT_CONTEXT.minVideoBitrate);
viewportWidth = bundle.getInt(FIELD_VIEWPORT_WIDTH, DEFAULT_WITHOUT_CONTEXT.viewportWidth);
viewportHeight = bundle.getInt(FIELD_VIEWPORT_HEIGHT, DEFAULT_WITHOUT_CONTEXT.viewportHeight);
viewportOrientationMayChange =
bundle.getBoolean(
FIELD_VIEWPORT_ORIENTATION_MAY_CHANGE, DEFAULT.viewportOrientationMayChange);
FIELD_VIEWPORT_ORIENTATION_MAY_CHANGE,
DEFAULT_WITHOUT_CONTEXT.viewportOrientationMayChange);
preferredVideoMimeTypes =
ImmutableList.copyOf(
firstNonNull(bundle.getStringArray(FIELD_PREFERRED_VIDEO_MIMETYPES), new String[0]));
preferredVideoLanguages =
ImmutableList.copyOf(
firstNonNull(bundle.getStringArray(FIELD_PREFERRED_VIDEO_LANGUAGES), new String[0]));
preferredVideoRoleFlags =
bundle.getInt(FIELD_PREFERRED_VIDEO_ROLE_FLAGS, DEFAULT.preferredVideoRoleFlags);
bundle.getInt(
FIELD_PREFERRED_VIDEO_ROLE_FLAGS, DEFAULT_WITHOUT_CONTEXT.preferredVideoRoleFlags);
// Audio
String[] preferredAudioLanguages1 =
firstNonNull(bundle.getStringArray(FIELD_PREFERRED_AUDIO_LANGUAGES), new String[0]);
preferredAudioLanguages = normalizeLanguageCodes(preferredAudioLanguages1);
preferredAudioRoleFlags =
bundle.getInt(FIELD_PREFERRED_AUDIO_ROLE_FLAGS, DEFAULT.preferredAudioRoleFlags);
bundle.getInt(
FIELD_PREFERRED_AUDIO_ROLE_FLAGS, DEFAULT_WITHOUT_CONTEXT.preferredAudioRoleFlags);
maxAudioChannelCount =
bundle.getInt(FIELD_MAX_AUDIO_CHANNEL_COUNT, DEFAULT.maxAudioChannelCount);
maxAudioBitrate = bundle.getInt(FIELD_MAX_AUDIO_BITRATE, DEFAULT.maxAudioBitrate);
bundle.getInt(
FIELD_MAX_AUDIO_CHANNEL_COUNT, DEFAULT_WITHOUT_CONTEXT.maxAudioChannelCount);
maxAudioBitrate =
bundle.getInt(FIELD_MAX_AUDIO_BITRATE, DEFAULT_WITHOUT_CONTEXT.maxAudioBitrate);
preferredAudioMimeTypes =
ImmutableList.copyOf(
firstNonNull(bundle.getStringArray(FIELD_PREFERRED_AUDIO_MIME_TYPES), new String[0]));
@ -211,29 +218,29 @@ public class TrackSelectionParameters {
normalizeLanguageCodes(
firstNonNull(bundle.getStringArray(FIELD_PREFERRED_TEXT_LANGUAGES), new String[0]));
preferredTextRoleFlags =
bundle.getInt(FIELD_PREFERRED_TEXT_ROLE_FLAGS, DEFAULT.preferredTextRoleFlags);
usePreferredTextLanguagesAndRoleFlagsFromCaptioningManager =
preferredTextLanguages.isEmpty()
&& preferredTextRoleFlags == 0
&& bundle.getBoolean(
FIELD_USE_PREFERRED_TEXT_LANGUAGES_AND_ROLE_FLAGS_FROM_CAPTIONING_MANAGER,
DEFAULT.usePreferredTextLanguagesAndRoleFlagsFromCaptioningManager);
bundle.getInt(
FIELD_PREFERRED_TEXT_ROLE_FLAGS, DEFAULT_WITHOUT_CONTEXT.preferredTextRoleFlags);
ignoredTextSelectionFlags =
bundle.getInt(FIELD_IGNORED_TEXT_SELECTION_FLAGS, DEFAULT.ignoredTextSelectionFlags);
bundle.getInt(
FIELD_IGNORED_TEXT_SELECTION_FLAGS,
DEFAULT_WITHOUT_CONTEXT.ignoredTextSelectionFlags);
selectUndeterminedTextLanguage =
bundle.getBoolean(
FIELD_SELECT_UNDETERMINED_TEXT_LANGUAGE, DEFAULT.selectUndeterminedTextLanguage);
FIELD_SELECT_UNDETERMINED_TEXT_LANGUAGE,
DEFAULT_WITHOUT_CONTEXT.selectUndeterminedTextLanguage);
// Image
isPrioritizeImageOverVideoEnabled =
bundle.getBoolean(
FIELD_IS_PREFER_IMAGE_OVER_VIDEO_ENABLED, DEFAULT.isPrioritizeImageOverVideoEnabled);
FIELD_IS_PREFER_IMAGE_OVER_VIDEO_ENABLED,
DEFAULT_WITHOUT_CONTEXT.isPrioritizeImageOverVideoEnabled);
// General
forceLowestBitrate =
bundle.getBoolean(FIELD_FORCE_LOWEST_BITRATE, DEFAULT.forceLowestBitrate);
bundle.getBoolean(FIELD_FORCE_LOWEST_BITRATE, DEFAULT_WITHOUT_CONTEXT.forceLowestBitrate);
forceHighestSupportedBitrate =
bundle.getBoolean(
FIELD_FORCE_HIGHEST_SUPPORTED_BITRATE, DEFAULT.forceHighestSupportedBitrate);
FIELD_FORCE_HIGHEST_SUPPORTED_BITRATE,
DEFAULT_WITHOUT_CONTEXT.forceHighestSupportedBitrate);
@Nullable
List<Bundle> overrideBundleList = bundle.getParcelableArrayList(FIELD_SELECTION_OVERRIDES);
List<TrackSelectionOverride> overrideList =
@ -277,7 +284,6 @@ public class TrackSelectionParameters {
/** Overrides the value of the builder with the value of {@link TrackSelectionParameters}. */
@EnsuresNonNull({
"preferredVideoMimeTypes",
"preferredVideoLanguages",
"preferredAudioLanguages",
"preferredAudioMimeTypes",
"audioOffloadPreferences",
@ -297,11 +303,8 @@ public class TrackSelectionParameters {
minVideoBitrate = parameters.minVideoBitrate;
viewportWidth = parameters.viewportWidth;
viewportHeight = parameters.viewportHeight;
isViewportSizeLimitedByPhysicalDisplaySize =
parameters.isViewportSizeLimitedByPhysicalDisplaySize;
viewportOrientationMayChange = parameters.viewportOrientationMayChange;
preferredVideoMimeTypes = parameters.preferredVideoMimeTypes;
preferredVideoLanguages = parameters.preferredVideoLanguages;
preferredVideoRoleFlags = parameters.preferredVideoRoleFlags;
// Audio
preferredAudioLanguages = parameters.preferredAudioLanguages;
@ -313,8 +316,6 @@ public class TrackSelectionParameters {
// Text
preferredTextLanguages = parameters.preferredTextLanguages;
preferredTextRoleFlags = parameters.preferredTextRoleFlags;
usePreferredTextLanguagesAndRoleFlagsFromCaptioningManager =
parameters.usePreferredTextLanguagesAndRoleFlagsFromCaptioningManager;
ignoredTextSelectionFlags = parameters.ignoredTextSelectionFlags;
selectUndeterminedTextLanguage = parameters.selectUndeterminedTextLanguage;
// Image
@ -433,31 +434,20 @@ public class TrackSelectionParameters {
}
/**
* Sets whether the viewport size should be assumed to the physical display size if no other
* specific viewport size constraint is specified. Constrains video track selections for
* adaptive content so that only tracks suitable for the viewport are selected.
* Equivalent to calling {@link #setViewportSize(int, int, boolean)} with the viewport size
* obtained from {@link Util#getCurrentDisplayModeSize(Context)}.
*
* @param context Any context.
* @param viewportOrientationMayChange Whether the viewport orientation may change during
* playback.
* @return This builder.
*/
@CanIgnoreReturnValue
public Builder setViewportSizeToPhysicalDisplaySize(boolean viewportOrientationMayChange) {
this.isViewportSizeLimitedByPhysicalDisplaySize = true;
this.viewportOrientationMayChange = viewportOrientationMayChange;
this.viewportHeight = Integer.MAX_VALUE;
this.viewportWidth = Integer.MAX_VALUE;
return this;
}
/**
* @deprecated Use {@link #setViewportSizeToPhysicalDisplaySize(boolean)} instead.
*/
@Deprecated
@CanIgnoreReturnValue
public Builder setViewportSizeToPhysicalDisplaySize(
Context context, boolean viewportOrientationMayChange) {
return setViewportSizeToPhysicalDisplaySize(viewportOrientationMayChange);
// Assume the viewport is fullscreen.
Point viewportSize = Util.getCurrentDisplayModeSize(context);
return setViewportSize(viewportSize.x, viewportSize.y, viewportOrientationMayChange);
}
/**
@ -487,7 +477,6 @@ public class TrackSelectionParameters {
this.viewportWidth = viewportWidth;
this.viewportHeight = viewportHeight;
this.viewportOrientationMayChange = viewportOrientationMayChange;
this.isViewportSizeLimitedByPhysicalDisplaySize = false;
return this;
}
@ -515,36 +504,6 @@ public class TrackSelectionParameters {
return this;
}
/**
* Sets the preferred language for video tracks.
*
* @param preferredVideoLanguage Preferred video language as an IETF BCP 47 conformant tag, or
* {@code null} to express no language preference for video track selection.
* @return This builder.
*/
@UnstableApi
@CanIgnoreReturnValue
public Builder setPreferredVideoLanguage(@Nullable String preferredVideoLanguage) {
return preferredVideoLanguage == null
? setPreferredVideoLanguages()
: setPreferredVideoLanguages(preferredVideoLanguage);
}
/**
* Sets the preferred languages for video tracks.
*
* @param preferredVideoLanguages Preferred video languages as IETF BCP 47 conformant tags in
* order of preference, or an empty array to express no language preference for video track
* selection.
* @return This builder.
*/
@UnstableApi
@CanIgnoreReturnValue
public Builder setPreferredVideoLanguages(String... preferredVideoLanguages) {
this.preferredVideoLanguages = normalizeLanguageCodes(preferredVideoLanguages);
return this;
}
/**
* Sets the preferred {@link C.RoleFlags} for video tracks.
*
@ -661,29 +620,33 @@ public class TrackSelectionParameters {
// Text
/**
* Sets whether the preferred languages and the preferred role flags for text tracks should be
* set according the {@link CaptioningManager} preferences, if enabled in the system settings
* and no other explicit language or role flag preferences are specified.
* Sets the preferred language and role flags for text tracks based on the accessibility
* settings of {@link CaptioningManager}.
*
* <p>Does nothing when the {@link CaptioningManager} is disabled.
*
* @param context A {@link Context}.
* @return This builder.
*/
@CanIgnoreReturnValue
public Builder setPreferredTextLanguageAndRoleFlagsToCaptioningManagerSettings() {
usePreferredTextLanguagesAndRoleFlagsFromCaptioningManager = true;
preferredTextLanguages = ImmutableList.of();
preferredTextRoleFlags = 0;
return this;
}
/**
* @deprecated Use {@link #setPreferredTextLanguageAndRoleFlagsToCaptioningManagerSettings()}
* instead.
*/
@Deprecated
@CanIgnoreReturnValue
public Builder setPreferredTextLanguageAndRoleFlagsToCaptioningManagerSettings(
Context context) {
return setPreferredTextLanguageAndRoleFlagsToCaptioningManagerSettings();
if (Util.SDK_INT < 23 && Looper.myLooper() == null) {
// Android platform bug (pre-Marshmallow) that causes RuntimeExceptions when
// CaptioningService is instantiated from a non-Looper thread. See [internal: b/143779904].
return this;
}
CaptioningManager captioningManager =
(CaptioningManager) context.getSystemService(Context.CAPTIONING_SERVICE);
if (captioningManager == null || !captioningManager.isEnabled()) {
return this;
}
preferredTextRoleFlags = C.ROLE_FLAG_CAPTION | C.ROLE_FLAG_DESCRIBES_MUSIC_AND_SOUND;
Locale preferredLocale = captioningManager.getLocale();
if (preferredLocale != null) {
preferredTextLanguages = ImmutableList.of(Util.getLocaleLanguageTag(preferredLocale));
}
return this;
}
/**
@ -710,7 +673,6 @@ public class TrackSelectionParameters {
@CanIgnoreReturnValue
public Builder setPreferredTextLanguages(String... preferredTextLanguages) {
this.preferredTextLanguages = normalizeLanguageCodes(preferredTextLanguages);
this.usePreferredTextLanguagesAndRoleFlagsFromCaptioningManager = false;
return this;
}
@ -723,7 +685,6 @@ public class TrackSelectionParameters {
@CanIgnoreReturnValue
public Builder setPreferredTextRoleFlags(@C.RoleFlags int preferredTextRoleFlags) {
this.preferredTextRoleFlags = preferredTextRoleFlags;
this.usePreferredTextLanguagesAndRoleFlagsFromCaptioningManager = false;
return this;
}
@ -1082,21 +1043,35 @@ public class TrackSelectionParameters {
}
}
/** An instance with default parameters. */
@UnstableApi public static final TrackSelectionParameters DEFAULT = new Builder().build();
/**
* An instance with default values, except those obtained from the {@link Context}.
*
* <p>If possible, use {@link #getDefaults(Context)} instead.
*
* <p>This instance will not have the following settings:
*
* <ul>
* <li>{@link Builder#setViewportSizeToPhysicalDisplaySize(Context, boolean) Viewport
* constraints} configured for the primary display.
* <li>{@link Builder#setPreferredTextLanguageAndRoleFlagsToCaptioningManagerSettings(Context)
* Preferred text language and role flags} configured to the accessibility settings of
* {@link CaptioningManager}.
* </ul>
*/
@UnstableApi
@SuppressWarnings("deprecation")
public static final TrackSelectionParameters DEFAULT_WITHOUT_CONTEXT = new Builder().build();
/**
* @deprecated Use {@link #DEFAULT} instead.
* @deprecated This instance is not configured using {@link Context} constraints. Use {@link
* #getDefaults(Context)} instead.
*/
@UnstableApi @Deprecated
public static final TrackSelectionParameters DEFAULT_WITHOUT_CONTEXT = DEFAULT;
public static final TrackSelectionParameters DEFAULT = DEFAULT_WITHOUT_CONTEXT;
/**
* @deprecated Use {@link #DEFAULT} instead.
*/
@Deprecated
/** Returns an instance configured with default values. */
public static TrackSelectionParameters getDefaults(Context context) {
return DEFAULT;
return new Builder(context).build();
}
// Video
@ -1160,13 +1135,6 @@ public class TrackSelectionParameters {
*/
public final int viewportHeight;
/**
* Whether the viewport size should be assumed to the physical display size if no other specific
* viewport size constraint is specified. Constrains video track selections for adaptive content
* so that only tracks suitable for the viewport are selected. The default value is {@code true}.
*/
public final boolean isViewportSizeLimitedByPhysicalDisplaySize;
/**
* Whether the viewport orientation may change during playback. Constrains video track selections
* for adaptive content so that only tracks suitable for the viewport are selected. The default
@ -1180,11 +1148,6 @@ public class TrackSelectionParameters {
*/
public final ImmutableList<String> preferredVideoMimeTypes;
/**
* The preferred languages for video tracks as IETF BCP 47 conformant tags in order of preference.
*/
@UnstableApi public final ImmutableList<String> preferredVideoLanguages;
/**
* The preferred {@link C.RoleFlags} for video tracks. {@code 0} selects the default track if
* there is one, or the first track if there's no default. The default value is {@code 0}.
@ -1233,24 +1196,19 @@ public class TrackSelectionParameters {
/**
* The preferred languages for text tracks as IETF BCP 47 conformant tags in order of preference.
* An empty list selects the default track if there is one, or no track otherwise. The default
* value is an empty list.
* value is an empty list, or the language of the accessibility {@link CaptioningManager} if
* enabled.
*/
public final ImmutableList<String> preferredTextLanguages;
/**
* The preferred {@link C.RoleFlags} for text tracks. {@code 0} selects the default track if there
* is one, or no track otherwise. The default value is {@code 0}.
* is one, or no track otherwise. The default value is {@code 0}, or {@link C#ROLE_FLAG_SUBTITLE}
* | {@link C#ROLE_FLAG_DESCRIBES_MUSIC_AND_SOUND} if the accessibility {@link CaptioningManager}
* is enabled.
*/
public final @C.RoleFlags int preferredTextRoleFlags;
/**
* Whether the preferred languages and the preferred role flags for text tracks should be set
* according the {@link CaptioningManager} preferences, if enabled in the system settings and no
* other explicit language or role flag preferences are specified. The default value is {@code
* true}.
*/
public final boolean usePreferredTextLanguagesAndRoleFlagsFromCaptioningManager;
/**
* Bitmask of selection flags that are ignored for text track selections. See {@link
* C.SelectionFlags}. The default value is {@code 0} (i.e., no flags are ignored).
@ -1307,11 +1265,8 @@ public class TrackSelectionParameters {
this.minVideoBitrate = builder.minVideoBitrate;
this.viewportWidth = builder.viewportWidth;
this.viewportHeight = builder.viewportHeight;
this.isViewportSizeLimitedByPhysicalDisplaySize =
builder.isViewportSizeLimitedByPhysicalDisplaySize;
this.viewportOrientationMayChange = builder.viewportOrientationMayChange;
this.preferredVideoMimeTypes = builder.preferredVideoMimeTypes;
this.preferredVideoLanguages = builder.preferredVideoLanguages;
this.preferredVideoRoleFlags = builder.preferredVideoRoleFlags;
// Audio
this.preferredAudioLanguages = builder.preferredAudioLanguages;
@ -1323,8 +1278,6 @@ public class TrackSelectionParameters {
// Text
this.preferredTextLanguages = builder.preferredTextLanguages;
this.preferredTextRoleFlags = builder.preferredTextRoleFlags;
this.usePreferredTextLanguagesAndRoleFlagsFromCaptioningManager =
builder.usePreferredTextLanguagesAndRoleFlagsFromCaptioningManager;
this.ignoredTextSelectionFlags = builder.ignoredTextSelectionFlags;
this.selectUndeterminedTextLanguage = builder.selectUndeterminedTextLanguage;
// Image
@ -1363,10 +1316,7 @@ public class TrackSelectionParameters {
&& viewportOrientationMayChange == other.viewportOrientationMayChange
&& viewportWidth == other.viewportWidth
&& viewportHeight == other.viewportHeight
&& isViewportSizeLimitedByPhysicalDisplaySize
== other.isViewportSizeLimitedByPhysicalDisplaySize
&& preferredVideoMimeTypes.equals(other.preferredVideoMimeTypes)
&& preferredVideoLanguages.equals(other.preferredVideoLanguages)
&& preferredVideoRoleFlags == other.preferredVideoRoleFlags
// Audio
&& preferredAudioLanguages.equals(other.preferredAudioLanguages)
@ -1378,8 +1328,6 @@ public class TrackSelectionParameters {
// Text
&& preferredTextLanguages.equals(other.preferredTextLanguages)
&& preferredTextRoleFlags == other.preferredTextRoleFlags
&& usePreferredTextLanguagesAndRoleFlagsFromCaptioningManager
== other.usePreferredTextLanguagesAndRoleFlagsFromCaptioningManager
&& ignoredTextSelectionFlags == other.ignoredTextSelectionFlags
&& selectUndeterminedTextLanguage == other.selectUndeterminedTextLanguage
// Image
@ -1406,9 +1354,7 @@ public class TrackSelectionParameters {
result = 31 * result + (viewportOrientationMayChange ? 1 : 0);
result = 31 * result + viewportWidth;
result = 31 * result + viewportHeight;
result = 31 * result + (isViewportSizeLimitedByPhysicalDisplaySize ? 1 : 0);
result = 31 * result + preferredVideoMimeTypes.hashCode();
result = 31 * result + preferredVideoLanguages.hashCode();
result = 31 * result + preferredVideoRoleFlags;
// Audio
result = 31 * result + preferredAudioLanguages.hashCode();
@ -1420,7 +1366,6 @@ public class TrackSelectionParameters {
// Text
result = 31 * result + preferredTextLanguages.hashCode();
result = 31 * result + preferredTextRoleFlags;
result = 31 * result + (usePreferredTextLanguagesAndRoleFlagsFromCaptioningManager ? 1 : 0);
result = 31 * result + ignoredTextSelectionFlags;
result = 31 * result + (selectUndeterminedTextLanguage ? 1 : 0);
// Image
@ -1465,12 +1410,6 @@ public class TrackSelectionParameters {
private static final String FIELD_AUDIO_OFFLOAD_PREFERENCES = Util.intToStringMaxRadix(30);
private static final String FIELD_IS_PREFER_IMAGE_OVER_VIDEO_ENABLED =
Util.intToStringMaxRadix(31);
private static final String FIELD_PREFERRED_VIDEO_LANGUAGES = Util.intToStringMaxRadix(32);
private static final String FIELD_IS_VIEWPORT_SIZE_LIMITED_BY_PHYSICAL_DISPLAY_SIZE =
Util.intToStringMaxRadix(33);
private static final String
FIELD_USE_PREFERRED_TEXT_LANGUAGES_AND_ROLE_FLAGS_FROM_CAPTIONING_MANAGER =
Util.intToStringMaxRadix(34);
/**
* Defines a minimum field ID value for subclasses to use when implementing {@link #toBundle()}
@ -1497,14 +1436,9 @@ public class TrackSelectionParameters {
bundle.putInt(FIELD_MIN_VIDEO_BITRATE, minVideoBitrate);
bundle.putInt(FIELD_VIEWPORT_WIDTH, viewportWidth);
bundle.putInt(FIELD_VIEWPORT_HEIGHT, viewportHeight);
bundle.putBoolean(
FIELD_IS_VIEWPORT_SIZE_LIMITED_BY_PHYSICAL_DISPLAY_SIZE,
isViewportSizeLimitedByPhysicalDisplaySize);
bundle.putBoolean(FIELD_VIEWPORT_ORIENTATION_MAY_CHANGE, viewportOrientationMayChange);
bundle.putStringArray(
FIELD_PREFERRED_VIDEO_MIMETYPES, preferredVideoMimeTypes.toArray(new String[0]));
bundle.putStringArray(
FIELD_PREFERRED_VIDEO_LANGUAGES, preferredVideoLanguages.toArray(new String[0]));
bundle.putInt(FIELD_PREFERRED_VIDEO_ROLE_FLAGS, preferredVideoRoleFlags);
// Audio
bundle.putStringArray(
@ -1518,9 +1452,6 @@ public class TrackSelectionParameters {
bundle.putStringArray(
FIELD_PREFERRED_TEXT_LANGUAGES, preferredTextLanguages.toArray(new String[0]));
bundle.putInt(FIELD_PREFERRED_TEXT_ROLE_FLAGS, preferredTextRoleFlags);
bundle.putBoolean(
FIELD_USE_PREFERRED_TEXT_LANGUAGES_AND_ROLE_FLAGS_FROM_CAPTIONING_MANAGER,
usePreferredTextLanguagesAndRoleFlagsFromCaptioningManager);
bundle.putInt(FIELD_IGNORED_TEXT_SELECTION_FLAGS, ignoredTextSelectionFlags);
bundle.putBoolean(FIELD_SELECT_UNDETERMINED_TEXT_LANGUAGE, selectUndeterminedTextLanguage);
bundle.putInt(FIELD_AUDIO_OFFLOAD_MODE_PREFERENCE, audioOffloadPreferences.audioOffloadMode);

View File

@ -84,8 +84,8 @@ public interface VideoFrameProcessor {
* Input frames come from the {@linkplain #getInputSurface input surface} and don't need to be
* {@linkplain #registerInputFrame registered} (unlike with {@link #INPUT_TYPE_SURFACE}).
*
* <p>Every frame must use the {@linkplain #registerInputStream input stream's registered} frame
* format. Also sets the surface's {@linkplain
* <p>Every frame must use the {@linkplain #registerInputStream(int, List, FrameInfo) input
* stream's registered} frame info. Also sets the surface's {@linkplain
* android.graphics.SurfaceTexture#setDefaultBufferSize(int, int) default buffer size}.
*/
int INPUT_TYPE_SURFACE_AUTOMATIC_FRAME_REGISTRATION = 4;
@ -131,8 +131,8 @@ public interface VideoFrameProcessor {
interface Listener {
/**
* Called when the {@link VideoFrameProcessor} finishes {@linkplain #registerInputStream
* registering an input stream}.
* Called when the {@link VideoFrameProcessor} finishes {@linkplain #registerInputStream(int,
* List, FrameInfo) registering an input stream}.
*
* <p>The {@link VideoFrameProcessor} is now ready to accept new input {@linkplain
* VideoFrameProcessor#registerInputFrame frames}, {@linkplain
@ -140,11 +140,11 @@ public interface VideoFrameProcessor {
* VideoFrameProcessor#queueInputTexture(int, long) textures}.
*
* @param inputType The {@link InputType} of the new input stream.
* @param format The {@link Format} of the new input stream.
* @param effects The list of {@link Effect effects} to apply to the new input stream.
* @param frameInfo The {@link FrameInfo} of the new input stream.
*/
default void onInputStreamRegistered(
@InputType int inputType, Format format, List<Effect> effects) {}
@InputType int inputType, List<Effect> effects, FrameInfo frameInfo) {}
/**
* Called when the output size changes.
@ -157,14 +157,6 @@ public interface VideoFrameProcessor {
*/
default void onOutputSizeChanged(int width, int height) {}
/**
* Called when the output frame rate changes.
*
* @param frameRate The output frame rate in frames per second, or {@link Format#NO_VALUE} if
* unknown.
*/
default void onOutputFrameRateChanged(float frameRate) {}
/**
* Called when an output frame with the given {@code presentationTimeUs} becomes available for
* rendering.
@ -204,8 +196,8 @@ public interface VideoFrameProcessor {
/**
* Provides an input {@link Bitmap} to the {@link VideoFrameProcessor}.
*
* <p>Can be called many times after {@link #registerInputStream registering the input stream} to
* put multiple frames in the same input stream.
* <p>Can be called many times after {@link #registerInputStream(int, List, FrameInfo) registering
* the input stream} to put multiple frames in the same input stream.
*
* @param inputBitmap The {@link Bitmap} queued to the {@code VideoFrameProcessor}.
* @param timestampIterator A {@link TimestampIterator} generating the exact timestamps that the
@ -279,20 +271,14 @@ public interface VideoFrameProcessor {
* #queueInputTexture queued}.
*
* <p>This method blocks the calling thread until the previous calls to this method finish, that
* is when {@link Listener#onInputStreamRegistered(int, Format, List)} is called after the
* is when {@link Listener#onInputStreamRegistered(int, List, FrameInfo)} is called after the
* underlying processing pipeline has been adapted to the registered input stream.
*
* @param inputType The {@link InputType} of the new input stream.
* @param format The {@link Format} of the new input stream. The {@link Format#colorInfo}, the
* {@link Format#width}, the {@link Format#height} and the {@link
* Format#pixelWidthHeightRatio} must be set.
* @param effects The list of {@link Effect effects} to apply to the new input stream.
* @param offsetToAddUs The offset that must be added to the frame presentation timestamps, in
* microseconds. This offset is not part of the input timestamps. It is added to the frame
* timestamps before processing, and is retained in the output timestamps.
* @param frameInfo The {@link FrameInfo} of the new input stream.
*/
void registerInputStream(
@InputType int inputType, Format format, List<Effect> effects, long offsetToAddUs);
void registerInputStream(@InputType int inputType, List<Effect> effects, FrameInfo frameInfo);
/**
* Informs the {@code VideoFrameProcessor} that a frame will be queued to its {@linkplain
@ -301,10 +287,11 @@ public interface VideoFrameProcessor {
* <p>Must be called before rendering a frame to the input surface. The caller must not render
* frames to the {@linkplain #getInputSurface input surface} when {@code false} is returned.
*
* @return Whether the input frame was successfully registered. If {@link #registerInputStream} is
* called, this method returns {@code false} until {@link
* Listener#onInputStreamRegistered(int, Format, List)} is called. Otherwise, a return value
* of {@code false} indicates the {@code VideoFrameProcessor} is not ready to accept input.
* @return Whether the input frame was successfully registered. If {@link
* #registerInputStream(int, List, FrameInfo)} is called, this method returns {@code false}
* until {@link Listener#onInputStreamRegistered(int, List, FrameInfo)} is called. Otherwise,
* a return value of {@code false} indicates the {@code VideoFrameProcessor} is not ready to
* accept input.
* @throws UnsupportedOperationException If the {@code VideoFrameProcessor} does not accept
* {@linkplain #INPUT_TYPE_SURFACE surface input}.
* @throws IllegalStateException If called after {@link #signalEndOfInput()} or before {@link

View File

@ -35,14 +35,6 @@ public interface VideoGraph {
*/
default void onOutputSizeChanged(int width, int height) {}
/**
* Called when the output frame rate changes.
*
* @param frameRate The output frame rate in frames per second, or {@link Format#NO_VALUE} if
* unknown.
*/
default void onOutputFrameRateChanged(float frameRate) {}
/**
* Called when an output frame with the given {@code framePresentationTimeUs} becomes available
* for rendering.

Some files were not shown because too many files have changed in this diff Show More