Merge pull request #498 from androidx/release-1.1.0

Release 1.1.0
This commit is contained in:
Tofunmi Adigun-Hameed 2023-07-05 08:56:12 +00:00 committed by GitHub
commit 5328d6464a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
892 changed files with 72990 additions and 20933 deletions

View File

@ -17,10 +17,10 @@ body:
tracker: https://github.com/google/ExoPlayer/issues?q=is%3Aissue tracker: https://github.com/google/ExoPlayer/issues?q=is%3Aissue
- type: dropdown - type: dropdown
attributes: attributes:
label: Media3 Version label: Version
description: What version of Media3 (or ExoPlayer) are you using? description: What version of Media3 (or ExoPlayer) are you using?
options: options:
- Media3 1.1.0-alpha01 - Media3 1.1.0
- Media3 1.0.2 - Media3 1.0.2
- Media3 1.0.1 - Media3 1.0.1
- Media3 1.0.0 - Media3 1.0.0
@ -33,6 +33,8 @@ body:
- Media3 1.0.0-alpha02 - Media3 1.0.0-alpha02
- Media3 1.0.0-alpha01 - Media3 1.0.0-alpha01
- Media3 `main` branch - Media3 `main` branch
- Media3 pre-release (alpha, beta or RC not in this list)
- ExoPlayer 2.19.0
- ExoPlayer 2.18.7 - ExoPlayer 2.18.7
- ExoPlayer 2.18.6 - ExoPlayer 2.18.6
- ExoPlayer 2.18.5 - ExoPlayer 2.18.5
@ -54,6 +56,12 @@ body:
- Older (unsupported) - Older (unsupported)
validations: validations:
required: true required: true
- type: textarea
attributes:
label: More version details
description: |
Required if you selected `main` or `dev-v2` (please provide an exact commit SHA),
or 'pre-release' or 'older' (please provide the version).
- type: textarea - type: textarea
attributes: attributes:
label: Devices that reproduce the issue label: Devices that reproduce the issue
@ -114,7 +122,7 @@ body:
* Attach a file here * Attach a file here
* Include a media URL * Include a media URL
* Refer to a piece of media from the demo app (e.g. `Misc > Dizzy (MP4)`) * Refer to a piece of media from the demo app (e.g. `Misc > Dizzy (MP4)`)
* If you don't want to post media publicly please email the info to dev.exoplayer@gmail.com with subject 'Issue #\<issuenumber\>' after filing this issue, and note that you will do this here. * If you don't want to post media publicly please email the info to android-media-github@google.com with subject 'Issue #\<issuenumber\>' after filing this issue, and note that you will do this here.
* If you are certain the issue does not depend on the media being played, enter "Not applicable" here. * If you are certain the issue does not depend on the media being played, enter "Not applicable" here.
For DRM-protected media please also include the scheme and license server URL. For DRM-protected media please also include the scheme and license server URL.

View File

@ -39,6 +39,6 @@ Don't forget to check ExoPlayer's supported formats and devices, if applicable
(https://developer.android.com/guide/topics/media/exoplayer/supported-formats). (https://developer.android.com/guide/topics/media/exoplayer/supported-formats).
If there's something you don't want to post publicly, please submit the issue, If there's something you don't want to post publicly, please submit the issue,
then email the link/bug report to dev.exoplayer@gmail.com using a subject in the then email the link/bug report to android-media-github@google.com using a
format "Issue #1234", where #1234 is your issue number (we don't reply to subject in the format "Issue #1234", where #1234 is your issue number (we don't
emails). reply to emails).

10
.idea/icon.svg generated Normal file
View File

@ -0,0 +1,10 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" viewBox="60 60 130 130">
<g>
<path transform="matrix(1,0,0,-1,91.2359,110.7836)" d="M0 0C-1.459 1.259-2.67 2.872-3.493 4.807-5.848 10.342-4.026 16.923 .843 20.454 7.261 25.107 16.099 23.076 19.927 16.386 20.683 15.065 21.18 13.667 21.437 12.251 21.753 10.51 23.603 9.59 25.201 10.181L42.333 20.072-3.502 46.535C-9.567 50.037-17.147 45.66-17.147 38.657V-14.113L-.508-4.594C1.175-3.631 1.468-1.267 0 0" fill="#fcb64e"/>
<path transform="matrix(1,0,0,-1,74.2803,124.942)" d="M0 0C-.064 0-.128-.002-.192-.004V-.005L-.101-.058Z" fill="#fcb64e"/>
<path transform="matrix(1,0,0,-1,112.6543,151.6317)" d="M0 0C-.354-1.895-1.137-3.753-2.395-5.438-5.992-10.259-12.595-11.998-18.097-9.568-25.348-6.366-28.043 2.293-24.19 8.968-23.429 10.287-22.471 11.42-21.377 12.355-19.908 13.611-20.189 15.969-21.862 16.935L-38.566 26.579V-26.242C-38.566-33.245-30.985-37.621-24.92-34.12L20.823-7.71 4.225 1.874C2.545 2.843 .355 1.906 0 0" fill="#56a0d7"/>
<path transform="matrix(1,0,0,-1,74.0884,124.9471)" d="M0 0V-.106L.091-.053Z" fill="#56a0d7"/>
<path transform="matrix(1,0,0,-1,129.8726,137.40271)" d="M0 0C-1.352-.476-2.805-.736-4.321-.736-12.028-.736-18.18 5.928-17.328 13.809-16.665 19.936-11.683 24.817-5.545 25.38-3.585 25.559-1.707 25.302 .007 24.697 1.712 24.096 3.467 25.291 3.696 27.027V46.485C3.711 46.51 3.728 46.535 3.742 46.56V46.561L3.696 46.535V46.691L-13.436 36.8C-15.034 36.209-16.884 37.129-17.2 38.87-17.457 40.286-17.954 41.684-18.71 43.005-22.538 49.696-31.376 51.726-37.793 47.073-42.663 43.542-44.484 36.961-42.13 31.426-41.307 29.491-40.096 27.878-38.637 26.619-37.169 25.352-37.461 22.988-39.145 22.026L-55.784 12.506-55.873 12.456C-55.843 12.456-55.814 12.457-55.784 12.457-55.721 12.459-55.657 12.46-55.592 12.461L-55.693 12.403-55.784 12.35-39.081 2.706C-37.407 1.74-37.126-.618-38.595-1.874-39.689-2.809-40.647-3.942-41.408-5.261-45.262-11.936-42.566-20.595-35.315-23.797-29.813-26.227-23.21-24.488-19.613-19.667-18.355-17.982-17.572-16.124-17.218-14.229-16.863-12.323-14.673-11.386-12.994-12.355L3.605-21.939 3.696-21.991V-2.331C3.467-.592 1.709 .602 0 0" fill="#ae1e59"/>
<path transform="matrix(1,0,0,-1,179.4517,117.17461)" d="M0 0-45.835 26.463V26.461C-45.835 26.461-45.836 26.462-45.837 26.463V26.333 26.332 7.172C-45.837 7.043-45.866 6.923-45.883 6.799-46.112 5.062-47.867 3.868-49.572 4.469-51.286 5.074-53.164 5.331-55.124 5.151-61.262 4.589-66.244-.292-66.907-6.42-67.759-14.3-61.607-20.964-53.9-20.964-52.384-20.964-50.931-20.704-49.579-20.228-47.87-19.626-46.112-20.82-45.883-22.559-45.866-22.683-45.837-22.803-45.837-22.932V-42.219C-45.836-42.219-45.836-42.218-45.836-42.218L-45.835-42.22 0-15.756C6.064-12.255 6.064-3.501 0 0" fill="#ef5451"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

@ -38,6 +38,24 @@ you made on top of `main` using
$ git diff -U0 main... | google-java-format-diff.py -p1 -i $ git diff -U0 main... | google-java-format-diff.py -p1 -i
``` ```
### Push access to PR branches
Please ensure maintainers of this repository have push access to your PR branch
by ticking the `Allow edits from maintainers` checkbox when creating the PR (or
after it's created). See the
[GitHub docs](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/creating-a-pull-request-from-a-fork)
for more info. This allows us to make changes and fixes to the PR while it goes
through internal review, and ensures we don't create an
['evil' merge](https://git-scm.com/docs/gitglossary#Documentation/gitglossary.txt-aiddefevilmergeaevilmerge)
when it gets merged.
This checkbox only appears on PRs from individual-owned forks
(https://github.com/orgs/community/discussions/5634). If you open a PR from an
organization-owned fork we will ask you to open a new one from an
individual-owned fork. If this isn't possible we can still merge the PR, but it
will result in an 'evil' merge because the changes and fixes we make during
internal review will be part of the merge commit.
## Contributor license agreement ## Contributor license agreement
Contributions to any Google project must be accompanied by a Contributor Contributions to any Google project must be accompanied by a Contributor

View File

@ -1,5 +1,300 @@
# Release notes # Release notes
## 1.1
### 1.1.0 (2023-07-05)
This release corresponds to the
[ExoPlayer 2.19.0 release](https://github.com/google/ExoPlayer/releases/tag/r2.19.0).
This release contains the following changes since the
[1.0.2 release](#102-2023-05-18):
* Common Library:
* Add suppression reason for unsuitable audio route and play when ready
change reason for suppressed too long.
([#15](https://github.com/androidx/media/issues/15)).
* Add commands to Player:
* `COMMAND_GET_METADATA`
* `COMMAND_SET_PLAYLIST_METADATA`
* `COMMAND_SET_DEVICE_VOLUME_WITH_FLAGS`
* `COMMAND_ADJUST_DEVICE_VOLUME_WITH_FLAGS`
* Add overloaded methods to Player which allow users to specify volume
flags:
* `void setDeviceVolume(int, int)`
* `void increaseDeviceVolume(int)`
* `void decreaseDeviceVolume(int)`
* `void setDeviceMuted(boolean, int)`
* Add `Builder` for `DeviceInfo` and deprecate existing constructor.
* Add `DeviceInfo.routingControllerId` to specify the routing controller
ID for remote playbacks.
* Add `Player.replaceMediaItem(s)` as a shortcut to adding and removing
items at the same position
([#8046](https://github.com/google/ExoPlayer/issues/8046)).
* ExoPlayer:
* Allow ExoPlayer to have control of device volume methods only if
explicitly opted in. Use
`ExoPlayer.Builder.setDeviceVolumeControlEnabled` to have access to:
* `getDeviceVolume()`
* `isDeviceMuted()`
* `setDeviceVolume(int)` and `setDeviceVolume(int, int)`
* `increaseDeviceVolume(int)` and `increaseDeviceVolume(int, int)`
* `decreaseDeviceVolume(int)` and `decreaseDeviceVolume(int, int)`
* Add `FilteringMediaSource` that allows to filter available track types
from a `MediaSource`.
* Add support for including Common Media Client Data (CMCD) in the
outgoing requests of adaptive streaming formats DASH, HLS, and
SmoothStreaming. The following fields, `br`, `bl`, `cid`, `rtp`, and
`sid`, have been incorporated
([#8699](https://github.com/google/ExoPlayer/issues/8699)). API
structure and API methods:
* CMCD logging is disabled by default, use
`MediaSource.Factory.setCmcdConfigurationFactory(CmcdConfiguration.Factory
cmcdConfigurationFactory)` to enable it.
* All keys are enabled by default, override
`CmcdConfiguration.RequestConfig.isKeyAllowed(String key)` to filter
out which keys are logged.
* Override `CmcdConfiguration.RequestConfig.getCustomData()` to enable
custom key logging.
* Add additional action to manifest of main demo to make it easier to
start the demo app with a custom `*.exolist.json` file
([#439](https://github.com/androidx/media/pull/439)).
* Add `ExoPlayer.setVideoEffects()` for using `Effect` during video
playback.
* Update `SampleQueue` to store `sourceId` as a `long` rather than an
`int`. This changes the signatures of public methods
`SampleQueue.sourceId` and `SampleQueue.peekSourceId`.
* Add parameters to `LoadControl` methods `shouldStartPlayback` and
`onTracksSelected` that allow associating these methods with the
relevant `MediaPeriod`.
* Change signature of
`ServerSideAdInsertionMediaSource.setAdPlaybackStates(Map<Object,
AdPlaybackState>)` by adding a timeline parameter that contains the
periods with the UIDs used as keys in the map. This is required to avoid
concurrency issues with multi-period live streams.
* Deprecate `EventDispatcher.withParameters(int windowIndex, @Nullable
MediaPeriodId mediaPeriodId, long mediaTimeOffsetMs)` and
`BaseMediaSource.createEventDispatcher(..., long mediaTimeOffsetMs)`.
The variant of the methods without the `mediaTimeOffsetUs` can be called
instead. Note that even for the deprecated variants, the offset is not
anymore added to `startTimeUs` and `endTimeUs` of the `MediaLoadData`
objects that are dispatched by the dispatcher.
* Rename `ExoTrackSelection.blacklist` to `excludeTrack` and
`isBlacklisted` to `isTrackExcluded`.
* Fix inconsistent behavior between `ExoPlayer.setMediaItem(s)` and
`addMediaItem(s)` when called on an empty playlist.
* Transformer:
* Remove `Transformer.Builder.setMediaSourceFactory(MediaSource.Factory)`.
Use `ExoPlayerAssetLoader.Factory(MediaSource.Factory)` and
`Transformer.Builder.setAssetLoaderFactory(AssetLoader.Factory)`
instead.
* Remove `Transformer.startTransformation(MediaItem,
ParcelFileDescriptor)`.
* Fix a bug where transformation could get stuck (leading to muxer
timeout) if the end of the video stream was signaled at the moment when
an input frame was pending processing.
* Query codecs via `MediaCodecList` instead of using
`findDecoder/EncoderForFormat` utilities, to expand support.
* Remove B-frame configuration in `DefaultEncoderFactory` because it
doesn't work on some devices.
* Track selection:
* Add
`DefaultTrackSelector.Parameters.allowInvalidateSelectionsForRendererCapabilitiesChange`
which is disabled by default. When enabled, the `DefaultTrackSelector`
will trigger a new track selection when the renderer capabilities
changed.
* Extractors:
* Ogg: Fix bug when seeking in files with a long duration
([#391](https://github.com/androidx/media/issues/391)).
* FMP4: Fix issue where `TimestampAdjuster` initializes a wrong timestamp
offset with metadata sample time from emsg atom
([#356](https://github.com/androidx/media/issues/356)).
* Audio:
* Fix bug where some playbacks fail when tunneling is enabled and
`AudioProcessors` are active, e.g. for gapless trimming
([#10847](https://github.com/google/ExoPlayer/issues/10847)).
* Encapsulate Opus frames in Ogg packets in direct playbacks (offload).
* Extrapolate current position during sleep with offload scheduling.
* Add `Renderer.release()` and `AudioSink.release()` for releasing the
resources at the end of player's lifecycle.
* Listen to audio capabilities changes in `DefaultAudioSink`. Add a
required parameter `context` in the constructor of `DefaultAudioSink`,
with which the `DefaultAudioSink` will register as the listener to the
`AudioCapabilitiesReceiver` and update its `audioCapabilities` property
when informed with a capabilities change.
* Propagate audio capabilities changes via a new event
`onAudioCapabilitiesChanged` in `AudioSink.Listener` interface, and a
new interface `RendererCapabilities.Listener` which triggers
`onRendererCapabilitiesChanged` events.
* Add `ChannelMixingAudioProcessor` for applying scaling/mixing to audio
channels.
* Add new int value `DISCARD_REASON_AUDIO_BYPASS_POSSIBLE` to
`DecoderDiscardReasons` to discard audio decoder when bypass mode is
possible after audio capabilities change.
* Add direct playback support for DTS Express and DTS:X
([#335](https://github.com/androidx/media/pull/335)).
* Video:
* Make `MediaCodecVideoRenderer` report a `VideoSize` with a width and
height of 0 when the renderer is disabled.
`Player.Listener.onVideoSizeChanged` is called accordingly when
`Player.getVideoSize()` changes. With this change, ExoPlayer's video
size with `MediaCodecVideoRenderer` has a width and height of 0 when
`Player.getCurrentTracks` does not support video, or the size of the
supported video track is not yet determined.
* DRM:
* Reduce the visibility of several internal-only methods on
`DefaultDrmSession` that aren't expected to be called from outside the
DRM package:
* `void onMediaDrmEvent(int)`
* `void provision()`
* `void onProvisionCompleted()`
* `onProvisionError(Exception, boolean)`
* Muxer:
* Add a new muxer library which can be used to create an MP4 container
file.
* IMA extension:
* Enable multi-period live DASH streams for DAI. Please note that the
current implementation does not yet support seeking in live streams
([#10912](https://github.com/google/ExoPlayer/issues/10912)).
* Fix a bug where a new ad group is inserted in live streams because the
calculated content position in consecutive timelines varies slightly.
* Session:
* Add helper method `MediaSession.getControllerForCurrentRequest` to
obtain information about the controller that is currently calling
a`Player` method.
* Add `androidx.media3.session.MediaButtonReceiver` to enable apps to
implement playback resumption with media button events sent by, for
example, a Bluetooth headset
([#167](https://github.com/androidx/media/issues/167)).
* Add default implementation to `MediaSession.Callback.onAddMediaItems` to
allow requested `MediaItems` to be passed onto `Player` if they have
`LocalConfiguration` (e.g. URI)
([#282](https://github.com/androidx/media/issues/282)).
* Add "seek to previous" and "seek to next" command buttons on compact
media notification view by default for Android 12 and below
([#410](https://github.com/androidx/media/issues/410)).
* Add default implementation to `MediaSession.Callback.onAddMediaItems` to
allow requested `MediaItems` to be passed onto `Player` if they have
`LocalConfiguration` (e.g. URI)
([#282](https://github.com/androidx/media/issues/282)).
* Add "seek to previous" and "seek to next" command buttons on compact
media notification view by default for Android 12 and below
([#410](https://github.com/androidx/media/issues/410)).
* UI:
* Add Util methods `shouldShowPlayButton` and
`handlePlayPauseButtonAction` to write custom UI elements with a
play/pause button.
* 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:
* 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:
* Add
`HlsMediaSource.Factory.setTimestampAdjusterInitializationTimeoutMs(long)`
to set a timeout for the loading thread to wait for the
`TimestampAdjuster` to initialize. If the initialization doesn't
complete before the timeout, a `PlaybackException` is thrown to avoid
the playback endless stalling. The timeout is set to zero by default
([#323](https://github.com/androidx/media/issues//323)).
* Test Utilities:
* Check for URI scheme case insensitivity in `DataSourceContractTest`.
* Remove deprecated symbols:
* Remove `DefaultAudioSink` constructors, use `DefaultAudioSink.Builder`
instead.
* Remove `HlsMasterPlaylist`, use `HlsMultivariantPlaylist` instead.
* Remove `Player.stop(boolean)`. Use `Player.stop()` and
`Player.clearMediaItems()` (if `reset` is `true`) instead.
* Remove two deprecated `SimpleCache` constructors, use a non-deprecated
constructor that takes a `DatabaseProvider` instead for better
performance.
* Remove `DefaultBandwidthMeter` constructor, use
`DefaultBandwidthMeter.Builder` instead.
* Remove `DefaultDrmSessionManager` constructors, use
`DefaultDrmSessionManager.Builder` instead.
* Remove two deprecated `HttpDataSource.InvalidResponseCodeException`
constructors, use a non-deprecated constructor that accepts additional
fields(`cause`, `responseBody`) to enhance error logging.
* Remove `DownloadHelper.forProgressive`, `DownloadHelper.forHls`,
`DownloadHelper.forDash`, and `DownloadHelper.forSmoothStreaming`, use
`DownloadHelper.forMediaItem` instead.
* Remove deprecated `DownloadService` constructor, use a non deprecated
constructor that includes the option to provide a
`channelDescriptionResourceId` parameter.
* Remove deprecated String constants for Charsets (`ASCII_NAME`,
`UTF8_NAME`, `ISO88591_NAME`, `UTF16_NAME` and `UTF16LE_NAME`), use
Kotlin Charsets from the `kotlin.text` package, the
`java.nio.charset.StandardCharsets` or the
`com.google.common.base.Charsets` instead.
* Remove deprecated `WorkManagerScheduler` constructor, use a non
deprecated constructor that includes the option to provide a `Context`
parameter instead.
* Remove the deprecated methods `createVideoSampleFormat`,
`createAudioSampleFormat`, `createContainerFormat`, and
`createSampleFormat`, which were used to instantiate the `Format` class.
Instead use `Format.Builder` for creating instances of `Format`.
* Remove the deprecated methods `copyWithMaxInputSize`,
`copyWithSubsampleOffsetUs`, `copyWithLabel`,
`copyWithManifestFormatInfo`, `copyWithGaplessInfo`,
`copyWithFrameRate`, `copyWithDrmInitData`, `copyWithMetadata`,
`copyWithBitrate` and `copyWithVideoSize`, use `Format.buildUpon()` and
setter methods instead.
* Remove deprecated `ExoPlayer.retry()`, use `prepare()` instead.
* Remove deprecated zero-arg `DefaultTrackSelector` constructor, use
`DefaultTrackSelector(Context)` instead.
* Remove deprecated `OfflineLicenseHelper` constructor, use
`OfflineLicenseHelper(DefaultDrmSessionManager,
DrmSessionEventListener.EventDispatcher)` instead.
* Remove deprecated `DownloadManager` constructor, use the constructor
that takes an `Executor` instead.
* Remove deprecated `Cue` constructors, use `Cue.Builder` instead.
* Remove deprecated `OfflineLicenseHelper` constructor, use
`OfflineLicenseHelper(DefaultDrmSessionManager,
DrmSessionEventListener.EventDispatcher)` instead.
* Remove four deprecated `AnalyticsListener` methods:
* `onDecoderEnabled`, use `onAudioEnabled` and/or `onVideoEnabled`
instead.
* `onDecoderInitialized`, use `onAudioDecoderInitialized` and/or
`onVideoDecoderInitialized` instead.
* `onDecoderInputFormatChanged`, use `onAudioInputFormatChanged`
and/or `onVideoInputFormatChanged` instead.
* `onDecoderDisabled`, use `onAudioDisabled` and/or `onVideoDisabled`
instead.
* Remove the deprecated `Player.Listener.onSeekProcessed` and
`AnalyticsListener.onSeekProcessed`, use `onPositionDiscontinuity` with
`DISCONTINUITY_REASON_SEEK` instead.
* Remove `ExoPlayer.setHandleWakeLock(boolean)`, use `setWakeMode(int)`
instead.
* Remove deprecated
`DefaultLoadControl.Builder.createDefaultLoadControl()`, use `build()`
instead.
* Remove deprecated `MediaItem.PlaybackProperties`, use
`MediaItem.LocalConfiguration` instead. Deprecated field
`MediaItem.playbackProperties` is now of type
`MediaItem.LocalConfiguration`.
### 1.1.0-rc01 (2023-06-21)
Use the 1.1.0 [stable version](#110-2023-07-05).
### 1.1.0-beta01 (2023-06-07)
Use the 1.1.0 [stable version](#110-2023-07-05).
### 1.1.0-alpha01 (2023-05-10)
Use the 1.1.0 [stable version](#110-2023-07-05).
## 1.0
### 1.0.2 (2023-05-18) ### 1.0.2 (2023-05-18)
This release corresponds to the This release corresponds to the

407
api.txt
View File

@ -127,6 +127,11 @@ package androidx.media3.common {
field public static final int USAGE_VOICE_COMMUNICATION = 2; // 0x2 field public static final int USAGE_VOICE_COMMUNICATION = 2; // 0x2
field public static final int USAGE_VOICE_COMMUNICATION_SIGNALLING = 3; // 0x3 field public static final int USAGE_VOICE_COMMUNICATION_SIGNALLING = 3; // 0x3
field public static final java.util.UUID UUID_NIL; field public static final java.util.UUID UUID_NIL;
field public static final int VOLUME_FLAG_ALLOW_RINGER_MODES = 2; // 0x2
field public static final int VOLUME_FLAG_PLAY_SOUND = 4; // 0x4
field public static final int VOLUME_FLAG_REMOVE_SOUND_AND_VIBRATE = 8; // 0x8
field public static final int VOLUME_FLAG_SHOW_UI = 1; // 0x1
field public static final int VOLUME_FLAG_VIBRATE = 16; // 0x10
field public static final int WAKE_MODE_LOCAL = 1; // 0x1 field public static final int WAKE_MODE_LOCAL = 1; // 0x1
field public static final int WAKE_MODE_NETWORK = 2; // 0x2 field public static final int WAKE_MODE_NETWORK = 2; // 0x2
field public static final int WAKE_MODE_NONE = 0; // 0x0 field public static final int WAKE_MODE_NONE = 0; // 0x0
@ -163,6 +168,9 @@ package androidx.media3.common {
@IntDef(open=true, value={androidx.media3.common.C.TRACK_TYPE_UNKNOWN, androidx.media3.common.C.TRACK_TYPE_DEFAULT, androidx.media3.common.C.TRACK_TYPE_AUDIO, androidx.media3.common.C.TRACK_TYPE_VIDEO, androidx.media3.common.C.TRACK_TYPE_TEXT, androidx.media3.common.C.TRACK_TYPE_IMAGE, androidx.media3.common.C.TRACK_TYPE_METADATA, androidx.media3.common.C.TRACK_TYPE_CAMERA_MOTION, androidx.media3.common.C.TRACK_TYPE_NONE}) @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 C.TrackType { @IntDef(open=true, value={androidx.media3.common.C.TRACK_TYPE_UNKNOWN, androidx.media3.common.C.TRACK_TYPE_DEFAULT, androidx.media3.common.C.TRACK_TYPE_AUDIO, androidx.media3.common.C.TRACK_TYPE_VIDEO, androidx.media3.common.C.TRACK_TYPE_TEXT, androidx.media3.common.C.TRACK_TYPE_IMAGE, androidx.media3.common.C.TRACK_TYPE_METADATA, androidx.media3.common.C.TRACK_TYPE_CAMERA_MOTION, androidx.media3.common.C.TRACK_TYPE_NONE}) @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 C.TrackType {
} }
@IntDef(flag=true, value={androidx.media3.common.C.VOLUME_FLAG_SHOW_UI, androidx.media3.common.C.VOLUME_FLAG_ALLOW_RINGER_MODES, androidx.media3.common.C.VOLUME_FLAG_PLAY_SOUND, androidx.media3.common.C.VOLUME_FLAG_REMOVE_SOUND_AND_VIBRATE, androidx.media3.common.C.VOLUME_FLAG_VIBRATE}) @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 C.VolumeFlags {
}
@IntDef({androidx.media3.common.C.WAKE_MODE_NONE, androidx.media3.common.C.WAKE_MODE_LOCAL, androidx.media3.common.C.WAKE_MODE_NETWORK}) @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 C.WakeMode { @IntDef({androidx.media3.common.C.WAKE_MODE_NONE, androidx.media3.common.C.WAKE_MODE_LOCAL, androidx.media3.common.C.WAKE_MODE_NETWORK}) @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 C.WakeMode {
} }
@ -170,9 +178,18 @@ package androidx.media3.common {
field public static final int PLAYBACK_TYPE_LOCAL = 0; // 0x0 field public static final int PLAYBACK_TYPE_LOCAL = 0; // 0x0
field public static final int PLAYBACK_TYPE_REMOTE = 1; // 0x1 field public static final int PLAYBACK_TYPE_REMOTE = 1; // 0x1
field public static final androidx.media3.common.DeviceInfo UNKNOWN; field public static final androidx.media3.common.DeviceInfo UNKNOWN;
field public final int maxVolume; field @IntRange(from=0) public final int maxVolume;
field public final int minVolume; field @IntRange(from=0) public final int minVolume;
field @androidx.media3.common.DeviceInfo.PlaybackType public final int playbackType; field @androidx.media3.common.DeviceInfo.PlaybackType public final int playbackType;
field @Nullable public final String routingControllerId;
}
public static final class DeviceInfo.Builder {
ctor public DeviceInfo.Builder(@androidx.media3.common.DeviceInfo.PlaybackType int);
method public androidx.media3.common.DeviceInfo build();
method public androidx.media3.common.DeviceInfo.Builder setMaxVolume(@IntRange(from=0) int);
method public androidx.media3.common.DeviceInfo.Builder setMinVolume(@IntRange(from=0) int);
method public androidx.media3.common.DeviceInfo.Builder setRoutingControllerId(@Nullable String);
} }
@IntDef({androidx.media3.common.DeviceInfo.PLAYBACK_TYPE_LOCAL, androidx.media3.common.DeviceInfo.PLAYBACK_TYPE_REMOTE}) @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 DeviceInfo.PlaybackType { @IntDef({androidx.media3.common.DeviceInfo.PLAYBACK_TYPE_LOCAL, androidx.media3.common.DeviceInfo.PLAYBACK_TYPE_REMOTE}) @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 DeviceInfo.PlaybackType {
@ -318,7 +335,7 @@ package androidx.media3.common {
method public androidx.media3.common.MediaItem.LiveConfiguration.Builder setTargetOffsetMs(long); method public androidx.media3.common.MediaItem.LiveConfiguration.Builder setTargetOffsetMs(long);
} }
public static class MediaItem.LocalConfiguration { public static final class MediaItem.LocalConfiguration {
field @Nullable public final androidx.media3.common.MediaItem.AdsConfiguration adsConfiguration; field @Nullable public final androidx.media3.common.MediaItem.AdsConfiguration adsConfiguration;
field @Nullable public final androidx.media3.common.MediaItem.DrmConfiguration drmConfiguration; field @Nullable public final androidx.media3.common.MediaItem.DrmConfiguration drmConfiguration;
field @Nullable public final String mimeType; field @Nullable public final String mimeType;
@ -369,14 +386,50 @@ package androidx.media3.common {
public final class MediaMetadata { public final class MediaMetadata {
method public androidx.media3.common.MediaMetadata.Builder buildUpon(); method public androidx.media3.common.MediaMetadata.Builder buildUpon();
field public static final androidx.media3.common.MediaMetadata EMPTY; field public static final androidx.media3.common.MediaMetadata EMPTY;
field public static final int FOLDER_TYPE_ALBUMS = 2; // 0x2 field @Deprecated public static final int FOLDER_TYPE_ALBUMS = 2; // 0x2
field public static final int FOLDER_TYPE_ARTISTS = 3; // 0x3 field @Deprecated public static final int FOLDER_TYPE_ARTISTS = 3; // 0x3
field public static final int FOLDER_TYPE_GENRES = 4; // 0x4 field @Deprecated public static final int FOLDER_TYPE_GENRES = 4; // 0x4
field public static final int FOLDER_TYPE_MIXED = 0; // 0x0 field @Deprecated public static final int FOLDER_TYPE_MIXED = 0; // 0x0
field public static final int FOLDER_TYPE_NONE = -1; // 0xffffffff field @Deprecated public static final int FOLDER_TYPE_NONE = -1; // 0xffffffff
field public static final int FOLDER_TYPE_PLAYLISTS = 5; // 0x5 field @Deprecated public static final int FOLDER_TYPE_PLAYLISTS = 5; // 0x5
field public static final int FOLDER_TYPE_TITLES = 1; // 0x1 field @Deprecated public static final int FOLDER_TYPE_TITLES = 1; // 0x1
field public static final int FOLDER_TYPE_YEARS = 6; // 0x6 field @Deprecated public static final int FOLDER_TYPE_YEARS = 6; // 0x6
field public static final int MEDIA_TYPE_ALBUM = 10; // 0xa
field public static final int MEDIA_TYPE_ARTIST = 11; // 0xb
field public static final int MEDIA_TYPE_AUDIO_BOOK = 15; // 0xf
field public static final int MEDIA_TYPE_AUDIO_BOOK_CHAPTER = 2; // 0x2
field public static final int MEDIA_TYPE_FOLDER_ALBUMS = 21; // 0x15
field public static final int MEDIA_TYPE_FOLDER_ARTISTS = 22; // 0x16
field public static final int MEDIA_TYPE_FOLDER_AUDIO_BOOKS = 26; // 0x1a
field public static final int MEDIA_TYPE_FOLDER_GENRES = 23; // 0x17
field public static final int MEDIA_TYPE_FOLDER_MIXED = 20; // 0x14
field public static final int MEDIA_TYPE_FOLDER_MOVIES = 35; // 0x23
field public static final int MEDIA_TYPE_FOLDER_NEWS = 32; // 0x20
field public static final int MEDIA_TYPE_FOLDER_PLAYLISTS = 24; // 0x18
field public static final int MEDIA_TYPE_FOLDER_PODCASTS = 27; // 0x1b
field public static final int MEDIA_TYPE_FOLDER_RADIO_STATIONS = 31; // 0x1f
field public static final int MEDIA_TYPE_FOLDER_TRAILERS = 34; // 0x22
field public static final int MEDIA_TYPE_FOLDER_TV_CHANNELS = 28; // 0x1c
field public static final int MEDIA_TYPE_FOLDER_TV_SERIES = 29; // 0x1d
field public static final int MEDIA_TYPE_FOLDER_TV_SHOWS = 30; // 0x1e
field public static final int MEDIA_TYPE_FOLDER_VIDEOS = 33; // 0x21
field public static final int MEDIA_TYPE_FOLDER_YEARS = 25; // 0x19
field public static final int MEDIA_TYPE_GENRE = 12; // 0xc
field public static final int MEDIA_TYPE_MIXED = 0; // 0x0
field public static final int MEDIA_TYPE_MOVIE = 8; // 0x8
field public static final int MEDIA_TYPE_MUSIC = 1; // 0x1
field public static final int MEDIA_TYPE_NEWS = 5; // 0x5
field public static final int MEDIA_TYPE_PLAYLIST = 13; // 0xd
field public static final int MEDIA_TYPE_PODCAST = 16; // 0x10
field public static final int MEDIA_TYPE_PODCAST_EPISODE = 3; // 0x3
field public static final int MEDIA_TYPE_RADIO_STATION = 4; // 0x4
field public static final int MEDIA_TYPE_TRAILER = 7; // 0x7
field public static final int MEDIA_TYPE_TV_CHANNEL = 17; // 0x11
field public static final int MEDIA_TYPE_TV_SEASON = 19; // 0x13
field public static final int MEDIA_TYPE_TV_SERIES = 18; // 0x12
field public static final int MEDIA_TYPE_TV_SHOW = 9; // 0x9
field public static final int MEDIA_TYPE_VIDEO = 6; // 0x6
field public static final int MEDIA_TYPE_YEAR = 14; // 0xe
field public static final int PICTURE_TYPE_ARTIST_PERFORMER = 8; // 0x8 field public static final int PICTURE_TYPE_ARTIST_PERFORMER = 8; // 0x8
field public static final int PICTURE_TYPE_A_BRIGHT_COLORED_FISH = 17; // 0x11 field public static final int PICTURE_TYPE_A_BRIGHT_COLORED_FISH = 17; // 0x11
field public static final int PICTURE_TYPE_BACK_COVER = 4; // 0x4 field public static final int PICTURE_TYPE_BACK_COVER = 4; // 0x4
@ -411,9 +464,11 @@ package androidx.media3.common {
field @Nullable public final Integer discNumber; field @Nullable public final Integer discNumber;
field @Nullable public final CharSequence displayTitle; field @Nullable public final CharSequence displayTitle;
field @Nullable public final android.os.Bundle extras; field @Nullable public final android.os.Bundle extras;
field @Nullable @androidx.media3.common.MediaMetadata.FolderType public final Integer folderType; field @Deprecated @Nullable @androidx.media3.common.MediaMetadata.FolderType public final Integer folderType;
field @Nullable public final CharSequence genre; field @Nullable public final CharSequence genre;
field @Nullable public final Boolean isBrowsable;
field @Nullable public final Boolean isPlayable; field @Nullable public final Boolean isPlayable;
field @Nullable @androidx.media3.common.MediaMetadata.MediaType public final Integer mediaType;
field @Nullable public final androidx.media3.common.Rating overallRating; field @Nullable public final androidx.media3.common.Rating overallRating;
field @Nullable public final Integer recordingDay; field @Nullable public final Integer recordingDay;
field @Nullable public final Integer recordingMonth; field @Nullable public final Integer recordingMonth;
@ -447,9 +502,11 @@ package androidx.media3.common {
method public androidx.media3.common.MediaMetadata.Builder setDiscNumber(@Nullable Integer); 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 setDisplayTitle(@Nullable CharSequence);
method public androidx.media3.common.MediaMetadata.Builder setExtras(@Nullable android.os.Bundle); method public androidx.media3.common.MediaMetadata.Builder setExtras(@Nullable android.os.Bundle);
method public androidx.media3.common.MediaMetadata.Builder setFolderType(@Nullable @androidx.media3.common.MediaMetadata.FolderType Integer); 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); method public androidx.media3.common.MediaMetadata.Builder setGenre(@Nullable CharSequence);
method public androidx.media3.common.MediaMetadata.Builder setIsBrowsable(@Nullable Boolean);
method public androidx.media3.common.MediaMetadata.Builder setIsPlayable(@Nullable Boolean); method public androidx.media3.common.MediaMetadata.Builder setIsPlayable(@Nullable Boolean);
method public androidx.media3.common.MediaMetadata.Builder setMediaType(@Nullable @androidx.media3.common.MediaMetadata.MediaType Integer);
method public androidx.media3.common.MediaMetadata.Builder setOverallRating(@Nullable androidx.media3.common.Rating); method public androidx.media3.common.MediaMetadata.Builder setOverallRating(@Nullable androidx.media3.common.Rating);
method public androidx.media3.common.MediaMetadata.Builder setRecordingDay(@IntRange(from=1, to=31) @Nullable Integer); method public androidx.media3.common.MediaMetadata.Builder setRecordingDay(@IntRange(from=1, to=31) @Nullable Integer);
method public androidx.media3.common.MediaMetadata.Builder setRecordingMonth(@IntRange(from=1, to=12) @Nullable Integer); method public androidx.media3.common.MediaMetadata.Builder setRecordingMonth(@IntRange(from=1, to=12) @Nullable Integer);
@ -467,7 +524,10 @@ package androidx.media3.common {
method public androidx.media3.common.MediaMetadata.Builder setWriter(@Nullable CharSequence); method public androidx.media3.common.MediaMetadata.Builder setWriter(@Nullable CharSequence);
} }
@IntDef({androidx.media3.common.MediaMetadata.FOLDER_TYPE_NONE, androidx.media3.common.MediaMetadata.FOLDER_TYPE_MIXED, androidx.media3.common.MediaMetadata.FOLDER_TYPE_TITLES, androidx.media3.common.MediaMetadata.FOLDER_TYPE_ALBUMS, androidx.media3.common.MediaMetadata.FOLDER_TYPE_ARTISTS, androidx.media3.common.MediaMetadata.FOLDER_TYPE_GENRES, androidx.media3.common.MediaMetadata.FOLDER_TYPE_PLAYLISTS, androidx.media3.common.MediaMetadata.FOLDER_TYPE_YEARS}) @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 MediaMetadata.FolderType { @Deprecated @IntDef({androidx.media3.common.MediaMetadata.FOLDER_TYPE_NONE, androidx.media3.common.MediaMetadata.FOLDER_TYPE_MIXED, androidx.media3.common.MediaMetadata.FOLDER_TYPE_TITLES, androidx.media3.common.MediaMetadata.FOLDER_TYPE_ALBUMS, androidx.media3.common.MediaMetadata.FOLDER_TYPE_ARTISTS, androidx.media3.common.MediaMetadata.FOLDER_TYPE_GENRES, androidx.media3.common.MediaMetadata.FOLDER_TYPE_PLAYLISTS, androidx.media3.common.MediaMetadata.FOLDER_TYPE_YEARS}) @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 MediaMetadata.FolderType {
}
@IntDef({androidx.media3.common.MediaMetadata.MEDIA_TYPE_MIXED, androidx.media3.common.MediaMetadata.MEDIA_TYPE_MUSIC, androidx.media3.common.MediaMetadata.MEDIA_TYPE_AUDIO_BOOK_CHAPTER, androidx.media3.common.MediaMetadata.MEDIA_TYPE_PODCAST_EPISODE, androidx.media3.common.MediaMetadata.MEDIA_TYPE_RADIO_STATION, androidx.media3.common.MediaMetadata.MEDIA_TYPE_NEWS, androidx.media3.common.MediaMetadata.MEDIA_TYPE_VIDEO, androidx.media3.common.MediaMetadata.MEDIA_TYPE_TRAILER, androidx.media3.common.MediaMetadata.MEDIA_TYPE_MOVIE, androidx.media3.common.MediaMetadata.MEDIA_TYPE_TV_SHOW, androidx.media3.common.MediaMetadata.MEDIA_TYPE_ALBUM, androidx.media3.common.MediaMetadata.MEDIA_TYPE_ARTIST, androidx.media3.common.MediaMetadata.MEDIA_TYPE_GENRE, androidx.media3.common.MediaMetadata.MEDIA_TYPE_PLAYLIST, androidx.media3.common.MediaMetadata.MEDIA_TYPE_YEAR, androidx.media3.common.MediaMetadata.MEDIA_TYPE_AUDIO_BOOK, androidx.media3.common.MediaMetadata.MEDIA_TYPE_PODCAST, androidx.media3.common.MediaMetadata.MEDIA_TYPE_TV_CHANNEL, androidx.media3.common.MediaMetadata.MEDIA_TYPE_TV_SERIES, androidx.media3.common.MediaMetadata.MEDIA_TYPE_TV_SEASON, androidx.media3.common.MediaMetadata.MEDIA_TYPE_FOLDER_MIXED, androidx.media3.common.MediaMetadata.MEDIA_TYPE_FOLDER_ALBUMS, androidx.media3.common.MediaMetadata.MEDIA_TYPE_FOLDER_ARTISTS, androidx.media3.common.MediaMetadata.MEDIA_TYPE_FOLDER_GENRES, androidx.media3.common.MediaMetadata.MEDIA_TYPE_FOLDER_PLAYLISTS, androidx.media3.common.MediaMetadata.MEDIA_TYPE_FOLDER_YEARS, androidx.media3.common.MediaMetadata.MEDIA_TYPE_FOLDER_AUDIO_BOOKS, androidx.media3.common.MediaMetadata.MEDIA_TYPE_FOLDER_PODCASTS, androidx.media3.common.MediaMetadata.MEDIA_TYPE_FOLDER_TV_CHANNELS, androidx.media3.common.MediaMetadata.MEDIA_TYPE_FOLDER_TV_SERIES, androidx.media3.common.MediaMetadata.MEDIA_TYPE_FOLDER_TV_SHOWS, androidx.media3.common.MediaMetadata.MEDIA_TYPE_FOLDER_RADIO_STATIONS, androidx.media3.common.MediaMetadata.MEDIA_TYPE_FOLDER_NEWS, androidx.media3.common.MediaMetadata.MEDIA_TYPE_FOLDER_VIDEOS, androidx.media3.common.MediaMetadata.MEDIA_TYPE_FOLDER_TRAILERS, androidx.media3.common.MediaMetadata.MEDIA_TYPE_FOLDER_MOVIES}) @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 MediaMetadata.MediaType {
} }
@IntDef({androidx.media3.common.MediaMetadata.PICTURE_TYPE_OTHER, androidx.media3.common.MediaMetadata.PICTURE_TYPE_FILE_ICON, androidx.media3.common.MediaMetadata.PICTURE_TYPE_FILE_ICON_OTHER, androidx.media3.common.MediaMetadata.PICTURE_TYPE_FRONT_COVER, androidx.media3.common.MediaMetadata.PICTURE_TYPE_BACK_COVER, androidx.media3.common.MediaMetadata.PICTURE_TYPE_LEAFLET_PAGE, androidx.media3.common.MediaMetadata.PICTURE_TYPE_MEDIA, androidx.media3.common.MediaMetadata.PICTURE_TYPE_LEAD_ARTIST_PERFORMER, androidx.media3.common.MediaMetadata.PICTURE_TYPE_ARTIST_PERFORMER, androidx.media3.common.MediaMetadata.PICTURE_TYPE_CONDUCTOR, androidx.media3.common.MediaMetadata.PICTURE_TYPE_BAND_ORCHESTRA, androidx.media3.common.MediaMetadata.PICTURE_TYPE_COMPOSER, androidx.media3.common.MediaMetadata.PICTURE_TYPE_LYRICIST, androidx.media3.common.MediaMetadata.PICTURE_TYPE_RECORDING_LOCATION, androidx.media3.common.MediaMetadata.PICTURE_TYPE_DURING_RECORDING, androidx.media3.common.MediaMetadata.PICTURE_TYPE_DURING_PERFORMANCE, androidx.media3.common.MediaMetadata.PICTURE_TYPE_MOVIE_VIDEO_SCREEN_CAPTURE, androidx.media3.common.MediaMetadata.PICTURE_TYPE_A_BRIGHT_COLORED_FISH, androidx.media3.common.MediaMetadata.PICTURE_TYPE_ILLUSTRATION, androidx.media3.common.MediaMetadata.PICTURE_TYPE_BAND_ARTIST_LOGO, androidx.media3.common.MediaMetadata.PICTURE_TYPE_PUBLISHER_STUDIO_LOGO}) @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 MediaMetadata.PictureType { @IntDef({androidx.media3.common.MediaMetadata.PICTURE_TYPE_OTHER, androidx.media3.common.MediaMetadata.PICTURE_TYPE_FILE_ICON, androidx.media3.common.MediaMetadata.PICTURE_TYPE_FILE_ICON_OTHER, androidx.media3.common.MediaMetadata.PICTURE_TYPE_FRONT_COVER, androidx.media3.common.MediaMetadata.PICTURE_TYPE_BACK_COVER, androidx.media3.common.MediaMetadata.PICTURE_TYPE_LEAFLET_PAGE, androidx.media3.common.MediaMetadata.PICTURE_TYPE_MEDIA, androidx.media3.common.MediaMetadata.PICTURE_TYPE_LEAD_ARTIST_PERFORMER, androidx.media3.common.MediaMetadata.PICTURE_TYPE_ARTIST_PERFORMER, androidx.media3.common.MediaMetadata.PICTURE_TYPE_CONDUCTOR, androidx.media3.common.MediaMetadata.PICTURE_TYPE_BAND_ORCHESTRA, androidx.media3.common.MediaMetadata.PICTURE_TYPE_COMPOSER, androidx.media3.common.MediaMetadata.PICTURE_TYPE_LYRICIST, androidx.media3.common.MediaMetadata.PICTURE_TYPE_RECORDING_LOCATION, androidx.media3.common.MediaMetadata.PICTURE_TYPE_DURING_RECORDING, androidx.media3.common.MediaMetadata.PICTURE_TYPE_DURING_PERFORMANCE, androidx.media3.common.MediaMetadata.PICTURE_TYPE_MOVIE_VIDEO_SCREEN_CAPTURE, androidx.media3.common.MediaMetadata.PICTURE_TYPE_A_BRIGHT_COLORED_FISH, androidx.media3.common.MediaMetadata.PICTURE_TYPE_ILLUSTRATION, androidx.media3.common.MediaMetadata.PICTURE_TYPE_BAND_ARTIST_LOGO, androidx.media3.common.MediaMetadata.PICTURE_TYPE_PUBLISHER_STUDIO_LOGO}) @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 MediaMetadata.PictureType {
@ -486,7 +546,7 @@ package androidx.media3.common {
field public static final String APPLICATION_MP4VTT = "application/x-mp4-vtt"; field public static final String APPLICATION_MP4VTT = "application/x-mp4-vtt";
field public static final String APPLICATION_MPD = "application/dash+xml"; field public static final String APPLICATION_MPD = "application/dash+xml";
field public static final String APPLICATION_PGS = "application/pgs"; field public static final String APPLICATION_PGS = "application/pgs";
field public static final String APPLICATION_RAWCC = "application/x-rawcc"; field @Deprecated public static final String APPLICATION_RAWCC = "application/x-rawcc";
field public static final String APPLICATION_RTSP = "application/x-rtsp"; field public static final String APPLICATION_RTSP = "application/x-rtsp";
field public static final String APPLICATION_SS = "application/vnd.ms-sstr+xml"; field public static final String APPLICATION_SS = "application/vnd.ms-sstr+xml";
field public static final String APPLICATION_SUBRIP = "application/x-subrip"; field public static final String APPLICATION_SUBRIP = "application/x-subrip";
@ -524,7 +584,11 @@ package androidx.media3.common {
field public static final String AUDIO_VORBIS = "audio/vorbis"; field public static final String AUDIO_VORBIS = "audio/vorbis";
field public static final String AUDIO_WAV = "audio/wav"; field public static final String AUDIO_WAV = "audio/wav";
field public static final String AUDIO_WEBM = "audio/webm"; field public static final String AUDIO_WEBM = "audio/webm";
field public static final String IMAGE_HEIC = "image/heic";
field public static final String IMAGE_HEIF = "image/heif";
field public static final String IMAGE_JPEG = "image/jpeg"; field public static final String IMAGE_JPEG = "image/jpeg";
field public static final String IMAGE_PNG = "image/png";
field public static final String IMAGE_WEBP = "image/webp";
field public static final String TEXT_SSA = "text/x-ssa"; field public static final String TEXT_SSA = "text/x-ssa";
field public static final String TEXT_VTT = "text/vtt"; field public static final String TEXT_VTT = "text/vtt";
field public static final String VIDEO_AV1 = "video/av01"; field public static final String VIDEO_AV1 = "video/av01";
@ -602,7 +666,7 @@ package androidx.media3.common {
} }
public final class PlaybackParameters { public final class PlaybackParameters {
ctor public PlaybackParameters(float); ctor public PlaybackParameters(@FloatRange(from=0, fromInclusive=false) float);
ctor public PlaybackParameters(@FloatRange(from=0, fromInclusive=false) float, @FloatRange(from=0, fromInclusive=false) float); ctor public PlaybackParameters(@FloatRange(from=0, fromInclusive=false) float, @FloatRange(from=0, fromInclusive=false) float);
method @CheckResult public androidx.media3.common.PlaybackParameters withSpeed(@FloatRange(from=0, fromInclusive=false) float); method @CheckResult public androidx.media3.common.PlaybackParameters withSpeed(@FloatRange(from=0, fromInclusive=false) float);
field public static final androidx.media3.common.PlaybackParameters DEFAULT; field public static final androidx.media3.common.PlaybackParameters DEFAULT;
@ -623,7 +687,8 @@ package androidx.media3.common {
method public void clearVideoSurfaceHolder(@Nullable android.view.SurfaceHolder); method public void clearVideoSurfaceHolder(@Nullable android.view.SurfaceHolder);
method public void clearVideoSurfaceView(@Nullable android.view.SurfaceView); method public void clearVideoSurfaceView(@Nullable android.view.SurfaceView);
method public void clearVideoTextureView(@Nullable android.view.TextureView); method public void clearVideoTextureView(@Nullable android.view.TextureView);
method public void decreaseDeviceVolume(); method @Deprecated public void decreaseDeviceVolume();
method public void decreaseDeviceVolume(@androidx.media3.common.C.VolumeFlags int);
method public android.os.Looper getApplicationLooper(); method public android.os.Looper getApplicationLooper();
method public androidx.media3.common.AudioAttributes getAudioAttributes(); method public androidx.media3.common.AudioAttributes getAudioAttributes();
method public androidx.media3.common.Player.Commands getAvailableCommands(); method public androidx.media3.common.Player.Commands getAvailableCommands();
@ -667,7 +732,8 @@ package androidx.media3.common {
method @FloatRange(from=0, to=1.0) public float getVolume(); method @FloatRange(from=0, to=1.0) public float getVolume();
method public boolean hasNextMediaItem(); method public boolean hasNextMediaItem();
method public boolean hasPreviousMediaItem(); method public boolean hasPreviousMediaItem();
method public void increaseDeviceVolume(); method @Deprecated public void increaseDeviceVolume();
method public void increaseDeviceVolume(@androidx.media3.common.C.VolumeFlags int);
method public boolean isCommandAvailable(@androidx.media3.common.Player.Command int); method public boolean isCommandAvailable(@androidx.media3.common.Player.Command int);
method public boolean isCurrentMediaItemDynamic(); method public boolean isCurrentMediaItemDynamic();
method public boolean isCurrentMediaItemLive(); method public boolean isCurrentMediaItemLive();
@ -685,6 +751,8 @@ package androidx.media3.common {
method public void removeListener(androidx.media3.common.Player.Listener); method public void removeListener(androidx.media3.common.Player.Listener);
method public void removeMediaItem(int); method public void removeMediaItem(int);
method public void removeMediaItems(int, int); method public void removeMediaItems(int, int);
method public void replaceMediaItem(int, androidx.media3.common.MediaItem);
method public void replaceMediaItems(int, int, java.util.List<androidx.media3.common.MediaItem>);
method public void seekBack(); method public void seekBack();
method public void seekForward(); method public void seekForward();
method public void seekTo(long); method public void seekTo(long);
@ -695,8 +763,10 @@ package androidx.media3.common {
method public void seekToNextMediaItem(); method public void seekToNextMediaItem();
method public void seekToPrevious(); method public void seekToPrevious();
method public void seekToPreviousMediaItem(); method public void seekToPreviousMediaItem();
method public void setDeviceMuted(boolean); method @Deprecated public void setDeviceMuted(boolean);
method public void setDeviceVolume(@IntRange(from=0) int); method public void setDeviceMuted(boolean, @androidx.media3.common.C.VolumeFlags int);
method @Deprecated public void setDeviceVolume(@IntRange(from=0) int);
method public void setDeviceVolume(@IntRange(from=0) int, int);
method public void setMediaItem(androidx.media3.common.MediaItem); method public void setMediaItem(androidx.media3.common.MediaItem);
method public void setMediaItem(androidx.media3.common.MediaItem, long); method public void setMediaItem(androidx.media3.common.MediaItem, long);
method public void setMediaItem(androidx.media3.common.MediaItem, boolean); method public void setMediaItem(androidx.media3.common.MediaItem, boolean);
@ -716,12 +786,14 @@ package androidx.media3.common {
method public void setVideoTextureView(@Nullable android.view.TextureView); method public void setVideoTextureView(@Nullable android.view.TextureView);
method public void setVolume(@FloatRange(from=0, to=1.0) float); method public void setVolume(@FloatRange(from=0, to=1.0) float);
method public void stop(); method public void stop();
field public static final int COMMAND_ADJUST_DEVICE_VOLUME = 26; // 0x1a field @Deprecated public static final int COMMAND_ADJUST_DEVICE_VOLUME = 26; // 0x1a
field public static final int COMMAND_ADJUST_DEVICE_VOLUME_WITH_FLAGS = 34; // 0x22
field public static final int COMMAND_CHANGE_MEDIA_ITEMS = 20; // 0x14 field public static final int COMMAND_CHANGE_MEDIA_ITEMS = 20; // 0x14
field public static final int COMMAND_GET_AUDIO_ATTRIBUTES = 21; // 0x15 field public static final int COMMAND_GET_AUDIO_ATTRIBUTES = 21; // 0x15
field public static final int COMMAND_GET_CURRENT_MEDIA_ITEM = 16; // 0x10 field public static final int COMMAND_GET_CURRENT_MEDIA_ITEM = 16; // 0x10
field public static final int COMMAND_GET_DEVICE_VOLUME = 23; // 0x17 field public static final int COMMAND_GET_DEVICE_VOLUME = 23; // 0x17
field public static final int COMMAND_GET_MEDIA_ITEMS_METADATA = 18; // 0x12 field @Deprecated public static final int COMMAND_GET_MEDIA_ITEMS_METADATA = 18; // 0x12
field public static final int COMMAND_GET_METADATA = 18; // 0x12
field public static final int COMMAND_GET_TEXT = 28; // 0x1c field public static final int COMMAND_GET_TEXT = 28; // 0x1c
field public static final int COMMAND_GET_TIMELINE = 17; // 0x11 field public static final int COMMAND_GET_TIMELINE = 17; // 0x11
field public static final int COMMAND_GET_TRACKS = 30; // 0x1e field public static final int COMMAND_GET_TRACKS = 30; // 0x1e
@ -729,6 +801,7 @@ package androidx.media3.common {
field public static final int COMMAND_INVALID = -1; // 0xffffffff field public static final int COMMAND_INVALID = -1; // 0xffffffff
field public static final int COMMAND_PLAY_PAUSE = 1; // 0x1 field public static final int COMMAND_PLAY_PAUSE = 1; // 0x1
field public static final int COMMAND_PREPARE = 2; // 0x2 field public static final int COMMAND_PREPARE = 2; // 0x2
field public static final int COMMAND_RELEASE = 32; // 0x20
field public static final int COMMAND_SEEK_BACK = 11; // 0xb field public static final int COMMAND_SEEK_BACK = 11; // 0xb
field public static final int COMMAND_SEEK_FORWARD = 12; // 0xc field public static final int COMMAND_SEEK_FORWARD = 12; // 0xc
field public static final int COMMAND_SEEK_IN_CURRENT_MEDIA_ITEM = 5; // 0x5 field public static final int COMMAND_SEEK_IN_CURRENT_MEDIA_ITEM = 5; // 0x5
@ -738,9 +811,11 @@ package androidx.media3.common {
field public static final int COMMAND_SEEK_TO_NEXT_MEDIA_ITEM = 8; // 0x8 field public static final int COMMAND_SEEK_TO_NEXT_MEDIA_ITEM = 8; // 0x8
field public static final int COMMAND_SEEK_TO_PREVIOUS = 7; // 0x7 field public static final int COMMAND_SEEK_TO_PREVIOUS = 7; // 0x7
field public static final int COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM = 6; // 0x6 field public static final int COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM = 6; // 0x6
field public static final int COMMAND_SET_DEVICE_VOLUME = 25; // 0x19 field @Deprecated public static final int COMMAND_SET_DEVICE_VOLUME = 25; // 0x19
field public static final int COMMAND_SET_DEVICE_VOLUME_WITH_FLAGS = 33; // 0x21
field public static final int COMMAND_SET_MEDIA_ITEM = 31; // 0x1f field public static final int COMMAND_SET_MEDIA_ITEM = 31; // 0x1f
field public static final int COMMAND_SET_MEDIA_ITEMS_METADATA = 19; // 0x13 field @Deprecated public static final int COMMAND_SET_MEDIA_ITEMS_METADATA = 19; // 0x13
field public static final int COMMAND_SET_PLAYLIST_METADATA = 19; // 0x13
field public static final int COMMAND_SET_REPEAT_MODE = 15; // 0xf field public static final int COMMAND_SET_REPEAT_MODE = 15; // 0xf
field public static final int COMMAND_SET_SHUFFLE_MODE = 14; // 0xe field public static final int COMMAND_SET_SHUFFLE_MODE = 14; // 0xe
field public static final int COMMAND_SET_SPEED_AND_PITCH = 13; // 0xd field public static final int COMMAND_SET_SPEED_AND_PITCH = 13; // 0xd
@ -791,10 +866,12 @@ package androidx.media3.common {
field public static final int MEDIA_ITEM_TRANSITION_REASON_SEEK = 2; // 0x2 field public static final int MEDIA_ITEM_TRANSITION_REASON_SEEK = 2; // 0x2
field public static final int PLAYBACK_SUPPRESSION_REASON_NONE = 0; // 0x0 field public static final int PLAYBACK_SUPPRESSION_REASON_NONE = 0; // 0x0
field public static final int PLAYBACK_SUPPRESSION_REASON_TRANSIENT_AUDIO_FOCUS_LOSS = 1; // 0x1 field public static final int PLAYBACK_SUPPRESSION_REASON_TRANSIENT_AUDIO_FOCUS_LOSS = 1; // 0x1
field public static final int PLAYBACK_SUPPRESSION_REASON_UNSUITABLE_AUDIO_ROUTE = 2; // 0x2
field public static final int PLAY_WHEN_READY_CHANGE_REASON_AUDIO_BECOMING_NOISY = 3; // 0x3 field public static final int PLAY_WHEN_READY_CHANGE_REASON_AUDIO_BECOMING_NOISY = 3; // 0x3
field public static final int PLAY_WHEN_READY_CHANGE_REASON_AUDIO_FOCUS_LOSS = 2; // 0x2 field public static final int PLAY_WHEN_READY_CHANGE_REASON_AUDIO_FOCUS_LOSS = 2; // 0x2
field public static final int PLAY_WHEN_READY_CHANGE_REASON_END_OF_MEDIA_ITEM = 5; // 0x5 field public static final int PLAY_WHEN_READY_CHANGE_REASON_END_OF_MEDIA_ITEM = 5; // 0x5
field public static final int PLAY_WHEN_READY_CHANGE_REASON_REMOTE = 4; // 0x4 field public static final int PLAY_WHEN_READY_CHANGE_REASON_REMOTE = 4; // 0x4
field public static final int PLAY_WHEN_READY_CHANGE_REASON_SUPPRESSED_TOO_LONG = 6; // 0x6
field public static final int PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST = 1; // 0x1 field public static final int PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST = 1; // 0x1
field public static final int REPEAT_MODE_ALL = 2; // 0x2 field public static final int REPEAT_MODE_ALL = 2; // 0x2
field public static final int REPEAT_MODE_OFF = 0; // 0x0 field public static final int REPEAT_MODE_OFF = 0; // 0x0
@ -807,7 +884,7 @@ package androidx.media3.common {
field public static final int TIMELINE_CHANGE_REASON_SOURCE_UPDATE = 1; // 0x1 field public static final int TIMELINE_CHANGE_REASON_SOURCE_UPDATE = 1; // 0x1
} }
@IntDef({androidx.media3.common.Player.COMMAND_INVALID, androidx.media3.common.Player.COMMAND_PLAY_PAUSE, androidx.media3.common.Player.COMMAND_PREPARE, androidx.media3.common.Player.COMMAND_STOP, androidx.media3.common.Player.COMMAND_SEEK_TO_DEFAULT_POSITION, androidx.media3.common.Player.COMMAND_SEEK_IN_CURRENT_MEDIA_ITEM, androidx.media3.common.Player.COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM, androidx.media3.common.Player.COMMAND_SEEK_TO_PREVIOUS, androidx.media3.common.Player.COMMAND_SEEK_TO_NEXT_MEDIA_ITEM, androidx.media3.common.Player.COMMAND_SEEK_TO_NEXT, androidx.media3.common.Player.COMMAND_SEEK_TO_MEDIA_ITEM, androidx.media3.common.Player.COMMAND_SEEK_BACK, androidx.media3.common.Player.COMMAND_SEEK_FORWARD, androidx.media3.common.Player.COMMAND_SET_SPEED_AND_PITCH, androidx.media3.common.Player.COMMAND_SET_SHUFFLE_MODE, androidx.media3.common.Player.COMMAND_SET_REPEAT_MODE, androidx.media3.common.Player.COMMAND_GET_CURRENT_MEDIA_ITEM, androidx.media3.common.Player.COMMAND_GET_TIMELINE, androidx.media3.common.Player.COMMAND_GET_MEDIA_ITEMS_METADATA, androidx.media3.common.Player.COMMAND_SET_MEDIA_ITEMS_METADATA, androidx.media3.common.Player.COMMAND_SET_MEDIA_ITEM, androidx.media3.common.Player.COMMAND_CHANGE_MEDIA_ITEMS, androidx.media3.common.Player.COMMAND_GET_AUDIO_ATTRIBUTES, androidx.media3.common.Player.COMMAND_GET_VOLUME, androidx.media3.common.Player.COMMAND_GET_DEVICE_VOLUME, androidx.media3.common.Player.COMMAND_SET_VOLUME, androidx.media3.common.Player.COMMAND_SET_DEVICE_VOLUME, androidx.media3.common.Player.COMMAND_ADJUST_DEVICE_VOLUME, androidx.media3.common.Player.COMMAND_SET_VIDEO_SURFACE, androidx.media3.common.Player.COMMAND_GET_TEXT, androidx.media3.common.Player.COMMAND_SET_TRACK_SELECTION_PARAMETERS, androidx.media3.common.Player.COMMAND_GET_TRACKS}) @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 Player.Command { @IntDef({androidx.media3.common.Player.COMMAND_INVALID, androidx.media3.common.Player.COMMAND_PLAY_PAUSE, androidx.media3.common.Player.COMMAND_PREPARE, androidx.media3.common.Player.COMMAND_STOP, androidx.media3.common.Player.COMMAND_SEEK_TO_DEFAULT_POSITION, androidx.media3.common.Player.COMMAND_SEEK_IN_CURRENT_MEDIA_ITEM, androidx.media3.common.Player.COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM, androidx.media3.common.Player.COMMAND_SEEK_TO_PREVIOUS, androidx.media3.common.Player.COMMAND_SEEK_TO_NEXT_MEDIA_ITEM, androidx.media3.common.Player.COMMAND_SEEK_TO_NEXT, androidx.media3.common.Player.COMMAND_SEEK_TO_MEDIA_ITEM, androidx.media3.common.Player.COMMAND_SEEK_BACK, androidx.media3.common.Player.COMMAND_SEEK_FORWARD, androidx.media3.common.Player.COMMAND_SET_SPEED_AND_PITCH, androidx.media3.common.Player.COMMAND_SET_SHUFFLE_MODE, androidx.media3.common.Player.COMMAND_SET_REPEAT_MODE, androidx.media3.common.Player.COMMAND_GET_CURRENT_MEDIA_ITEM, androidx.media3.common.Player.COMMAND_GET_TIMELINE, androidx.media3.common.Player.COMMAND_GET_MEDIA_ITEMS_METADATA, androidx.media3.common.Player.COMMAND_GET_METADATA, androidx.media3.common.Player.COMMAND_SET_MEDIA_ITEMS_METADATA, androidx.media3.common.Player.COMMAND_SET_PLAYLIST_METADATA, androidx.media3.common.Player.COMMAND_SET_MEDIA_ITEM, androidx.media3.common.Player.COMMAND_CHANGE_MEDIA_ITEMS, androidx.media3.common.Player.COMMAND_GET_AUDIO_ATTRIBUTES, androidx.media3.common.Player.COMMAND_GET_VOLUME, androidx.media3.common.Player.COMMAND_GET_DEVICE_VOLUME, androidx.media3.common.Player.COMMAND_SET_VOLUME, androidx.media3.common.Player.COMMAND_SET_DEVICE_VOLUME, androidx.media3.common.Player.COMMAND_SET_DEVICE_VOLUME_WITH_FLAGS, androidx.media3.common.Player.COMMAND_ADJUST_DEVICE_VOLUME, androidx.media3.common.Player.COMMAND_ADJUST_DEVICE_VOLUME_WITH_FLAGS, androidx.media3.common.Player.COMMAND_SET_VIDEO_SURFACE, androidx.media3.common.Player.COMMAND_GET_TEXT, androidx.media3.common.Player.COMMAND_SET_TRACK_SELECTION_PARAMETERS, androidx.media3.common.Player.COMMAND_GET_TRACKS, androidx.media3.common.Player.COMMAND_RELEASE}) @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 Player.Command {
} }
public static final class Player.Commands { public static final class Player.Commands {
@ -868,10 +945,10 @@ package androidx.media3.common {
@IntDef({androidx.media3.common.Player.MEDIA_ITEM_TRANSITION_REASON_REPEAT, androidx.media3.common.Player.MEDIA_ITEM_TRANSITION_REASON_AUTO, androidx.media3.common.Player.MEDIA_ITEM_TRANSITION_REASON_SEEK, androidx.media3.common.Player.MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED}) @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 Player.MediaItemTransitionReason { @IntDef({androidx.media3.common.Player.MEDIA_ITEM_TRANSITION_REASON_REPEAT, androidx.media3.common.Player.MEDIA_ITEM_TRANSITION_REASON_AUTO, androidx.media3.common.Player.MEDIA_ITEM_TRANSITION_REASON_SEEK, androidx.media3.common.Player.MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED}) @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 Player.MediaItemTransitionReason {
} }
@IntDef({androidx.media3.common.Player.PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST, androidx.media3.common.Player.PLAY_WHEN_READY_CHANGE_REASON_AUDIO_FOCUS_LOSS, androidx.media3.common.Player.PLAY_WHEN_READY_CHANGE_REASON_AUDIO_BECOMING_NOISY, androidx.media3.common.Player.PLAY_WHEN_READY_CHANGE_REASON_REMOTE, androidx.media3.common.Player.PLAY_WHEN_READY_CHANGE_REASON_END_OF_MEDIA_ITEM}) @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 Player.PlayWhenReadyChangeReason { @IntDef({androidx.media3.common.Player.PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST, androidx.media3.common.Player.PLAY_WHEN_READY_CHANGE_REASON_AUDIO_FOCUS_LOSS, androidx.media3.common.Player.PLAY_WHEN_READY_CHANGE_REASON_AUDIO_BECOMING_NOISY, androidx.media3.common.Player.PLAY_WHEN_READY_CHANGE_REASON_REMOTE, androidx.media3.common.Player.PLAY_WHEN_READY_CHANGE_REASON_END_OF_MEDIA_ITEM, androidx.media3.common.Player.PLAY_WHEN_READY_CHANGE_REASON_SUPPRESSED_TOO_LONG}) @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 Player.PlayWhenReadyChangeReason {
} }
@IntDef({androidx.media3.common.Player.PLAYBACK_SUPPRESSION_REASON_NONE, androidx.media3.common.Player.PLAYBACK_SUPPRESSION_REASON_TRANSIENT_AUDIO_FOCUS_LOSS}) @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 Player.PlaybackSuppressionReason { @IntDef({androidx.media3.common.Player.PLAYBACK_SUPPRESSION_REASON_NONE, androidx.media3.common.Player.PLAYBACK_SUPPRESSION_REASON_TRANSIENT_AUDIO_FOCUS_LOSS, androidx.media3.common.Player.PLAYBACK_SUPPRESSION_REASON_UNSUITABLE_AUDIO_ROUTE}) @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 Player.PlaybackSuppressionReason {
} }
public static final class Player.PositionInfo { public static final class Player.PositionInfo {
@ -1162,11 +1239,15 @@ package androidx.media3.common.util {
method public static boolean checkCleartextTrafficPermitted(androidx.media3.common.MediaItem...); method public static boolean checkCleartextTrafficPermitted(androidx.media3.common.MediaItem...);
method @Nullable public static String getAdaptiveMimeTypeForContentType(@androidx.media3.common.C.ContentType int); method @Nullable public static String getAdaptiveMimeTypeForContentType(@androidx.media3.common.C.ContentType int);
method @Nullable public static java.util.UUID getDrmUuid(String); method @Nullable public static java.util.UUID getDrmUuid(String);
method public static boolean handlePauseButtonAction(@Nullable androidx.media3.common.Player);
method public static boolean handlePlayButtonAction(@Nullable androidx.media3.common.Player);
method public static boolean handlePlayPauseButtonAction(@Nullable androidx.media3.common.Player);
method @androidx.media3.common.C.ContentType public static int inferContentType(android.net.Uri); method @androidx.media3.common.C.ContentType public static int inferContentType(android.net.Uri);
method @androidx.media3.common.C.ContentType public static int inferContentTypeForExtension(String); method @androidx.media3.common.C.ContentType public static int inferContentTypeForExtension(String);
method @androidx.media3.common.C.ContentType public static int inferContentTypeForUriAndMimeType(android.net.Uri, @Nullable String); method @androidx.media3.common.C.ContentType public static int inferContentTypeForUriAndMimeType(android.net.Uri, @Nullable String);
method public static boolean maybeRequestReadExternalStoragePermission(android.app.Activity, android.net.Uri...); method public static boolean maybeRequestReadExternalStoragePermission(android.app.Activity, android.net.Uri...);
method public static boolean maybeRequestReadExternalStoragePermission(android.app.Activity, androidx.media3.common.MediaItem...); method public static boolean maybeRequestReadExternalStoragePermission(android.app.Activity, androidx.media3.common.MediaItem...);
method @org.checkerframework.checker.nullness.qual.EnsuresNonNullIf(result=false, expression="#1") public static boolean shouldShowPlayButton(@Nullable androidx.media3.common.Player);
} }
} }
@ -1438,122 +1519,128 @@ package androidx.media3.session {
field public static final String EXTRAS_KEY_SLOT_RESERVATION_SEEK_TO_PREV = "android.media.playback.ALWAYS_RESERVE_SPACE_FOR.ACTION_SKIP_TO_PREVIOUS"; field public static final String EXTRAS_KEY_SLOT_RESERVATION_SEEK_TO_PREV = "android.media.playback.ALWAYS_RESERVE_SPACE_FOR.ACTION_SKIP_TO_PREVIOUS";
} }
public class MediaController implements androidx.media3.common.Player { @com.google.errorprone.annotations.DoNotMock public class MediaController implements androidx.media3.common.Player {
method public void addListener(androidx.media3.common.Player.Listener); method public final void addListener(androidx.media3.common.Player.Listener);
method public void addMediaItem(androidx.media3.common.MediaItem); method public final void addMediaItem(androidx.media3.common.MediaItem);
method public void addMediaItem(int, androidx.media3.common.MediaItem); method public final void addMediaItem(int, androidx.media3.common.MediaItem);
method public void addMediaItems(java.util.List<androidx.media3.common.MediaItem>); method public final void addMediaItems(java.util.List<androidx.media3.common.MediaItem>);
method public void addMediaItems(int, java.util.List<androidx.media3.common.MediaItem>); method public final void addMediaItems(int, java.util.List<androidx.media3.common.MediaItem>);
method public boolean canAdvertiseSession(); method public final boolean canAdvertiseSession();
method public void clearMediaItems(); method public final void clearMediaItems();
method public void clearVideoSurface(); method public final void clearVideoSurface();
method public void clearVideoSurface(@Nullable android.view.Surface); method public final void clearVideoSurface(@Nullable android.view.Surface);
method public void clearVideoSurfaceHolder(@Nullable android.view.SurfaceHolder); method public final void clearVideoSurfaceHolder(@Nullable android.view.SurfaceHolder);
method public void clearVideoSurfaceView(@Nullable android.view.SurfaceView); method public final void clearVideoSurfaceView(@Nullable android.view.SurfaceView);
method public void clearVideoTextureView(@Nullable android.view.TextureView); method public final void clearVideoTextureView(@Nullable android.view.TextureView);
method public void decreaseDeviceVolume(); method @Deprecated public final void decreaseDeviceVolume();
method public android.os.Looper getApplicationLooper(); method public final void decreaseDeviceVolume(@androidx.media3.common.C.VolumeFlags int);
method public androidx.media3.common.AudioAttributes getAudioAttributes(); method public final android.os.Looper getApplicationLooper();
method public androidx.media3.common.Player.Commands getAvailableCommands(); method public final androidx.media3.common.AudioAttributes getAudioAttributes();
method public androidx.media3.session.SessionCommands getAvailableSessionCommands(); method public final androidx.media3.common.Player.Commands getAvailableCommands();
method @IntRange(from=0, to=100) public int getBufferedPercentage(); method public final androidx.media3.session.SessionCommands getAvailableSessionCommands();
method public long getBufferedPosition(); method @IntRange(from=0, to=100) public final int getBufferedPercentage();
method @Nullable public androidx.media3.session.SessionToken getConnectedToken(); method public final long getBufferedPosition();
method public long getContentBufferedPosition(); method @Nullable public final androidx.media3.session.SessionToken getConnectedToken();
method public long getContentDuration(); method public final long getContentBufferedPosition();
method public long getContentPosition(); method public final long getContentDuration();
method public int getCurrentAdGroupIndex(); method public final long getContentPosition();
method public int getCurrentAdIndexInAdGroup(); method public final int getCurrentAdGroupIndex();
method public androidx.media3.common.text.CueGroup getCurrentCues(); method public final int getCurrentAdIndexInAdGroup();
method public long getCurrentLiveOffset(); method public final androidx.media3.common.text.CueGroup getCurrentCues();
method @Nullable public androidx.media3.common.MediaItem getCurrentMediaItem(); method public final long getCurrentLiveOffset();
method public int getCurrentMediaItemIndex(); method @Nullable public final androidx.media3.common.MediaItem getCurrentMediaItem();
method public int getCurrentPeriodIndex(); method public final int getCurrentMediaItemIndex();
method public long getCurrentPosition(); method public final int getCurrentPeriodIndex();
method public androidx.media3.common.Timeline getCurrentTimeline(); method public final long getCurrentPosition();
method public androidx.media3.common.Tracks getCurrentTracks(); method public final androidx.media3.common.Timeline getCurrentTimeline();
method public androidx.media3.common.DeviceInfo getDeviceInfo(); method public final androidx.media3.common.Tracks getCurrentTracks();
method @IntRange(from=0) public int getDeviceVolume(); method public final androidx.media3.common.DeviceInfo getDeviceInfo();
method public long getDuration(); method @IntRange(from=0) public final int getDeviceVolume();
method public long getMaxSeekToPreviousPosition(); method public final long getDuration();
method public androidx.media3.common.MediaItem getMediaItemAt(int); method public final long getMaxSeekToPreviousPosition();
method public int getMediaItemCount(); method public final androidx.media3.common.MediaItem getMediaItemAt(int);
method public androidx.media3.common.MediaMetadata getMediaMetadata(); method public final int getMediaItemCount();
method public int getNextMediaItemIndex(); method public final androidx.media3.common.MediaMetadata getMediaMetadata();
method public boolean getPlayWhenReady(); method public final int getNextMediaItemIndex();
method public androidx.media3.common.PlaybackParameters getPlaybackParameters(); method public final boolean getPlayWhenReady();
method @androidx.media3.common.Player.State public int getPlaybackState(); method public final androidx.media3.common.PlaybackParameters getPlaybackParameters();
method @androidx.media3.common.Player.PlaybackSuppressionReason public int getPlaybackSuppressionReason(); method @androidx.media3.common.Player.State public final int getPlaybackState();
method @Nullable public androidx.media3.common.PlaybackException getPlayerError(); method @androidx.media3.common.Player.PlaybackSuppressionReason public final int getPlaybackSuppressionReason();
method public androidx.media3.common.MediaMetadata getPlaylistMetadata(); method @Nullable public final androidx.media3.common.PlaybackException getPlayerError();
method public int getPreviousMediaItemIndex(); method public final androidx.media3.common.MediaMetadata getPlaylistMetadata();
method @androidx.media3.common.Player.RepeatMode public int getRepeatMode(); method public final int getPreviousMediaItemIndex();
method public long getSeekBackIncrement(); method @androidx.media3.common.Player.RepeatMode public final int getRepeatMode();
method public long getSeekForwardIncrement(); method public final long getSeekBackIncrement();
method @Nullable public android.app.PendingIntent getSessionActivity(); method public final long getSeekForwardIncrement();
method public boolean getShuffleModeEnabled(); method @Nullable public final android.app.PendingIntent getSessionActivity();
method public long getTotalBufferedDuration(); method public final boolean getShuffleModeEnabled();
method public androidx.media3.common.TrackSelectionParameters getTrackSelectionParameters(); method public final long getTotalBufferedDuration();
method public androidx.media3.common.VideoSize getVideoSize(); method public final androidx.media3.common.TrackSelectionParameters getTrackSelectionParameters();
method @FloatRange(from=0, to=1) public float getVolume(); method public final androidx.media3.common.VideoSize getVideoSize();
method public boolean hasNextMediaItem(); method @FloatRange(from=0, to=1) public final float getVolume();
method public boolean hasPreviousMediaItem(); method public final boolean hasNextMediaItem();
method public void increaseDeviceVolume(); method public final boolean hasPreviousMediaItem();
method public boolean isCommandAvailable(@androidx.media3.common.Player.Command int); method @Deprecated public final void increaseDeviceVolume();
method public boolean isConnected(); method public final void increaseDeviceVolume(@androidx.media3.common.C.VolumeFlags int);
method public boolean isCurrentMediaItemDynamic(); method public final boolean isCommandAvailable(@androidx.media3.common.Player.Command int);
method public boolean isCurrentMediaItemLive(); method public final boolean isConnected();
method public boolean isCurrentMediaItemSeekable(); method public final boolean isCurrentMediaItemDynamic();
method public boolean isDeviceMuted(); method public final boolean isCurrentMediaItemLive();
method public boolean isLoading(); method public final boolean isCurrentMediaItemSeekable();
method public boolean isPlaying(); method public final boolean isDeviceMuted();
method public boolean isPlayingAd(); method public final boolean isLoading();
method public boolean isSessionCommandAvailable(@androidx.media3.session.SessionCommand.CommandCode int); method public final boolean isPlaying();
method public boolean isSessionCommandAvailable(androidx.media3.session.SessionCommand); method public final boolean isPlayingAd();
method public void moveMediaItem(int, int); method public final boolean isSessionCommandAvailable(@androidx.media3.session.SessionCommand.CommandCode int);
method public void moveMediaItems(int, int, int); method public final boolean isSessionCommandAvailable(androidx.media3.session.SessionCommand);
method public void pause(); method public final void moveMediaItem(int, int);
method public void play(); method public final void moveMediaItems(int, int, int);
method public void prepare(); method public final void pause();
method public void release(); method public final void play();
method public final void prepare();
method public final void release();
method public static void releaseFuture(java.util.concurrent.Future<? extends androidx.media3.session.MediaController>); method public static void releaseFuture(java.util.concurrent.Future<? extends androidx.media3.session.MediaController>);
method public void removeListener(androidx.media3.common.Player.Listener); method public final void removeListener(androidx.media3.common.Player.Listener);
method public void removeMediaItem(int); method public final void removeMediaItem(int);
method public void removeMediaItems(int, int); method public final void removeMediaItems(int, int);
method public void seekBack(); method public final void replaceMediaItem(int, androidx.media3.common.MediaItem);
method public void seekForward(); method public final void replaceMediaItems(int, int, java.util.List<androidx.media3.common.MediaItem>);
method public void seekTo(long); method public final void seekBack();
method public void seekTo(int, long); method public final void seekForward();
method public void seekToDefaultPosition(); method public final void seekTo(long);
method public void seekToDefaultPosition(int); method public final void seekTo(int, long);
method public void seekToNext(); method public final void seekToDefaultPosition();
method public void seekToNextMediaItem(); method public final void seekToDefaultPosition(int);
method public void seekToPrevious(); method public final void seekToNext();
method public void seekToPreviousMediaItem(); method public final void seekToNextMediaItem();
method public com.google.common.util.concurrent.ListenableFuture<androidx.media3.session.SessionResult> sendCustomCommand(androidx.media3.session.SessionCommand, android.os.Bundle); method public final void seekToPrevious();
method public void setDeviceMuted(boolean); method public final void seekToPreviousMediaItem();
method public void setDeviceVolume(@IntRange(from=0) int); method public final com.google.common.util.concurrent.ListenableFuture<androidx.media3.session.SessionResult> sendCustomCommand(androidx.media3.session.SessionCommand, android.os.Bundle);
method public void setMediaItem(androidx.media3.common.MediaItem); method @Deprecated public final void setDeviceMuted(boolean);
method public void setMediaItem(androidx.media3.common.MediaItem, long); method public final void setDeviceMuted(boolean, @androidx.media3.common.C.VolumeFlags int);
method public void setMediaItem(androidx.media3.common.MediaItem, boolean); method @Deprecated public final void setDeviceVolume(@IntRange(from=0) int);
method public void setMediaItems(java.util.List<androidx.media3.common.MediaItem>); method public final void setDeviceVolume(@IntRange(from=0) int, @androidx.media3.common.C.VolumeFlags int);
method public void setMediaItems(java.util.List<androidx.media3.common.MediaItem>, boolean); method public final void setMediaItem(androidx.media3.common.MediaItem);
method public void setMediaItems(java.util.List<androidx.media3.common.MediaItem>, int, long); method public final void setMediaItem(androidx.media3.common.MediaItem, long);
method public void setPlayWhenReady(boolean); method public final void setMediaItem(androidx.media3.common.MediaItem, boolean);
method public void setPlaybackParameters(androidx.media3.common.PlaybackParameters); method public final void setMediaItems(java.util.List<androidx.media3.common.MediaItem>);
method public void setPlaybackSpeed(float); method public final void setMediaItems(java.util.List<androidx.media3.common.MediaItem>, boolean);
method public void setPlaylistMetadata(androidx.media3.common.MediaMetadata); method public final void setMediaItems(java.util.List<androidx.media3.common.MediaItem>, int, long);
method public com.google.common.util.concurrent.ListenableFuture<androidx.media3.session.SessionResult> setRating(String, androidx.media3.common.Rating); method public final void setPlayWhenReady(boolean);
method public com.google.common.util.concurrent.ListenableFuture<androidx.media3.session.SessionResult> setRating(androidx.media3.common.Rating); method public final void setPlaybackParameters(androidx.media3.common.PlaybackParameters);
method public void setRepeatMode(@androidx.media3.common.Player.RepeatMode int); method public final void setPlaybackSpeed(float);
method public void setShuffleModeEnabled(boolean); method public final void setPlaylistMetadata(androidx.media3.common.MediaMetadata);
method public void setTrackSelectionParameters(androidx.media3.common.TrackSelectionParameters); method public final com.google.common.util.concurrent.ListenableFuture<androidx.media3.session.SessionResult> setRating(String, androidx.media3.common.Rating);
method public void setVideoSurface(@Nullable android.view.Surface); method public final com.google.common.util.concurrent.ListenableFuture<androidx.media3.session.SessionResult> setRating(androidx.media3.common.Rating);
method public void setVideoSurfaceHolder(@Nullable android.view.SurfaceHolder); method public final void setRepeatMode(@androidx.media3.common.Player.RepeatMode int);
method public void setVideoSurfaceView(@Nullable android.view.SurfaceView); method public final void setShuffleModeEnabled(boolean);
method public void setVideoTextureView(@Nullable android.view.TextureView); method public final void setTrackSelectionParameters(androidx.media3.common.TrackSelectionParameters);
method public void setVolume(@FloatRange(from=0, to=1) float); method public final void setVideoSurface(@Nullable android.view.Surface);
method public void stop(); method public final void setVideoSurfaceHolder(@Nullable android.view.SurfaceHolder);
method public final void setVideoSurfaceView(@Nullable android.view.SurfaceView);
method public final void setVideoTextureView(@Nullable android.view.TextureView);
method public final void setVolume(@FloatRange(from=0, to=1) float);
method public final void stop();
} }
public static final class MediaController.Builder { public static final class MediaController.Builder {
@ -1622,21 +1709,22 @@ package androidx.media3.session {
field @IntRange(from=1) public final int notificationId; field @IntRange(from=1) public final int notificationId;
} }
public class MediaSession { @com.google.errorprone.annotations.DoNotMock public class MediaSession {
method public void broadcastCustomCommand(androidx.media3.session.SessionCommand, android.os.Bundle); method public final void broadcastCustomCommand(androidx.media3.session.SessionCommand, android.os.Bundle);
method public java.util.List<androidx.media3.session.MediaSession.ControllerInfo> getConnectedControllers(); method public final java.util.List<androidx.media3.session.MediaSession.ControllerInfo> getConnectedControllers();
method public String getId(); method @Nullable public final androidx.media3.session.MediaSession.ControllerInfo getControllerForCurrentRequest();
method public androidx.media3.common.Player getPlayer(); method public final String getId();
method @Nullable public android.app.PendingIntent getSessionActivity(); method public final androidx.media3.common.Player getPlayer();
method public androidx.media3.session.SessionToken getToken(); method @Nullable public final android.app.PendingIntent getSessionActivity();
method public void release(); method public final androidx.media3.session.SessionToken getToken();
method public com.google.common.util.concurrent.ListenableFuture<androidx.media3.session.SessionResult> sendCustomCommand(androidx.media3.session.MediaSession.ControllerInfo, androidx.media3.session.SessionCommand, android.os.Bundle); method public final void release();
method public void setAvailableCommands(androidx.media3.session.MediaSession.ControllerInfo, androidx.media3.session.SessionCommands, androidx.media3.common.Player.Commands); 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);
method public com.google.common.util.concurrent.ListenableFuture<androidx.media3.session.SessionResult> setCustomLayout(androidx.media3.session.MediaSession.ControllerInfo, java.util.List<androidx.media3.session.CommandButton>); method public final void setAvailableCommands(androidx.media3.session.MediaSession.ControllerInfo, androidx.media3.session.SessionCommands, androidx.media3.common.Player.Commands);
method public void setCustomLayout(java.util.List<androidx.media3.session.CommandButton>); method public final com.google.common.util.concurrent.ListenableFuture<androidx.media3.session.SessionResult> setCustomLayout(androidx.media3.session.MediaSession.ControllerInfo, java.util.List<androidx.media3.session.CommandButton>);
method public void setPlayer(androidx.media3.common.Player); method public final void setCustomLayout(java.util.List<androidx.media3.session.CommandButton>);
method public void setSessionExtras(android.os.Bundle); method public final void setPlayer(androidx.media3.common.Player);
method public void setSessionExtras(androidx.media3.session.MediaSession.ControllerInfo, android.os.Bundle); method public final void setSessionExtras(android.os.Bundle);
method public final void setSessionExtras(androidx.media3.session.MediaSession.ControllerInfo, android.os.Bundle);
} }
public static final class MediaSession.Builder { public static final class MediaSession.Builder {
@ -1653,7 +1741,7 @@ package androidx.media3.session {
method public default androidx.media3.session.MediaSession.ConnectionResult onConnect(androidx.media3.session.MediaSession, androidx.media3.session.MediaSession.ControllerInfo); method public default androidx.media3.session.MediaSession.ConnectionResult onConnect(androidx.media3.session.MediaSession, androidx.media3.session.MediaSession.ControllerInfo);
method public default com.google.common.util.concurrent.ListenableFuture<androidx.media3.session.SessionResult> onCustomCommand(androidx.media3.session.MediaSession, androidx.media3.session.MediaSession.ControllerInfo, androidx.media3.session.SessionCommand, android.os.Bundle); method public default com.google.common.util.concurrent.ListenableFuture<androidx.media3.session.SessionResult> onCustomCommand(androidx.media3.session.MediaSession, androidx.media3.session.MediaSession.ControllerInfo, androidx.media3.session.SessionCommand, android.os.Bundle);
method public default void onDisconnected(androidx.media3.session.MediaSession, androidx.media3.session.MediaSession.ControllerInfo); method public default void onDisconnected(androidx.media3.session.MediaSession, androidx.media3.session.MediaSession.ControllerInfo);
method @androidx.media3.session.SessionResult.Code public default int onPlayerCommandRequest(androidx.media3.session.MediaSession, androidx.media3.session.MediaSession.ControllerInfo, @androidx.media3.common.Player.Command int); method @Deprecated @androidx.media3.session.SessionResult.Code public default int onPlayerCommandRequest(androidx.media3.session.MediaSession, androidx.media3.session.MediaSession.ControllerInfo, @androidx.media3.common.Player.Command int);
method public default void onPostConnect(androidx.media3.session.MediaSession, androidx.media3.session.MediaSession.ControllerInfo); method public default void onPostConnect(androidx.media3.session.MediaSession, androidx.media3.session.MediaSession.ControllerInfo);
method public default com.google.common.util.concurrent.ListenableFuture<androidx.media3.session.SessionResult> onSetRating(androidx.media3.session.MediaSession, androidx.media3.session.MediaSession.ControllerInfo, String, androidx.media3.common.Rating); method public default com.google.common.util.concurrent.ListenableFuture<androidx.media3.session.SessionResult> onSetRating(androidx.media3.session.MediaSession, androidx.media3.session.MediaSession.ControllerInfo, String, androidx.media3.common.Rating);
method public default com.google.common.util.concurrent.ListenableFuture<androidx.media3.session.SessionResult> onSetRating(androidx.media3.session.MediaSession, androidx.media3.session.MediaSession.ControllerInfo, androidx.media3.common.Rating); method public default com.google.common.util.concurrent.ListenableFuture<androidx.media3.session.SessionResult> onSetRating(androidx.media3.session.MediaSession, androidx.media3.session.MediaSession.ControllerInfo, androidx.media3.common.Rating);
@ -1682,7 +1770,8 @@ package androidx.media3.session {
method public final boolean isSessionAdded(androidx.media3.session.MediaSession); method public final boolean isSessionAdded(androidx.media3.session.MediaSession);
method @CallSuper @Nullable public android.os.IBinder onBind(@Nullable android.content.Intent); method @CallSuper @Nullable public android.os.IBinder onBind(@Nullable android.content.Intent);
method @Nullable public abstract androidx.media3.session.MediaSession onGetSession(androidx.media3.session.MediaSession.ControllerInfo); method @Nullable public abstract androidx.media3.session.MediaSession onGetSession(androidx.media3.session.MediaSession.ControllerInfo);
method public void onUpdateNotification(androidx.media3.session.MediaSession); method @Deprecated public void onUpdateNotification(androidx.media3.session.MediaSession);
method public void onUpdateNotification(androidx.media3.session.MediaSession, boolean);
method public final void removeSession(androidx.media3.session.MediaSession); method public final void removeSession(androidx.media3.session.MediaSession);
field public static final String SERVICE_INTERFACE = "androidx.media3.session.MediaSessionService"; field public static final String SERVICE_INTERFACE = "androidx.media3.session.MediaSessionService";
} }

View File

@ -17,9 +17,9 @@ buildscript {
mavenCentral() mavenCentral()
} }
dependencies { dependencies {
classpath 'com.android.tools.build:gradle:7.2.1' classpath 'com.android.tools.build:gradle:7.2.2'
classpath 'com.google.android.gms:strict-version-matcher-plugin:1.2.2' classpath 'com.google.android.gms:strict-version-matcher-plugin:1.2.4'
classpath 'org.jetbrains.kotlin:kotlin-gradle-plugin:1.5.21' classpath 'org.jetbrains.kotlin:kotlin-gradle-plugin:1.8.20'
} }
} }
allprojects { allprojects {

View File

@ -25,9 +25,11 @@ android {
aarMetadata { aarMetadata {
minCompileSdk = project.ext.compileSdkVersion minCompileSdk = project.ext.compileSdkVersion
} }
multiDexEnabled true
} }
compileOptions { compileOptions {
coreLibraryDesugaringEnabled true
sourceCompatibility JavaVersion.VERSION_1_8 sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8
} }
@ -39,3 +41,8 @@ android {
unitTests.includeAndroidResources true unitTests.includeAndroidResources true
} }
} }
dependencies {
implementation 'androidx.multidex:multidex:' + androidxMultidexVersion
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5'
}

View File

@ -12,8 +12,8 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
project.ext { project.ext {
releaseVersion = '1.0.2' releaseVersion = '1.1.0'
releaseVersionCode = 1_000_002_3_00 releaseVersionCode = 1_001_000_3_00
minSdkVersion = 16 minSdkVersion = 16
appTargetSdkVersion = 33 appTargetSdkVersion = 33
// API version before restricting local file access. // API version before restricting local file access.
@ -27,35 +27,38 @@ project.ext {
junitVersion = '4.13.2' junitVersion = '4.13.2'
// Use the same Guava version as the Android repo: // Use the same Guava version as the Android repo:
// https://cs.android.com/android/platform/superproject/+/master:external/guava/METADATA // https://cs.android.com/android/platform/superproject/+/master:external/guava/METADATA
guavaVersion = '31.0.1-android' guavaVersion = '31.1-android'
mockitoVersion = '3.12.4' mockitoVersion = '3.12.4'
robolectricVersion = '4.8.1' robolectricVersion = '4.8.1'
// Keep this in sync with Google's internal Checker Framework version. // Keep this in sync with Google's internal Checker Framework version.
checkerframeworkVersion = '3.13.0' checkerframeworkVersion = '3.13.0'
checkerframeworkCompatVersion = '2.5.5' checkerframeworkCompatVersion = '2.5.5'
errorProneVersion = '2.10.0' errorProneVersion = '2.18.0'
jsr305Version = '3.0.2' jsr305Version = '3.0.2'
kotlinAnnotationsVersion = '1.5.31' kotlinAnnotationsVersion = '1.8.20'
// Updating this to 1.4.0+ will import Kotlin stdlib [internal ref: b/277891049].
androidxAnnotationVersion = '1.3.0' androidxAnnotationVersion = '1.3.0'
// Updating this to 1.3.0+ will import Kotlin stdlib [internal ref: b/277891049].
androidxAnnotationExperimentalVersion = '1.2.0' androidxAnnotationExperimentalVersion = '1.2.0'
androidxAppCompatVersion = '1.3.1' androidxAppCompatVersion = '1.6.1'
androidxCollectionVersion = '1.1.0' androidxCollectionVersion = '1.2.0'
androidxConstraintLayoutVersion = '2.0.4' androidxConstraintLayoutVersion = '2.1.4'
androidxCoreVersion = '1.7.0' // Updating this to 1.9.0+ will import Kotlin stdlib [internal ref: b/277891049].
androidxCoreVersion = '1.8.0'
androidxFuturesVersion = '1.1.0' androidxFuturesVersion = '1.1.0'
androidxMediaVersion = '1.6.0' androidxMediaVersion = '1.6.0'
androidxMedia2Version = '1.2.0' androidxMedia2Version = '1.2.1'
androidxMultidexVersion = '2.0.1' androidxMultidexVersion = '2.0.1'
androidxRecyclerViewVersion = '1.2.1' androidxRecyclerViewVersion = '1.3.0'
androidxMaterialVersion = '1.4.0' androidxMaterialVersion = '1.8.0'
androidxTestCoreVersion = '1.4.0' androidxTestCoreVersion = '1.5.0'
androidxTestJUnitVersion = '1.1.3' androidxTestJUnitVersion = '1.1.5'
androidxTestRunnerVersion = '1.4.0' androidxTestRunnerVersion = '1.5.2'
androidxTestRulesVersion = '1.4.0' androidxTestRulesVersion = '1.5.0'
androidxTestServicesStorageVersion = '1.4.0' androidxTestServicesStorageVersion = '1.4.2'
androidxTestTruthVersion = '1.4.0' androidxTestTruthVersion = '1.5.0'
truthVersion = '1.1.3' truthVersion = '1.1.3'
okhttpVersion = '4.9.2' okhttpVersion = '4.11.0'
modulePrefix = ':' modulePrefix = ':'
if (gradle.ext.has('androidxMediaModulePrefix')) { if (gradle.ext.has('androidxMediaModulePrefix')) {
modulePrefix += gradle.ext.androidxMediaModulePrefix modulePrefix += gradle.ext.androidxMediaModulePrefix

View File

@ -21,11 +21,12 @@ if (gradle.ext.has('androidxMediaModulePrefix')) {
modulePrefix += gradle.ext.androidxMediaModulePrefix modulePrefix += gradle.ext.androidxMediaModulePrefix
} }
rootProject.name = gradle.ext.androidxMediaProjectName
include modulePrefix + 'lib-common' include modulePrefix + 'lib-common'
project(modulePrefix + 'lib-common').projectDir = new File(rootDir, 'libraries/common') project(modulePrefix + 'lib-common').projectDir = new File(rootDir, 'libraries/common')
include modulePrefix + 'lib-container'
project(modulePrefix + 'lib-container').projectDir = new File(rootDir, 'libraries/container')
include modulePrefix + 'lib-session' include modulePrefix + 'lib-session'
project(modulePrefix + 'lib-session').projectDir = new File(rootDir, 'libraries/session') project(modulePrefix + 'lib-session').projectDir = new File(rootDir, 'libraries/session')
@ -83,6 +84,9 @@ project(modulePrefix + 'lib-cast').projectDir = new File(rootDir, 'libraries/cas
include modulePrefix + 'lib-effect' include modulePrefix + 'lib-effect'
project(modulePrefix + 'lib-effect').projectDir = new File(rootDir, 'libraries/effect') project(modulePrefix + 'lib-effect').projectDir = new File(rootDir, 'libraries/effect')
include modulePrefix + 'lib-muxer'
project(modulePrefix + 'lib-muxer').projectDir = new File(rootDir, 'libraries/muxer')
include modulePrefix + 'lib-transformer' include modulePrefix + 'lib-transformer'
project(modulePrefix + 'lib-transformer').projectDir = new File(rootDir, 'libraries/transformer') project(modulePrefix + 'lib-transformer').projectDir = new File(rootDir, 'libraries/transformer')

View File

@ -1,7 +1,116 @@
# Cast demo # Cast demo
This app demonstrates integration with Google Cast, as well as switching between This app demonstrates switching between Google Cast and local playback by using
Google Cast and local playback using ExoPlayer. `CastPlayer` and `ExoPlayer`.
## Building the demo app
See the [demos README](../README.md) for instructions on how to build and run See the [demos README](../README.md) for instructions on how to build and run
this demo. this demo.
Test your streams by adding a `MediaItem` with URI and mime type to the
`DemoUtil` and deploy the app on a real device for casting.
## Customization with `OptionsProvider`
The Cast SDK behaviour in the demo app or your own app can be customized by
providing a custom `OptionsProvider` (see
[`DefaultCastOptionsProvider`](https://github.com/androidx/media/blob/release/libraries/cast/src/main/java/androidx/media3/cast/DefaultCastOptionsProvider.java)
also).
Replace the default options provider in the `AndroidManifest.xml` with your own:
```xml
<meta-data
android:name="com.google.android.gms.cast.framework.OPTIONS_PROVIDER_CLASS_NAME"
android:value="com.example.cast.MyOptionsProvider"/>
```
### Using a different Cast receiver app with the Media3 cast demo sender app
The Media3 cast demo app is an implementation of an
[Android Cast *sender app*](https://developers.google.com/cast/docs/android_sender)
that uses a *default Cast receiver app* (running on the Cast device) that is
customized to support DRM protected streams
[by passing DRM configuration via `MediaInfo`](https://developers.google.com/cast/docs/android_sender/exoplayer).
Hence Widevine DRM credentials can also be populated with a
`MediaItem.DrmConfiguration.Builder` (see the samples in `DemoUtil` marked with
`Widevine`).
If you test your own streams with this demo app, keep in mind that for your
production app you need to
[choose your own receiver app](https://developers.google.com/cast/docs/web_receiver#choose_a_web_receiver)
and have your own receiver app ID.
If you have a receiver app already and want to quickly test whether it works
well together with the `CastPlayer`, then you can configure the demo app to use
your receiver:
```java
public class MyOptionsProvider implements OptionsProvider {
@NonNull
@Override
public CastOptions getCastOptions(Context context) {
return new CastOptions.Builder()
.setReceiverApplicationId(YOUR_RECEIVER_APP_ID)
// other options
.build();
}
}
```
You can also use the plain
[default Cast receiver app](https://developers.google.com/cast/docs/web_receiver#default_media_web_receiver)
by using `CastMediaControlIntent.DEFAULT_MEDIA_RECEIVER_APPLICATION_ID`.
#### Converting a Media3 `MediaItem` to a Cast `MediaQueueItem`
This demo app uses the
[`DefaultMediaItemConverter`](https://github.com/androidx/media/blob/release/libraries/cast/src/main/java/androidx/media3/cast/DefaultMediaItemConverter.java)
to convert a Media3 `MediaItem` to a `MediaQueueItem` of the Cast API. Apps that
use a custom receiver app, can use a custom `MediaItemConverter` instance by
passing it into the constructor of `CastPlayer`.
### Media session and notification
This Media3 cast demo app uses the media session and notification support
provided by the Cast SDK. If your app already integrates with a `MediaSession`,
the Cast session can be disabled to avoid duplicate notifications or sessions:
```java
public class MyOptionsProvider implements OptionsProvider {
@NonNull
@Override
public CastOptions getCastOptions(Context context) {
return new CastOptions.Builder()
.setCastMediaOptions(
new CastMediaOptions.Builder()
.setMediaSessionEnabled(false)
.setNotificationOptions(null)
.build())
// other options
.build();
}
}
```
## Supported media formats
Whether a specific stream is supported on a Cast device largely depends on the
receiver app, the media player used by the receiver and the Cast device, rather
then the implementation of the sender that basically only provides media URI and
metadata.
Generally, Google Cast and all Cast Web Receiver applications support the media
facilities and types listed on
[this page](https://developers.google.com/cast/docs/media). If you build a
custom receiver that uses a media player different to the media player of the
Cast receiver SDK, your app may support
[other formats or features](https://github.com/shaka-project/shaka-player) than
listed in the reference above.
The Media3 team can't give support for building a receiver app or investigations
regarding support for certain media formats on a cast devices. Please consult
the Cast documentation around
[building a receiver application](https://developers.google.com/cast/docs/web_receiver)
for further details.

View File

@ -14,6 +14,7 @@
apply from: '../../constants.gradle' apply from: '../../constants.gradle'
apply plugin: 'com.android.application' apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
android { android {
compileSdkVersion project.ext.compileSdkVersion compileSdkVersion project.ext.compileSdkVersion

View File

@ -52,6 +52,7 @@
</intent-filter> </intent-filter>
<intent-filter> <intent-filter>
<action android:name="android.intent.action.VIEW"/> <action android:name="android.intent.action.VIEW"/>
<action android:name="androidx.media3.demo.main.action.BROWSE"/>
<category android:name="android.intent.category.DEFAULT"/> <category android:name="android.intent.category.DEFAULT"/>
<category android:name="android.intent.category.BROWSABLE"/> <category android:name="android.intent.category.BROWSABLE"/>
<data android:scheme="http"/> <data android:scheme="http"/>

View File

@ -406,6 +406,18 @@
"name": "DASH VOD: Tears of Steel (11 periods, pre/mid/post), 2/5/2 ads [5/10s]", "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" "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"
},
{
"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": "DASH live: Unencrypted stream with 30s ad breaks every minute",
"uri": "ssai://dai.google.com/?assetKey=0ndl1dJcRmKDUPxTRjvdog&format=0&adsId=21"
},
{ {
"name": "Playlist: No ads - HLS VOD: Demo (skippable pre/post) - No ads", "name": "Playlist: No ads - HLS VOD: Demo (skippable pre/post) - No ads",
"playlist": [ "playlist": [
@ -434,20 +446,6 @@
} }
] ]
}, },
{
"name": "Playlist: No ads - HLS Live: Big Buck Bunny (mid) - 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 VOD: Tears of Steel (11 periods, pre/mid/post) - No ads", "name": "Playlist: No ads - DASH VOD: Tears of Steel (11 periods, pre/mid/post) - No ads",
"playlist": [ "playlist": [
@ -494,7 +492,7 @@
] ]
}, },
{ {
"name": "Audio -> Video -> Audio", "name": "Audio -> Video (MKV) -> Video (MKV) -> Audio -> Video (MKV) -> Video (DASH) -> Audio",
"playlist": [ "playlist": [
{ {
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/gen-3/screens/dash-vod-single-segment/audio-141.mp4" "uri": "https://storage.googleapis.com/exoplayer-test-media-1/gen-3/screens/dash-vod-single-segment/audio-141.mp4"
@ -502,6 +500,18 @@
{ {
"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-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv"
}, },
{
"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-1/gen-3/screens/dash-vod-single-segment/audio-141.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/wvmedia/clear/h264/tears/tears.mpd"
},
{ {
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/gen-3/screens/dash-vod-single-segment/audio-141.mp4" "uri": "https://storage.googleapis.com/exoplayer-test-media-1/gen-3/screens/dash-vod-single-segment/audio-141.mp4"
} }

View File

@ -94,7 +94,7 @@ public class IntentUtil {
if (mediaItem.mediaMetadata.title != null) { if (mediaItem.mediaMetadata.title != null) {
intent.putExtra(TITLE_EXTRA, mediaItem.mediaMetadata.title); intent.putExtra(TITLE_EXTRA, mediaItem.mediaMetadata.title);
} }
addPlaybackPropertiesToIntent(localConfiguration, intent, /* extrasKeySuffix= */ ""); addLocalConfigurationToIntent(localConfiguration, intent, /* extrasKeySuffix= */ "");
addClippingConfigurationToIntent( addClippingConfigurationToIntent(
mediaItem.clippingConfiguration, intent, /* extrasKeySuffix= */ ""); mediaItem.clippingConfiguration, intent, /* extrasKeySuffix= */ "");
} else { } else {
@ -104,7 +104,7 @@ public class IntentUtil {
MediaItem.LocalConfiguration localConfiguration = MediaItem.LocalConfiguration localConfiguration =
checkNotNull(mediaItem.localConfiguration); checkNotNull(mediaItem.localConfiguration);
intent.putExtra(URI_EXTRA + ("_" + i), localConfiguration.uri.toString()); intent.putExtra(URI_EXTRA + ("_" + i), localConfiguration.uri.toString());
addPlaybackPropertiesToIntent(localConfiguration, intent, /* extrasKeySuffix= */ "_" + i); addLocalConfigurationToIntent(localConfiguration, intent, /* extrasKeySuffix= */ "_" + i);
addClippingConfigurationToIntent( addClippingConfigurationToIntent(
mediaItem.clippingConfiguration, intent, /* extrasKeySuffix= */ "_" + i); mediaItem.clippingConfiguration, intent, /* extrasKeySuffix= */ "_" + i);
if (mediaItem.mediaMetadata.title != null) { if (mediaItem.mediaMetadata.title != null) {
@ -195,7 +195,7 @@ public class IntentUtil {
return builder; return builder;
} }
private static void addPlaybackPropertiesToIntent( private static void addLocalConfigurationToIntent(
MediaItem.LocalConfiguration localConfiguration, Intent intent, String extrasKeySuffix) { MediaItem.LocalConfiguration localConfiguration, Intent intent, String extrasKeySuffix) {
intent intent
.putExtra(MIME_TYPE_EXTRA + extrasKeySuffix, localConfiguration.mimeType) .putExtra(MIME_TYPE_EXTRA + extrasKeySuffix, localConfiguration.mimeType)

View File

@ -57,6 +57,8 @@ android {
} }
dependencies { dependencies {
// For detecting and debugging leaks only. LeakCanary is not needed for demo app to work.
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.10'
implementation 'androidx.core:core-ktx:' + androidxCoreVersion implementation 'androidx.core:core-ktx:' + androidxCoreVersion
implementation 'androidx.appcompat:appcompat:' + androidxAppCompatVersion implementation 'androidx.appcompat:appcompat:' + androidxAppCompatVersion
implementation 'androidx.multidex:multidex:' + androidxMultidexVersion implementation 'androidx.multidex:multidex:' + androidxMultidexVersion

View File

@ -40,7 +40,9 @@
<activity <activity
android:name=".PlayerActivity" android:name=".PlayerActivity"
android:exported="true"/> android:exported="true"
android:launchMode="singleTop"
android:theme="@style/Theme.AppCompat.NoActionBar"/>
<activity <activity
android:name=".PlayableFolderActivity" android:name=".PlayableFolderActivity"

View File

@ -501,6 +501,94 @@
"totalTrackCount": 2, "totalTrackCount": 2,
"duration": 160, "duration": 160,
"site": "https://www.youtube.com/audiolibrary/music" "site": "https://www.youtube.com/audiolibrary/music"
},
{
"id": "mixed_media_01",
"title": "Tear of steal - DASH",
"album": "Mixed media",
"artist": "Mixed artists",
"genre": "Mixed",
"source": "https://storage.googleapis.com/wvmedia/clear/h264/tears/tears.mpd",
"image": "https://storage.googleapis.com/wvmedia/clear/h264/tears/tears.mpd"
},
{
"id": "mixed_media_02",
"title": "Intro - The Way Of Waking Up (feat. Alan Watts - MP3)",
"album": "Mixed media",
"artist": "Mixed artists",
"genre": "Mixed",
"source": "https://storage.googleapis.com/uamp/The_Kyoto_Connection_-_Wake_Up/01_-_Intro_-_The_Way_Of_Waking_Up_feat_Alan_Watts.mp3",
"image": "https://storage.googleapis.com/uamp/The_Kyoto_Connection_-_Wake_Up/art.jpg"
},
{
"id": "mixed_media_03",
"title": "TTML Netflix Japanese examples (MP4)",
"source": "https://storage.googleapis.com/exoplayer-test-media-1/gen-3/screens/dash-vod-single-segment/video-avc-baseline-480.mp4",
"album": "Mixed media",
"artist": "Mixed artists",
"genre": "Mixed",
"image": "https://cdn.pixabay.com/photo/2014/10/09/13/14/video-481821_960_720.png",
"subtitles": [
{
"subtitle_uri": "https://storage.googleapis.com/exoplayer-test-media-1/ttml/netflix_japanese_ttml.xml",
"subtitle_mime_type": "application/ttml+xml",
"subtitle_lang": "ja"
}
]
},
{
"id": "mixed_media_04",
"title": "The Coldest Shoulder",
"album": "Mixed media",
"artist": "Mixed artists",
"genre": "Mixed",
"source": "https://storage.googleapis.com/automotive-media/The_Coldest_Shoulder.mp3",
"image": "https://storage.googleapis.com/automotive-media/album_art_3.jpg"
},
{
"id": "mixed_media_05",
"title": "Dizzy - MPEG-4 Timed Text",
"album": "Mixed media",
"artist": "Mixed artists",
"genre": "Mixed",
"source": "https://storage.googleapis.com/exoplayer-test-media-1/mp4/dizzy-with-tx3g.mp4",
"image": "https://cdn.pixabay.com/photo/2014/10/09/13/14/video-481821_960_720.png"
},
{
"id": "mixed_media_06",
"title": "Apple 4x3 basic stream (TS)",
"album": "Mixed media",
"artist": "Mixed artists",
"genre": "Mixed",
"source": "https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_4x3/bipbop_4x3_variant.m3u8",
"image": "https://storage.googleapis.com/uamp/The_Kyoto_Connection_-_Wake_Up/art.jpg"
},
{
"id": "mixed_media_07",
"title": "The Calm Before The Storm",
"album": "Mixed media",
"artist": "Mixed artists",
"genre": "Mixed",
"source": "https://storage.googleapis.com/uamp/The_Kyoto_Connection_-_Wake_Up/05_-_The_Calm_Before_The_Storm.mp3",
"image": "https://storage.googleapis.com/uamp/The_Kyoto_Connection_-_Wake_Up/art.jpg"
},
{
"id": "mixed_media_08",
"title": "Android screens (MKV)",
"album": "Mixed media",
"artist": "Mixed artists",
"genre": "Mixed",
"source": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv",
"image": "https://storage.googleapis.com/uamp/The_Kyoto_Connection_-_Wake_Up/art.jpg"
},
{
"id": "mixed_media_09",
"title": "No Pain, No Gain",
"album": "Mixed media",
"artist": "Mixed artists",
"genre": "Mixed",
"source": "https://storage.googleapis.com/uamp/The_Kyoto_Connection_-_Wake_Up/06_-_No_Pain_No_Gain.mp3",
"image": "https://storage.googleapis.com/uamp/The_Kyoto_Connection_-_Wake_Up/art.jpg"
} }
] ]
} }

View File

@ -17,7 +17,6 @@ package androidx.media3.demo.session
import android.content.ComponentName import android.content.ComponentName
import android.content.Context import android.content.Context
import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.MenuItem import android.view.MenuItem
@ -70,13 +69,13 @@ class MainActivity : AppCompatActivity() {
findViewById<ExtendedFloatingActionButton>(R.id.open_player_floating_button) findViewById<ExtendedFloatingActionButton>(R.id.open_player_floating_button)
.setOnClickListener { .setOnClickListener {
// display the playing media items // Start the session activity that shows the playback activity. The System UI uses the same
val intent = Intent(this, PlayerActivity::class.java) // intent in the same way to start the activity from the notification.
startActivity(intent) browser?.sessionActivity?.send()
} }
onBackPressedDispatcher.addCallback( onBackPressedDispatcher.addCallback(
object : OnBackPressedCallback(/* enabled= */ true) { object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() { override fun handleOnBackPressed() {
popPathStack() popPathStack()
} }

View File

@ -20,8 +20,8 @@ import android.net.Uri
import androidx.media3.common.MediaItem import androidx.media3.common.MediaItem
import androidx.media3.common.MediaItem.SubtitleConfiguration import androidx.media3.common.MediaItem.SubtitleConfiguration
import androidx.media3.common.MediaMetadata import androidx.media3.common.MediaMetadata
import androidx.media3.common.util.Util
import com.google.common.collect.ImmutableList import com.google.common.collect.ImmutableList
import java.io.BufferedReader
import org.json.JSONObject import org.json.JSONObject
/** /**
@ -91,10 +91,8 @@ object MediaItemTree {
.build() .build()
} }
private fun loadJSONFromAsset(assets: AssetManager): String { private fun loadJSONFromAsset(assets: AssetManager): String =
val buffer = assets.open("catalog.json").use { Util.toByteArray(it) } assets.open("catalog.json").bufferedReader().use(BufferedReader::readText)
return String(buffer, Charsets.UTF_8)
}
fun initialize(assets: AssetManager) { fun initialize(assets: AssetManager) {
if (isInitialized) return if (isInitialized) return

View File

@ -51,6 +51,7 @@ class PlayableFolderActivity : AppCompatActivity() {
companion object { companion object {
private const val MEDIA_ITEM_ID_KEY = "MEDIA_ITEM_ID_KEY" private const val MEDIA_ITEM_ID_KEY = "MEDIA_ITEM_ID_KEY"
fun createIntent(context: Context, mediaItemID: String): Intent { fun createIntent(context: Context, mediaItemID: String): Intent {
val intent = Intent(context, PlayableFolderActivity::class.java) val intent = Intent(context, PlayableFolderActivity::class.java)
intent.putExtra(MEDIA_ITEM_ID_KEY, mediaItemID) intent.putExtra(MEDIA_ITEM_ID_KEY, mediaItemID)
@ -77,8 +78,7 @@ class PlayableFolderActivity : AppCompatActivity() {
browser.shuffleModeEnabled = false browser.shuffleModeEnabled = false
browser.prepare() browser.prepare()
browser.play() browser.play()
val intent = Intent(this, PlayerActivity::class.java) browser.sessionActivity?.send()
startActivity(intent)
} }
} }
@ -88,8 +88,7 @@ class PlayableFolderActivity : AppCompatActivity() {
browser.shuffleModeEnabled = true browser.shuffleModeEnabled = true
browser.prepare() browser.prepare()
browser.play() browser.play()
val intent = Intent(this, PlayerActivity::class.java) browser?.sessionActivity?.send()
startActivity(intent)
} }
findViewById<Button>(R.id.play_button).setOnClickListener { findViewById<Button>(R.id.play_button).setOnClickListener {
@ -104,9 +103,9 @@ class PlayableFolderActivity : AppCompatActivity() {
findViewById<ExtendedFloatingActionButton>(R.id.open_player_floating_button) findViewById<ExtendedFloatingActionButton>(R.id.open_player_floating_button)
.setOnClickListener { .setOnClickListener {
// display the playing media items // Start the session activity that shows the playback activity. The System UI uses the same
val intent = Intent(this, PlayerActivity::class.java) // intent in the same way to start the activity from the notification.
startActivity(intent) browser?.sessionActivity?.send()
} }
} }

View File

@ -18,6 +18,7 @@ package androidx.media3.demo.session
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.NotificationChannel import android.app.NotificationChannel
import android.app.NotificationManager import android.app.NotificationManager
import android.app.PendingIntent
import android.app.PendingIntent.* import android.app.PendingIntent.*
import android.app.TaskStackBuilder import android.app.TaskStackBuilder
import android.content.Intent import android.content.Intent
@ -28,6 +29,7 @@ import androidx.core.app.NotificationManagerCompat
import androidx.media3.common.AudioAttributes import androidx.media3.common.AudioAttributes
import androidx.media3.common.MediaItem import androidx.media3.common.MediaItem
import androidx.media3.common.util.Util import androidx.media3.common.util.Util
import androidx.media3.datasource.DataSourceBitmapLoader
import androidx.media3.exoplayer.ExoPlayer import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.session.* import androidx.media3.session.*
import androidx.media3.session.LibraryResult.RESULT_ERROR_NOT_SUPPORTED import androidx.media3.session.LibraryResult.RESULT_ERROR_NOT_SUPPORTED
@ -54,6 +56,7 @@ class PlaybackService : MediaLibraryService() {
"android.media3.session.demo.SHUFFLE_OFF" "android.media3.session.demo.SHUFFLE_OFF"
private const val NOTIFICATION_ID = 123 private const val NOTIFICATION_ID = 123
private const val CHANNEL_ID = "demo_session_notification_channel_id" private const val CHANNEL_ID = "demo_session_notification_channel_id"
private val immutableFlag = if (Build.VERSION.SDK_INT >= 23) FLAG_IMMUTABLE else 0
} }
override fun onCreate() { override fun onCreate() {
@ -77,14 +80,15 @@ class PlaybackService : MediaLibraryService() {
} }
override fun onTaskRemoved(rootIntent: Intent?) { override fun onTaskRemoved(rootIntent: Intent?) {
if (!player.playWhenReady) { if (!player.playWhenReady || player.mediaItemCount == 0) {
stopSelf() stopSelf()
} }
} }
override fun onDestroy() { override fun onDestroy() {
player.release() mediaLibrarySession.setSessionActivity(getBackStackedActivity())
mediaLibrarySession.release() mediaLibrarySession.release()
player.release()
clearListener() clearListener()
super.onDestroy() super.onDestroy()
} }
@ -234,18 +238,10 @@ class PlaybackService : MediaLibraryService() {
.build() .build()
MediaItemTree.initialize(assets) MediaItemTree.initialize(assets)
val sessionActivityPendingIntent =
TaskStackBuilder.create(this).run {
addNextIntent(Intent(this@PlaybackService, MainActivity::class.java))
addNextIntent(Intent(this@PlaybackService, PlayerActivity::class.java))
val immutableFlag = if (Build.VERSION.SDK_INT >= 23) FLAG_IMMUTABLE else 0
getPendingIntent(0, immutableFlag or FLAG_UPDATE_CURRENT)
}
mediaLibrarySession = mediaLibrarySession =
MediaLibrarySession.Builder(this, player, librarySessionCallback) MediaLibrarySession.Builder(this, player, librarySessionCallback)
.setSessionActivity(sessionActivityPendingIntent) .setSessionActivity(getSingleTopActivity())
.setBitmapLoader(CacheBitmapLoader(DataSourceBitmapLoader(/* context= */ this)))
.build() .build()
if (!customLayout.isEmpty()) { if (!customLayout.isEmpty()) {
// Send custom layout to legacy session. // Send custom layout to legacy session.
@ -253,6 +249,23 @@ class PlaybackService : MediaLibraryService() {
} }
} }
private fun getSingleTopActivity(): PendingIntent {
return getActivity(
this,
0,
Intent(this, PlayerActivity::class.java),
immutableFlag or FLAG_UPDATE_CURRENT
)
}
private fun getBackStackedActivity(): PendingIntent {
return TaskStackBuilder.create(this).run {
addNextIntent(Intent(this@PlaybackService, MainActivity::class.java))
addNextIntent(Intent(this@PlaybackService, PlayerActivity::class.java))
getPendingIntent(0, immutableFlag or FLAG_UPDATE_CURRENT)
}
}
private fun getShuffleCommandButton(sessionCommand: SessionCommand): CommandButton { private fun getShuffleCommandButton(sessionCommand: SessionCommand): CommandButton {
val isOn = sessionCommand.customAction == CUSTOM_COMMAND_TOGGLE_SHUFFLE_MODE_ON val isOn = sessionCommand.customAction == CUSTOM_COMMAND_TOGGLE_SHUFFLE_MODE_ON
return CommandButton.Builder() return CommandButton.Builder()
@ -285,8 +298,6 @@ class PlaybackService : MediaLibraryService() {
val pendingIntent = val pendingIntent =
TaskStackBuilder.create(this@PlaybackService).run { TaskStackBuilder.create(this@PlaybackService).run {
addNextIntent(Intent(this@PlaybackService, MainActivity::class.java)) addNextIntent(Intent(this@PlaybackService, MainActivity::class.java))
val immutableFlag = if (Build.VERSION.SDK_INT >= 23) FLAG_IMMUTABLE else 0
getPendingIntent(0, immutableFlag or FLAG_UPDATE_CURRENT) getPendingIntent(0, immutableFlag or FLAG_UPDATE_CURRENT)
} }
val builder = val builder =

View File

@ -19,7 +19,6 @@ import android.content.ComponentName
import android.content.Context import android.content.Context
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.MenuItem
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.ArrayAdapter import android.widget.ArrayAdapter
@ -46,41 +45,33 @@ class PlayerActivity : AppCompatActivity() {
get() = if (controllerFuture.isDone) controllerFuture.get() else null get() = if (controllerFuture.isDone) controllerFuture.get() else null
private lateinit var playerView: PlayerView private lateinit var playerView: PlayerView
private lateinit var mediaList: ListView private lateinit var mediaItemListView: ListView
private lateinit var mediaListAdapter: PlayingMediaItemArrayAdapter private lateinit var mediaItemListAdapter: MediaItemListAdapter
private val subItemMediaList: MutableList<MediaItem> = mutableListOf() private val mediaItemList: MutableList<MediaItem> = mutableListOf()
private var lastMediaItemId: String? = null
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContentView(R.layout.activity_player) setContentView(R.layout.activity_player)
playerView = findViewById(R.id.player_view) playerView = findViewById(R.id.player_view)
mediaList = findViewById(R.id.current_playing_list) mediaItemListView = findViewById(R.id.current_playing_list)
mediaListAdapter = PlayingMediaItemArrayAdapter(this, R.layout.folder_items, subItemMediaList) mediaItemListAdapter = MediaItemListAdapter(this, R.layout.folder_items, mediaItemList)
mediaList.adapter = mediaListAdapter mediaItemListView.adapter = mediaItemListAdapter
mediaList.setOnItemClickListener { _, _, position, _ -> mediaItemListView.setOnItemClickListener { _, _, position, _ ->
run { run {
val controller = this.controller ?: return@run val controller = this.controller ?: return@run
controller.seekToDefaultPosition(/* windowIndex= */ position) if (controller.currentMediaItemIndex == position) {
mediaListAdapter.notifyDataSetChanged() controller.playWhenReady = !controller.playWhenReady
if (controller.playWhenReady) {
playerView.hideController()
}
} else {
controller.seekToDefaultPosition(/* mediaItemIndex= */ position)
mediaItemListAdapter.notifyDataSetChanged()
} }
} }
findViewById<ImageView>(R.id.shuffle_switch).setOnClickListener {
val controller = this.controller ?: return@setOnClickListener
controller.shuffleModeEnabled = !controller.shuffleModeEnabled
} }
findViewById<ImageView>(R.id.repeat_switch).setOnClickListener {
val controller = this.controller ?: return@setOnClickListener
when (controller.repeatMode) {
Player.REPEAT_MODE_ALL -> controller.repeatMode = Player.REPEAT_MODE_OFF
Player.REPEAT_MODE_OFF -> controller.repeatMode = Player.REPEAT_MODE_ONE
Player.REPEAT_MODE_ONE -> controller.repeatMode = Player.REPEAT_MODE_ALL
}
}
supportActionBar!!.setDisplayHomeAsUpEnabled(true)
} }
override fun onStart() { override fun onStart() {
@ -94,14 +85,6 @@ class PlayerActivity : AppCompatActivity() {
releaseController() releaseController()
} }
override fun onOptionsItemSelected(item: MenuItem): Boolean {
if (item.itemId == android.R.id.home) {
onBackPressed()
return true
}
return super.onOptionsItemSelected(item)
}
private fun initializeController() { private fun initializeController() {
controllerFuture = controllerFuture =
MediaController.Builder( MediaController.Builder(
@ -123,8 +106,6 @@ class PlayerActivity : AppCompatActivity() {
updateCurrentPlaylistUI() updateCurrentPlaylistUI()
updateMediaMetadataUI(controller.mediaMetadata) updateMediaMetadataUI(controller.mediaMetadata)
updateShuffleSwitchUI(controller.shuffleModeEnabled)
updateRepeatSwitchUI(controller.repeatMode)
playerView.setShowSubtitleButton(controller.currentTracks.isTypeSupported(TRACK_TYPE_TEXT)) playerView.setShowSubtitleButton(controller.currentTracks.isTypeSupported(TRACK_TYPE_TEXT))
controller.addListener( controller.addListener(
@ -133,14 +114,6 @@ class PlayerActivity : AppCompatActivity() {
updateMediaMetadataUI(mediaItem?.mediaMetadata ?: MediaMetadata.EMPTY) updateMediaMetadataUI(mediaItem?.mediaMetadata ?: MediaMetadata.EMPTY)
} }
override fun onShuffleModeEnabledChanged(shuffleModeEnabled: Boolean) {
updateShuffleSwitchUI(shuffleModeEnabled)
}
override fun onRepeatModeChanged(repeatMode: Int) {
updateRepeatSwitchUI(repeatMode)
}
override fun onTracksChanged(tracks: Tracks) { override fun onTracksChanged(tracks: Tracks) {
playerView.setShowSubtitleButton(tracks.isTypeSupported(TRACK_TYPE_TEXT)) playerView.setShowSubtitleButton(tracks.isTypeSupported(TRACK_TYPE_TEXT))
} }
@ -148,48 +121,26 @@ class PlayerActivity : AppCompatActivity() {
) )
} }
private fun updateShuffleSwitchUI(shuffleModeEnabled: Boolean) {
val resId =
if (shuffleModeEnabled) R.drawable.exo_styled_controls_shuffle_on
else R.drawable.exo_styled_controls_shuffle_off
findViewById<ImageView>(R.id.shuffle_switch)
.setImageDrawable(ContextCompat.getDrawable(this, resId))
}
private fun updateRepeatSwitchUI(repeatMode: Int) {
val resId: Int =
when (repeatMode) {
Player.REPEAT_MODE_OFF -> R.drawable.exo_styled_controls_repeat_off
Player.REPEAT_MODE_ONE -> R.drawable.exo_styled_controls_repeat_one
Player.REPEAT_MODE_ALL -> R.drawable.exo_styled_controls_repeat_all
else -> R.drawable.exo_styled_controls_repeat_off
}
findViewById<ImageView>(R.id.repeat_switch)
.setImageDrawable(ContextCompat.getDrawable(this, resId))
}
private fun updateMediaMetadataUI(mediaMetadata: MediaMetadata) { private fun updateMediaMetadataUI(mediaMetadata: MediaMetadata) {
val title: CharSequence = mediaMetadata.title ?: getString(R.string.no_item_prompt) val title: CharSequence = mediaMetadata.title ?: ""
findViewById<TextView>(R.id.video_title).text = title findViewById<TextView>(R.id.media_title).text = title
findViewById<TextView>(R.id.video_album).text = mediaMetadata.albumTitle findViewById<TextView>(R.id.media_artist).text = mediaMetadata.artist
findViewById<TextView>(R.id.video_artist).text = mediaMetadata.artist
findViewById<TextView>(R.id.video_genre).text = mediaMetadata.genre
// Trick to update playlist UI // Trick to update playlist UI
mediaListAdapter.notifyDataSetChanged() mediaItemListAdapter.notifyDataSetChanged()
} }
private fun updateCurrentPlaylistUI() { private fun updateCurrentPlaylistUI() {
val controller = this.controller ?: return val controller = this.controller ?: return
subItemMediaList.clear() mediaItemList.clear()
for (i in 0 until controller.mediaItemCount) { for (i in 0 until controller.mediaItemCount) {
subItemMediaList.add(controller.getMediaItemAt(i)) mediaItemList.add(controller.getMediaItemAt(i))
} }
mediaListAdapter.notifyDataSetChanged() mediaItemListAdapter.notifyDataSetChanged()
} }
private inner class PlayingMediaItemArrayAdapter( private inner class MediaItemListAdapter(
context: Context, context: Context,
viewID: Int, viewID: Int,
mediaItemList: List<MediaItem> mediaItemList: List<MediaItem>
@ -201,23 +152,31 @@ class PlayerActivity : AppCompatActivity() {
returnConvertView.findViewById<TextView>(R.id.media_item).text = mediaItem.mediaMetadata.title returnConvertView.findViewById<TextView>(R.id.media_item).text = mediaItem.mediaMetadata.title
val deleteButton = returnConvertView.findViewById<Button>(R.id.delete_button)
if (position == controller?.currentMediaItemIndex) { if (position == controller?.currentMediaItemIndex) {
returnConvertView.setBackgroundColor(ContextCompat.getColor(context, R.color.white)) // Styles for the current media item list item.
returnConvertView returnConvertView.setBackgroundColor(
.findViewById<TextView>(R.id.media_item) ContextCompat.getColor(context, R.color.playlist_item_background)
.setTextColor(ContextCompat.getColor(context, R.color.black)) )
} else {
returnConvertView.setBackgroundColor(ContextCompat.getColor(context, R.color.black))
returnConvertView returnConvertView
.findViewById<TextView>(R.id.media_item) .findViewById<TextView>(R.id.media_item)
.setTextColor(ContextCompat.getColor(context, R.color.white)) .setTextColor(ContextCompat.getColor(context, R.color.white))
} deleteButton.visibility = View.GONE
} else {
returnConvertView.findViewById<Button>(R.id.delete_button).setOnClickListener { // Styles for any other media item list item.
returnConvertView.setBackgroundColor(
ContextCompat.getColor(context, R.color.player_background)
)
returnConvertView
.findViewById<TextView>(R.id.media_item)
.setTextColor(ContextCompat.getColor(context, R.color.white))
deleteButton.visibility = View.VISIBLE
deleteButton.setOnClickListener {
val controller = this@PlayerActivity.controller ?: return@setOnClickListener val controller = this@PlayerActivity.controller ?: return@setOnClickListener
controller.removeMediaItem(position) controller.removeMediaItem(position)
updateCurrentPlaylistUI() updateCurrentPlaylistUI()
} }
}
return returnConvertView return returnConvertView
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

View File

@ -19,7 +19,7 @@
android:orientation="vertical" android:orientation="vertical"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:background="@color/black" android:background="@color/player_background"
tools:context=".PlayerActivity"> tools:context=".PlayerActivity">
<androidx.media3.ui.AspectRatioFrameLayout <androidx.media3.ui.AspectRatioFrameLayout
@ -28,77 +28,48 @@
> >
<androidx.media3.ui.PlayerView <androidx.media3.ui.PlayerView
android:id="@+id/player_view" android:id="@+id/player_view"
android:background="@color/player_background"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
app:use_artwork="true" /> app:artwork_display_mode="fill"
app:default_artwork="@drawable/artwork_placeholder"
app:repeat_toggle_modes="one|all"
app:show_shuffle_button="true"
app:shutter_background_color="@color/player_background" />
</androidx.media3.ui.AspectRatioFrameLayout> </androidx.media3.ui.AspectRatioFrameLayout>
<TextView <TextView
android:id="@+id/video_title" android:id="@+id/media_artist"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:paddingLeft="10dp" android:paddingLeft="10dp"
android:paddingStart="10dp"
android:paddingTop="10dp"
android:textColor="@color/white"
android:textSize="14sp"/>
<TextView
android:id="@+id/media_title"
android:ellipsize="end"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:maxLines="1"
android:paddingLeft="10dp"
android:paddingBottom="10dp" android:paddingBottom="10dp"
android:paddingStart="10dp"
android:textColor="@color/white" android:textColor="@color/white"
android:textSize="20sp" /> android:textSize="20sp" />
<TextView <View
android:id="@+id/video_album" android:background="@color/divider"
android:layout_width="match_parent" android:layout_height="1dp"
android:layout_height="wrap_content" android:layout_width="match_parent" />
android:paddingLeft="10dp"
android:paddingBottom="10dp"
android:textColor="@color/white"
android:textSize="20sp" />
<TextView
android:id="@+id/video_artist"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="@color/white"
android:paddingLeft="10dp"
android:paddingBottom="10dp"/>
<TextView
android:id="@+id/video_genre"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="@color/white"
android:paddingLeft="10dp"
android:paddingBottom="10dp"
android:textSize="12sp" />
<LinearLayout
android:gravity="center_vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<ImageView
android:id="@+id/shuffle_switch"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:contentDescription="@string/shuffle"
android:src="@drawable/exo_styled_controls_shuffle_off"
android:textColor="@color/white" />
<ImageView
android:layout_margin="@dimen/exo_icon_horizontal_margin"
android:id="@+id/repeat_switch"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/exo_styled_controls_repeat_off"
android:textColor="@color/white"
android:contentDescription="@string/repeat"
/>
</LinearLayout>
<ListView <ListView
android:id="@+id/current_playing_list" android:id="@+id/current_playing_list"
android:layout_width="match_parent"
android:divider="@drawable/divider" android:divider="@drawable/divider"
android:dividerHeight="1px" android:dividerHeight="1px"
android:layout_height="wrap_content"/> android:layout_width="match_parent"
android:layout_height="wrap_content" />
</LinearLayout> </LinearLayout>

View File

@ -25,6 +25,8 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:gravity="center_vertical" android:gravity="center_vertical"
android:paddingStart="10dp" android:paddingStart="10dp"
android:maxLines="1"
android:ellipsize="end"
android:textColor="@color/white" android:textColor="@color/white"
android:paddingEnd="10dp" android:paddingEnd="10dp"
android:minHeight="50dp" /> android:minHeight="50dp" />
@ -35,6 +37,7 @@
android:layout_width="50dp" android:layout_width="50dp"
android:layout_height="match_parent" android:layout_height="match_parent"
android:id="@+id/delete_button" android:id="@+id/delete_button"
android:backgroundTint="@color/playlist_item_foreground"
android:background="@drawable/baseline_playlist_remove_white_48" android:background="@drawable/baseline_playlist_remove_white_48"
/> />

View File

@ -22,4 +22,9 @@
<color name="black">#FF000000</color> <color name="black">#FF000000</color>
<color name="white">#FFFFFFFF</color> <color name="white">#FFFFFFFF</color>
<color name="grey">#FF999999</color> <color name="grey">#FF999999</color>
<color name="background">#292929</color>
<color name="player_background">#1c1c1c</color>
<color name="playlist_item_background">#363434</color>
<color name="playlist_item_foreground">#635E5E</color>
<color name="divider">#646464</color>
</resources> </resources>

View File

@ -22,6 +22,7 @@
<uses-permission android:name="android.permission.INTERNET"/> <uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO"/>
<application <application
android:allowBackup="false" android:allowBackup="false"

View File

@ -1,181 +0,0 @@
/*
* Copyright 2022 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.checkArgument;
import static androidx.media3.common.util.Assertions.checkStateNotNull;
import android.content.Context;
import android.content.pm.PackageManager;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.drawable.BitmapDrawable;
import android.opengl.GLES20;
import android.opengl.GLUtils;
import android.util.Pair;
import androidx.media3.common.C;
import androidx.media3.common.FrameProcessingException;
import androidx.media3.common.util.GlProgram;
import androidx.media3.common.util.GlUtil;
import androidx.media3.effect.SingleFrameGlTextureProcessor;
import java.io.IOException;
import java.util.Locale;
/**
* A {@link SingleFrameGlTextureProcessor} that overlays a bitmap with a logo and timer on each
* frame.
*
* <p>The bitmap is drawn using an Android {@link Canvas}.
*/
// TODO(b/227625365): Delete this class and use a texture processor from the Transformer library,
// once overlaying a bitmap and text is supported in Transformer.
/* package */ final class BitmapOverlayProcessor extends SingleFrameGlTextureProcessor {
private static final String VERTEX_SHADER_PATH = "vertex_shader_copy_es2.glsl";
private static final String FRAGMENT_SHADER_PATH = "fragment_shader_bitmap_overlay_es2.glsl";
private static final int BITMAP_WIDTH_HEIGHT = 512;
private final Paint paint;
private final Bitmap overlayBitmap;
private final Bitmap logoBitmap;
private final Canvas overlayCanvas;
private final GlProgram glProgram;
private float bitmapScaleX;
private float bitmapScaleY;
private int bitmapTexId;
/**
* Creates a new instance.
*
* @param context The {@link Context}.
* @param useHdr Whether input textures come from an HDR source. If {@code true}, colors will be
* in linear RGB BT.2020. If {@code false}, colors will be in linear RGB BT.709.
* @throws FrameProcessingException If a problem occurs while reading shader files.
*/
public BitmapOverlayProcessor(Context context, boolean useHdr) throws FrameProcessingException {
super(useHdr);
checkArgument(!useHdr, "BitmapOverlayProcessor does not support HDR colors.");
paint = new Paint();
paint.setTextSize(64);
paint.setAntiAlias(true);
paint.setARGB(0xFF, 0xFF, 0xFF, 0xFF);
paint.setColor(Color.GRAY);
overlayBitmap =
Bitmap.createBitmap(BITMAP_WIDTH_HEIGHT, BITMAP_WIDTH_HEIGHT, Bitmap.Config.ARGB_8888);
overlayCanvas = new Canvas(overlayBitmap);
try {
logoBitmap =
((BitmapDrawable)
context.getPackageManager().getApplicationIcon(context.getPackageName()))
.getBitmap();
} catch (PackageManager.NameNotFoundException e) {
throw new IllegalStateException(e);
}
try {
bitmapTexId =
GlUtil.createTexture(
BITMAP_WIDTH_HEIGHT,
BITMAP_WIDTH_HEIGHT,
/* useHighPrecisionColorComponents= */ false);
GLUtils.texImage2D(GLES20.GL_TEXTURE_2D, /* level= */ 0, overlayBitmap, /* border= */ 0);
glProgram = new GlProgram(context, VERTEX_SHADER_PATH, FRAGMENT_SHADER_PATH);
} catch (GlUtil.GlException | IOException e) {
throw new FrameProcessingException(e);
}
// Draw the frame on the entire normalized device coordinate space, from -1 to 1, for x and y.
glProgram.setBufferAttribute(
"aFramePosition",
GlUtil.getNormalizedCoordinateBounds(),
GlUtil.HOMOGENEOUS_COORDINATE_VECTOR_SIZE);
glProgram.setSamplerTexIdUniform("uTexSampler1", bitmapTexId, /* texUnitIndex= */ 1);
}
@Override
public Pair<Integer, Integer> configure(int inputWidth, int inputHeight) {
if (inputWidth > inputHeight) {
bitmapScaleX = inputWidth / (float) inputHeight;
bitmapScaleY = 1f;
} else {
bitmapScaleX = 1f;
bitmapScaleY = inputHeight / (float) inputWidth;
}
glProgram.setFloatUniform("uScaleX", bitmapScaleX);
glProgram.setFloatUniform("uScaleY", bitmapScaleY);
return Pair.create(inputWidth, inputHeight);
}
@Override
public void drawFrame(int inputTexId, long presentationTimeUs) throws FrameProcessingException {
try {
glProgram.use();
// Draw to the canvas and store it in a texture.
String text =
String.format(Locale.US, "%.02f", presentationTimeUs / (float) C.MICROS_PER_SECOND);
overlayBitmap.eraseColor(Color.TRANSPARENT);
overlayCanvas.drawBitmap(checkStateNotNull(logoBitmap), /* left= */ 3, /* top= */ 378, paint);
overlayCanvas.drawText(text, /* x= */ 160, /* y= */ 466, paint);
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, bitmapTexId);
GLUtils.texSubImage2D(
GLES20.GL_TEXTURE_2D,
/* level= */ 0,
/* xoffset= */ 0,
/* yoffset= */ 0,
flipBitmapVertically(overlayBitmap));
GlUtil.checkGlError();
glProgram.setSamplerTexIdUniform("uTexSampler0", inputTexId, /* texUnitIndex= */ 0);
glProgram.bindAttributesAndUniforms();
// The four-vertex triangle strip forms a quad.
GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, /* first= */ 0, /* count= */ 4);
GlUtil.checkGlError();
} catch (GlUtil.GlException e) {
throw new FrameProcessingException(e, presentationTimeUs);
}
}
@Override
public void release() throws FrameProcessingException {
super.release();
try {
glProgram.delete();
} catch (GlUtil.GlException e) {
throw new FrameProcessingException(e);
}
}
private static Bitmap flipBitmapVertically(Bitmap bitmap) {
Matrix flip = new Matrix();
flip.postScale(1f, -1f);
return Bitmap.createBitmap(
bitmap,
/* x= */ 0,
/* y= */ 0,
bitmap.getWidth(),
bitmap.getHeight(),
flip,
/* filter= */ true);
}
}

View File

@ -15,20 +15,24 @@
*/ */
package androidx.media3.demo.transformer; package androidx.media3.demo.transformer;
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.checkNotNull;
import static androidx.media3.common.util.Assertions.checkState; import static androidx.media3.common.util.Assertions.checkState;
import static androidx.media3.common.util.Util.SDK_INT;
import android.Manifest;
import android.app.Activity; import android.app.Activity;
import android.content.DialogInterface; import android.content.DialogInterface;
import android.content.Intent; import android.content.Intent;
import android.content.pm.PackageManager; import android.content.pm.PackageManager;
import android.graphics.Color;
import android.net.Uri; import android.net.Uri;
import android.os.Bundle; import android.os.Bundle;
import android.view.View; import android.view.View;
import android.widget.ArrayAdapter; import android.widget.ArrayAdapter;
import android.widget.Button; import android.widget.Button;
import android.widget.CheckBox; import android.widget.CheckBox;
import android.widget.EditText;
import android.widget.Spinner; import android.widget.Spinner;
import android.widget.TextView; import android.widget.TextView;
import android.widget.Toast; import android.widget.Toast;
@ -41,22 +45,24 @@ import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.ActivityCompat; import androidx.core.app.ActivityCompat;
import androidx.media3.common.C; import androidx.media3.common.C;
import androidx.media3.common.MimeTypes; import androidx.media3.common.MimeTypes;
import androidx.media3.common.util.Util; import androidx.media3.transformer.TransformationRequest;
import com.google.android.material.slider.RangeSlider; import com.google.android.material.slider.RangeSlider;
import com.google.android.material.slider.Slider; import com.google.android.material.slider.Slider;
import com.google.common.collect.ImmutableMap;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull; import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
import org.checkerframework.checker.nullness.qual.RequiresNonNull; import org.checkerframework.checker.nullness.qual.RequiresNonNull;
/** /**
* An {@link Activity} that sets the configuration to use for transforming and playing media, using * An {@link Activity} that sets the configuration to use for exporting and playing media, using
* {@link TransformerActivity}. * {@link TransformerActivity}.
*/ */
public final class ConfigurationActivity extends AppCompatActivity { public final class ConfigurationActivity extends AppCompatActivity {
public static final String SHOULD_REMOVE_AUDIO = "should_remove_audio"; public static final String SHOULD_REMOVE_AUDIO = "should_remove_audio";
public static final String SHOULD_REMOVE_VIDEO = "should_remove_video"; public static final String SHOULD_REMOVE_VIDEO = "should_remove_video";
public static final String SHOULD_FLATTEN_FOR_SLOW_MOTION = "should_flatten_for_slow_motion"; public static final String SHOULD_FLATTEN_FOR_SLOW_MOTION = "should_flatten_for_slow_motion";
public static final String FORCE_AUDIO_TRACK = "force_audio_track";
public static final String AUDIO_MIME_TYPE = "audio_mime_type"; public static final String AUDIO_MIME_TYPE = "audio_mime_type";
public static final String VIDEO_MIME_TYPE = "video_mime_type"; public static final String VIDEO_MIME_TYPE = "video_mime_type";
public static final String RESOLUTION_HEIGHT = "resolution_height"; public static final String RESOLUTION_HEIGHT = "resolution_height";
@ -67,10 +73,10 @@ public final class ConfigurationActivity extends AppCompatActivity {
public static final String TRIM_END_MS = "trim_end_ms"; public static final String TRIM_END_MS = "trim_end_ms";
public static final String ENABLE_FALLBACK = "enable_fallback"; public static final String ENABLE_FALLBACK = "enable_fallback";
public static final String ENABLE_DEBUG_PREVIEW = "enable_debug_preview"; public static final String ENABLE_DEBUG_PREVIEW = "enable_debug_preview";
public static final String ENABLE_REQUEST_SDR_TONE_MAPPING = "enable_request_sdr_tone_mapping"; public static final String ABORT_SLOW_EXPORT = "abort_slow_export";
public static final String FORCE_INTERPRET_HDR_VIDEO_AS_SDR = "force_interpret_hdr_video_as_sdr"; public static final String HDR_MODE = "hdr_mode";
public static final String ENABLE_HDR_EDITING = "enable_hdr_editing"; public static final String AUDIO_EFFECTS_SELECTIONS = "audio_effects_selections";
public static final String DEMO_EFFECTS_SELECTIONS = "demo_effects_selections"; public static final String VIDEO_EFFECTS_SELECTIONS = "video_effects_selections";
public static final String PERIODIC_VIGNETTE_CENTER_X = "periodic_vignette_center_x"; public static final String PERIODIC_VIGNETTE_CENTER_X = "periodic_vignette_center_x";
public static final String PERIODIC_VIGNETTE_CENTER_Y = "periodic_vignette_center_y"; public static final String PERIODIC_VIGNETTE_CENTER_Y = "periodic_vignette_center_y";
public static final String PERIODIC_VIGNETTE_INNER_RADIUS = "periodic_vignette_inner_radius"; public static final String PERIODIC_VIGNETTE_INNER_RADIUS = "periodic_vignette_inner_radius";
@ -83,9 +89,37 @@ public final class ConfigurationActivity extends AppCompatActivity {
public static final String HSL_ADJUSTMENTS_HUE = "hsl_adjustments_hue"; public static final String HSL_ADJUSTMENTS_HUE = "hsl_adjustments_hue";
public static final String HSL_ADJUSTMENTS_SATURATION = "hsl_adjustments_saturation"; public static final String HSL_ADJUSTMENTS_SATURATION = "hsl_adjustments_saturation";
public static final String HSL_ADJUSTMENTS_LIGHTNESS = "hsl_adjustments_lightness"; public static final String HSL_ADJUSTMENTS_LIGHTNESS = "hsl_adjustments_lightness";
public static final String BITMAP_OVERLAY_URI = "bitmap_overlay_uri";
public static final String BITMAP_OVERLAY_ALPHA = "bitmap_overlay_alpha";
public static final String TEXT_OVERLAY_TEXT = "text_overlay_text";
public static final String TEXT_OVERLAY_TEXT_COLOR = "text_overlay_text_color";
public static final String TEXT_OVERLAY_ALPHA = "text_overlay_alpha";
// Video effect selections.
public static final int DIZZY_CROP_INDEX = 0;
public static final int EDGE_DETECTOR_INDEX = 1;
public static final int COLOR_FILTERS_INDEX = 2;
public static final int MAP_WHITE_TO_GREEN_LUT_INDEX = 3;
public static final int RGB_ADJUSTMENTS_INDEX = 4;
public static final int HSL_ADJUSTMENT_INDEX = 5;
public static final int CONTRAST_INDEX = 6;
public static final int PERIODIC_VIGNETTE_INDEX = 7;
public static final int SPIN_3D_INDEX = 8;
public static final int ZOOM_IN_INDEX = 9;
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;
// Audio effect selections.
public static final int HIGH_PITCHED_INDEX = 0;
public static final int SAMPLE_RATE_INDEX = 1;
public static final int SKIP_SILENCE_INDEX = 2;
// Color filter options.
public static final int COLOR_FILTER_GRAYSCALE = 0; public static final int COLOR_FILTER_GRAYSCALE = 0;
public static final int COLOR_FILTER_INVERTED = 1; public static final int COLOR_FILTER_INVERTED = 1;
public static final int COLOR_FILTER_SEPIA = 2; public static final int COLOR_FILTER_SEPIA = 2;
public static final int FILE_PERMISSION_REQUEST_CODE = 1; public static final int FILE_PERMISSION_REQUEST_CODE = 1;
private static final String[] PRESET_FILE_URIS = { private static final String[] PRESET_FILE_URIS = {
"https://storage.googleapis.com/exoplayer-test-media-1/mp4/android-screens-10s.mp4", "https://storage.googleapis.com/exoplayer-test-media-1/mp4/android-screens-10s.mp4",
@ -98,9 +132,13 @@ public final class ConfigurationActivity extends AppCompatActivity {
"https://storage.googleapis.com/exoplayer-test-media-0/BigBuckBunny_320x180.mp4", "https://storage.googleapis.com/exoplayer-test-media-0/BigBuckBunny_320x180.mp4",
"https://storage.googleapis.com/exoplayer-test-media-1/mp4/portrait_avc_aac.mp4", "https://storage.googleapis.com/exoplayer-test-media-1/mp4/portrait_avc_aac.mp4",
"https://storage.googleapis.com/exoplayer-test-media-1/mp4/portrait_rotated_avc_aac.mp4", "https://storage.googleapis.com/exoplayer-test-media-1/mp4/portrait_rotated_avc_aac.mp4",
"https://storage.googleapis.com/exoplayer-test-media-1/jpg/london.jpg",
"https://storage.googleapis.com/exoplayer-test-media-1/jpg/tokyo.jpg",
"https://storage.googleapis.com/exoplayer-test-media-1/mp4/slow-motion/slowMotion_stopwatch_240fps_long.mp4", "https://storage.googleapis.com/exoplayer-test-media-1/mp4/slow-motion/slowMotion_stopwatch_240fps_long.mp4",
"https://storage.googleapis.com/exoplayer-test-media-1/gen/screens/dash-vod-single-segment/manifest-baseline.mpd", "https://storage.googleapis.com/exoplayer-test-media-1/gen/screens/dash-vod-single-segment/manifest-baseline.mpd",
"https://storage.googleapis.com/exoplayer-test-media-1/mp4/samsung-s21-hdr-hdr10.mp4", "https://storage.googleapis.com/exoplayer-test-media-1/mp4/samsung-s21-hdr-hdr10.mp4",
"https://storage.googleapis.com/exoplayer-test-media-1/mp4/Pixel7Pro_HLG_1080P.mp4",
"https://storage.googleapis.com/exoplayer-test-media-1/mp4/sample_video_track_only.mp4",
}; };
private static final String[] PRESET_FILE_URI_DESCRIPTIONS = { // same order as PRESET_FILE_URIS private static final String[] PRESET_FILE_URI_DESCRIPTIONS = { // same order as PRESET_FILE_URIS
"720p H264 video and AAC audio", "720p H264 video and AAC audio",
@ -111,13 +149,20 @@ public final class ConfigurationActivity extends AppCompatActivity {
"8k H265 video and AAC audio", "8k H265 video and AAC audio",
"Short 1080p H265 video and AAC audio", "Short 1080p H265 video and AAC audio",
"Long 180p H264 video and AAC audio", "Long 180p H264 video and AAC audio",
"H264 video and AAC audio (portrait, H > W, 0\u00B0)", "H264 video and AAC audio (portrait, H > W, 0°)",
"H264 video and AAC audio (portrait, H < W, 90\u00B0)", "H264 video and AAC audio (portrait, H < W, 90°)",
"London JPG image (Plays for 5secs at 30fps)",
"Tokyo JPG image (Portrait, Plays for 5secs at 30fps)",
"SEF slow motion with 240 fps", "SEF slow motion with 240 fps",
"480p DASH (non-square pixels)", "480p DASH (non-square pixels)",
"HDR (HDR10) H265 limited range video (encoding may fail)", "HDR (HDR10) H265 limited range video (encoding may fail)",
"HDR (HLG) H265 limited range video (encoding may fail)",
"720p H264 video with no audio",
}; };
private static final String[] DEMO_EFFECTS = { private static final String[] AUDIO_EFFECTS = {
"High pitched", "Sample rate of 48000Hz", "Skip silence"
};
private static final String[] VIDEO_EFFECTS = {
"Dizzy crop", "Dizzy crop",
"Edge detector (Media Pipe)", "Edge detector (Media Pipe)",
"Color filters", "Color filters",
@ -127,24 +172,52 @@ public final class ConfigurationActivity extends AppCompatActivity {
"Contrast", "Contrast",
"Periodic vignette", "Periodic vignette",
"3D spin", "3D spin",
"Overlay logo & timer",
"Zoom in start", "Zoom in start",
"Overlay logo & timer",
"Custom Bitmap Overlay",
"Custom Text Overlay",
}; };
private static final int COLOR_FILTERS_INDEX = 2; private static final ImmutableMap<String, @TransformationRequest.HdrMode Integer>
private static final int RGB_ADJUSTMENTS_INDEX = 4; HDR_MODE_DESCRIPTIONS =
private static final int HSL_ADJUSTMENT_INDEX = 5; new ImmutableMap.Builder<String, @TransformationRequest.HdrMode Integer>()
private static final int CONTRAST_INDEX = 6; .put("Keep HDR", TransformationRequest.HDR_MODE_KEEP_HDR)
private static final int PERIODIC_VIGNETTE_INDEX = 7; .put(
"MediaCodec tone-map HDR to SDR",
TransformationRequest.HDR_MODE_TONE_MAP_HDR_TO_SDR_USING_MEDIACODEC)
.put(
"OpenGL tone-map HDR to SDR",
TransformationRequest.HDR_MODE_TONE_MAP_HDR_TO_SDR_USING_OPEN_GL)
.put(
"Force Interpret HDR as SDR",
TransformationRequest.HDR_MODE_EXPERIMENTAL_FORCE_INTERPRET_HDR_AS_SDR)
.build();
private static final ImmutableMap<String, Integer> OVERLAY_COLORS =
new ImmutableMap.Builder<String, Integer>()
.put("BLACK", Color.BLACK)
.put("BLUE", Color.BLUE)
.put("CYAN", Color.CYAN)
.put("DKGRAY", Color.DKGRAY)
.put("GRAY", Color.GRAY)
.put("GREEN", Color.GREEN)
.put("LTGRAY", Color.LTGRAY)
.put("MAGENTA", Color.MAGENTA)
.put("RED", Color.RED)
.put("WHITE", Color.WHITE)
.put("YELLOW", Color.YELLOW)
.build();
private static final String SAME_AS_INPUT_OPTION = "same as input"; private static final String SAME_AS_INPUT_OPTION = "same as input";
private static final float HALF_DIAGONAL = 1f / (float) Math.sqrt(2); private static final float HALF_DIAGONAL = 1f / (float) Math.sqrt(2);
private @MonotonicNonNull ActivityResultLauncher<Intent> localFilePickerLauncher; private @MonotonicNonNull Runnable onPermissionsGranted;
private @MonotonicNonNull ActivityResultLauncher<Intent> videoLocalFilePickerLauncher;
private @MonotonicNonNull ActivityResultLauncher<Intent> overlayLocalFilePickerLauncher;
private @MonotonicNonNull Button selectPresetFileButton; private @MonotonicNonNull Button selectPresetFileButton;
private @MonotonicNonNull Button selectLocalFileButton; private @MonotonicNonNull Button selectLocalFileButton;
private @MonotonicNonNull TextView selectedFileTextView; private @MonotonicNonNull TextView selectedFileTextView;
private @MonotonicNonNull CheckBox removeAudioCheckbox; private @MonotonicNonNull CheckBox removeAudioCheckbox;
private @MonotonicNonNull CheckBox removeVideoCheckbox; private @MonotonicNonNull CheckBox removeVideoCheckbox;
private @MonotonicNonNull CheckBox flattenForSlowMotionCheckbox; private @MonotonicNonNull CheckBox flattenForSlowMotionCheckbox;
private @MonotonicNonNull CheckBox forceAudioTrackCheckbox;
private @MonotonicNonNull Spinner audioMimeSpinner; private @MonotonicNonNull Spinner audioMimeSpinner;
private @MonotonicNonNull Spinner videoMimeSpinner; private @MonotonicNonNull Spinner videoMimeSpinner;
private @MonotonicNonNull Spinner resolutionHeightSpinner; private @MonotonicNonNull Spinner resolutionHeightSpinner;
@ -153,11 +226,12 @@ public final class ConfigurationActivity extends AppCompatActivity {
private @MonotonicNonNull CheckBox trimCheckBox; private @MonotonicNonNull CheckBox trimCheckBox;
private @MonotonicNonNull CheckBox enableFallbackCheckBox; private @MonotonicNonNull CheckBox enableFallbackCheckBox;
private @MonotonicNonNull CheckBox enableDebugPreviewCheckBox; private @MonotonicNonNull CheckBox enableDebugPreviewCheckBox;
private @MonotonicNonNull CheckBox enableRequestSdrToneMappingCheckBox; private @MonotonicNonNull CheckBox abortSlowExportCheckBox;
private @MonotonicNonNull CheckBox forceInterpretHdrVideoAsSdrCheckBox; private @MonotonicNonNull Spinner hdrModeSpinner;
private @MonotonicNonNull CheckBox enableHdrEditingCheckBox; private @MonotonicNonNull Button selectAudioEffectsButton;
private @MonotonicNonNull Button selectDemoEffectsButton; private @MonotonicNonNull Button selectVideoEffectsButton;
private boolean @MonotonicNonNull [] demoEffectsSelections; private boolean @MonotonicNonNull [] audioEffectsSelections;
private boolean @MonotonicNonNull [] videoEffectsSelections;
private @Nullable Uri localFileUri; private @Nullable Uri localFileUri;
private int inputUriPosition; private int inputUriPosition;
private long trimStartMs; private long trimStartMs;
@ -174,15 +248,36 @@ public final class ConfigurationActivity extends AppCompatActivity {
private float periodicVignetteCenterY; private float periodicVignetteCenterY;
private float periodicVignetteInnerRadius; private float periodicVignetteInnerRadius;
private float periodicVignetteOuterRadius; private float periodicVignetteOuterRadius;
private @MonotonicNonNull String bitmapOverlayUri;
private float bitmapOverlayAlpha;
private @MonotonicNonNull String textOverlayText;
private int textOverlayTextColor;
private float textOverlayAlpha;
@Override @Override
protected void onCreate(@Nullable Bundle savedInstanceState) { protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
setContentView(R.layout.configuration_activity); setContentView(R.layout.configuration_activity);
findViewById(R.id.transform_button).setOnClickListener(this::startTransformation); findViewById(R.id.export_button).setOnClickListener(this::startExport);
flattenForSlowMotionCheckbox = findViewById(R.id.flatten_for_slow_motion_checkbox); videoLocalFilePickerLauncher =
registerForActivityResult(
new ActivityResultContracts.StartActivityForResult(),
this::videoLocalFilePickerLauncherResult);
overlayLocalFilePickerLauncher =
registerForActivityResult(
new ActivityResultContracts.StartActivityForResult(),
this::overlayLocalFilePickerLauncherResult);
selectPresetFileButton = findViewById(R.id.select_preset_file_button);
selectPresetFileButton.setOnClickListener(this::selectPresetFile);
selectLocalFileButton = findViewById(R.id.select_local_file_button);
selectLocalFileButton.setOnClickListener(
view ->
selectLocalFile(
view, checkNotNull(videoLocalFilePickerLauncher), /* mimeType= */ "video/*"));
selectedFileTextView = findViewById(R.id.selected_file_text_view); selectedFileTextView = findViewById(R.id.selected_file_text_view);
selectedFileTextView.setText(PRESET_FILE_URI_DESCRIPTIONS[inputUriPosition]); selectedFileTextView.setText(PRESET_FILE_URI_DESCRIPTIONS[inputUriPosition]);
@ -193,11 +288,9 @@ public final class ConfigurationActivity extends AppCompatActivity {
removeVideoCheckbox = findViewById(R.id.remove_video_checkbox); removeVideoCheckbox = findViewById(R.id.remove_video_checkbox);
removeVideoCheckbox.setOnClickListener(this::onRemoveVideo); removeVideoCheckbox.setOnClickListener(this::onRemoveVideo);
selectPresetFileButton = findViewById(R.id.select_preset_file_button); flattenForSlowMotionCheckbox = findViewById(R.id.flatten_for_slow_motion_checkbox);
selectPresetFileButton.setOnClickListener(this::selectPresetFile);
selectLocalFileButton = findViewById(R.id.select_local_file_button); forceAudioTrackCheckbox = findViewById(R.id.force_audio_track_checkbox);
selectLocalFileButton.setOnClickListener(this::selectLocalFile);
ArrayAdapter<String> audioMimeAdapter = ArrayAdapter<String> audioMimeAdapter =
new ArrayAdapter<>(/* context= */ this, R.layout.spinner_item); new ArrayAdapter<>(/* context= */ this, R.layout.spinner_item);
@ -214,7 +307,7 @@ public final class ConfigurationActivity extends AppCompatActivity {
videoMimeSpinner.setAdapter(videoMimeAdapter); videoMimeSpinner.setAdapter(videoMimeAdapter);
videoMimeAdapter.addAll( videoMimeAdapter.addAll(
SAME_AS_INPUT_OPTION, MimeTypes.VIDEO_H263, MimeTypes.VIDEO_H264, MimeTypes.VIDEO_MP4V); SAME_AS_INPUT_OPTION, MimeTypes.VIDEO_H263, MimeTypes.VIDEO_H264, MimeTypes.VIDEO_MP4V);
if (Util.SDK_INT >= 24) { if (SDK_INT >= 24) {
videoMimeAdapter.add(MimeTypes.VIDEO_H265); videoMimeAdapter.add(MimeTypes.VIDEO_H265);
} }
@ -247,21 +340,23 @@ public final class ConfigurationActivity extends AppCompatActivity {
enableFallbackCheckBox = findViewById(R.id.enable_fallback_checkbox); enableFallbackCheckBox = findViewById(R.id.enable_fallback_checkbox);
enableDebugPreviewCheckBox = findViewById(R.id.enable_debug_preview_checkbox); enableDebugPreviewCheckBox = findViewById(R.id.enable_debug_preview_checkbox);
enableRequestSdrToneMappingCheckBox = findViewById(R.id.request_sdr_tone_mapping_checkbox);
enableRequestSdrToneMappingCheckBox.setEnabled(isRequestSdrToneMappingSupported());
findViewById(R.id.request_sdr_tone_mapping).setEnabled(isRequestSdrToneMappingSupported());
forceInterpretHdrVideoAsSdrCheckBox =
findViewById(R.id.force_interpret_hdr_video_as_sdr_checkbox);
enableHdrEditingCheckBox = findViewById(R.id.hdr_editing_checkbox);
demoEffectsSelections = new boolean[DEMO_EFFECTS.length]; abortSlowExportCheckBox = findViewById(R.id.abort_slow_export_checkbox);
selectDemoEffectsButton = findViewById(R.id.select_demo_effects_button);
selectDemoEffectsButton.setOnClickListener(this::selectDemoEffects);
localFilePickerLauncher = ArrayAdapter<String> hdrModeAdapter =
registerForActivityResult( new ArrayAdapter<>(/* context= */ this, R.layout.spinner_item);
new ActivityResultContracts.StartActivityForResult(), hdrModeAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
this::localFilePickerLauncherResult); hdrModeSpinner = findViewById(R.id.hdr_mode_spinner);
hdrModeSpinner.setAdapter(hdrModeAdapter);
hdrModeAdapter.addAll(HDR_MODE_DESCRIPTIONS.keySet());
audioEffectsSelections = new boolean[AUDIO_EFFECTS.length];
selectAudioEffectsButton = findViewById(R.id.select_audio_effects_button);
selectAudioEffectsButton.setOnClickListener(this::selectAudioEffects);
videoEffectsSelections = new boolean[VIDEO_EFFECTS.length];
selectVideoEffectsButton = findViewById(R.id.select_video_effects_button);
selectVideoEffectsButton.setOnClickListener(this::selectVideoEffects);
} }
@Override @Override
@ -272,7 +367,7 @@ public final class ConfigurationActivity extends AppCompatActivity {
if (requestCode == FILE_PERMISSION_REQUEST_CODE if (requestCode == FILE_PERMISSION_REQUEST_CODE
&& grantResults.length == 1 && grantResults.length == 1
&& grantResults[0] == PackageManager.PERMISSION_GRANTED) { && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
launchLocalFilePicker(); checkNotNull(onPermissionsGranted).run();
} else { } else {
Toast.makeText( Toast.makeText(
getApplicationContext(), getString(R.string.permission_denied), Toast.LENGTH_LONG) getApplicationContext(), getString(R.string.permission_denied), Toast.LENGTH_LONG)
@ -301,6 +396,7 @@ public final class ConfigurationActivity extends AppCompatActivity {
"removeAudioCheckbox", "removeAudioCheckbox",
"removeVideoCheckbox", "removeVideoCheckbox",
"flattenForSlowMotionCheckbox", "flattenForSlowMotionCheckbox",
"forceAudioTrackCheckbox",
"audioMimeSpinner", "audioMimeSpinner",
"videoMimeSpinner", "videoMimeSpinner",
"resolutionHeightSpinner", "resolutionHeightSpinner",
@ -309,17 +405,18 @@ public final class ConfigurationActivity extends AppCompatActivity {
"trimCheckBox", "trimCheckBox",
"enableFallbackCheckBox", "enableFallbackCheckBox",
"enableDebugPreviewCheckBox", "enableDebugPreviewCheckBox",
"enableRequestSdrToneMappingCheckBox", "abortSlowExportCheckBox",
"forceInterpretHdrVideoAsSdrCheckBox", "hdrModeSpinner",
"enableHdrEditingCheckBox", "audioEffectsSelections",
"demoEffectsSelections" "videoEffectsSelections"
}) })
private void startTransformation(View view) { private void startExport(View view) {
Intent transformerIntent = new Intent(/* packageContext= */ this, TransformerActivity.class); Intent transformerIntent = new Intent(/* packageContext= */ this, TransformerActivity.class);
Bundle bundle = new Bundle(); Bundle bundle = new Bundle();
bundle.putBoolean(SHOULD_REMOVE_AUDIO, removeAudioCheckbox.isChecked()); bundle.putBoolean(SHOULD_REMOVE_AUDIO, removeAudioCheckbox.isChecked());
bundle.putBoolean(SHOULD_REMOVE_VIDEO, removeVideoCheckbox.isChecked()); bundle.putBoolean(SHOULD_REMOVE_VIDEO, removeVideoCheckbox.isChecked());
bundle.putBoolean(SHOULD_FLATTEN_FOR_SLOW_MOTION, flattenForSlowMotionCheckbox.isChecked()); bundle.putBoolean(SHOULD_FLATTEN_FOR_SLOW_MOTION, flattenForSlowMotionCheckbox.isChecked());
bundle.putBoolean(FORCE_AUDIO_TRACK, forceAudioTrackCheckbox.isChecked());
String selectedAudioMimeType = String.valueOf(audioMimeSpinner.getSelectedItem()); String selectedAudioMimeType = String.valueOf(audioMimeSpinner.getSelectedItem());
if (!SAME_AS_INPUT_OPTION.equals(selectedAudioMimeType)) { if (!SAME_AS_INPUT_OPTION.equals(selectedAudioMimeType)) {
bundle.putString(AUDIO_MIME_TYPE, selectedAudioMimeType); bundle.putString(AUDIO_MIME_TYPE, selectedAudioMimeType);
@ -349,12 +446,11 @@ public final class ConfigurationActivity extends AppCompatActivity {
} }
bundle.putBoolean(ENABLE_FALLBACK, enableFallbackCheckBox.isChecked()); bundle.putBoolean(ENABLE_FALLBACK, enableFallbackCheckBox.isChecked());
bundle.putBoolean(ENABLE_DEBUG_PREVIEW, enableDebugPreviewCheckBox.isChecked()); bundle.putBoolean(ENABLE_DEBUG_PREVIEW, enableDebugPreviewCheckBox.isChecked());
bundle.putBoolean( bundle.putBoolean(ABORT_SLOW_EXPORT, abortSlowExportCheckBox.isChecked());
ENABLE_REQUEST_SDR_TONE_MAPPING, enableRequestSdrToneMappingCheckBox.isChecked()); String selectedhdrMode = String.valueOf(hdrModeSpinner.getSelectedItem());
bundle.putBoolean( bundle.putInt(HDR_MODE, checkNotNull(HDR_MODE_DESCRIPTIONS.get(selectedhdrMode)));
FORCE_INTERPRET_HDR_VIDEO_AS_SDR, forceInterpretHdrVideoAsSdrCheckBox.isChecked()); bundle.putBooleanArray(AUDIO_EFFECTS_SELECTIONS, audioEffectsSelections);
bundle.putBoolean(ENABLE_HDR_EDITING, enableHdrEditingCheckBox.isChecked()); bundle.putBooleanArray(VIDEO_EFFECTS_SELECTIONS, videoEffectsSelections);
bundle.putBooleanArray(DEMO_EFFECTS_SELECTIONS, demoEffectsSelections);
bundle.putInt(COLOR_FILTER_SELECTION, colorFilterSelection); bundle.putInt(COLOR_FILTER_SELECTION, colorFilterSelection);
bundle.putFloat(CONTRAST_VALUE, contrastValue); bundle.putFloat(CONTRAST_VALUE, contrastValue);
bundle.putFloat(RGB_ADJUSTMENT_RED_SCALE, rgbAdjustmentRedScale); bundle.putFloat(RGB_ADJUSTMENT_RED_SCALE, rgbAdjustmentRedScale);
@ -367,6 +463,11 @@ public final class ConfigurationActivity extends AppCompatActivity {
bundle.putFloat(PERIODIC_VIGNETTE_CENTER_Y, periodicVignetteCenterY); bundle.putFloat(PERIODIC_VIGNETTE_CENTER_Y, periodicVignetteCenterY);
bundle.putFloat(PERIODIC_VIGNETTE_INNER_RADIUS, periodicVignetteInnerRadius); bundle.putFloat(PERIODIC_VIGNETTE_INNER_RADIUS, periodicVignetteInnerRadius);
bundle.putFloat(PERIODIC_VIGNETTE_OUTER_RADIUS, periodicVignetteOuterRadius); bundle.putFloat(PERIODIC_VIGNETTE_OUTER_RADIUS, periodicVignetteOuterRadius);
bundle.putString(BITMAP_OVERLAY_URI, bitmapOverlayUri);
bundle.putFloat(BITMAP_OVERLAY_ALPHA, bitmapOverlayAlpha);
bundle.putString(TEXT_OVERLAY_TEXT, textOverlayText);
bundle.putInt(TEXT_OVERLAY_TEXT_COLOR, textOverlayTextColor);
bundle.putFloat(TEXT_OVERLAY_ALPHA, textOverlayAlpha);
transformerIntent.putExtras(bundle); transformerIntent.putExtras(bundle);
@Nullable Uri intentUri; @Nullable Uri intentUri;
@ -399,39 +500,69 @@ public final class ConfigurationActivity extends AppCompatActivity {
selectedFileTextView.setText(PRESET_FILE_URI_DESCRIPTIONS[inputUriPosition]); selectedFileTextView.setText(PRESET_FILE_URI_DESCRIPTIONS[inputUriPosition]);
} }
private void selectLocalFile(View view) { private void selectLocalFile(
int permissionStatus = View view, ActivityResultLauncher<Intent> localFilePickerLauncher, String mimeType) {
ActivityCompat.checkSelfPermission( String permission = SDK_INT >= 33 ? READ_MEDIA_VIDEO : READ_EXTERNAL_STORAGE;
ConfigurationActivity.this, Manifest.permission.READ_EXTERNAL_STORAGE); if (ActivityCompat.checkSelfPermission(/* context= */ this, permission)
if (permissionStatus != PackageManager.PERMISSION_GRANTED) { != PackageManager.PERMISSION_GRANTED) {
String[] neededPermissions = {Manifest.permission.READ_EXTERNAL_STORAGE}; onPermissionsGranted = () -> launchLocalFilePicker(localFilePickerLauncher, mimeType);
ActivityCompat.requestPermissions( ActivityCompat.requestPermissions(
ConfigurationActivity.this, neededPermissions, FILE_PERMISSION_REQUEST_CODE); /* activity= */ this, new String[] {permission}, FILE_PERMISSION_REQUEST_CODE);
} else { } else {
launchLocalFilePicker(); launchLocalFilePicker(localFilePickerLauncher, mimeType);
} }
} }
private void launchLocalFilePicker() { private void launchLocalFilePicker(
ActivityResultLauncher<Intent> localFilePickerLauncher, String mimeType) {
Intent intent = new Intent(Intent.ACTION_GET_CONTENT); Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
intent.setType("video/*"); intent.setType(mimeType);
checkNotNull(localFilePickerLauncher).launch(intent); checkNotNull(localFilePickerLauncher).launch(intent);
} }
@RequiresNonNull("selectedFileTextView") @RequiresNonNull("selectedFileTextView")
private void localFilePickerLauncherResult(ActivityResult result) { private void videoLocalFilePickerLauncherResult(ActivityResult result) {
Intent data = result.getData(); Intent data = result.getData();
if (data != null) { if (data != null) {
localFileUri = checkNotNull(data.getData()); localFileUri = checkNotNull(data.getData());
selectedFileTextView.setText(localFileUri.toString()); selectedFileTextView.setText(localFileUri.toString());
} else {
Toast.makeText(
getApplicationContext(),
getString(R.string.local_file_picker_failed),
Toast.LENGTH_SHORT)
.show();
} }
} }
private void selectDemoEffects(View view) { private void overlayLocalFilePickerLauncherResult(ActivityResult result) {
Intent data = result.getData();
if (data != null) {
bitmapOverlayUri = checkNotNull(data.getData()).toString();
} else {
Toast.makeText(
getApplicationContext(),
getString(R.string.local_file_picker_failed),
Toast.LENGTH_SHORT)
.show();
}
}
private void selectAudioEffects(View view) {
new AlertDialog.Builder(/* context= */ this) new AlertDialog.Builder(/* context= */ this)
.setTitle(R.string.select_demo_effects) .setTitle(R.string.select_audio_effects)
.setMultiChoiceItems( .setMultiChoiceItems(
DEMO_EFFECTS, checkNotNull(demoEffectsSelections), this::selectDemoEffect) AUDIO_EFFECTS, checkNotNull(audioEffectsSelections), this::selectAudioEffect)
.setPositiveButton(android.R.string.ok, /* listener= */ null)
.create()
.show();
}
private void selectVideoEffects(View view) {
new AlertDialog.Builder(/* context= */ this)
.setTitle(R.string.select_video_effects)
.setMultiChoiceItems(
VIDEO_EFFECTS, checkNotNull(videoEffectsSelections), this::selectVideoEffect)
.setPositiveButton(android.R.string.ok, /* listener= */ null) .setPositiveButton(android.R.string.ok, /* listener= */ null)
.create() .create()
.show(); .show();
@ -444,7 +575,7 @@ public final class ConfigurationActivity extends AppCompatActivity {
View dialogView = getLayoutInflater().inflate(R.layout.trim_options, /* root= */ null); View dialogView = getLayoutInflater().inflate(R.layout.trim_options, /* root= */ null);
RangeSlider trimRangeSlider = RangeSlider trimRangeSlider =
checkNotNull(dialogView.findViewById(R.id.trim_bounds_range_slider)); checkNotNull(dialogView.findViewById(R.id.trim_bounds_range_slider));
trimRangeSlider.setValues(0f, 10f); // seconds trimRangeSlider.setValues(0f, 1f); // seconds
new AlertDialog.Builder(/* context= */ this) new AlertDialog.Builder(/* context= */ this)
.setView(dialogView) .setView(dialogView)
.setPositiveButton( .setPositiveButton(
@ -458,9 +589,14 @@ public final class ConfigurationActivity extends AppCompatActivity {
.show(); .show();
} }
@RequiresNonNull("demoEffectsSelections") @RequiresNonNull("audioEffectsSelections")
private void selectDemoEffect(DialogInterface dialog, int which, boolean isChecked) { private void selectAudioEffect(DialogInterface dialog, int which, boolean isChecked) {
demoEffectsSelections[which] = isChecked; audioEffectsSelections[which] = isChecked;
}
@RequiresNonNull("videoEffectsSelections")
private void selectVideoEffect(DialogInterface dialog, int which, boolean isChecked) {
videoEffectsSelections[which] = isChecked;
if (!isChecked) { if (!isChecked) {
return; return;
} }
@ -481,6 +617,12 @@ public final class ConfigurationActivity extends AppCompatActivity {
case PERIODIC_VIGNETTE_INDEX: case PERIODIC_VIGNETTE_INDEX:
controlPeriodicVignetteSettings(); controlPeriodicVignetteSettings();
break; break;
case BITMAP_OVERLAY_INDEX:
controlBitmapOverlaySettings();
break;
case TEXT_OVERLAY_INDEX:
controlTextOverlaySettings();
break;
} }
} }
@ -583,18 +725,66 @@ public final class ConfigurationActivity extends AppCompatActivity {
.show(); .show();
} }
private void controlBitmapOverlaySettings() {
View dialogView =
getLayoutInflater().inflate(R.layout.bitmap_overlay_options, /* root= */ null);
Button uriButton = checkNotNull(dialogView.findViewById(R.id.bitmap_overlay_uri));
uriButton.setOnClickListener(
(view ->
selectLocalFile(
view, checkNotNull(overlayLocalFilePickerLauncher), /* mimeType= */ "image/*")));
Slider alphaSlider = checkNotNull(dialogView.findViewById(R.id.bitmap_overlay_alpha_slider));
new AlertDialog.Builder(/* context= */ this)
.setTitle(R.string.bitmap_overlay_settings)
.setView(dialogView)
.setPositiveButton(
android.R.string.ok,
(DialogInterface dialogInterface, int i) -> {
bitmapOverlayAlpha = alphaSlider.getValue();
})
.create()
.show();
}
private void controlTextOverlaySettings() {
View dialogView = getLayoutInflater().inflate(R.layout.text_overlay_options, /* root= */ null);
EditText textEditText = checkNotNull(dialogView.findViewById(R.id.text_overlay_text));
ArrayAdapter<String> textColorAdapter =
new ArrayAdapter<>(/* context= */ this, R.layout.spinner_item);
textColorAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
Spinner textColorSpinner = checkNotNull(dialogView.findViewById(R.id.text_overlay_text_color));
textColorSpinner.setAdapter(textColorAdapter);
textColorAdapter.addAll(OVERLAY_COLORS.keySet());
Slider alphaSlider = checkNotNull(dialogView.findViewById(R.id.text_overlay_alpha_slider));
new AlertDialog.Builder(/* context= */ this)
.setTitle(R.string.bitmap_overlay_settings)
.setView(dialogView)
.setPositiveButton(
android.R.string.ok,
(DialogInterface dialogInterface, int i) -> {
textOverlayText = textEditText.getText().toString();
String selectedTextColor = String.valueOf(textColorSpinner.getSelectedItem());
textOverlayTextColor = checkNotNull(OVERLAY_COLORS.get(selectedTextColor));
textOverlayAlpha = alphaSlider.getValue();
})
.create()
.show();
}
@RequiresNonNull({ @RequiresNonNull({
"removeVideoCheckbox", "removeVideoCheckbox",
"forceAudioTrackCheckbox",
"audioMimeSpinner", "audioMimeSpinner",
"videoMimeSpinner", "videoMimeSpinner",
"resolutionHeightSpinner", "resolutionHeightSpinner",
"scaleSpinner", "scaleSpinner",
"rotateSpinner", "rotateSpinner",
"enableDebugPreviewCheckBox", "enableDebugPreviewCheckBox",
"enableRequestSdrToneMappingCheckBox", "hdrModeSpinner",
"forceInterpretHdrVideoAsSdrCheckBox", "selectAudioEffectsButton",
"enableHdrEditingCheckBox", "selectVideoEffectsButton"
"selectDemoEffectsButton"
}) })
private void onRemoveAudio(View view) { private void onRemoveAudio(View view) {
if (((CheckBox) view).isChecked()) { if (((CheckBox) view).isChecked()) {
@ -607,16 +797,16 @@ public final class ConfigurationActivity extends AppCompatActivity {
@RequiresNonNull({ @RequiresNonNull({
"removeAudioCheckbox", "removeAudioCheckbox",
"forceAudioTrackCheckbox",
"audioMimeSpinner", "audioMimeSpinner",
"videoMimeSpinner", "videoMimeSpinner",
"resolutionHeightSpinner", "resolutionHeightSpinner",
"scaleSpinner", "scaleSpinner",
"rotateSpinner", "rotateSpinner",
"enableDebugPreviewCheckBox", "enableDebugPreviewCheckBox",
"enableRequestSdrToneMappingCheckBox", "hdrModeSpinner",
"forceInterpretHdrVideoAsSdrCheckBox", "selectAudioEffectsButton",
"enableHdrEditingCheckBox", "selectVideoEffectsButton"
"selectDemoEffectsButton"
}) })
private void onRemoveVideo(View view) { private void onRemoveVideo(View view) {
if (((CheckBox) view).isChecked()) { if (((CheckBox) view).isChecked()) {
@ -628,42 +818,34 @@ public final class ConfigurationActivity extends AppCompatActivity {
} }
@RequiresNonNull({ @RequiresNonNull({
"forceAudioTrackCheckbox",
"audioMimeSpinner", "audioMimeSpinner",
"videoMimeSpinner", "videoMimeSpinner",
"resolutionHeightSpinner", "resolutionHeightSpinner",
"scaleSpinner", "scaleSpinner",
"rotateSpinner", "rotateSpinner",
"enableDebugPreviewCheckBox", "enableDebugPreviewCheckBox",
"enableRequestSdrToneMappingCheckBox", "hdrModeSpinner",
"forceInterpretHdrVideoAsSdrCheckBox", "selectAudioEffectsButton",
"enableHdrEditingCheckBox", "selectVideoEffectsButton"
"selectDemoEffectsButton"
}) })
private void enableTrackSpecificOptions(boolean isAudioEnabled, boolean isVideoEnabled) { private void enableTrackSpecificOptions(boolean isAudioEnabled, boolean isVideoEnabled) {
forceAudioTrackCheckbox.setEnabled(isVideoEnabled);
audioMimeSpinner.setEnabled(isAudioEnabled); audioMimeSpinner.setEnabled(isAudioEnabled);
videoMimeSpinner.setEnabled(isVideoEnabled); videoMimeSpinner.setEnabled(isVideoEnabled);
resolutionHeightSpinner.setEnabled(isVideoEnabled); resolutionHeightSpinner.setEnabled(isVideoEnabled);
scaleSpinner.setEnabled(isVideoEnabled); scaleSpinner.setEnabled(isVideoEnabled);
rotateSpinner.setEnabled(isVideoEnabled); rotateSpinner.setEnabled(isVideoEnabled);
enableDebugPreviewCheckBox.setEnabled(isVideoEnabled); enableDebugPreviewCheckBox.setEnabled(isVideoEnabled);
enableRequestSdrToneMappingCheckBox.setEnabled( hdrModeSpinner.setEnabled(isVideoEnabled);
isRequestSdrToneMappingSupported() && isVideoEnabled); selectAudioEffectsButton.setEnabled(isAudioEnabled);
forceInterpretHdrVideoAsSdrCheckBox.setEnabled(isVideoEnabled); selectVideoEffectsButton.setEnabled(isVideoEnabled);
enableHdrEditingCheckBox.setEnabled(isVideoEnabled);
selectDemoEffectsButton.setEnabled(isVideoEnabled);
findViewById(R.id.audio_mime_text_view).setEnabled(isAudioEnabled); findViewById(R.id.audio_mime_text_view).setEnabled(isAudioEnabled);
findViewById(R.id.video_mime_text_view).setEnabled(isVideoEnabled); findViewById(R.id.video_mime_text_view).setEnabled(isVideoEnabled);
findViewById(R.id.resolution_height_text_view).setEnabled(isVideoEnabled); findViewById(R.id.resolution_height_text_view).setEnabled(isVideoEnabled);
findViewById(R.id.scale).setEnabled(isVideoEnabled); findViewById(R.id.scale).setEnabled(isVideoEnabled);
findViewById(R.id.rotate).setEnabled(isVideoEnabled); findViewById(R.id.rotate).setEnabled(isVideoEnabled);
findViewById(R.id.request_sdr_tone_mapping) findViewById(R.id.hdr_mode).setEnabled(isVideoEnabled);
.setEnabled(isRequestSdrToneMappingSupported() && isVideoEnabled);
findViewById(R.id.force_interpret_hdr_video_as_sdr).setEnabled(isVideoEnabled);
findViewById(R.id.hdr_editing).setEnabled(isVideoEnabled);
}
private static boolean isRequestSdrToneMappingSupported() {
return Util.SDK_INT >= 31;
} }
} }

View File

@ -19,18 +19,18 @@ import static androidx.media3.common.util.Assertions.checkArgument;
import android.content.Context; import android.content.Context;
import android.opengl.GLES20; import android.opengl.GLES20;
import android.util.Pair; import androidx.media3.common.VideoFrameProcessingException;
import androidx.media3.common.FrameProcessingException;
import androidx.media3.common.util.GlProgram; import androidx.media3.common.util.GlProgram;
import androidx.media3.common.util.GlUtil; import androidx.media3.common.util.GlUtil;
import androidx.media3.effect.SingleFrameGlTextureProcessor; import androidx.media3.common.util.Size;
import androidx.media3.effect.SingleFrameGlShaderProgram;
import java.io.IOException; import java.io.IOException;
/** /**
* A {@link SingleFrameGlTextureProcessor} that periodically dims the frames such that pixels are * A {@link SingleFrameGlShaderProgram} that periodically dims the frames such that pixels are
* darker the further they are away from the frame center. * darker the further they are away from the frame center.
*/ */
/* package */ final class PeriodicVignetteProcessor extends SingleFrameGlTextureProcessor { /* package */ final class PeriodicVignetteShaderProgram extends SingleFrameGlShaderProgram {
private static final String VERTEX_SHADER_PATH = "vertex_shader_copy_es2.glsl"; private static final String VERTEX_SHADER_PATH = "vertex_shader_copy_es2.glsl";
private static final String FRAGMENT_SHADER_PATH = "fragment_shader_vignette_es2.glsl"; private static final String FRAGMENT_SHADER_PATH = "fragment_shader_vignette_es2.glsl";
@ -59,9 +59,9 @@ import java.io.IOException;
* @param minInnerRadius The lower bound of the radius that is unaffected by the effect. * @param minInnerRadius The lower bound of the radius that is unaffected by the effect.
* @param maxInnerRadius The upper bound of the radius that is unaffected by the effect. * @param maxInnerRadius The upper bound of the radius that is unaffected by the effect.
* @param outerRadius The radius after which all pixels are black. * @param outerRadius The radius after which all pixels are black.
* @throws FrameProcessingException If a problem occurs while reading shader files. * @throws VideoFrameProcessingException If a problem occurs while reading shader files.
*/ */
public PeriodicVignetteProcessor( public PeriodicVignetteShaderProgram(
Context context, Context context,
boolean useHdr, boolean useHdr,
float centerX, float centerX,
@ -69,7 +69,7 @@ import java.io.IOException;
float minInnerRadius, float minInnerRadius,
float maxInnerRadius, float maxInnerRadius,
float outerRadius) float outerRadius)
throws FrameProcessingException { throws VideoFrameProcessingException {
super(useHdr); super(useHdr);
checkArgument(minInnerRadius <= maxInnerRadius); checkArgument(minInnerRadius <= maxInnerRadius);
checkArgument(maxInnerRadius <= outerRadius); checkArgument(maxInnerRadius <= outerRadius);
@ -78,7 +78,7 @@ import java.io.IOException;
try { try {
glProgram = new GlProgram(context, VERTEX_SHADER_PATH, FRAGMENT_SHADER_PATH); glProgram = new GlProgram(context, VERTEX_SHADER_PATH, FRAGMENT_SHADER_PATH);
} catch (IOException | GlUtil.GlException e) { } catch (IOException | GlUtil.GlException e) {
throw new FrameProcessingException(e); throw new VideoFrameProcessingException(e);
} }
glProgram.setFloatsUniform("uCenter", new float[] {centerX, centerY}); glProgram.setFloatsUniform("uCenter", new float[] {centerX, centerY});
glProgram.setFloatsUniform("uOuterRadius", new float[] {outerRadius}); glProgram.setFloatsUniform("uOuterRadius", new float[] {outerRadius});
@ -90,12 +90,13 @@ import java.io.IOException;
} }
@Override @Override
public Pair<Integer, Integer> configure(int inputWidth, int inputHeight) { public Size configure(int inputWidth, int inputHeight) {
return Pair.create(inputWidth, inputHeight); return new Size(inputWidth, inputHeight);
} }
@Override @Override
public void drawFrame(int inputTexId, long presentationTimeUs) throws FrameProcessingException { public void drawFrame(int inputTexId, long presentationTimeUs)
throws VideoFrameProcessingException {
try { try {
glProgram.use(); glProgram.use();
glProgram.setSamplerTexIdUniform("uTexSampler", inputTexId, /* texUnitIndex= */ 0); glProgram.setSamplerTexIdUniform("uTexSampler", inputTexId, /* texUnitIndex= */ 0);
@ -107,17 +108,17 @@ import java.io.IOException;
// The four-vertex triangle strip forms a quad. // The four-vertex triangle strip forms a quad.
GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, /* first= */ 0, /* count= */ 4); GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, /* first= */ 0, /* count= */ 4);
} catch (GlUtil.GlException e) { } catch (GlUtil.GlException e) {
throw new FrameProcessingException(e, presentationTimeUs); throw new VideoFrameProcessingException(e, presentationTimeUs);
} }
} }
@Override @Override
public void release() throws FrameProcessingException { public void release() throws VideoFrameProcessingException {
super.release(); super.release();
try { try {
glProgram.delete(); glProgram.delete();
} catch (GlUtil.GlException e) { } catch (GlUtil.GlException e) {
throw new FrameProcessingException(e); throw new VideoFrameProcessingException(e);
} }
} }
} }

View File

@ -0,0 +1,66 @@
/*
* Copyright 2022 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.graphics.Color;
import android.opengl.Matrix;
import android.text.Spannable;
import android.text.SpannableString;
import android.text.style.ForegroundColorSpan;
import androidx.media3.common.C;
import androidx.media3.common.util.GlUtil;
import androidx.media3.effect.OverlaySettings;
import androidx.media3.effect.TextOverlay;
import androidx.media3.effect.TextureOverlay;
import java.util.Locale;
/**
* A {@link TextureOverlay} that displays a "time elapsed" timer in the bottom left corner of the
* frame.
*/
/* package */ final class TimerOverlay extends TextOverlay {
private final OverlaySettings overlaySettings;
public TimerOverlay() {
float[] positioningMatrix = GlUtil.create4x4IdentityMatrix();
Matrix.translateM(
positioningMatrix, /* mOffset= */ 0, /* x= */ -0.7f, /* y= */ -0.95f, /* z= */ 1);
overlaySettings =
new OverlaySettings.Builder()
.setAnchor(/* x= */ -1f, /* y= */ -1f)
.setMatrix(positioningMatrix)
.build();
}
@Override
public SpannableString getText(long presentationTimeUs) {
SpannableString text =
new SpannableString(
String.format(Locale.US, "%.02f", presentationTimeUs / (float) C.MICROS_PER_SECOND));
text.setSpan(
new ForegroundColorSpan(Color.WHITE),
/* start= */ 0,
text.length(),
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
return text;
}
@Override
public OverlaySettings getOverlaySettings(long presentationTimeUs) {
return overlaySettings;
}
}

View File

@ -16,48 +16,78 @@
package androidx.media3.demo.transformer; package androidx.media3.demo.transformer;
import static android.Manifest.permission.READ_EXTERNAL_STORAGE; 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.checkNotNull;
import static androidx.media3.common.util.Assertions.checkState; import static androidx.media3.common.util.Assertions.checkState;
import static androidx.media3.common.util.Assertions.checkStateNotNull;
import static androidx.media3.common.util.Util.SDK_INT;
import static androidx.media3.transformer.Transformer.PROGRESS_STATE_NOT_STARTED;
import android.app.Activity; import android.app.Activity;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.content.pm.PackageManager; import android.content.pm.PackageManager;
import android.graphics.Bitmap;
import android.graphics.Color; import android.graphics.Color;
import android.graphics.drawable.Drawable;
import android.net.Uri; import android.net.Uri;
import android.opengl.Matrix;
import android.os.Bundle; import android.os.Bundle;
import android.os.Handler; import android.os.Handler;
import android.text.Spannable;
import android.text.SpannableString;
import android.text.style.ForegroundColorSpan;
import android.view.SurfaceHolder; import android.view.SurfaceHolder;
import android.view.SurfaceView; import android.view.SurfaceView;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.widget.Button; import android.widget.Button;
import android.widget.ImageView;
import android.widget.TextView; import android.widget.TextView;
import android.widget.Toast; import android.widget.Toast;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.annotation.StringRes; import androidx.annotation.StringRes;
import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.ActivityCompat;
import androidx.media3.common.C; import androidx.media3.common.C;
import androidx.media3.common.DebugViewProvider; import androidx.media3.common.DebugViewProvider;
import androidx.media3.common.Effect; import androidx.media3.common.Effect;
import androidx.media3.common.MediaItem; import androidx.media3.common.MediaItem;
import androidx.media3.common.audio.AudioProcessor;
import androidx.media3.common.audio.SonicAudioProcessor;
import androidx.media3.common.util.BitmapLoader;
import androidx.media3.common.util.GlUtil;
import androidx.media3.common.util.Log; import androidx.media3.common.util.Log;
import androidx.media3.common.util.Util; import androidx.media3.datasource.DataSourceBitmapLoader;
import androidx.media3.effect.BitmapOverlay;
import androidx.media3.effect.Contrast; import androidx.media3.effect.Contrast;
import androidx.media3.effect.DrawableOverlay;
import androidx.media3.effect.GlEffect; import androidx.media3.effect.GlEffect;
import androidx.media3.effect.GlTextureProcessor; import androidx.media3.effect.GlShaderProgram;
import androidx.media3.effect.HslAdjustment; import androidx.media3.effect.HslAdjustment;
import androidx.media3.effect.OverlayEffect;
import androidx.media3.effect.OverlaySettings;
import androidx.media3.effect.Presentation;
import androidx.media3.effect.RgbAdjustment; import androidx.media3.effect.RgbAdjustment;
import androidx.media3.effect.RgbFilter; import androidx.media3.effect.RgbFilter;
import androidx.media3.effect.RgbMatrix; import androidx.media3.effect.RgbMatrix;
import androidx.media3.effect.ScaleAndRotateTransformation;
import androidx.media3.effect.SingleColorLut; import androidx.media3.effect.SingleColorLut;
import androidx.media3.effect.TextOverlay;
import androidx.media3.effect.TextureOverlay;
import androidx.media3.exoplayer.ExoPlayer; import androidx.media3.exoplayer.ExoPlayer;
import androidx.media3.exoplayer.audio.SilenceSkippingAudioProcessor;
import androidx.media3.exoplayer.util.DebugTextViewHelper; import androidx.media3.exoplayer.util.DebugTextViewHelper;
import androidx.media3.transformer.Composition;
import androidx.media3.transformer.DefaultEncoderFactory; import androidx.media3.transformer.DefaultEncoderFactory;
import androidx.media3.transformer.DefaultMuxer;
import androidx.media3.transformer.EditedMediaItem;
import androidx.media3.transformer.EditedMediaItemSequence;
import androidx.media3.transformer.Effects;
import androidx.media3.transformer.ExportException;
import androidx.media3.transformer.ExportResult;
import androidx.media3.transformer.ProgressHolder; import androidx.media3.transformer.ProgressHolder;
import androidx.media3.transformer.TransformationException;
import androidx.media3.transformer.TransformationRequest; import androidx.media3.transformer.TransformationRequest;
import androidx.media3.transformer.TransformationResult;
import androidx.media3.transformer.Transformer; import androidx.media3.transformer.Transformer;
import androidx.media3.ui.AspectRatioFrameLayout; import androidx.media3.ui.AspectRatioFrameLayout;
import androidx.media3.ui.PlayerView; import androidx.media3.ui.PlayerView;
@ -66,27 +96,34 @@ import com.google.android.material.progressindicator.LinearProgressIndicator;
import com.google.common.base.Stopwatch; import com.google.common.base.Stopwatch;
import com.google.common.base.Ticker; import com.google.common.base.Ticker;
import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableList;
import com.google.common.util.concurrent.ListenableFuture;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.lang.reflect.Constructor; import java.lang.reflect.Constructor;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CountDownLatch; import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull; import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
import org.checkerframework.checker.nullness.qual.RequiresNonNull; import org.checkerframework.checker.nullness.qual.RequiresNonNull;
/** An {@link Activity} that transforms and plays media using {@link Transformer}. */ /** An {@link Activity} that exports and plays media using {@link Transformer}. */
public final class TransformerActivity extends AppCompatActivity { public final class TransformerActivity extends AppCompatActivity {
private static final String TAG = "TransformerActivity"; private static final String TAG = "TransformerActivity";
private @MonotonicNonNull Button displayInputButton; private @MonotonicNonNull Button displayInputButton;
private @MonotonicNonNull MaterialCardView inputCardView; private @MonotonicNonNull MaterialCardView inputCardView;
private @MonotonicNonNull TextView inputTextView;
private @MonotonicNonNull ImageView inputImageView;
private @MonotonicNonNull PlayerView inputPlayerView; private @MonotonicNonNull PlayerView inputPlayerView;
private @MonotonicNonNull PlayerView outputPlayerView; private @MonotonicNonNull PlayerView outputPlayerView;
private @MonotonicNonNull TextView outputVideoTextView;
private @MonotonicNonNull TextView debugTextView; private @MonotonicNonNull TextView debugTextView;
private @MonotonicNonNull TextView informationTextView; private @MonotonicNonNull TextView informationTextView;
private @MonotonicNonNull ViewGroup progressViewGroup; private @MonotonicNonNull ViewGroup progressViewGroup;
private @MonotonicNonNull LinearProgressIndicator progressIndicator; private @MonotonicNonNull LinearProgressIndicator progressIndicator;
private @MonotonicNonNull Stopwatch transformationStopwatch; private @MonotonicNonNull Stopwatch exportStopwatch;
private @MonotonicNonNull AspectRatioFrameLayout debugFrame; private @MonotonicNonNull AspectRatioFrameLayout debugFrame;
@Nullable private DebugTextViewHelper debugTextViewHelper; @Nullable private DebugTextViewHelper debugTextViewHelper;
@ -101,8 +138,11 @@ public final class TransformerActivity extends AppCompatActivity {
setContentView(R.layout.transformer_activity); setContentView(R.layout.transformer_activity);
inputCardView = findViewById(R.id.input_card_view); inputCardView = findViewById(R.id.input_card_view);
inputTextView = findViewById(R.id.input_text_view);
inputImageView = findViewById(R.id.input_image_view);
inputPlayerView = findViewById(R.id.input_player_view); inputPlayerView = findViewById(R.id.input_player_view);
outputPlayerView = findViewById(R.id.output_player_view); outputPlayerView = findViewById(R.id.output_player_view);
outputVideoTextView = findViewById(R.id.output_video_text_view);
debugTextView = findViewById(R.id.debug_text_view); debugTextView = findViewById(R.id.debug_text_view);
informationTextView = findViewById(R.id.information_text_view); informationTextView = findViewById(R.id.information_text_view);
progressViewGroup = findViewById(R.id.progress_view_group); progressViewGroup = findViewById(R.id.progress_view_group);
@ -111,7 +151,7 @@ public final class TransformerActivity extends AppCompatActivity {
displayInputButton = findViewById(R.id.display_input_button); displayInputButton = findViewById(R.id.display_input_button);
displayInputButton.setOnClickListener(this::toggleInputVideoDisplay); displayInputButton.setOnClickListener(this::toggleInputVideoDisplay);
transformationStopwatch = exportStopwatch =
Stopwatch.createUnstarted( Stopwatch.createUnstarted(
new Ticker() { new Ticker() {
@Override @Override
@ -127,15 +167,18 @@ public final class TransformerActivity extends AppCompatActivity {
checkNotNull(progressIndicator); checkNotNull(progressIndicator);
checkNotNull(informationTextView); checkNotNull(informationTextView);
checkNotNull(transformationStopwatch); checkNotNull(exportStopwatch);
checkNotNull(inputCardView); checkNotNull(inputCardView);
checkNotNull(inputTextView);
checkNotNull(inputImageView);
checkNotNull(inputPlayerView); checkNotNull(inputPlayerView);
checkNotNull(outputPlayerView); checkNotNull(outputPlayerView);
checkNotNull(outputVideoTextView);
checkNotNull(debugTextView); checkNotNull(debugTextView);
checkNotNull(progressViewGroup); checkNotNull(progressViewGroup);
checkNotNull(debugFrame); checkNotNull(debugFrame);
checkNotNull(displayInputButton); checkNotNull(displayInputButton);
startTransformation(); startExport();
inputPlayerView.onResume(); inputPlayerView.onResume();
outputPlayerView.onResume(); outputPlayerView.onResume();
@ -148,9 +191,9 @@ public final class TransformerActivity extends AppCompatActivity {
checkNotNull(transformer).cancel(); checkNotNull(transformer).cancel();
transformer = null; transformer = null;
// The stop watch is reset after cancelling the transformation, in case cancelling causes the // The stop watch is reset after cancelling the export, in case cancelling causes the stop watch
// stop watch to be stopped in a transformer callback. // to be stopped in a transformer callback.
checkNotNull(transformationStopwatch).reset(); checkNotNull(exportStopwatch).reset();
checkNotNull(inputPlayerView).onPause(); checkNotNull(inputPlayerView).onPause();
checkNotNull(outputPlayerView).onPause(); checkNotNull(outputPlayerView).onPause();
@ -161,37 +204,49 @@ public final class TransformerActivity extends AppCompatActivity {
} }
@RequiresNonNull({ @RequiresNonNull({
"displayInputButton",
"inputCardView", "inputCardView",
"inputTextView",
"inputImageView",
"inputPlayerView", "inputPlayerView",
"outputPlayerView", "outputPlayerView",
"displayInputButton", "outputVideoTextView",
"debugTextView", "debugTextView",
"informationTextView", "informationTextView",
"progressIndicator", "progressIndicator",
"transformationStopwatch", "exportStopwatch",
"progressViewGroup", "progressViewGroup",
"debugFrame", "debugFrame",
}) })
private void startTransformation() { private void startExport() {
requestTransformerPermission(); requestReadVideoPermission(/* activity= */ this);
Intent intent = getIntent(); Intent intent = getIntent();
Uri uri = checkNotNull(intent.getData()); Uri inputUri = checkNotNull(intent.getData());
try { try {
externalCacheFile = createExternalCacheFile("transformer-output.mp4"); externalCacheFile = createExternalCacheFile("transformer-output.mp4");
String filePath = externalCacheFile.getAbsolutePath();
@Nullable Bundle bundle = intent.getExtras();
MediaItem mediaItem = createMediaItem(bundle, uri);
Transformer transformer = createTransformer(bundle, filePath);
transformationStopwatch.start();
transformer.startTransformation(mediaItem, filePath);
this.transformer = transformer;
} catch (IOException e) { } catch (IOException e) {
throw new IllegalStateException(e); throw new IllegalStateException(e);
} }
informationTextView.setText(R.string.transformation_started); String filePath = externalCacheFile.getAbsolutePath();
@Nullable Bundle bundle = intent.getExtras();
MediaItem mediaItem = createMediaItem(bundle, inputUri);
try {
Transformer transformer = createTransformer(bundle, inputUri, filePath);
Composition composition = createComposition(mediaItem, bundle);
exportStopwatch.start();
transformer.start(composition, filePath);
this.transformer = transformer;
} catch (PackageManager.NameNotFoundException e) {
throw new IllegalStateException(e);
}
displayInputButton.setVisibility(View.GONE);
inputCardView.setVisibility(View.GONE); inputCardView.setVisibility(View.GONE);
outputPlayerView.setVisibility(View.GONE); outputPlayerView.setVisibility(View.GONE);
outputVideoTextView.setVisibility(View.GONE);
debugTextView.setVisibility(View.GONE);
informationTextView.setText(R.string.export_started);
progressViewGroup.setVisibility(View.VISIBLE);
Handler mainHandler = new Handler(getMainLooper()); Handler mainHandler = new Handler(getMainLooper());
ProgressHolder progressHolder = new ProgressHolder(); ProgressHolder progressHolder = new ProgressHolder();
mainHandler.post( mainHandler.post(
@ -199,13 +254,10 @@ public final class TransformerActivity extends AppCompatActivity {
@Override @Override
public void run() { public void run() {
if (transformer != null if (transformer != null
&& transformer.getProgress(progressHolder) && transformer.getProgress(progressHolder) != PROGRESS_STATE_NOT_STARTED) {
!= Transformer.PROGRESS_STATE_NO_TRANSFORMATION) {
progressIndicator.setProgress(progressHolder.progress); progressIndicator.setProgress(progressHolder.progress);
informationTextView.setText( informationTextView.setText(
getString( getString(R.string.export_timer, exportStopwatch.elapsed(TimeUnit.SECONDS)));
R.string.transformation_timer,
transformationStopwatch.elapsed(TimeUnit.SECONDS)));
mainHandler.postDelayed(/* r= */ this, /* delayMillis= */ 500); mainHandler.postDelayed(/* r= */ this, /* delayMillis= */ 500);
} }
} }
@ -232,21 +284,22 @@ public final class TransformerActivity extends AppCompatActivity {
@RequiresNonNull({ @RequiresNonNull({
"inputCardView", "inputCardView",
"inputTextView",
"inputImageView",
"inputPlayerView", "inputPlayerView",
"outputPlayerView", "outputPlayerView",
"outputVideoTextView",
"displayInputButton", "displayInputButton",
"debugTextView", "debugTextView",
"informationTextView", "informationTextView",
"transformationStopwatch", "exportStopwatch",
"progressViewGroup", "progressViewGroup",
"debugFrame", "debugFrame",
}) })
private Transformer createTransformer(@Nullable Bundle bundle, String filePath) { private Transformer createTransformer(@Nullable Bundle bundle, Uri inputUri, String filePath) {
Transformer.Builder transformerBuilder = new Transformer.Builder(/* context= */ this); Transformer.Builder transformerBuilder = new Transformer.Builder(/* context= */ this);
if (bundle != null) { if (bundle != null) {
TransformationRequest.Builder requestBuilder = new TransformationRequest.Builder(); TransformationRequest.Builder requestBuilder = new TransformationRequest.Builder();
requestBuilder.setFlattenForSlowMotion(
bundle.getBoolean(ConfigurationActivity.SHOULD_FLATTEN_FOR_SLOW_MOTION));
@Nullable String audioMimeType = bundle.getString(ConfigurationActivity.AUDIO_MIME_TYPE); @Nullable String audioMimeType = bundle.getString(ConfigurationActivity.AUDIO_MIME_TYPE);
if (audioMimeType != null) { if (audioMimeType != null) {
requestBuilder.setAudioMimeType(audioMimeType); requestBuilder.setAudioMimeType(audioMimeType);
@ -255,55 +308,38 @@ public final class TransformerActivity extends AppCompatActivity {
if (videoMimeType != null) { if (videoMimeType != null) {
requestBuilder.setVideoMimeType(videoMimeType); requestBuilder.setVideoMimeType(videoMimeType);
} }
int resolutionHeight = requestBuilder.setHdrMode(bundle.getInt(ConfigurationActivity.HDR_MODE));
bundle.getInt( transformerBuilder.setTransformationRequest(requestBuilder.build());
ConfigurationActivity.RESOLUTION_HEIGHT, /* defaultValue= */ C.LENGTH_UNSET);
if (resolutionHeight != C.LENGTH_UNSET) {
requestBuilder.setResolution(resolutionHeight);
}
float scaleX = bundle.getFloat(ConfigurationActivity.SCALE_X, /* defaultValue= */ 1); transformerBuilder.setEncoderFactory(
float scaleY = bundle.getFloat(ConfigurationActivity.SCALE_Y, /* defaultValue= */ 1);
requestBuilder.setScale(scaleX, scaleY);
float rotateDegrees =
bundle.getFloat(ConfigurationActivity.ROTATE_DEGREES, /* defaultValue= */ 0);
requestBuilder.setRotationDegrees(rotateDegrees);
requestBuilder.setEnableRequestSdrToneMapping(
bundle.getBoolean(ConfigurationActivity.ENABLE_REQUEST_SDR_TONE_MAPPING));
requestBuilder.experimental_setForceInterpretHdrVideoAsSdr(
bundle.getBoolean(ConfigurationActivity.FORCE_INTERPRET_HDR_VIDEO_AS_SDR));
requestBuilder.experimental_setEnableHdrEditing(
bundle.getBoolean(ConfigurationActivity.ENABLE_HDR_EDITING));
transformerBuilder
.setTransformationRequest(requestBuilder.build())
.setRemoveAudio(bundle.getBoolean(ConfigurationActivity.SHOULD_REMOVE_AUDIO))
.setRemoveVideo(bundle.getBoolean(ConfigurationActivity.SHOULD_REMOVE_VIDEO))
.setEncoderFactory(
new DefaultEncoderFactory.Builder(this.getApplicationContext()) new DefaultEncoderFactory.Builder(this.getApplicationContext())
.setEnableFallback(bundle.getBoolean(ConfigurationActivity.ENABLE_FALLBACK)) .setEnableFallback(bundle.getBoolean(ConfigurationActivity.ENABLE_FALLBACK))
.build()); .build());
transformerBuilder.setVideoEffects(createVideoEffectsListFromBundle(bundle)); if (!bundle.getBoolean(ConfigurationActivity.ABORT_SLOW_EXPORT)) {
transformerBuilder.setMuxerFactory(
new DefaultMuxer.Factory(/* maxDelayBetweenSamplesMs= */ C.TIME_UNSET));
}
if (bundle.getBoolean(ConfigurationActivity.ENABLE_DEBUG_PREVIEW)) { if (bundle.getBoolean(ConfigurationActivity.ENABLE_DEBUG_PREVIEW)) {
transformerBuilder.setDebugViewProvider(new DemoDebugViewProvider()); transformerBuilder.setDebugViewProvider(new DemoDebugViewProvider());
} }
} }
return transformerBuilder return transformerBuilder
.addListener( .addListener(
new Transformer.Listener() { new Transformer.Listener() {
@Override @Override
public void onTransformationCompleted( public void onCompleted(Composition composition, ExportResult exportResult) {
MediaItem mediaItem, TransformationResult transformationResult) { TransformerActivity.this.onCompleted(inputUri, filePath);
TransformerActivity.this.onTransformationCompleted(filePath, mediaItem);
} }
@Override @Override
public void onTransformationError( public void onError(
MediaItem mediaItem, TransformationException exception) { Composition composition,
TransformerActivity.this.onTransformationError(exception); ExportResult exportResult,
ExportException exportException) {
TransformerActivity.this.onError(exportException);
} }
}) })
.build(); .build();
@ -313,26 +349,88 @@ public final class TransformerActivity extends AppCompatActivity {
private File createExternalCacheFile(String fileName) throws IOException { private File createExternalCacheFile(String fileName) throws IOException {
File file = new File(getExternalCacheDir(), fileName); File file = new File(getExternalCacheDir(), fileName);
if (file.exists() && !file.delete()) { if (file.exists() && !file.delete()) {
throw new IllegalStateException("Could not delete the previous transformer output file"); throw new IllegalStateException("Could not delete the previous export output file");
} }
if (!file.createNewFile()) { if (!file.createNewFile()) {
throw new IllegalStateException("Could not create the transformer output file"); throw new IllegalStateException("Could not create the export output file");
} }
return file; return file;
} }
private ImmutableList<Effect> createVideoEffectsListFromBundle(Bundle bundle) { @RequiresNonNull({
"inputCardView",
"outputPlayerView",
"exportStopwatch",
"progressViewGroup",
})
private Composition createComposition(MediaItem mediaItem, @Nullable Bundle bundle)
throws PackageManager.NameNotFoundException {
EditedMediaItem.Builder editedMediaItemBuilder = new EditedMediaItem.Builder(mediaItem);
// For image inputs. Automatically ignored if input is audio/video.
editedMediaItemBuilder.setDurationUs(5_000_000).setFrameRate(30);
boolean forceAudioTrack = false;
if (bundle != null) {
ImmutableList<AudioProcessor> audioProcessors = createAudioProcessorsFromBundle(bundle);
ImmutableList<Effect> videoEffects = createVideoEffectsFromBundle(bundle);
editedMediaItemBuilder
.setRemoveAudio(bundle.getBoolean(ConfigurationActivity.SHOULD_REMOVE_AUDIO))
.setRemoveVideo(bundle.getBoolean(ConfigurationActivity.SHOULD_REMOVE_VIDEO))
.setFlattenForSlowMotion(
bundle.getBoolean(ConfigurationActivity.SHOULD_FLATTEN_FOR_SLOW_MOTION))
.setEffects(new Effects(audioProcessors, videoEffects));
forceAudioTrack = bundle.getBoolean(ConfigurationActivity.FORCE_AUDIO_TRACK);
}
List<EditedMediaItem> editedMediaItems = new ArrayList<>();
editedMediaItems.add(editedMediaItemBuilder.build());
List<EditedMediaItemSequence> sequences = new ArrayList<>();
sequences.add(new EditedMediaItemSequence(editedMediaItems));
return new Composition.Builder(sequences)
.experimentalSetForceAudioTrack(forceAudioTrack)
.build();
}
private ImmutableList<AudioProcessor> createAudioProcessorsFromBundle(Bundle bundle) {
@Nullable @Nullable
boolean[] selectedEffects = boolean[] selectedAudioEffects =
bundle.getBooleanArray(ConfigurationActivity.DEMO_EFFECTS_SELECTIONS); bundle.getBooleanArray(ConfigurationActivity.AUDIO_EFFECTS_SELECTIONS);
if (selectedEffects == null) {
if (selectedAudioEffects == null) {
return ImmutableList.of(); return ImmutableList.of();
} }
ImmutableList.Builder<AudioProcessor> processors = new ImmutableList.Builder<>();
if (selectedAudioEffects[ConfigurationActivity.HIGH_PITCHED_INDEX]
|| selectedAudioEffects[ConfigurationActivity.SAMPLE_RATE_INDEX]) {
SonicAudioProcessor sonicAudioProcessor = new SonicAudioProcessor();
if (selectedAudioEffects[ConfigurationActivity.HIGH_PITCHED_INDEX]) {
sonicAudioProcessor.setPitch(2f);
}
if (selectedAudioEffects[ConfigurationActivity.SAMPLE_RATE_INDEX]) {
sonicAudioProcessor.setOutputSampleRateHz(48_000);
}
processors.add(sonicAudioProcessor);
}
if (selectedAudioEffects[ConfigurationActivity.SKIP_SILENCE_INDEX]) {
SilenceSkippingAudioProcessor silenceSkippingAudioProcessor =
new SilenceSkippingAudioProcessor();
silenceSkippingAudioProcessor.setEnabled(true);
processors.add(silenceSkippingAudioProcessor);
}
return processors.build();
}
private ImmutableList<Effect> createVideoEffectsFromBundle(Bundle bundle)
throws PackageManager.NameNotFoundException {
boolean[] selectedEffects =
checkStateNotNull(bundle.getBooleanArray(ConfigurationActivity.VIDEO_EFFECTS_SELECTIONS));
ImmutableList.Builder<Effect> effects = new ImmutableList.Builder<>(); ImmutableList.Builder<Effect> effects = new ImmutableList.Builder<>();
if (selectedEffects[0]) { if (selectedEffects[ConfigurationActivity.DIZZY_CROP_INDEX]) {
effects.add(MatrixTransformationFactory.createDizzyCropEffect()); effects.add(MatrixTransformationFactory.createDizzyCropEffect());
} }
if (selectedEffects[1]) { if (selectedEffects[ConfigurationActivity.EDGE_DETECTOR_INDEX]) {
try { try {
Class<?> clazz = Class.forName("androidx.media3.demo.transformer.MediaPipeProcessor"); Class<?> clazz = Class.forName("androidx.media3.demo.transformer.MediaPipeProcessor");
Constructor<?> constructor = Constructor<?> constructor =
@ -347,7 +445,7 @@ public final class TransformerActivity extends AppCompatActivity {
(GlEffect) (GlEffect)
(Context context, boolean useHdr) -> { (Context context, boolean useHdr) -> {
try { try {
return (GlTextureProcessor) return (GlShaderProgram)
constructor.newInstance( constructor.newInstance(
context, context,
useHdr, useHdr,
@ -357,14 +455,14 @@ public final class TransformerActivity extends AppCompatActivity {
/* outputStreamName= */ "output_video"); /* outputStreamName= */ "output_video");
} catch (Exception e) { } catch (Exception e) {
runOnUiThread(() -> showToast(R.string.no_media_pipe_error)); runOnUiThread(() -> showToast(R.string.no_media_pipe_error));
throw new RuntimeException("Failed to load MediaPipe processor", e); throw new RuntimeException("Failed to load MediaPipeShaderProgram", e);
} }
}); });
} catch (Exception e) { } catch (Exception e) {
showToast(R.string.no_media_pipe_error); showToast(R.string.no_media_pipe_error);
} }
} }
if (selectedEffects[2]) { if (selectedEffects[ConfigurationActivity.COLOR_FILTERS_INDEX]) {
switch (bundle.getInt(ConfigurationActivity.COLOR_FILTER_SELECTION)) { switch (bundle.getInt(ConfigurationActivity.COLOR_FILTER_SELECTION)) {
case ConfigurationActivity.COLOR_FILTER_GRAYSCALE: case ConfigurationActivity.COLOR_FILTER_GRAYSCALE:
effects.add(RgbFilter.createGrayscaleFilter()); effects.add(RgbFilter.createGrayscaleFilter());
@ -390,7 +488,7 @@ public final class TransformerActivity extends AppCompatActivity {
+ bundle.getInt(ConfigurationActivity.COLOR_FILTER_SELECTION)); + bundle.getInt(ConfigurationActivity.COLOR_FILTER_SELECTION));
} }
} }
if (selectedEffects[3]) { if (selectedEffects[ConfigurationActivity.MAP_WHITE_TO_GREEN_LUT_INDEX]) {
int length = 3; int length = 3;
int[][][] mapWhiteToGreenLut = new int[length][length][length]; int[][][] mapWhiteToGreenLut = new int[length][length][length];
int scale = 255 / (length - 1); int scale = 255 / (length - 1);
@ -405,7 +503,7 @@ public final class TransformerActivity extends AppCompatActivity {
mapWhiteToGreenLut[length - 1][length - 1][length - 1] = Color.GREEN; mapWhiteToGreenLut[length - 1][length - 1][length - 1] = Color.GREEN;
effects.add(SingleColorLut.createFromCube(mapWhiteToGreenLut)); effects.add(SingleColorLut.createFromCube(mapWhiteToGreenLut));
} }
if (selectedEffects[4]) { if (selectedEffects[ConfigurationActivity.RGB_ADJUSTMENTS_INDEX]) {
effects.add( effects.add(
new RgbAdjustment.Builder() new RgbAdjustment.Builder()
.setRedScale(bundle.getFloat(ConfigurationActivity.RGB_ADJUSTMENT_RED_SCALE)) .setRedScale(bundle.getFloat(ConfigurationActivity.RGB_ADJUSTMENT_RED_SCALE))
@ -413,7 +511,7 @@ public final class TransformerActivity extends AppCompatActivity {
.setBlueScale(bundle.getFloat(ConfigurationActivity.RGB_ADJUSTMENT_BLUE_SCALE)) .setBlueScale(bundle.getFloat(ConfigurationActivity.RGB_ADJUSTMENT_BLUE_SCALE))
.build()); .build());
} }
if (selectedEffects[5]) { if (selectedEffects[ConfigurationActivity.HSL_ADJUSTMENT_INDEX]) {
effects.add( effects.add(
new HslAdjustment.Builder() new HslAdjustment.Builder()
.adjustHue(bundle.getFloat(ConfigurationActivity.HSL_ADJUSTMENTS_HUE)) .adjustHue(bundle.getFloat(ConfigurationActivity.HSL_ADJUSTMENTS_HUE))
@ -421,14 +519,14 @@ public final class TransformerActivity extends AppCompatActivity {
.adjustLightness(bundle.getFloat(ConfigurationActivity.HSL_ADJUSTMENTS_LIGHTNESS)) .adjustLightness(bundle.getFloat(ConfigurationActivity.HSL_ADJUSTMENTS_LIGHTNESS))
.build()); .build());
} }
if (selectedEffects[6]) { if (selectedEffects[ConfigurationActivity.CONTRAST_INDEX]) {
effects.add(new Contrast(bundle.getFloat(ConfigurationActivity.CONTRAST_VALUE))); effects.add(new Contrast(bundle.getFloat(ConfigurationActivity.CONTRAST_VALUE)));
} }
if (selectedEffects[7]) { if (selectedEffects[ConfigurationActivity.PERIODIC_VIGNETTE_INDEX]) {
effects.add( effects.add(
(GlEffect) (GlEffect)
(Context context, boolean useHdr) -> (Context context, boolean useHdr) ->
new PeriodicVignetteProcessor( new PeriodicVignetteShaderProgram(
context, context,
useHdr, useHdr,
bundle.getFloat(ConfigurationActivity.PERIODIC_VIGNETTE_CENTER_X), bundle.getFloat(ConfigurationActivity.PERIODIC_VIGNETTE_CENTER_X),
@ -439,61 +537,144 @@ public final class TransformerActivity extends AppCompatActivity {
ConfigurationActivity.PERIODIC_VIGNETTE_OUTER_RADIUS), ConfigurationActivity.PERIODIC_VIGNETTE_OUTER_RADIUS),
bundle.getFloat(ConfigurationActivity.PERIODIC_VIGNETTE_OUTER_RADIUS))); bundle.getFloat(ConfigurationActivity.PERIODIC_VIGNETTE_OUTER_RADIUS)));
} }
if (selectedEffects[8]) { if (selectedEffects[ConfigurationActivity.SPIN_3D_INDEX]) {
effects.add(MatrixTransformationFactory.createSpin3dEffect()); effects.add(MatrixTransformationFactory.createSpin3dEffect());
} }
if (selectedEffects[9]) { if (selectedEffects[ConfigurationActivity.ZOOM_IN_INDEX]) {
effects.add((GlEffect) BitmapOverlayProcessor::new);
}
if (selectedEffects[10]) {
effects.add(MatrixTransformationFactory.createZoomInTransition()); effects.add(MatrixTransformationFactory.createZoomInTransition());
} }
@Nullable OverlayEffect overlayEffect = createOverlayEffectFromBundle(bundle, selectedEffects);
if (overlayEffect != null) {
effects.add(overlayEffect);
}
float scaleX = bundle.getFloat(ConfigurationActivity.SCALE_X, /* defaultValue= */ 1);
float scaleY = bundle.getFloat(ConfigurationActivity.SCALE_Y, /* defaultValue= */ 1);
float rotateDegrees =
bundle.getFloat(ConfigurationActivity.ROTATE_DEGREES, /* defaultValue= */ 0);
if (scaleX != 1f || scaleY != 1f || rotateDegrees != 0f) {
effects.add(
new ScaleAndRotateTransformation.Builder()
.setScale(scaleX, scaleY)
.setRotationDegrees(rotateDegrees)
.build());
}
int resolutionHeight =
bundle.getInt(ConfigurationActivity.RESOLUTION_HEIGHT, /* defaultValue= */ C.LENGTH_UNSET);
if (resolutionHeight != C.LENGTH_UNSET) {
effects.add(Presentation.createForHeight(resolutionHeight));
}
return effects.build(); return effects.build();
} }
@Nullable
private OverlayEffect createOverlayEffectFromBundle(Bundle bundle, boolean[] selectedEffects)
throws PackageManager.NameNotFoundException {
ImmutableList.Builder<TextureOverlay> overlaysBuilder = new ImmutableList.Builder<>();
if (selectedEffects[ConfigurationActivity.OVERLAY_LOGO_AND_TIMER_INDEX]) {
float[] logoPositioningMatrix = GlUtil.create4x4IdentityMatrix();
Matrix.translateM(
logoPositioningMatrix, /* mOffset= */ 0, /* x= */ -0.95f, /* y= */ -0.95f, /* z= */ 1);
OverlaySettings logoSettings =
new OverlaySettings.Builder()
.setMatrix(logoPositioningMatrix)
.setAnchor(/* x= */ -1f, /* y= */ -1f)
.build();
Drawable logo = getPackageManager().getApplicationIcon(getPackageName());
logo.setBounds(
/* left= */ 0, /* top= */ 0, logo.getIntrinsicWidth(), logo.getIntrinsicHeight());
TextureOverlay logoOverlay = DrawableOverlay.createStaticDrawableOverlay(logo, logoSettings);
TextureOverlay timerOverlay = new TimerOverlay();
overlaysBuilder.add(logoOverlay, timerOverlay);
}
if (selectedEffects[ConfigurationActivity.BITMAP_OVERLAY_INDEX]) {
OverlaySettings overlaySettings =
new OverlaySettings.Builder()
.setAlpha(
bundle.getFloat(
ConfigurationActivity.BITMAP_OVERLAY_ALPHA, /* defaultValue= */ 1))
.build();
BitmapOverlay bitmapOverlay =
BitmapOverlay.createStaticBitmapOverlay(
getApplicationContext(),
Uri.parse(checkNotNull(bundle.getString(ConfigurationActivity.BITMAP_OVERLAY_URI))),
overlaySettings);
overlaysBuilder.add(bitmapOverlay);
}
if (selectedEffects[ConfigurationActivity.TEXT_OVERLAY_INDEX]) {
OverlaySettings overlaySettings =
new OverlaySettings.Builder()
.setAlpha(
bundle.getFloat(ConfigurationActivity.TEXT_OVERLAY_ALPHA, /* defaultValue= */ 1))
.build();
SpannableString overlayText =
new SpannableString(
checkNotNull(bundle.getString(ConfigurationActivity.TEXT_OVERLAY_TEXT)));
overlayText.setSpan(
new ForegroundColorSpan(bundle.getInt(ConfigurationActivity.TEXT_OVERLAY_TEXT_COLOR)),
/* start= */ 0,
overlayText.length(),
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
TextOverlay textOverlay = TextOverlay.createStaticTextOverlay(overlayText, overlaySettings);
overlaysBuilder.add(textOverlay);
}
ImmutableList<TextureOverlay> overlays = overlaysBuilder.build();
return overlays.isEmpty() ? null : new OverlayEffect(overlays);
}
@RequiresNonNull({ @RequiresNonNull({
"informationTextView", "informationTextView",
"progressViewGroup", "progressViewGroup",
"debugFrame", "debugFrame",
"transformationStopwatch", "exportStopwatch",
}) })
private void onTransformationError(TransformationException exception) { private void onError(ExportException exportException) {
transformationStopwatch.stop(); exportStopwatch.stop();
informationTextView.setText(R.string.transformation_error); informationTextView.setText(R.string.export_error);
progressViewGroup.setVisibility(View.GONE); progressViewGroup.setVisibility(View.GONE);
debugFrame.removeAllViews(); debugFrame.removeAllViews();
Toast.makeText(getApplicationContext(), "Transformation error: " + exception, Toast.LENGTH_LONG) Toast.makeText(getApplicationContext(), "Export error: " + exportException, Toast.LENGTH_LONG)
.show(); .show();
Log.e(TAG, "Transformation error", exception); Log.e(TAG, "Export error", exportException);
} }
@RequiresNonNull({ @RequiresNonNull({
"inputCardView", "inputCardView",
"inputTextView",
"inputImageView",
"inputPlayerView", "inputPlayerView",
"outputPlayerView", "outputPlayerView",
"displayInputButton", "outputVideoTextView",
"debugTextView", "debugTextView",
"displayInputButton",
"informationTextView", "informationTextView",
"progressViewGroup", "progressViewGroup",
"debugFrame", "debugFrame",
"transformationStopwatch", "exportStopwatch",
}) })
private void onTransformationCompleted(String filePath, MediaItem inputMediaItem) { private void onCompleted(Uri inputUri, String filePath) {
transformationStopwatch.stop(); exportStopwatch.stop();
informationTextView.setText( informationTextView.setText(
getString( getString(R.string.export_completed, exportStopwatch.elapsed(TimeUnit.SECONDS), filePath));
R.string.transformation_completed, transformationStopwatch.elapsed(TimeUnit.SECONDS)));
progressViewGroup.setVisibility(View.GONE); progressViewGroup.setVisibility(View.GONE);
debugFrame.removeAllViews(); debugFrame.removeAllViews();
inputCardView.setVisibility(View.VISIBLE); inputCardView.setVisibility(View.VISIBLE);
outputPlayerView.setVisibility(View.VISIBLE); outputPlayerView.setVisibility(View.VISIBLE);
outputVideoTextView.setVisibility(View.VISIBLE);
debugTextView.setVisibility(View.VISIBLE);
displayInputButton.setVisibility(View.VISIBLE); displayInputButton.setVisibility(View.VISIBLE);
playMediaItems(inputMediaItem, MediaItem.fromUri("file://" + filePath)); playMediaItems(MediaItem.fromUri(inputUri), MediaItem.fromUri("file://" + filePath));
Log.d(TAG, "Output file path: file://" + filePath); Log.d(TAG, "Output file path: file://" + filePath);
} }
@RequiresNonNull({ @RequiresNonNull({
"inputCardView", "inputCardView",
"inputTextView",
"inputImageView",
"inputPlayerView", "inputPlayerView",
"outputPlayerView", "outputPlayerView",
"debugTextView", "debugTextView",
@ -503,14 +684,7 @@ public final class TransformerActivity extends AppCompatActivity {
outputPlayerView.setPlayer(null); outputPlayerView.setPlayer(null);
releasePlayer(); releasePlayer();
ExoPlayer inputPlayer = new ExoPlayer.Builder(/* context= */ this).build(); Uri uri = checkNotNull(inputMediaItem.localConfiguration).uri;
inputPlayerView.setPlayer(inputPlayer);
inputPlayerView.setControllerAutoShow(false);
inputPlayer.setMediaItem(inputMediaItem);
inputPlayer.prepare();
this.inputPlayer = inputPlayer;
inputPlayer.setVolume(0f);
ExoPlayer outputPlayer = new ExoPlayer.Builder(/* context= */ this).build(); ExoPlayer outputPlayer = new ExoPlayer.Builder(/* context= */ this).build();
outputPlayerView.setPlayer(outputPlayer); outputPlayerView.setPlayer(outputPlayer);
outputPlayerView.setControllerAutoShow(false); outputPlayerView.setControllerAutoShow(false);
@ -518,13 +692,60 @@ public final class TransformerActivity extends AppCompatActivity {
outputPlayer.prepare(); outputPlayer.prepare();
this.outputPlayer = outputPlayer; this.outputPlayer = outputPlayer;
// Only support showing jpg images.
if (uri.toString().endsWith("jpg")) {
inputPlayerView.setVisibility(View.GONE);
inputImageView.setVisibility(View.VISIBLE);
inputTextView.setText(getString(R.string.input_image));
BitmapLoader bitmapLoader = new DataSourceBitmapLoader(getApplicationContext());
ListenableFuture<Bitmap> future = bitmapLoader.loadBitmap(uri);
try {
Bitmap bitmap = future.get();
inputImageView.setImageBitmap(bitmap);
} catch (ExecutionException | InterruptedException e) {
throw new IllegalArgumentException("Failed to load bitmap.", e);
}
} else {
inputPlayerView.setVisibility(View.VISIBLE);
inputImageView.setVisibility(View.GONE);
inputTextView.setText(getString(R.string.input_video_no_sound));
ExoPlayer inputPlayer = new ExoPlayer.Builder(/* context= */ this).build();
inputPlayerView.setPlayer(inputPlayer);
inputPlayerView.setControllerAutoShow(false);
inputPlayerView.setOnClickListener(this::onClickingPlayerView);
outputPlayerView.setOnClickListener(this::onClickingPlayerView);
inputPlayer.setMediaItem(inputMediaItem);
inputPlayer.prepare();
this.inputPlayer = inputPlayer;
inputPlayer.setVolume(0f);
inputPlayer.play(); inputPlayer.play();
}
outputPlayer.play(); outputPlayer.play();
debugTextViewHelper = new DebugTextViewHelper(outputPlayer, debugTextView); debugTextViewHelper = new DebugTextViewHelper(outputPlayer, debugTextView);
debugTextViewHelper.start(); debugTextViewHelper.start();
} }
private void onClickingPlayerView(View view) {
if (view == inputPlayerView) {
if (inputPlayer != null && inputTextView != null) {
inputPlayer.setVolume(1f);
inputTextView.setText(R.string.input_video_playing_sound);
}
checkNotNull(outputPlayer).setVolume(0f);
checkNotNull(outputVideoTextView).setText(R.string.output_video_no_sound);
} else {
if (inputPlayer != null && inputTextView != null) {
inputPlayer.setVolume(0f);
inputTextView.setText(getString(R.string.input_video_no_sound));
}
checkNotNull(outputPlayer).setVolume(1f);
checkNotNull(outputVideoTextView).setText(R.string.output_video_playing_sound);
}
}
private void releasePlayer() { private void releasePlayer() {
if (debugTextViewHelper != null) { if (debugTextViewHelper != null) {
debugTextViewHelper.stop(); debugTextViewHelper.stop();
@ -540,12 +761,11 @@ public final class TransformerActivity extends AppCompatActivity {
} }
} }
private void requestTransformerPermission() { private static void requestReadVideoPermission(AppCompatActivity activity) {
if (Util.SDK_INT < 23) { String permission = SDK_INT >= 33 ? READ_MEDIA_VIDEO : READ_EXTERNAL_STORAGE;
return; if (ActivityCompat.checkSelfPermission(activity, permission)
} != PackageManager.PERMISSION_GRANTED) {
if (checkSelfPermission(READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { ActivityCompat.requestPermissions(activity, new String[] {permission}, /* requestCode= */ 0);
requestPermissions(new String[] {READ_EXTERNAL_STORAGE}, /* requestCode= */ 0);
} }
} }
@ -562,7 +782,9 @@ public final class TransformerActivity extends AppCompatActivity {
inputCardView.setVisibility(View.VISIBLE); inputCardView.setVisibility(View.VISIBLE);
displayInputButton.setText(getString(R.string.hide_input_video)); displayInputButton.setText(getString(R.string.hide_input_video));
} else if (inputCardView.getVisibility() == View.VISIBLE) { } else if (inputCardView.getVisibility() == View.VISIBLE) {
checkNotNull(inputPlayer).pause(); if (inputPlayer != null) {
inputPlayer.pause();
}
inputCardView.setVisibility(View.GONE); inputCardView.setVisibility(View.GONE);
displayInputButton.setText(getString(R.string.show_input_video)); displayInputButton.setText(getString(R.string.show_input_video));
} }
@ -584,7 +806,7 @@ public final class TransformerActivity extends AppCompatActivity {
public SurfaceView getDebugPreviewSurfaceView(int width, int height) { public SurfaceView getDebugPreviewSurfaceView(int width, int height) {
checkState( checkState(
surfaceView == null || (this.width == width && this.height == height), surfaceView == null || (this.width == width && this.height == height),
"Transformer should not change the output size mid-transformation."); "Transformer should not change the output size mid-export.");
if (surfaceView != null) { if (surfaceView != null) {
return surfaceView; return surfaceView;
} }
@ -594,9 +816,9 @@ public final class TransformerActivity extends AppCompatActivity {
// Update the UI on the main thread and wait for the output surface to be available. // Update the UI on the main thread and wait for the output surface to be available.
CountDownLatch surfaceCreatedCountDownLatch = new CountDownLatch(1); CountDownLatch surfaceCreatedCountDownLatch = new CountDownLatch(1);
SurfaceView surfaceView = new SurfaceView(/* context= */ TransformerActivity.this);
runOnUiThread( runOnUiThread(
() -> { () -> {
surfaceView = new SurfaceView(/* context= */ TransformerActivity.this);
AspectRatioFrameLayout debugFrame = checkNotNull(TransformerActivity.this.debugFrame); AspectRatioFrameLayout debugFrame = checkNotNull(TransformerActivity.this.debugFrame);
debugFrame.addView(surfaceView); debugFrame.addView(surfaceView);
debugFrame.setAspectRatio((float) width / height); debugFrame.setAspectRatio((float) width / height);
@ -628,7 +850,6 @@ public final class TransformerActivity extends AppCompatActivity {
Thread.currentThread().interrupt(); Thread.currentThread().interrupt();
return null; return null;
} }
this.surfaceView = surfaceView;
return surfaceView; return surfaceView;
} }
} }

View File

@ -0,0 +1,59 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright 2022 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.
-->
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
tools:context=".ConfigurationActivity">
<TableLayout
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:stretchColumns="1"
android:layout_marginTop="32dp"
android:measureWithLargestChild="true"
android:paddingLeft="24dp"
android:paddingRight="12dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent">
<TableRow
android:layout_weight="1"
android:gravity="center_vertical" >
<TextView
android:text="@string/overlay_uri" />
<Button
android:id="@+id/bitmap_overlay_uri"
android:text="@string/select_local_image"
android:layout_width="0dp"
android:layout_height="wrap_content" />
</TableRow>
<TableRow
android:layout_weight="1"
android:gravity="center_vertical" >
<TextView android:text="@string/overlay_alpha" />
<com.google.android.material.slider.Slider
android:id="@+id/bitmap_overlay_alpha_slider"
android:valueFrom="0"
android:value="1"
android:valueTo="1"
android:layout_gravity="right|center_vertical"/>
</TableRow>
</TableLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -75,11 +75,12 @@
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/selected_file_text_view" app:layout_constraintTop_toBottomOf="@+id/selected_file_text_view"
app:layout_constraintBottom_toTopOf="@+id/select_demo_effects_button"> app:layout_constraintBottom_toTopOf="@+id/select_audio_effects_button">
<TableLayout <TableLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:stretchColumns="0" android:stretchColumns="0"
android:shrinkColumns="0"
android:layout_marginTop="32dp" android:layout_marginTop="32dp"
android:measureWithLargestChild="true" android:measureWithLargestChild="true"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
@ -112,6 +113,15 @@
android:id="@+id/flatten_for_slow_motion_checkbox" android:id="@+id/flatten_for_slow_motion_checkbox"
android:layout_gravity="end" /> android:layout_gravity="end" />
</TableRow> </TableRow>
<TableRow
android:layout_weight="1">
<TextView
android:layout_gravity="center_vertical"
android:text="@string/force_audio_track" />
<CheckBox
android:id="@+id/force_audio_track_checkbox"
android:layout_gravity="end" />
</TableRow>
<TableRow <TableRow
android:layout_weight="1" android:layout_weight="1"
android:gravity="center_vertical" > android:gravity="center_vertical" >
@ -201,53 +211,54 @@
android:layout_weight="1"> android:layout_weight="1">
<TextView <TextView
android:layout_gravity="center_vertical" android:layout_gravity="center_vertical"
android:id="@+id/request_sdr_tone_mapping" android:text="@string/abort_slow_export" />
android:text="@string/request_sdr_tone_mapping" />
<CheckBox <CheckBox
android:id="@+id/request_sdr_tone_mapping_checkbox" android:id="@+id/abort_slow_export_checkbox"
android:layout_gravity="end" /> android:layout_gravity="end"/>
</TableRow> </TableRow>
<TableRow <TableRow
android:layout_weight="1"> android:layout_weight="1"
android:gravity="center_vertical" >
<TextView <TextView
android:layout_gravity="center_vertical" android:id="@+id/hdr_mode"
android:id="@+id/hdr_editing" android:text="@string/hdr_mode"/>
android:text="@string/hdr_editing" /> <Spinner
<CheckBox android:id="@+id/hdr_mode_spinner"
android:id="@+id/hdr_editing_checkbox" android:layout_gravity="right|center_vertical"
android:layout_gravity="end" /> android:gravity="right" />
</TableRow>
<TableRow
android:layout_weight="1">
<TextView
android:layout_gravity="center_vertical"
android:id="@+id/force_interpret_hdr_video_as_sdr"
android:text="@string/force_interpret_hdr_video_as_sdr" />
<CheckBox
android:id="@+id/force_interpret_hdr_video_as_sdr_checkbox"
android:layout_gravity="end" />
</TableRow> </TableRow>
</TableLayout> </TableLayout>
</androidx.core.widget.NestedScrollView> </androidx.core.widget.NestedScrollView>
<Button <Button
android:id="@+id/select_demo_effects_button" android:id="@+id/select_audio_effects_button"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="32dp" android:layout_marginTop="32dp"
android:layout_marginStart="32dp" android:layout_marginStart="32dp"
android:layout_marginEnd="32dp" android:layout_marginEnd="32dp"
android:text="@string/select_demo_effects" android:text="@string/select_audio_effects"
app:layout_constraintBottom_toTopOf="@+id/transform_button" app:layout_constraintBottom_toTopOf="@+id/select_video_effects_button"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" /> app:layout_constraintStart_toStartOf="parent" />
<Button <Button
android:id="@+id/transform_button" android:id="@+id/select_video_effects_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="32dp"
android:layout_marginStart="32dp"
android:layout_marginEnd="32dp"
android:text="@string/select_video_effects"
app:layout_constraintBottom_toTopOf="@+id/export_button"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
<Button
android:id="@+id/export_button"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginBottom="28dp" android:layout_marginBottom="28dp"
android:layout_marginStart="32dp" android:layout_marginStart="32dp"
android:layout_marginEnd="32dp" android:layout_marginEnd="32dp"
android:text="@string/transform" android:text="@string/export"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" /> app:layout_constraintStart_toStartOf="parent" />

View File

@ -0,0 +1,69 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright 2022 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.
-->
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
tools:context=".ConfigurationActivity">
<TableLayout
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:stretchColumns="1"
android:layout_marginTop="32dp"
android:measureWithLargestChild="true"
android:paddingLeft="24dp"
android:paddingRight="12dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent">
<TableRow
android:layout_weight="1"
android:gravity="center_vertical" >
<TextView
android:text="@string/overlay_text" />
<EditText
android:id="@+id/text_overlay_text"
android:inputType="textMultiLine"
android:layout_width="0dp"
android:layout_height="wrap_content" />
</TableRow>
<TableRow
android:layout_weight="1"
android:gravity="center_vertical" >
<TextView
android:text="@string/overlay_text_color"/>
<Spinner
android:id="@+id/text_overlay_text_color"
android:layout_gravity="right|center_vertical"
android:gravity="right" />
</TableRow>
<TableRow
android:layout_weight="1"
android:gravity="center_vertical" >
<TextView android:text="@string/overlay_alpha" />
<com.google.android.material.slider.Slider
android:id="@+id/text_overlay_alpha_slider"
android:valueFrom="0"
android:value="1"
android:valueTo="1"
android:layout_gravity="right|center_vertical"/>
</TableRow>
</TableLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -69,16 +69,22 @@
android:layout_height="wrap_content" > android:layout_height="wrap_content" >
<TextView <TextView
android:id="@+id/input_text_view"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginBottom="8dp" android:layout_marginBottom="8dp"
android:padding="8dp" android:padding="8dp"
android:text="@string/input_video" /> android:text="@string/input_video_no_sound" />
<FrameLayout <FrameLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" > android:layout_height="wrap_content" >
<ImageView
android:id="@+id/input_image_view"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<androidx.media3.ui.PlayerView <androidx.media3.ui.PlayerView
android:id="@+id/input_player_view" android:id="@+id/input_player_view"
android:layout_width="match_parent" android:layout_width="match_parent"
@ -108,10 +114,11 @@
android:layout_height="wrap_content" > android:layout_height="wrap_content" >
<TextView <TextView
android:id="@+id/output_video_text_view"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_margin="8dp" android:layout_margin="8dp"
android:text="@string/output_video" /> android:text="@string/output_video_playing_sound" />
<TextView <TextView
android:id="@+id/debug_text_view" android:id="@+id/debug_text_view"

View File

@ -41,7 +41,7 @@
<com.google.android.material.slider.RangeSlider <com.google.android.material.slider.RangeSlider
android:id="@+id/trim_bounds_range_slider" android:id="@+id/trim_bounds_range_slider"
android:valueFrom="0.0" android:valueFrom="0.0"
android:valueTo="60.0" android:valueTo="10.0"
android:layout_gravity="right"/> android:layout_gravity="right"/>
</TableRow> </TableRow>
</TableLayout> </TableLayout>

View File

@ -19,6 +19,7 @@
<string name="configuration" translatable="false">Configuration</string> <string name="configuration" translatable="false">Configuration</string>
<string name="select_preset_file_title" translatable="false">Choose preset file</string> <string name="select_preset_file_title" translatable="false">Choose preset file</string>
<string name="select_local_file_title">Choose local file</string> <string name="select_local_file_title">Choose local file</string>
<string name="local_file_picker_failed">File couldn\'t be opened. Please try again.</string>
<string name="remove_audio" translatable="false">Remove audio</string> <string name="remove_audio" translatable="false">Remove audio</string>
<string name="remove_video" translatable="false">Remove video</string> <string name="remove_video" translatable="false">Remove video</string>
<string name="flatten_for_slow_motion" translatable="false">Flatten for slow motion</string> <string name="flatten_for_slow_motion" translatable="false">Flatten for slow motion</string>
@ -29,20 +30,20 @@
<string name="rotate" translatable="false">Rotate video (degrees)</string> <string name="rotate" translatable="false">Rotate video (degrees)</string>
<string name="enable_fallback" translatable="false">Enable fallback</string> <string name="enable_fallback" translatable="false">Enable fallback</string>
<string name="enable_debug_preview" translatable="false">Enable debug preview</string> <string name="enable_debug_preview" translatable="false">Enable debug preview</string>
<string name="abort_slow_export" translatable="false">Abort slow export</string>
<string name="trim" translatable="false">Trim</string> <string name="trim" translatable="false">Trim</string>
<string name="request_sdr_tone_mapping" translatable="false">Request SDR tone-mapping (API 31+)</string> <string name="hdr_mode" translatable="false">HDR mode</string>
<string name="force_interpret_hdr_video_as_sdr" translatable="false">Interpret HDR as SDR (API 29+)</string> <string name="select_audio_effects" translatable="false">Add audio effects</string>
<string name="hdr_editing" translatable="false">HDR editing (API 31+)</string> <string name="select_video_effects" translatable="false">Add video effects</string>
<string name="select_demo_effects" translatable="false">Add demo effects</string>
<string name="periodic_vignette_options" translatable="false">Periodic vignette options</string> <string name="periodic_vignette_options" translatable="false">Periodic vignette options</string>
<string name="no_media_pipe_error" translatable="false">Failed to load MediaPipe processor. Check the README for instructions.</string> <string name="no_media_pipe_error" translatable="false">Failed to load MediaPipeShaderProgram. Check the README for instructions.</string>
<string name="transform" translatable="false">Transform</string> <string name="export" translatable="false">Export</string>
<string name="debug_preview" translatable="false">Debug preview:</string> <string name="debug_preview" translatable="false">Debug preview:</string>
<string name="debug_preview_not_available" translatable="false">No debug preview available.</string> <string name="debug_preview_not_available" translatable="false">No debug preview available.</string>
<string name="transformation_started" translatable="false">Transformation started</string> <string name="export_started" translatable="false">Export started</string>
<string name="transformation_timer" translatable="false">Transformation started %d seconds ago.</string> <string name="export_timer" translatable="false">Export started %d seconds ago.</string>
<string name="transformation_completed" translatable="false">Transformation completed in %d seconds.</string> <string name="export_completed" translatable="false">Export completed in %d seconds.\nOutput: %s</string>
<string name="transformation_error" translatable="false">Transformation error</string> <string name="export_error" translatable="false">Export error</string>
<string name="trim_range">Bounds in seconds</string> <string name="trim_range">Bounds in seconds</string>
<string-array name="color_filter_options"> <string-array name="color_filter_options">
<item>Grayscale</item> <item>Grayscale</item>
@ -61,9 +62,20 @@
<string name="hue_adjustment">Hue adjustment</string> <string name="hue_adjustment">Hue adjustment</string>
<string name="saturation_adjustment">Saturation adjustment</string> <string name="saturation_adjustment">Saturation adjustment</string>
<string name="lightness_adjustment">Lightness adjustment</string> <string name="lightness_adjustment">Lightness adjustment</string>
<string name="input_video">Input video:</string> <string name="input_image">Input image:</string>
<string name="output_video">Output video:</string> <string name="input_video_no_sound">Input video (tap to play sound):</string>
<string name="input_video_playing_sound">Input video (sound playing):</string>
<string name="output_video_no_sound">Output video (tap to play sound):</string>
<string name="output_video_playing_sound">Output video (sound playing):</string>
<string name="permission_denied">Permission Denied</string> <string name="permission_denied">Permission Denied</string>
<string name="hide_input_video">Hide input video</string> <string name="hide_input_video">Hide input video</string>
<string name="show_input_video">Show input video</string> <string name="show_input_video">Show input video</string>
<string name="force_audio_track">Force audio track</string>
<string name="overlay_alpha">Alpha</string>
<string name="overlay_uri">Uri</string>
<string name="bitmap_overlay_settings">Specify bitmap overlay settings</string>
<string name="select_local_image">Select local image</string>
<string name="overlay_text">Text</string>
<string name="overlay_text_color">Text color</string>
<string name="text_overlay_settings">Specify text overlay settings</string>
</resources> </resources>

View File

@ -24,11 +24,13 @@ import android.content.Context;
import android.opengl.EGL14; import android.opengl.EGL14;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.media3.common.C; import androidx.media3.common.C;
import androidx.media3.common.FrameProcessingException; import androidx.media3.common.GlObjectsProvider;
import androidx.media3.common.GlTextureInfo;
import androidx.media3.common.VideoFrameProcessingException;
import androidx.media3.common.util.LibraryLoader; import androidx.media3.common.util.LibraryLoader;
import androidx.media3.common.util.Util; import androidx.media3.common.util.Util;
import androidx.media3.effect.GlTextureProcessor; import androidx.media3.effect.GlShaderProgram;
import androidx.media3.effect.TextureInfo; import com.google.common.util.concurrent.MoreExecutors;
import com.google.mediapipe.components.FrameProcessor; import com.google.mediapipe.components.FrameProcessor;
import com.google.mediapipe.framework.AppTextureFrame; import com.google.mediapipe.framework.AppTextureFrame;
import com.google.mediapipe.framework.TextureFrame; import com.google.mediapipe.framework.TextureFrame;
@ -37,13 +39,14 @@ import java.util.ArrayDeque;
import java.util.Queue; import java.util.Queue;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService; import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future; import java.util.concurrent.Future;
/** Runs a MediaPipe graph on input frames. */ /** Runs a MediaPipe graph on input frames. */
/* package */ final class MediaPipeProcessor implements GlTextureProcessor { /* package */ final class MediaPipeShaderProgram implements GlShaderProgram {
private static final String THREAD_NAME = "Demo:MediaPipeProcessor"; private static final String THREAD_NAME = "Demo:MediaPipeShaderProgram";
private static final long RELEASE_WAIT_TIME_MS = 100; private static final long RELEASE_WAIT_TIME_MS = 100;
private static final long RETRY_WAIT_TIME_MS = 1; private static final long RETRY_WAIT_TIME_MS = 1;
@ -66,7 +69,7 @@ import java.util.concurrent.Future;
} }
private final FrameProcessor frameProcessor; private final FrameProcessor frameProcessor;
private final ConcurrentHashMap<TextureInfo, TextureFrame> outputFrames; private final ConcurrentHashMap<GlTextureInfo, TextureFrame> outputFrames;
private final boolean isSingleFrameGraph; private final boolean isSingleFrameGraph;
@Nullable private final ExecutorService singleThreadExecutorService; @Nullable private final ExecutorService singleThreadExecutorService;
private final Queue<Future<?>> futures; private final Queue<Future<?>> futures;
@ -74,14 +77,15 @@ import java.util.concurrent.Future;
private InputListener inputListener; private InputListener inputListener;
private OutputListener outputListener; private OutputListener outputListener;
private ErrorListener errorListener; private ErrorListener errorListener;
private Executor errorListenerExecutor;
private boolean acceptedFrame; private boolean acceptedFrame;
/** /**
* Creates a new texture processor that wraps a MediaPipe graph. * Creates a new shader program that wraps a MediaPipe graph.
* *
* <p>If {@code isSingleFrameGraph} is {@code false}, the {@code MediaPipeProcessor} may waste CPU * <p>If {@code isSingleFrameGraph} is {@code false}, the {@code MediaPipeShaderProgram} may waste
* time by continuously attempting to queue input frames to MediaPipe until they are accepted or * CPU time by continuously attempting to queue input frames to MediaPipe until they are accepted
* waste memory if MediaPipe accepts and stores many frames internally. * or waste memory if MediaPipe accepts and stores many frames internally.
* *
* @param context The {@link Context}. * @param context The {@link Context}.
* @param useHdr Whether input textures come from an HDR source. If {@code true}, colors will be * @param useHdr Whether input textures come from an HDR source. If {@code true}, colors will be
@ -92,7 +96,7 @@ import java.util.concurrent.Future;
* @param inputStreamName Name of the input video stream in the graph. * @param inputStreamName Name of the input video stream in the graph.
* @param outputStreamName Name of the input video stream in the graph. * @param outputStreamName Name of the input video stream in the graph.
*/ */
public MediaPipeProcessor( public MediaPipeShaderProgram(
Context context, Context context,
boolean useHdr, boolean useHdr,
String graphName, String graphName,
@ -100,8 +104,8 @@ import java.util.concurrent.Future;
String inputStreamName, String inputStreamName,
String outputStreamName) { String outputStreamName) {
checkState(LOADER.isAvailable()); checkState(LOADER.isAvailable());
// TODO(b/227624622): Confirm whether MediaPipeProcessor could support HDR colors. // TODO(b/227624622): Confirm whether MediaPipeShaderProgram could support HDR colors.
checkArgument(!useHdr, "MediaPipeProcessor does not support HDR colors."); checkArgument(!useHdr, "MediaPipeShaderProgram does not support HDR colors.");
this.isSingleFrameGraph = isSingleFrameGraph; this.isSingleFrameGraph = isSingleFrameGraph;
singleThreadExecutorService = singleThreadExecutorService =
@ -109,7 +113,8 @@ import java.util.concurrent.Future;
futures = new ArrayDeque<>(); futures = new ArrayDeque<>();
inputListener = new InputListener() {}; inputListener = new InputListener() {};
outputListener = new OutputListener() {}; outputListener = new OutputListener() {};
errorListener = (frameProcessingException) -> {}; errorListener = (videoFrameProcessingException) -> {};
errorListenerExecutor = MoreExecutors.directExecutor();
EglManager eglManager = new EglManager(EGL14.eglGetCurrentContext()); EglManager eglManager = new EglManager(EGL14.eglGetCurrentContext());
frameProcessor = frameProcessor =
new FrameProcessor( new FrameProcessor(
@ -133,10 +138,11 @@ import java.util.concurrent.Future;
this.outputListener = outputListener; this.outputListener = outputListener;
frameProcessor.setConsumer( frameProcessor.setConsumer(
frame -> { frame -> {
TextureInfo texture = GlTextureInfo texture =
new TextureInfo( new GlTextureInfo(
frame.getTextureName(), frame.getTextureName(),
/* fboId= */ C.INDEX_UNSET, /* fboId= */ C.INDEX_UNSET,
/* rboId= */ C.INDEX_UNSET,
frame.getWidth(), frame.getWidth(),
frame.getHeight()); frame.getHeight());
outputFrames.put(texture, frame); outputFrames.put(texture, frame);
@ -145,16 +151,23 @@ import java.util.concurrent.Future;
} }
@Override @Override
public void setErrorListener(ErrorListener errorListener) { public void setErrorListener(Executor executor, ErrorListener errorListener) {
this.errorListenerExecutor = executor;
this.errorListener = errorListener; this.errorListener = errorListener;
frameProcessor.setAsynchronousErrorListener( frameProcessor.setAsynchronousErrorListener(
error -> errorListener.onFrameProcessingError(new FrameProcessingException(error))); error ->
errorListenerExecutor.execute(
() -> errorListener.onError(new VideoFrameProcessingException(error))));
} }
@Override @Override
public void queueInputFrame(TextureInfo inputTexture, long presentationTimeUs) { public void setGlObjectsProvider(GlObjectsProvider glObjectsProvider) {}
@Override
public void queueInputFrame(GlTextureInfo inputTexture, long presentationTimeUs) {
AppTextureFrame appTextureFrame = AppTextureFrame appTextureFrame =
new AppTextureFrame(inputTexture.texId, inputTexture.width, inputTexture.height); new AppTextureFrame(
inputTexture.getTexId(), inputTexture.getWidth(), inputTexture.getHeight());
// TODO(b/238302213): Handle timestamps restarting from 0 when applying effects to a playlist. // TODO(b/238302213): Handle timestamps restarting from 0 when applying effects to a playlist.
// MediaPipe will fail if the timestamps are not monotonically increasing. // MediaPipe will fail if the timestamps are not monotonically increasing.
// Also make sure that a MediaPipe graph producing additional frames only starts producing // Also make sure that a MediaPipe graph producing additional frames only starts producing
@ -176,14 +189,15 @@ import java.util.concurrent.Future;
} }
private boolean maybeQueueInputFrameSynchronous( private boolean maybeQueueInputFrameSynchronous(
AppTextureFrame appTextureFrame, TextureInfo inputTexture) { AppTextureFrame appTextureFrame, GlTextureInfo inputTexture) {
acceptedFrame = false; acceptedFrame = false;
frameProcessor.onNewFrame(appTextureFrame); frameProcessor.onNewFrame(appTextureFrame);
try { try {
appTextureFrame.waitUntilReleasedWithGpuSync(); appTextureFrame.waitUntilReleasedWithGpuSync();
} catch (InterruptedException e) { } catch (InterruptedException e) {
Thread.currentThread().interrupt(); Thread.currentThread().interrupt();
errorListener.onFrameProcessingError(new FrameProcessingException(e)); errorListenerExecutor.execute(
() -> errorListener.onError(new VideoFrameProcessingException(e)));
} }
if (acceptedFrame) { if (acceptedFrame) {
inputListener.onInputFrameProcessed(inputTexture); inputListener.onInputFrameProcessed(inputTexture);
@ -192,7 +206,7 @@ import java.util.concurrent.Future;
} }
private void queueInputFrameAsynchronous( private void queueInputFrameAsynchronous(
AppTextureFrame appTextureFrame, TextureInfo inputTexture) { AppTextureFrame appTextureFrame, GlTextureInfo inputTexture) {
removeFinishedFutures(); removeFinishedFutures();
futures.add( futures.add(
checkStateNotNull(singleThreadExecutorService) checkStateNotNull(singleThreadExecutorService)
@ -204,7 +218,8 @@ import java.util.concurrent.Future;
} catch (InterruptedException e) { } catch (InterruptedException e) {
Thread.currentThread().interrupt(); Thread.currentThread().interrupt();
if (errorListener != null) { if (errorListener != null) {
errorListener.onFrameProcessingError(new FrameProcessingException(e)); errorListenerExecutor.execute(
() -> errorListener.onError(new VideoFrameProcessingException(e)));
} }
} }
} }
@ -213,13 +228,19 @@ import java.util.concurrent.Future;
} }
@Override @Override
public void releaseOutputFrame(TextureInfo outputTexture) { public void releaseOutputFrame(GlTextureInfo outputTexture) {
checkStateNotNull(outputFrames.get(outputTexture)).release(); checkStateNotNull(outputFrames.get(outputTexture)).release();
if (isSingleFrameGraph) { if (isSingleFrameGraph) {
inputListener.onReadyToAcceptInputFrame(); inputListener.onReadyToAcceptInputFrame();
} }
} }
@Override
public void flush() {
// TODO(b/238302341) Support seeking in MediaPipeShaderProgram.
throw new UnsupportedOperationException();
}
@Override @Override
public void release() { public void release() {
if (isSingleFrameGraph) { if (isSingleFrameGraph) {
@ -236,11 +257,13 @@ import java.util.concurrent.Future;
singleThreadExecutorService.shutdown(); singleThreadExecutorService.shutdown();
try { try {
if (!singleThreadExecutorService.awaitTermination(RELEASE_WAIT_TIME_MS, MILLISECONDS)) { if (!singleThreadExecutorService.awaitTermination(RELEASE_WAIT_TIME_MS, MILLISECONDS)) {
errorListener.onFrameProcessingError(new FrameProcessingException("Release timed out")); errorListenerExecutor.execute(
() -> errorListener.onError(new VideoFrameProcessingException("Release timed out")));
} }
} catch (InterruptedException e) { } catch (InterruptedException e) {
Thread.currentThread().interrupt(); Thread.currentThread().interrupt();
errorListener.onFrameProcessingError(new FrameProcessingException(e)); errorListenerExecutor.execute(
() -> errorListener.onError(new VideoFrameProcessingException(e)));
} }
frameProcessor.close(); frameProcessor.close();
@ -272,10 +295,12 @@ import java.util.concurrent.Future;
try { try {
futures.remove().get(); futures.remove().get();
} catch (ExecutionException e) { } catch (ExecutionException e) {
errorListener.onFrameProcessingError(new FrameProcessingException(e)); errorListenerExecutor.execute(
() -> errorListener.onError(new VideoFrameProcessingException(e)));
} catch (InterruptedException e) { } catch (InterruptedException e) {
Thread.currentThread().interrupt(); Thread.currentThread().interrupt();
errorListener.onFrameProcessingError(new FrameProcessingException(e)); errorListenerExecutor.execute(
() -> errorListener.onError(new VideoFrameProcessingException(e)));
} }
} }
} }

View File

@ -17,11 +17,6 @@ apply from: "${buildscript.sourceFile.parentFile}/javadoc_util.gradle"
class CombinedJavadocPlugin implements Plugin<Project> { class CombinedJavadocPlugin implements Plugin<Project> {
static final String JAVADOC_TASK_NAME = "generateCombinedJavadoc" static final String JAVADOC_TASK_NAME = "generateCombinedJavadoc"
static final String DACKKA_TASK_NAME = "generateCombinedDackka"
// Dackka snapshots are listed at https://androidx.dev/dackka/builds.
static final String DACKKA_JAR_URL =
"https://androidx.dev/dackka/builds/9221390/artifacts/dackka-1.0.4-all.jar"
@Override @Override
void apply(Project project) { void apply(Project project) {
@ -83,71 +78,6 @@ class CombinedJavadocPlugin implements Plugin<Project> {
project.fixJavadoc() project.fixJavadoc()
} }
} }
def dackkaOutputDir = project.file("$project.buildDir/docs/dackka")
project.task(DACKKA_TASK_NAME, type: JavaExec) {
doFirst {
// Recreate the output directory to remove any leftover files from a previous run.
project.delete dackkaOutputDir
project.mkdir dackkaOutputDir
// Download the Dackka JAR.
new URL(DACKKA_JAR_URL).withInputStream {
i -> classpath.getSingleFile().withOutputStream { it << i }
}
// Build lists of source files and dependencies.
def sources = []
def dependencies = []
libraryModules.each { libraryModule ->
libraryModule.android.libraryVariants.all { variant ->
def name = variant.buildType.name
if (name == "release") {
def classpathFiles =
project.files(variant.javaCompileProvider.get().classpath.files)
variant.sourceSets.inject(sources) {
acc, val -> acc << val.javaDirectories
}
dependencies << classpathFiles.filter { f -> !(f.path.contains("/buildout/")) }
dependencies << libraryModule.project.android.getBootClasspath()
}
}
}
// Set command line arguments to Dackka.
def guavaPackageListFile = getGuavaPackageListFile(getTemporaryDir())
def globalLinksString = "$guavaReferenceUrl^$guavaPackageListFile^^"
def sourcesString = project.files(sources.flatten())
.filter({ f -> project.file(f).exists() }).join(";")
def dependenciesString = project.files(dependencies).asPath.replace(':', ';')
def sourceSet = [
"-src", sourcesString,
"-classpath", dependenciesString,
"-documentedVisibilities", "PUBLIC;PROTECTED"
].join(" ")
args("-moduleName", "",
"-outputDir", "$dackkaOutputDir",
"-globalLinks", "$globalLinksString",
"-loggingLevel", "WARN",
"-sourceSet", "$sourceSet",
"-offlineMode")
environment("DEVSITE_TENANT", "androidx/media3")
}
description = "Generates combined javadoc for developer.android.com."
classpath = project.files(new File(getTemporaryDir(), "dackka.jar"))
doLast {
libraryModules.each { libraryModule ->
project.copy {
from "${libraryModule.projectDir}/src/main/javadoc"
into "${dackkaOutputDir}/reference/"
}
project.copy {
from "${libraryModule.projectDir}/src/main/javadoc"
into "${dackkaOutputDir}/reference/kotlin/"
}
}
}
}
} }
} }
} }
@ -160,17 +90,6 @@ class CombinedJavadocPlugin implements Plugin<Project> {
} }
} }
// Returns a file containing the list of packages that should be linked to Guava documentation.
private static File getGuavaPackageListFile(File directory) {
def packageListFile = new File(directory, "guava")
packageListFile.text = ["com.google.common.base", "com.google.common.collect",
"com.google.common.io", "com.google.common.math",
"com.google.common.net", "com.google.common.primitives",
"com.google.common.truth", "com.google.common.util.concurrent"]
.join('\n')
return packageListFile
}
} }
apply plugin: CombinedJavadocPlugin apply plugin: CombinedJavadocPlugin

View File

@ -14,7 +14,7 @@
apply from: "$gradle.ext.androidxMediaSettingsDir/common_library_config.gradle" apply from: "$gradle.ext.androidxMediaSettingsDir/common_library_config.gradle"
dependencies { dependencies {
api 'com.google.android.gms:play-services-cast-framework:21.2.0' api 'com.google.android.gms:play-services-cast-framework:21.3.0'
implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion
implementation project(modulePrefix + 'lib-common') implementation project(modulePrefix + 'lib-common')
compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion

View File

@ -85,7 +85,7 @@ public final class CastPlayer extends BasePlayer {
/** The {@link DeviceInfo} returned by {@link #getDeviceInfo() this player}. */ /** The {@link DeviceInfo} returned by {@link #getDeviceInfo() this player}. */
public static final DeviceInfo DEVICE_INFO = public static final DeviceInfo DEVICE_INFO =
new DeviceInfo(DeviceInfo.PLAYBACK_TYPE_REMOTE, /* minVolume= */ 0, /* maxVolume= */ 0); new DeviceInfo.Builder(DeviceInfo.PLAYBACK_TYPE_REMOTE).build();
static { static {
MediaLibraryInfo.registerModule("media3.cast"); MediaLibraryInfo.registerModule("media3.cast");
@ -104,11 +104,12 @@ public final class CastPlayer extends BasePlayer {
COMMAND_SET_SPEED_AND_PITCH, COMMAND_SET_SPEED_AND_PITCH,
COMMAND_GET_CURRENT_MEDIA_ITEM, COMMAND_GET_CURRENT_MEDIA_ITEM,
COMMAND_GET_TIMELINE, COMMAND_GET_TIMELINE,
COMMAND_GET_MEDIA_ITEMS_METADATA, COMMAND_GET_METADATA,
COMMAND_SET_MEDIA_ITEMS_METADATA, COMMAND_SET_PLAYLIST_METADATA,
COMMAND_SET_MEDIA_ITEM, COMMAND_SET_MEDIA_ITEM,
COMMAND_CHANGE_MEDIA_ITEMS, COMMAND_CHANGE_MEDIA_ITEMS,
COMMAND_GET_TRACKS) COMMAND_GET_TRACKS,
COMMAND_RELEASE)
.build(); .build();
public static final float MIN_SPEED_SUPPORTED = 0.5f; public static final float MIN_SPEED_SUPPORTED = 0.5f;
@ -320,6 +321,18 @@ public final class CastPlayer extends BasePlayer {
moveMediaItemsInternal(uids, fromIndex, newIndex); moveMediaItemsInternal(uids, fromIndex, newIndex);
} }
@Override
public void replaceMediaItems(int fromIndex, int toIndex, List<MediaItem> mediaItems) {
checkArgument(fromIndex >= 0 && fromIndex <= toIndex);
int playlistSize = currentTimeline.getWindowCount();
if (fromIndex > playlistSize) {
return;
}
toIndex = min(toIndex, playlistSize);
addMediaItems(toIndex, mediaItems);
removeMediaItems(fromIndex, toIndex);
}
@Override @Override
public void removeMediaItems(int fromIndex, int toIndex) { public void removeMediaItems(int fromIndex, int toIndex) {
checkArgument(fromIndex >= 0 && toIndex >= fromIndex); checkArgument(fromIndex >= 0 && toIndex >= fromIndex);
@ -393,8 +406,9 @@ public final class CastPlayer extends BasePlayer {
return playWhenReady.value; return playWhenReady.value;
} }
// We still call Listener#onSeekProcessed() for backwards compatibility with listeners that // We still call Listener#onPositionDiscontinuity(@DiscontinuityReason int) for backwards
// don't implement onPositionDiscontinuity(). // compatibility with listeners that don't implement
// onPositionDiscontinuity(PositionInfo, PositionInfo, @DiscontinuityReason int).
@SuppressWarnings("deprecation") @SuppressWarnings("deprecation")
@Override @Override
@VisibleForTesting(otherwise = PROTECTED) @VisibleForTesting(otherwise = PROTECTED)
@ -448,8 +462,6 @@ public final class CastPlayer extends BasePlayer {
} }
} }
updateAvailableCommandsAndNotifyIfChanged(); updateAvailableCommandsAndNotifyIfChanged();
} else if (pendingSeekCount == 0) {
listeners.queueEvent(/* eventFlag= */ C.INDEX_UNSET, Listener::onSeekProcessed);
} }
listeners.flushEvents(); listeners.flushEvents();
} }
@ -476,17 +488,6 @@ public final class CastPlayer extends BasePlayer {
@Override @Override
public void stop() { public void stop() {
stop(/* reset= */ false);
}
/**
* @deprecated Use {@link #stop()} and {@link #clearMediaItems()} (if {@code reset} is true) or
* just {@link #stop()} (if {@code reset} is false). Any player error will be cleared when
* {@link #prepare() re-preparing} the player.
*/
@Deprecated
@Override
public void stop(boolean reset) {
playbackState = STATE_IDLE; playbackState = STATE_IDLE;
if (remoteMediaClient != null) { if (remoteMediaClient != null) {
// TODO(b/69792021): Support or emulate stop without position reset. // TODO(b/69792021): Support or emulate stop without position reset.
@ -765,22 +766,50 @@ public final class CastPlayer extends BasePlayer {
return false; return false;
} }
/** This method is not supported and does nothing. */ /**
* @deprecated Use {@link #setDeviceVolume(int, int)} instead.
*/
@Deprecated
@Override @Override
public void setDeviceVolume(int volume) {} public void setDeviceVolume(int volume) {}
/** This method is not supported and does nothing. */ /** This method is not supported and does nothing. */
@Override @Override
public void setDeviceVolume(int volume, @C.VolumeFlags int flags) {}
/**
* @deprecated Use {@link #increaseDeviceVolume(int)} instead.
*/
@Deprecated
@Override
public void increaseDeviceVolume() {} public void increaseDeviceVolume() {}
/** This method is not supported and does nothing. */ /** This method is not supported and does nothing. */
@Override @Override
public void increaseDeviceVolume(@C.VolumeFlags int flags) {}
/**
* @deprecated Use {@link #decreaseDeviceVolume(int)} instead.
*/
@Deprecated
@Override
public void decreaseDeviceVolume() {} public void decreaseDeviceVolume() {}
/** This method is not supported and does nothing. */ /** This method is not supported and does nothing. */
@Override @Override
public void decreaseDeviceVolume(@C.VolumeFlags int flags) {}
/**
* @deprecated Use {@link #setDeviceMuted(boolean, int)} instead.
*/
@Deprecated
@Override
public void setDeviceMuted(boolean muted) {} public void setDeviceMuted(boolean muted) {}
/** This method is not supported and does nothing. */
@Override
public void setDeviceMuted(boolean muted, @C.VolumeFlags int flags) {}
// Internal methods. // Internal methods.
// Call deprecated callbacks. // Call deprecated callbacks.
@ -1419,9 +1448,6 @@ public final class CastPlayer extends BasePlayer {
private final class SeekResultCallback implements ResultCallback<MediaChannelResult> { private final class SeekResultCallback implements ResultCallback<MediaChannelResult> {
// We still call Listener#onSeekProcessed() for backwards compatibility with listeners that
// don't implement onPositionDiscontinuity().
@SuppressWarnings("deprecation")
@Override @Override
public void onResult(MediaChannelResult result) { public void onResult(MediaChannelResult result) {
int statusCode = result.getStatus().getStatusCode(); int statusCode = result.getStatus().getStatusCode();
@ -1434,7 +1460,6 @@ public final class CastPlayer extends BasePlayer {
currentWindowIndex = pendingSeekWindowIndex; currentWindowIndex = pendingSeekWindowIndex;
pendingSeekWindowIndex = C.INDEX_UNSET; pendingSeekWindowIndex = C.INDEX_UNSET;
pendingSeekPositionMs = C.TIME_UNSET; pendingSeekPositionMs = C.TIME_UNSET;
listeners.sendEvent(/* eventFlag= */ C.INDEX_UNSET, Listener::onSeekProcessed);
} }
} }
} }

View File

@ -20,7 +20,7 @@ import static androidx.media3.common.Player.COMMAND_CHANGE_MEDIA_ITEMS;
import static androidx.media3.common.Player.COMMAND_GET_AUDIO_ATTRIBUTES; import static androidx.media3.common.Player.COMMAND_GET_AUDIO_ATTRIBUTES;
import static androidx.media3.common.Player.COMMAND_GET_CURRENT_MEDIA_ITEM; import static androidx.media3.common.Player.COMMAND_GET_CURRENT_MEDIA_ITEM;
import static androidx.media3.common.Player.COMMAND_GET_DEVICE_VOLUME; import static androidx.media3.common.Player.COMMAND_GET_DEVICE_VOLUME;
import static androidx.media3.common.Player.COMMAND_GET_MEDIA_ITEMS_METADATA; import static androidx.media3.common.Player.COMMAND_GET_METADATA;
import static androidx.media3.common.Player.COMMAND_GET_TEXT; import static androidx.media3.common.Player.COMMAND_GET_TEXT;
import static androidx.media3.common.Player.COMMAND_GET_TIMELINE; import static androidx.media3.common.Player.COMMAND_GET_TIMELINE;
import static androidx.media3.common.Player.COMMAND_GET_VOLUME; import static androidx.media3.common.Player.COMMAND_GET_VOLUME;
@ -37,7 +37,7 @@ import static androidx.media3.common.Player.COMMAND_SEEK_TO_PREVIOUS;
import static androidx.media3.common.Player.COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM; import static androidx.media3.common.Player.COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM;
import static androidx.media3.common.Player.COMMAND_SET_DEVICE_VOLUME; import static androidx.media3.common.Player.COMMAND_SET_DEVICE_VOLUME;
import static androidx.media3.common.Player.COMMAND_SET_MEDIA_ITEM; import static androidx.media3.common.Player.COMMAND_SET_MEDIA_ITEM;
import static androidx.media3.common.Player.COMMAND_SET_MEDIA_ITEMS_METADATA; import static androidx.media3.common.Player.COMMAND_SET_PLAYLIST_METADATA;
import static androidx.media3.common.Player.COMMAND_SET_REPEAT_MODE; import static androidx.media3.common.Player.COMMAND_SET_REPEAT_MODE;
import static androidx.media3.common.Player.COMMAND_SET_SHUFFLE_MODE; import static androidx.media3.common.Player.COMMAND_SET_SHUFFLE_MODE;
import static androidx.media3.common.Player.COMMAND_SET_SPEED_AND_PITCH; import static androidx.media3.common.Player.COMMAND_SET_SPEED_AND_PITCH;
@ -699,6 +699,38 @@ public class CastPlayerTest {
.queueRemoveItems(new int[] {1, 2, 3, 4, 5}, /* customData= */ null); .queueRemoveItems(new int[] {1, 2, 3, 4, 5}, /* customData= */ null);
} }
@Test
public void replaceMediaItems_callsRemoteMediaClient() {
int[] mediaQueueItemIds = createMediaQueueItemIds(/* numberOfIds= */ 2);
List<MediaItem> mediaItems = createMediaItems(mediaQueueItemIds);
// Add two items.
addMediaItemsAndUpdateTimeline(mediaItems, mediaQueueItemIds);
String uri = "http://www.google.com/video3";
MediaItem anotherMediaItem =
new MediaItem.Builder().setUri(uri).setMimeType(MimeTypes.APPLICATION_MPD).build();
ImmutableList<MediaItem> newPlaylist = ImmutableList.of(mediaItems.get(0), anotherMediaItem);
// Replace item at position 1.
castPlayer.replaceMediaItems(
/* fromIndex= */ 1, /* toIndex= */ 2, ImmutableList.of(anotherMediaItem));
updateTimeLine(
newPlaylist,
/* mediaQueueItemIds= */ new int[] {mediaQueueItemIds[0], 123},
/* currentItemId= */ 123);
verify(mockRemoteMediaClient, times(2))
.queueInsertItems(queueItemsArgumentCaptor.capture(), anyInt(), any());
verify(mockRemoteMediaClient).queueRemoveItems(new int[] {2}, /* customData= */ null);
assertThat(queueItemsArgumentCaptor.getAllValues().get(1)[0])
.isEqualTo(mediaItemConverter.toMediaQueueItem(anotherMediaItem));
Timeline.Window currentWindow =
castPlayer
.getCurrentTimeline()
.getWindow(castPlayer.getCurrentMediaItemIndex(), new Timeline.Window());
assertThat(currentWindow.uid).isEqualTo(123);
assertThat(currentWindow.mediaItem).isEqualTo(anotherMediaItem);
}
@SuppressWarnings("ConstantConditions") @SuppressWarnings("ConstantConditions")
@Test @Test
public void addMediaItems_fillsTimeline() { public void addMediaItems_fillsTimeline() {
@ -1360,8 +1392,8 @@ public class CastPlayerTest {
assertThat(castPlayer.isCommandAvailable(COMMAND_SET_REPEAT_MODE)).isTrue(); assertThat(castPlayer.isCommandAvailable(COMMAND_SET_REPEAT_MODE)).isTrue();
assertThat(castPlayer.isCommandAvailable(COMMAND_GET_CURRENT_MEDIA_ITEM)).isTrue(); assertThat(castPlayer.isCommandAvailable(COMMAND_GET_CURRENT_MEDIA_ITEM)).isTrue();
assertThat(castPlayer.isCommandAvailable(COMMAND_GET_TIMELINE)).isTrue(); assertThat(castPlayer.isCommandAvailable(COMMAND_GET_TIMELINE)).isTrue();
assertThat(castPlayer.isCommandAvailable(COMMAND_GET_MEDIA_ITEMS_METADATA)).isTrue(); assertThat(castPlayer.isCommandAvailable(COMMAND_GET_METADATA)).isTrue();
assertThat(castPlayer.isCommandAvailable(COMMAND_SET_MEDIA_ITEMS_METADATA)).isTrue(); assertThat(castPlayer.isCommandAvailable(COMMAND_SET_PLAYLIST_METADATA)).isTrue();
assertThat(castPlayer.isCommandAvailable(COMMAND_CHANGE_MEDIA_ITEMS)).isTrue(); assertThat(castPlayer.isCommandAvailable(COMMAND_CHANGE_MEDIA_ITEMS)).isTrue();
assertThat(castPlayer.isCommandAvailable(COMMAND_SET_MEDIA_ITEM)).isTrue(); assertThat(castPlayer.isCommandAvailable(COMMAND_SET_MEDIA_ITEM)).isTrue();
assertThat(castPlayer.isCommandAvailable(COMMAND_GET_AUDIO_ATTRIBUTES)).isFalse(); assertThat(castPlayer.isCommandAvailable(COMMAND_GET_AUDIO_ATTRIBUTES)).isFalse();
@ -1372,6 +1404,7 @@ public class CastPlayerTest {
assertThat(castPlayer.isCommandAvailable(COMMAND_ADJUST_DEVICE_VOLUME)).isFalse(); assertThat(castPlayer.isCommandAvailable(COMMAND_ADJUST_DEVICE_VOLUME)).isFalse();
assertThat(castPlayer.isCommandAvailable(COMMAND_SET_VIDEO_SURFACE)).isFalse(); assertThat(castPlayer.isCommandAvailable(COMMAND_SET_VIDEO_SURFACE)).isFalse();
assertThat(castPlayer.isCommandAvailable(COMMAND_GET_TEXT)).isFalse(); assertThat(castPlayer.isCommandAvailable(COMMAND_GET_TEXT)).isFalse();
assertThat(castPlayer.isCommandAvailable(Player.COMMAND_RELEASE)).isTrue();
} }
@Test @Test

View File

@ -311,7 +311,7 @@ public class CastTimelineTrackerTest {
} }
private static MediaInfo getMediaInfo(long durationMs) { private static MediaInfo getMediaInfo(long durationMs) {
return new MediaInfo.Builder(/*contentId= */ "") return new MediaInfo.Builder(/* contentId= */ "")
.setStreamDuration(durationMs) .setStreamDuration(durationMs)
.setContentType(MimeTypes.APPLICATION_MP4) .setContentType(MimeTypes.APPLICATION_MP4)
.setStreamType(MediaInfo.STREAM_TYPE_NONE) .setStreamType(MediaInfo.STREAM_TYPE_NONE)

View File

@ -172,6 +172,10 @@ public final class AdPlaybackState implements Bundleable {
return false; return false;
} }
private boolean isLivePostrollPlaceholder() {
return isServerSideInserted && timeUs == C.TIME_END_OF_SOURCE && count == C.LENGTH_UNSET;
}
@Override @Override
public boolean equals(@Nullable Object o) { public boolean equals(@Nullable Object o) {
if (this == o) { if (this == o) {
@ -629,6 +633,7 @@ public final class AdPlaybackState implements Bundleable {
// Use a linear search as the array elements may not be increasing due to TIME_END_OF_SOURCE. // Use a linear search as the array elements may not be increasing due to TIME_END_OF_SOURCE.
// In practice we expect there to be few ad groups so the search shouldn't be expensive. // In practice we expect there to be few ad groups so the search shouldn't be expensive.
int index = adGroupCount - 1; int index = adGroupCount - 1;
index -= isLivePostrollPlaceholder(index) ? 1 : 0;
while (index >= 0 && isPositionBeforeAdGroup(positionUs, periodDurationUs, index)) { while (index >= 0 && isPositionBeforeAdGroup(positionUs, periodDurationUs, index)) {
index--; index--;
} }
@ -976,6 +981,49 @@ public final class AdPlaybackState implements Bundleable {
adsId, adGroups, adResumePositionUs, contentDurationUs, removedAdGroupCount); adsId, adGroups, adResumePositionUs, contentDurationUs, removedAdGroupCount);
} }
/**
* Appends a live postroll placeholder ad group to the ad playback state.
*
* <p>Adding such a placeholder is only required for periods of server side ad insertion live
* streams.
*
* <p>When building the media period queue, it sets {@link MediaPeriodId#nextAdGroupIndex} of a
* content period to the index of the placeholder. However, the placeholder will not produce a
* period in the media period queue. This only happens when an actual ad group is inserted at the
* given {@code nextAdGroupIndex}. In this case the newly inserted ad group will be used to insert
* an ad period into the media period queue following the content period with the given {@link
* MediaPeriodId#nextAdGroupIndex}.
*
* <p>See {@link #endsWithLivePostrollPlaceHolder()} also.
*
* @return The new ad playback state instance ending with a live postroll placeholder.
*/
public AdPlaybackState withLivePostrollPlaceholderAppended() {
return withNewAdGroup(adGroupCount, /* adGroupTimeUs= */ C.TIME_END_OF_SOURCE)
.withIsServerSideInserted(adGroupCount, true);
}
/**
* Returns whether the last ad group is a live postroll placeholder as inserted by {@link
* #withLivePostrollPlaceholderAppended()}.
*
* @return Whether the ad playback state ends with a live postroll placeholder.
*/
public boolean endsWithLivePostrollPlaceHolder() {
int adGroupIndex = adGroupCount - 1;
return adGroupIndex >= 0 && isLivePostrollPlaceholder(adGroupIndex);
}
/**
* 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.
*/
public boolean isLivePostrollPlaceholder(int adGroupIndex) {
return adGroupIndex == adGroupCount - 1 && getAdGroup(adGroupIndex).isLivePostrollPlaceholder();
}
/** /**
* Returns a copy of the ad playback state with the given ads ID. * Returns a copy of the ad playback state with the given ads ID.
* *
@ -1088,15 +1136,21 @@ public final class AdPlaybackState implements Bundleable {
private boolean isPositionBeforeAdGroup( private boolean isPositionBeforeAdGroup(
long positionUs, long periodDurationUs, int adGroupIndex) { long positionUs, long periodDurationUs, int adGroupIndex) {
if (positionUs == C.TIME_END_OF_SOURCE) { if (positionUs == C.TIME_END_OF_SOURCE) {
// The end of the content is at (but not before) any postroll ad, and after any other ads. // The end of the content is at (but not before) any postroll ad, and after any other ad.
return false; return false;
} }
long adGroupPositionUs = getAdGroup(adGroupIndex).timeUs; AdGroup adGroup = getAdGroup(adGroupIndex);
long adGroupPositionUs = adGroup.timeUs;
if (adGroupPositionUs == C.TIME_END_OF_SOURCE) { if (adGroupPositionUs == C.TIME_END_OF_SOURCE) {
return periodDurationUs == C.TIME_UNSET || positionUs < periodDurationUs; // Handling postroll: The requested position is considered before a postroll when a)
} else { // the period duration is unknown (last period in a live stream), or when b) the postroll is a
return positionUs < adGroupPositionUs; // 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.isServerSideInserted && adGroup.count == C.LENGTH_UNSET)
|| positionUs < periodDurationUs;
} }
return positionUs < adGroupPositionUs;
} }
// Bundleable implementation. // Bundleable implementation.

View File

@ -78,6 +78,12 @@ public abstract class BasePlayer implements Player {
} }
} }
@Override
public final void replaceMediaItem(int index, MediaItem mediaItem) {
replaceMediaItems(
/* fromIndex= */ index, /* toIndex= */ index + 1, ImmutableList.of(mediaItem));
}
@Override @Override
public final void removeMediaItem(int index) { public final void removeMediaItem(int index) {
removeMediaItems(/* fromIndex= */ index, /* toIndex= */ index + 1); removeMediaItems(/* fromIndex= */ index, /* toIndex= */ index + 1);

View File

@ -60,11 +60,13 @@ public final class C {
*/ */
public static final long TIME_UNSET = Long.MIN_VALUE + 1; public static final long TIME_UNSET = Long.MIN_VALUE + 1;
/** Represents an unset or unknown index. */ /** Represents an unset or unknown index or byte position. */
public static final int INDEX_UNSET = -1; public static final int INDEX_UNSET = -1;
/** Represents an unset or unknown position. */ /**
@UnstableApi public static final int POSITION_UNSET = -1; * @deprecated Use {@link #INDEX_UNSET}.
*/
@Deprecated @UnstableApi public static final int POSITION_UNSET = INDEX_UNSET;
/** Represents an unset or unknown rate. */ /** Represents an unset or unknown rate. */
public static final float RATE_UNSET = -Float.MAX_VALUE; public static final float RATE_UNSET = -Float.MAX_VALUE;
@ -93,36 +95,6 @@ public final class C {
/** The number of bytes per float. */ /** The number of bytes per float. */
@UnstableApi public static final int BYTES_PER_FLOAT = 4; @UnstableApi public static final int BYTES_PER_FLOAT = 4;
/**
* @deprecated Use {@link java.nio.charset.StandardCharsets} or {@link
* com.google.common.base.Charsets} instead.
*/
@UnstableApi @Deprecated public static final String ASCII_NAME = "US-ASCII";
/**
* @deprecated Use {@link java.nio.charset.StandardCharsets} or {@link
* com.google.common.base.Charsets} instead.
*/
@UnstableApi @Deprecated public static final String UTF8_NAME = "UTF-8";
/**
* @deprecated Use {@link java.nio.charset.StandardCharsets} or {@link
* com.google.common.base.Charsets} instead.
*/
@UnstableApi @Deprecated public static final String ISO88591_NAME = "ISO-8859-1";
/**
* @deprecated Use {@link java.nio.charset.StandardCharsets} or {@link
* com.google.common.base.Charsets} instead.
*/
@UnstableApi @Deprecated public static final String UTF16_NAME = "UTF-16";
/**
* @deprecated Use {@link java.nio.charset.StandardCharsets} or {@link
* com.google.common.base.Charsets} instead.
*/
@UnstableApi @Deprecated public static final String UTF16LE_NAME = "UTF-16LE";
/** The name of the serif font family. */ /** The name of the serif font family. */
@UnstableApi public static final String SERIF_NAME = "serif"; @UnstableApi public static final String SERIF_NAME = "serif";
@ -170,17 +142,11 @@ public final class C {
@IntDef({CRYPTO_MODE_UNENCRYPTED, CRYPTO_MODE_AES_CTR, CRYPTO_MODE_AES_CBC}) @IntDef({CRYPTO_MODE_UNENCRYPTED, CRYPTO_MODE_AES_CTR, CRYPTO_MODE_AES_CBC})
@UnstableApi @UnstableApi
public @interface CryptoMode {} public @interface CryptoMode {}
/** /** See {@link MediaCodec#CRYPTO_MODE_UNENCRYPTED}. */
* @see MediaCodec#CRYPTO_MODE_UNENCRYPTED
*/
@UnstableApi public static final int CRYPTO_MODE_UNENCRYPTED = MediaCodec.CRYPTO_MODE_UNENCRYPTED; @UnstableApi public static final int CRYPTO_MODE_UNENCRYPTED = MediaCodec.CRYPTO_MODE_UNENCRYPTED;
/** /** See {@link MediaCodec#CRYPTO_MODE_AES_CTR}. */
* @see MediaCodec#CRYPTO_MODE_AES_CTR
*/
@UnstableApi public static final int CRYPTO_MODE_AES_CTR = MediaCodec.CRYPTO_MODE_AES_CTR; @UnstableApi public static final int CRYPTO_MODE_AES_CTR = MediaCodec.CRYPTO_MODE_AES_CTR;
/** /** See {@link MediaCodec#CRYPTO_MODE_AES_CBC}. */
* @see MediaCodec#CRYPTO_MODE_AES_CBC
*/
@UnstableApi public static final int CRYPTO_MODE_AES_CBC = MediaCodec.CRYPTO_MODE_AES_CBC; @UnstableApi public static final int CRYPTO_MODE_AES_CBC = MediaCodec.CRYPTO_MODE_AES_CBC;
/** /**
@ -226,6 +192,7 @@ public final class C {
ENCODING_DTS_HD, ENCODING_DTS_HD,
ENCODING_DOLBY_TRUEHD, ENCODING_DOLBY_TRUEHD,
ENCODING_OPUS, ENCODING_OPUS,
ENCODING_DTS_UHD_P2,
}) })
public @interface Encoding {} public @interface Encoding {}
@ -250,17 +217,11 @@ public final class C {
ENCODING_PCM_FLOAT ENCODING_PCM_FLOAT
}) })
public @interface PcmEncoding {} public @interface PcmEncoding {}
/** /** See {@link AudioFormat#ENCODING_INVALID}. */
* @see AudioFormat#ENCODING_INVALID
*/
@UnstableApi public static final int ENCODING_INVALID = AudioFormat.ENCODING_INVALID; @UnstableApi public static final int ENCODING_INVALID = AudioFormat.ENCODING_INVALID;
/** /** See {@link AudioFormat#ENCODING_PCM_8BIT}. */
* @see AudioFormat#ENCODING_PCM_8BIT
*/
@UnstableApi public static final int ENCODING_PCM_8BIT = AudioFormat.ENCODING_PCM_8BIT; @UnstableApi public static final int ENCODING_PCM_8BIT = AudioFormat.ENCODING_PCM_8BIT;
/** /** See {@link AudioFormat#ENCODING_PCM_16BIT}. */
* @see AudioFormat#ENCODING_PCM_16BIT
*/
@UnstableApi public static final int ENCODING_PCM_16BIT = AudioFormat.ENCODING_PCM_16BIT; @UnstableApi public static final int ENCODING_PCM_16BIT = AudioFormat.ENCODING_PCM_16BIT;
/** Like {@link #ENCODING_PCM_16BIT}, but with the bytes in big endian order. */ /** Like {@link #ENCODING_PCM_16BIT}, but with the bytes in big endian order. */
@UnstableApi public static final int ENCODING_PCM_16BIT_BIG_ENDIAN = 0x10000000; @UnstableApi public static final int ENCODING_PCM_16BIT_BIG_ENDIAN = 0x10000000;
@ -268,67 +229,39 @@ public final class C {
@UnstableApi public static final int ENCODING_PCM_24BIT = 0x20000000; @UnstableApi public static final int ENCODING_PCM_24BIT = 0x20000000;
/** PCM encoding with 32 bits per sample. */ /** PCM encoding with 32 bits per sample. */
@UnstableApi public static final int ENCODING_PCM_32BIT = 0x30000000; @UnstableApi public static final int ENCODING_PCM_32BIT = 0x30000000;
/** /** See {@link AudioFormat#ENCODING_PCM_FLOAT}. */
* @see AudioFormat#ENCODING_PCM_FLOAT
*/
@UnstableApi public static final int ENCODING_PCM_FLOAT = AudioFormat.ENCODING_PCM_FLOAT; @UnstableApi public static final int ENCODING_PCM_FLOAT = AudioFormat.ENCODING_PCM_FLOAT;
/** /** See {@link AudioFormat#ENCODING_MP3}. */
* @see AudioFormat#ENCODING_MP3
*/
@UnstableApi public static final int ENCODING_MP3 = AudioFormat.ENCODING_MP3; @UnstableApi public static final int ENCODING_MP3 = AudioFormat.ENCODING_MP3;
/** /** See {@link AudioFormat#ENCODING_AAC_LC}. */
* @see AudioFormat#ENCODING_AAC_LC
*/
@UnstableApi public static final int ENCODING_AAC_LC = AudioFormat.ENCODING_AAC_LC; @UnstableApi public static final int ENCODING_AAC_LC = AudioFormat.ENCODING_AAC_LC;
/** /** See {@link AudioFormat#ENCODING_AAC_HE_V1}. */
* @see AudioFormat#ENCODING_AAC_HE_V1
*/
@UnstableApi public static final int ENCODING_AAC_HE_V1 = AudioFormat.ENCODING_AAC_HE_V1; @UnstableApi public static final int ENCODING_AAC_HE_V1 = AudioFormat.ENCODING_AAC_HE_V1;
/** /** See {@link AudioFormat#ENCODING_AAC_HE_V2}. */
* @see AudioFormat#ENCODING_AAC_HE_V2
*/
@UnstableApi public static final int ENCODING_AAC_HE_V2 = AudioFormat.ENCODING_AAC_HE_V2; @UnstableApi public static final int ENCODING_AAC_HE_V2 = AudioFormat.ENCODING_AAC_HE_V2;
/** /** See {@link AudioFormat#ENCODING_AAC_XHE}. */
* @see AudioFormat#ENCODING_AAC_XHE
*/
@UnstableApi public static final int ENCODING_AAC_XHE = AudioFormat.ENCODING_AAC_XHE; @UnstableApi public static final int ENCODING_AAC_XHE = AudioFormat.ENCODING_AAC_XHE;
/** /** See {@link AudioFormat#ENCODING_AAC_ELD}. */
* @see AudioFormat#ENCODING_AAC_ELD
*/
@UnstableApi public static final int ENCODING_AAC_ELD = AudioFormat.ENCODING_AAC_ELD; @UnstableApi public static final int ENCODING_AAC_ELD = AudioFormat.ENCODING_AAC_ELD;
/** AAC Error Resilient Bit-Sliced Arithmetic Coding. */ /** AAC Error Resilient Bit-Sliced Arithmetic Coding. */
@UnstableApi public static final int ENCODING_AAC_ER_BSAC = 0x40000000; @UnstableApi public static final int ENCODING_AAC_ER_BSAC = 0x40000000;
/** /** See {@link AudioFormat#ENCODING_AC3}. */
* @see AudioFormat#ENCODING_AC3
*/
@UnstableApi public static final int ENCODING_AC3 = AudioFormat.ENCODING_AC3; @UnstableApi public static final int ENCODING_AC3 = AudioFormat.ENCODING_AC3;
/** /** See {@link AudioFormat#ENCODING_E_AC3}. */
* @see AudioFormat#ENCODING_E_AC3
*/
@UnstableApi public static final int ENCODING_E_AC3 = AudioFormat.ENCODING_E_AC3; @UnstableApi public static final int ENCODING_E_AC3 = AudioFormat.ENCODING_E_AC3;
/** /** See {@link AudioFormat#ENCODING_E_AC3_JOC}. */
* @see AudioFormat#ENCODING_E_AC3_JOC
*/
@UnstableApi public static final int ENCODING_E_AC3_JOC = AudioFormat.ENCODING_E_AC3_JOC; @UnstableApi public static final int ENCODING_E_AC3_JOC = AudioFormat.ENCODING_E_AC3_JOC;
/** /** See {@link AudioFormat#ENCODING_AC4}. */
* @see AudioFormat#ENCODING_AC4
*/
@UnstableApi public static final int ENCODING_AC4 = AudioFormat.ENCODING_AC4; @UnstableApi public static final int ENCODING_AC4 = AudioFormat.ENCODING_AC4;
/** /** See {@link AudioFormat#ENCODING_DTS}. */
* @see AudioFormat#ENCODING_DTS
*/
@UnstableApi public static final int ENCODING_DTS = AudioFormat.ENCODING_DTS; @UnstableApi public static final int ENCODING_DTS = AudioFormat.ENCODING_DTS;
/** /** See {@link AudioFormat#ENCODING_DTS_HD}. */
* @see AudioFormat#ENCODING_DTS_HD
*/
@UnstableApi public static final int ENCODING_DTS_HD = AudioFormat.ENCODING_DTS_HD; @UnstableApi public static final int ENCODING_DTS_HD = AudioFormat.ENCODING_DTS_HD;
/** // TODO(internal b/283949283): Use AudioFormat.ENCODING_DTS_UHD_P2 when Android 14 is released.
* @see AudioFormat#ENCODING_DOLBY_TRUEHD @UnstableApi public static final int ENCODING_DTS_UHD_P2 = 0x0000001e;
*/ /** See {@link AudioFormat#ENCODING_DOLBY_TRUEHD}. */
@UnstableApi public static final int ENCODING_DOLBY_TRUEHD = AudioFormat.ENCODING_DOLBY_TRUEHD; @UnstableApi public static final int ENCODING_DOLBY_TRUEHD = AudioFormat.ENCODING_DOLBY_TRUEHD;
/** /** See {@link AudioFormat#ENCODING_OPUS}. */
* @see AudioFormat#ENCODING_OPUS
*/
@UnstableApi public static final int ENCODING_OPUS = AudioFormat.ENCODING_OPUS; @UnstableApi public static final int ENCODING_OPUS = AudioFormat.ENCODING_OPUS;
/** /**
@ -341,14 +274,10 @@ public final class C {
@IntDef({SPATIALIZATION_BEHAVIOR_AUTO, SPATIALIZATION_BEHAVIOR_NEVER}) @IntDef({SPATIALIZATION_BEHAVIOR_AUTO, SPATIALIZATION_BEHAVIOR_NEVER})
public @interface SpatializationBehavior {} public @interface SpatializationBehavior {}
/** /** See {@link AudioAttributes#SPATIALIZATION_BEHAVIOR_AUTO}. */
* @see AudioAttributes#SPATIALIZATION_BEHAVIOR_AUTO
*/
public static final int SPATIALIZATION_BEHAVIOR_AUTO = public static final int SPATIALIZATION_BEHAVIOR_AUTO =
AudioAttributes.SPATIALIZATION_BEHAVIOR_AUTO; AudioAttributes.SPATIALIZATION_BEHAVIOR_AUTO;
/** /** See {@link AudioAttributes#SPATIALIZATION_BEHAVIOR_NEVER}. */
* @see AudioAttributes#SPATIALIZATION_BEHAVIOR_NEVER
*/
public static final int SPATIALIZATION_BEHAVIOR_NEVER = public static final int SPATIALIZATION_BEHAVIOR_NEVER =
AudioAttributes.SPATIALIZATION_BEHAVIOR_NEVER; AudioAttributes.SPATIALIZATION_BEHAVIOR_NEVER;
@ -376,37 +305,54 @@ public final class C {
STREAM_TYPE_DEFAULT STREAM_TYPE_DEFAULT
}) })
public @interface StreamType {} public @interface StreamType {}
/** /** See {@link AudioManager#STREAM_ALARM}. */
* @see AudioManager#STREAM_ALARM
*/
@UnstableApi public static final int STREAM_TYPE_ALARM = AudioManager.STREAM_ALARM; @UnstableApi public static final int STREAM_TYPE_ALARM = AudioManager.STREAM_ALARM;
/** /** See {@link AudioManager#STREAM_DTMF}. */
* @see AudioManager#STREAM_DTMF
*/
@UnstableApi public static final int STREAM_TYPE_DTMF = AudioManager.STREAM_DTMF; @UnstableApi public static final int STREAM_TYPE_DTMF = AudioManager.STREAM_DTMF;
/** /** See {@link AudioManager#STREAM_MUSIC}. */
* @see AudioManager#STREAM_MUSIC
*/
@UnstableApi public static final int STREAM_TYPE_MUSIC = AudioManager.STREAM_MUSIC; @UnstableApi public static final int STREAM_TYPE_MUSIC = AudioManager.STREAM_MUSIC;
/** /** See {@link AudioManager#STREAM_NOTIFICATION}. */
* @see AudioManager#STREAM_NOTIFICATION
*/
@UnstableApi public static final int STREAM_TYPE_NOTIFICATION = AudioManager.STREAM_NOTIFICATION; @UnstableApi public static final int STREAM_TYPE_NOTIFICATION = AudioManager.STREAM_NOTIFICATION;
/** /** See {@link AudioManager#STREAM_RING}. */
* @see AudioManager#STREAM_RING
*/
@UnstableApi public static final int STREAM_TYPE_RING = AudioManager.STREAM_RING; @UnstableApi public static final int STREAM_TYPE_RING = AudioManager.STREAM_RING;
/** /** See {@link AudioManager#STREAM_SYSTEM}. */
* @see AudioManager#STREAM_SYSTEM
*/
@UnstableApi public static final int STREAM_TYPE_SYSTEM = AudioManager.STREAM_SYSTEM; @UnstableApi public static final int STREAM_TYPE_SYSTEM = AudioManager.STREAM_SYSTEM;
/** /** See {@link AudioManager#STREAM_VOICE_CALL}. */
* @see AudioManager#STREAM_VOICE_CALL
*/
@UnstableApi public static final int STREAM_TYPE_VOICE_CALL = AudioManager.STREAM_VOICE_CALL; @UnstableApi public static final int STREAM_TYPE_VOICE_CALL = AudioManager.STREAM_VOICE_CALL;
/** The default stream type used by audio renderers. Equal to {@link #STREAM_TYPE_MUSIC}. */ /** 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; @UnstableApi public static final int STREAM_TYPE_DEFAULT = STREAM_TYPE_MUSIC;
/**
* Volume flags to be used when setting or adjusting device volume. The value can be either 0 or a
* combination of the following flags: {@link #VOLUME_FLAG_SHOW_UI}, {@link
* #VOLUME_FLAG_ALLOW_RINGER_MODES}, {@link #VOLUME_FLAG_PLAY_SOUND}, {@link
* #VOLUME_FLAG_REMOVE_SOUND_AND_VIBRATE}, {@link #VOLUME_FLAG_VIBRATE}.
*/
@Documented
@Retention(RetentionPolicy.SOURCE)
@Target({TYPE_USE})
@IntDef(
flag = true,
value = {
VOLUME_FLAG_SHOW_UI,
VOLUME_FLAG_ALLOW_RINGER_MODES,
VOLUME_FLAG_PLAY_SOUND,
VOLUME_FLAG_REMOVE_SOUND_AND_VIBRATE,
VOLUME_FLAG_VIBRATE,
})
public @interface VolumeFlags {}
/** See {@link AudioManager#FLAG_SHOW_UI}. */
public static final int VOLUME_FLAG_SHOW_UI = AudioManager.FLAG_SHOW_UI;
/** See {@link AudioManager#FLAG_ALLOW_RINGER_MODES}. */
public static final int VOLUME_FLAG_ALLOW_RINGER_MODES = AudioManager.FLAG_ALLOW_RINGER_MODES;
/** See {@link AudioManager#FLAG_PLAY_SOUND}. */
public static final int VOLUME_FLAG_PLAY_SOUND = AudioManager.FLAG_PLAY_SOUND;
/** See {@link AudioManager#FLAG_REMOVE_SOUND_AND_VIBRATE}. */
public static final int VOLUME_FLAG_REMOVE_SOUND_AND_VIBRATE =
AudioManager.FLAG_REMOVE_SOUND_AND_VIBRATE;
/** See {@link AudioManager#FLAG_VIBRATE}. */
public static final int VOLUME_FLAG_VIBRATE = AudioManager.FLAG_VIBRATE;
/** /**
* Content types for audio attributes. One of: * Content types for audio attributes. One of:
* *
@ -480,9 +426,7 @@ public final class C {
flag = true, flag = true,
value = {FLAG_AUDIBILITY_ENFORCED}) value = {FLAG_AUDIBILITY_ENFORCED})
public @interface AudioFlags {} public @interface AudioFlags {}
/** /** See {@link android.media.AudioAttributes#FLAG_AUDIBILITY_ENFORCED}. */
* @see android.media.AudioAttributes#FLAG_AUDIBILITY_ENFORCED
*/
public static final int FLAG_AUDIBILITY_ENFORCED = public static final int FLAG_AUDIBILITY_ENFORCED =
android.media.AudioAttributes.FLAG_AUDIBILITY_ENFORCED; android.media.AudioAttributes.FLAG_AUDIBILITY_ENFORCED;
@ -520,78 +464,46 @@ public final class C {
USAGE_VOICE_COMMUNICATION_SIGNALLING USAGE_VOICE_COMMUNICATION_SIGNALLING
}) })
public @interface AudioUsage {} public @interface AudioUsage {}
/** /** See {@link android.media.AudioAttributes#USAGE_ALARM}. */
* @see android.media.AudioAttributes#USAGE_ALARM
*/
public static final int USAGE_ALARM = android.media.AudioAttributes.USAGE_ALARM; public static final int USAGE_ALARM = android.media.AudioAttributes.USAGE_ALARM;
/** /** See {@link android.media.AudioAttributes#USAGE_ASSISTANCE_ACCESSIBILITY}. */
* @see android.media.AudioAttributes#USAGE_ASSISTANCE_ACCESSIBILITY
*/
public static final int USAGE_ASSISTANCE_ACCESSIBILITY = public static final int USAGE_ASSISTANCE_ACCESSIBILITY =
android.media.AudioAttributes.USAGE_ASSISTANCE_ACCESSIBILITY; android.media.AudioAttributes.USAGE_ASSISTANCE_ACCESSIBILITY;
/** /** See {@link android.media.AudioAttributes#USAGE_ASSISTANCE_NAVIGATION_GUIDANCE}. */
* @see android.media.AudioAttributes#USAGE_ASSISTANCE_NAVIGATION_GUIDANCE
*/
public static final int USAGE_ASSISTANCE_NAVIGATION_GUIDANCE = public static final int USAGE_ASSISTANCE_NAVIGATION_GUIDANCE =
android.media.AudioAttributes.USAGE_ASSISTANCE_NAVIGATION_GUIDANCE; android.media.AudioAttributes.USAGE_ASSISTANCE_NAVIGATION_GUIDANCE;
/** /** See {@link android.media.AudioAttributes#USAGE_ASSISTANCE_SONIFICATION}. */
* @see android.media.AudioAttributes#USAGE_ASSISTANCE_SONIFICATION
*/
public static final int USAGE_ASSISTANCE_SONIFICATION = public static final int USAGE_ASSISTANCE_SONIFICATION =
android.media.AudioAttributes.USAGE_ASSISTANCE_SONIFICATION; android.media.AudioAttributes.USAGE_ASSISTANCE_SONIFICATION;
/** /** See {@link android.media.AudioAttributes#USAGE_ASSISTANT}. */
* @see android.media.AudioAttributes#USAGE_ASSISTANT
*/
public static final int USAGE_ASSISTANT = android.media.AudioAttributes.USAGE_ASSISTANT; public static final int USAGE_ASSISTANT = android.media.AudioAttributes.USAGE_ASSISTANT;
/** /** See {@link android.media.AudioAttributes#USAGE_GAME}. */
* @see android.media.AudioAttributes#USAGE_GAME
*/
public static final int USAGE_GAME = android.media.AudioAttributes.USAGE_GAME; public static final int USAGE_GAME = android.media.AudioAttributes.USAGE_GAME;
/** /** See {@link android.media.AudioAttributes#USAGE_MEDIA}. */
* @see android.media.AudioAttributes#USAGE_MEDIA
*/
public static final int USAGE_MEDIA = android.media.AudioAttributes.USAGE_MEDIA; public static final int USAGE_MEDIA = android.media.AudioAttributes.USAGE_MEDIA;
/** /** See {@link android.media.AudioAttributes#USAGE_NOTIFICATION}. */
* @see android.media.AudioAttributes#USAGE_NOTIFICATION
*/
public static final int USAGE_NOTIFICATION = android.media.AudioAttributes.USAGE_NOTIFICATION; public static final int USAGE_NOTIFICATION = android.media.AudioAttributes.USAGE_NOTIFICATION;
/** /** See {@link android.media.AudioAttributes#USAGE_NOTIFICATION_COMMUNICATION_DELAYED}. */
* @see android.media.AudioAttributes#USAGE_NOTIFICATION_COMMUNICATION_DELAYED
*/
public static final int USAGE_NOTIFICATION_COMMUNICATION_DELAYED = public static final int USAGE_NOTIFICATION_COMMUNICATION_DELAYED =
android.media.AudioAttributes.USAGE_NOTIFICATION_COMMUNICATION_DELAYED; android.media.AudioAttributes.USAGE_NOTIFICATION_COMMUNICATION_DELAYED;
/** /** See {@link android.media.AudioAttributes#USAGE_NOTIFICATION_COMMUNICATION_INSTANT}. */
* @see android.media.AudioAttributes#USAGE_NOTIFICATION_COMMUNICATION_INSTANT
*/
public static final int USAGE_NOTIFICATION_COMMUNICATION_INSTANT = public static final int USAGE_NOTIFICATION_COMMUNICATION_INSTANT =
android.media.AudioAttributes.USAGE_NOTIFICATION_COMMUNICATION_INSTANT; android.media.AudioAttributes.USAGE_NOTIFICATION_COMMUNICATION_INSTANT;
/** /** See {@link android.media.AudioAttributes#USAGE_NOTIFICATION_COMMUNICATION_REQUEST}. */
* @see android.media.AudioAttributes#USAGE_NOTIFICATION_COMMUNICATION_REQUEST
*/
public static final int USAGE_NOTIFICATION_COMMUNICATION_REQUEST = public static final int USAGE_NOTIFICATION_COMMUNICATION_REQUEST =
android.media.AudioAttributes.USAGE_NOTIFICATION_COMMUNICATION_REQUEST; android.media.AudioAttributes.USAGE_NOTIFICATION_COMMUNICATION_REQUEST;
/** /** See {@link android.media.AudioAttributes#USAGE_NOTIFICATION_EVENT}. */
* @see android.media.AudioAttributes#USAGE_NOTIFICATION_EVENT
*/
public static final int USAGE_NOTIFICATION_EVENT = public static final int USAGE_NOTIFICATION_EVENT =
android.media.AudioAttributes.USAGE_NOTIFICATION_EVENT; android.media.AudioAttributes.USAGE_NOTIFICATION_EVENT;
/** /** See {@link android.media.AudioAttributes#USAGE_NOTIFICATION_RINGTONE}. */
* @see android.media.AudioAttributes#USAGE_NOTIFICATION_RINGTONE
*/
public static final int USAGE_NOTIFICATION_RINGTONE = public static final int USAGE_NOTIFICATION_RINGTONE =
android.media.AudioAttributes.USAGE_NOTIFICATION_RINGTONE; android.media.AudioAttributes.USAGE_NOTIFICATION_RINGTONE;
/** /** See {@link android.media.AudioAttributes#USAGE_UNKNOWN}. */
* @see android.media.AudioAttributes#USAGE_UNKNOWN
*/
public static final int USAGE_UNKNOWN = android.media.AudioAttributes.USAGE_UNKNOWN; public static final int USAGE_UNKNOWN = android.media.AudioAttributes.USAGE_UNKNOWN;
/** /** See {@link android.media.AudioAttributes#USAGE_VOICE_COMMUNICATION}. */
* @see android.media.AudioAttributes#USAGE_VOICE_COMMUNICATION
*/
public static final int USAGE_VOICE_COMMUNICATION = public static final int USAGE_VOICE_COMMUNICATION =
android.media.AudioAttributes.USAGE_VOICE_COMMUNICATION; android.media.AudioAttributes.USAGE_VOICE_COMMUNICATION;
/** /** See {@link android.media.AudioAttributes#USAGE_VOICE_COMMUNICATION_SIGNALLING}. */
* @see android.media.AudioAttributes#USAGE_VOICE_COMMUNICATION_SIGNALLING
*/
public static final int USAGE_VOICE_COMMUNICATION_SIGNALLING = public static final int USAGE_VOICE_COMMUNICATION_SIGNALLING =
android.media.AudioAttributes.USAGE_VOICE_COMMUNICATION_SIGNALLING; android.media.AudioAttributes.USAGE_VOICE_COMMUNICATION_SIGNALLING;
@ -1054,8 +966,8 @@ public final class C {
// LINT.IfChange(color_space) // LINT.IfChange(color_space)
/** /**
* Video colorspaces. One of {@link Format#NO_VALUE}, {@link #COLOR_SPACE_BT601}, {@link * Video color spaces, also referred to as color standards. One of {@link Format#NO_VALUE}, {@link
* #COLOR_SPACE_BT709} or {@link #COLOR_SPACE_BT2020}. * #COLOR_SPACE_BT601}, {@link #COLOR_SPACE_BT709} or {@link #COLOR_SPACE_BT2020}.
*/ */
@UnstableApi @UnstableApi
@Documented @Documented
@ -1063,41 +975,52 @@ public final class C {
@Target(TYPE_USE) @Target(TYPE_USE)
@IntDef({Format.NO_VALUE, COLOR_SPACE_BT601, COLOR_SPACE_BT709, COLOR_SPACE_BT2020}) @IntDef({Format.NO_VALUE, COLOR_SPACE_BT601, COLOR_SPACE_BT709, COLOR_SPACE_BT2020})
public @interface ColorSpace {} public @interface ColorSpace {}
/** /** See {@link MediaFormat#COLOR_STANDARD_BT601_PAL}. */
* @see MediaFormat#COLOR_STANDARD_BT601_PAL
*/
@UnstableApi public static final int COLOR_SPACE_BT601 = MediaFormat.COLOR_STANDARD_BT601_PAL; @UnstableApi public static final int COLOR_SPACE_BT601 = MediaFormat.COLOR_STANDARD_BT601_PAL;
/** /** See {@link MediaFormat#COLOR_STANDARD_BT709}. */
* @see MediaFormat#COLOR_STANDARD_BT709
*/
@UnstableApi public static final int COLOR_SPACE_BT709 = MediaFormat.COLOR_STANDARD_BT709; @UnstableApi public static final int COLOR_SPACE_BT709 = MediaFormat.COLOR_STANDARD_BT709;
/** /** See {@link MediaFormat#COLOR_STANDARD_BT2020}. */
* @see MediaFormat#COLOR_STANDARD_BT2020
*/
@UnstableApi public static final int COLOR_SPACE_BT2020 = MediaFormat.COLOR_STANDARD_BT2020; @UnstableApi public static final int COLOR_SPACE_BT2020 = MediaFormat.COLOR_STANDARD_BT2020;
// LINT.IfChange(color_transfer) // LINT.IfChange(color_transfer)
/** /**
* Video color transfer characteristics. One of {@link Format#NO_VALUE}, {@link * Video/image color transfer characteristics. One of {@link Format#NO_VALUE}, {@link
* #COLOR_TRANSFER_SDR}, {@link #COLOR_TRANSFER_ST2084} or {@link #COLOR_TRANSFER_HLG}. * #COLOR_TRANSFER_LINEAR}, {@link #COLOR_TRANSFER_SDR}, {@link #COLOR_TRANSFER_SRGB}, {@link
* #COLOR_TRANSFER_GAMMA_2_2}, {@link #COLOR_TRANSFER_ST2084} or {@link #COLOR_TRANSFER_HLG}.
*/ */
@UnstableApi @UnstableApi
@Documented @Documented
@Retention(RetentionPolicy.SOURCE) @Retention(RetentionPolicy.SOURCE)
@Target(TYPE_USE) @Target(TYPE_USE)
@IntDef({Format.NO_VALUE, COLOR_TRANSFER_SDR, COLOR_TRANSFER_ST2084, COLOR_TRANSFER_HLG}) @IntDef({
Format.NO_VALUE,
COLOR_TRANSFER_LINEAR,
COLOR_TRANSFER_SDR,
COLOR_TRANSFER_SRGB,
COLOR_TRANSFER_GAMMA_2_2,
COLOR_TRANSFER_ST2084,
COLOR_TRANSFER_HLG
})
public @interface ColorTransfer {} public @interface ColorTransfer {}
/** /** See {@link MediaFormat#COLOR_TRANSFER_LINEAR}. */
* @see MediaFormat#COLOR_TRANSFER_SDR_VIDEO @UnstableApi public static final int COLOR_TRANSFER_LINEAR = MediaFormat.COLOR_TRANSFER_LINEAR;
*/ /** See {@link MediaFormat#COLOR_TRANSFER_SDR_VIDEO}. The SMPTE 170M transfer function. */
@UnstableApi public static final int COLOR_TRANSFER_SDR = MediaFormat.COLOR_TRANSFER_SDR_VIDEO; @UnstableApi public static final int COLOR_TRANSFER_SDR = MediaFormat.COLOR_TRANSFER_SDR_VIDEO;
/** /**
* @see MediaFormat#COLOR_TRANSFER_ST2084 * See {@link android.hardware.DataSpace#TRANSFER_SRGB}. The standard RGB transfer function, used
* for some SDR use-cases like image input.
*/ */
@UnstableApi public static final int COLOR_TRANSFER_ST2084 = MediaFormat.COLOR_TRANSFER_ST2084; // Value sourced from ordering here:
// https://cs.android.com/android/platform/superproject/+/master:frameworks/native/headers/media_plugin/media/hardware/VideoAPI.h;drc=55e9bd7c487ee235631f302ab8626776547ac913;l=138.
@UnstableApi public static final int COLOR_TRANSFER_SRGB = 2;
/** /**
* @see MediaFormat#COLOR_TRANSFER_HLG * See {@link android.hardware.DataSpace#TRANSFER_GAMMA2_2}. The Gamma 2.2 transfer function, used
* for some SDR use-cases like tone-mapping.
*/ */
@UnstableApi public static final int COLOR_TRANSFER_GAMMA_2_2 = 10;
/** See {@link MediaFormat#COLOR_TRANSFER_ST2084}. */
@UnstableApi public static final int COLOR_TRANSFER_ST2084 = MediaFormat.COLOR_TRANSFER_ST2084;
/** See {@link MediaFormat#COLOR_TRANSFER_HLG}. */
@UnstableApi public static final int COLOR_TRANSFER_HLG = MediaFormat.COLOR_TRANSFER_HLG; @UnstableApi public static final int COLOR_TRANSFER_HLG = MediaFormat.COLOR_TRANSFER_HLG;
// LINT.IfChange(color_range) // LINT.IfChange(color_range)
@ -1111,13 +1034,9 @@ public final class C {
@Target(TYPE_USE) @Target(TYPE_USE)
@IntDef({Format.NO_VALUE, COLOR_RANGE_LIMITED, COLOR_RANGE_FULL}) @IntDef({Format.NO_VALUE, COLOR_RANGE_LIMITED, COLOR_RANGE_FULL})
public @interface ColorRange {} public @interface ColorRange {}
/** /** See {@link MediaFormat#COLOR_RANGE_LIMITED}. */
* @see MediaFormat#COLOR_RANGE_LIMITED
*/
@UnstableApi public static final int COLOR_RANGE_LIMITED = MediaFormat.COLOR_RANGE_LIMITED; @UnstableApi public static final int COLOR_RANGE_LIMITED = MediaFormat.COLOR_RANGE_LIMITED;
/** /** See {@link MediaFormat#COLOR_RANGE_FULL}. */
* @see MediaFormat#COLOR_RANGE_FULL
*/
@UnstableApi public static final int COLOR_RANGE_FULL = MediaFormat.COLOR_RANGE_FULL; @UnstableApi public static final int COLOR_RANGE_FULL = MediaFormat.COLOR_RANGE_FULL;
/** Video projection types. */ /** Video projection types. */

View File

@ -19,6 +19,7 @@ import android.os.Bundle;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.media3.common.util.UnstableApi; import androidx.media3.common.util.UnstableApi;
import androidx.media3.common.util.Util; import androidx.media3.common.util.Util;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
import java.util.Arrays; import java.util.Arrays;
import org.checkerframework.dataflow.qual.Pure; import org.checkerframework.dataflow.qual.Pure;
@ -31,6 +32,96 @@ import org.checkerframework.dataflow.qual.Pure;
@UnstableApi @UnstableApi
public final class ColorInfo implements Bundleable { public final class ColorInfo implements Bundleable {
/**
* Builds {@link ColorInfo} instances.
*
* <p>Use {@link ColorInfo#buildUpon} to obtain a builder representing an existing {@link
* ColorInfo}.
*/
public static final class Builder {
private @C.ColorSpace int colorSpace;
private @C.ColorRange int colorRange;
private @C.ColorTransfer int colorTransfer;
@Nullable private byte[] hdrStaticInfo;
/** Creates a new instance with default values. */
public Builder() {
colorSpace = Format.NO_VALUE;
colorRange = Format.NO_VALUE;
colorTransfer = Format.NO_VALUE;
}
/** Creates a new instance to build upon the provided {@link ColorInfo}. */
private Builder(ColorInfo colorInfo) {
this.colorSpace = colorInfo.colorSpace;
this.colorRange = colorInfo.colorRange;
this.colorTransfer = colorInfo.colorTransfer;
this.hdrStaticInfo = colorInfo.hdrStaticInfo;
}
/**
* Sets the color space.
*
* <p>Valid values are {@link C#COLOR_SPACE_BT601}, {@link C#COLOR_SPACE_BT709}, {@link
* C#COLOR_SPACE_BT2020} or {@link Format#NO_VALUE} if unknown.
*
* @param colorSpace The color space. The default value is {@link Format#NO_VALUE}.
* @return This {@code Builder}.
*/
@CanIgnoreReturnValue
public Builder setColorSpace(@C.ColorSpace int colorSpace) {
this.colorSpace = colorSpace;
return this;
}
/**
* Sets the color range.
*
* <p>Valid values are {@link C#COLOR_RANGE_LIMITED}, {@link C#COLOR_RANGE_FULL} or {@link
* Format#NO_VALUE} if unknown.
*
* @param colorRange The color range. The default value is {@link Format#NO_VALUE}.
* @return This {@code Builder}.
*/
@CanIgnoreReturnValue
public Builder setColorRange(@C.ColorRange int colorRange) {
this.colorRange = colorRange;
return this;
}
/**
* Sets the color transfer.
*
* <p>Valid values are {@link C#COLOR_TRANSFER_LINEAR}, {@link C#COLOR_TRANSFER_HLG}, {@link
* C#COLOR_TRANSFER_ST2084}, {@link C#COLOR_TRANSFER_SDR} or {@link Format#NO_VALUE} if unknown.
*
* @param colorTransfer The color transfer. The default value is {@link Format#NO_VALUE}.
* @return This {@code Builder}.
*/
@CanIgnoreReturnValue
public Builder setColorTransfer(@C.ColorTransfer int colorTransfer) {
this.colorTransfer = colorTransfer;
return this;
}
/**
* Sets the HdrStaticInfo as defined in CTA-861.3.
*
* @param hdrStaticInfo The HdrStaticInfo. The default value is {@code null}.
* @return This {@code Builder}.
*/
@CanIgnoreReturnValue
public Builder setHdrStaticInfo(@Nullable byte[] hdrStaticInfo) {
this.hdrStaticInfo = hdrStaticInfo;
return this;
}
/** Builds a new {@link ColorInfo} instance. */
public ColorInfo build() {
return new ColorInfo(colorSpace, colorRange, colorTransfer, hdrStaticInfo);
}
}
/** Color info representing SDR BT.709 limited range, which is a common SDR video color format. */ /** Color info representing SDR BT.709 limited range, which is a common SDR video color format. */
public static final ColorInfo SDR_BT709_LIMITED = public static final ColorInfo SDR_BT709_LIMITED =
new ColorInfo( new ColorInfo(
@ -39,6 +130,17 @@ public final class ColorInfo implements Bundleable {
C.COLOR_TRANSFER_SDR, C.COLOR_TRANSFER_SDR,
/* hdrStaticInfo= */ null); /* hdrStaticInfo= */ null);
/**
* Color info representing SDR sRGB in accordance with {@link
* android.hardware.DataSpace#DATASPACE_SRGB}, which is a common SDR image color format.
*/
public static final ColorInfo SRGB_BT709_FULL =
new ColorInfo.Builder()
.setColorSpace(C.COLOR_SPACE_BT709)
.setColorRange(C.COLOR_RANGE_FULL)
.setColorTransfer(C.COLOR_TRANSFER_SRGB)
.build();
/** /**
* Returns the {@link C.ColorSpace} corresponding to the given ISO color primary code, as per * 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 (03/2009), 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
@ -74,6 +176,10 @@ public final class ColorInfo implements Bundleable {
case 6: // SMPTE 170M. case 6: // SMPTE 170M.
case 7: // SMPTE 240M. case 7: // SMPTE 240M.
return C.COLOR_TRANSFER_SDR; return C.COLOR_TRANSFER_SDR;
case 4:
return C.COLOR_TRANSFER_GAMMA_2_2;
case 13:
return C.COLOR_TRANSFER_SRGB;
case 16: case 16:
return C.COLOR_TRANSFER_ST2084; return C.COLOR_TRANSFER_ST2084;
case 18: case 18:
@ -83,30 +189,25 @@ public final class ColorInfo implements Bundleable {
} }
} }
/** Returns whether the {@code ColorInfo} uses an HDR {@link C.ColorTransfer}. */ /**
* Returns whether the {@code ColorInfo} uses an HDR {@link C.ColorTransfer}.
*
* <p>{@link C#COLOR_TRANSFER_LINEAR} is not considered to be an HDR {@link C.ColorTransfer},
* because it may represent either SDR or HDR contents.
*/
public static boolean isTransferHdr(@Nullable ColorInfo colorInfo) { public static boolean isTransferHdr(@Nullable ColorInfo colorInfo) {
return colorInfo != null return colorInfo != null
&& colorInfo.colorTransfer != Format.NO_VALUE && (colorInfo.colorTransfer == C.COLOR_TRANSFER_HLG
&& colorInfo.colorTransfer != C.COLOR_TRANSFER_SDR; || colorInfo.colorTransfer == C.COLOR_TRANSFER_ST2084);
} }
/** /** The {@link C.ColorSpace}. */
* The color space of the video. Valid values are {@link C#COLOR_SPACE_BT601}, {@link
* C#COLOR_SPACE_BT709}, {@link C#COLOR_SPACE_BT2020} or {@link Format#NO_VALUE} if unknown.
*/
public final @C.ColorSpace int colorSpace; public final @C.ColorSpace int colorSpace;
/** /** The {@link C.ColorRange}. */
* The color range of the video. Valid values are {@link C#COLOR_RANGE_LIMITED}, {@link
* C#COLOR_RANGE_FULL} or {@link Format#NO_VALUE} if unknown.
*/
public final @C.ColorRange int colorRange; public final @C.ColorRange int colorRange;
/** /** The {@link C.ColorTransfer}. */
* The color transfer characteristics of the video. Valid values are {@link C#COLOR_TRANSFER_HLG},
* {@link C#COLOR_TRANSFER_ST2084}, {@link C#COLOR_TRANSFER_SDR} or {@link Format#NO_VALUE} if
* unknown.
*/
public final @C.ColorTransfer int colorTransfer; public final @C.ColorTransfer int colorTransfer;
/** HdrStaticInfo as defined in CTA-861.3, or null if none specified. */ /** HdrStaticInfo as defined in CTA-861.3, or null if none specified. */
@ -122,7 +223,9 @@ public final class ColorInfo implements Bundleable {
* @param colorRange The color range of the video. * @param colorRange The color range of the video.
* @param colorTransfer The color transfer characteristics of the video. * @param colorTransfer The color transfer characteristics of the video.
* @param hdrStaticInfo HdrStaticInfo as defined in CTA-861.3, or null if none specified. * @param hdrStaticInfo HdrStaticInfo as defined in CTA-861.3, or null if none specified.
* @deprecated Use {@link Builder}.
*/ */
@Deprecated
public ColorInfo( public ColorInfo(
@C.ColorSpace int colorSpace, @C.ColorSpace int colorSpace,
@C.ColorRange int colorRange, @C.ColorRange int colorRange,
@ -134,6 +237,39 @@ public final class ColorInfo implements Bundleable {
this.hdrStaticInfo = hdrStaticInfo; this.hdrStaticInfo = hdrStaticInfo;
} }
/** Returns a {@link Builder} initialized with the values of this instance. */
public Builder buildUpon() {
return new Builder(this);
}
/**
* Returns whether this instance is valid.
*
* <p>This instance is valid if no members are {@link Format#NO_VALUE}.
*/
public boolean isValid() {
return colorSpace != Format.NO_VALUE
&& colorRange != Format.NO_VALUE
&& colorTransfer != Format.NO_VALUE;
}
/**
* Returns a prettier {@link String} than {@link #toString()}, intended for logging.
*
* @see Format#toLogString(Format)
*/
public String toLogString() {
if (!isValid()) {
return "NA";
}
return Util.formatInvariant(
"%s/%s/%s",
colorSpaceToString(colorSpace),
colorRangeToString(colorRange),
colorTransferToString(colorTransfer));
}
@Override @Override
public boolean equals(@Nullable Object obj) { public boolean equals(@Nullable Object obj) {
if (this == obj) { if (this == obj) {
@ -152,16 +288,68 @@ public final class ColorInfo implements Bundleable {
@Override @Override
public String toString() { public String toString() {
return "ColorInfo(" return "ColorInfo("
+ colorSpace + colorSpaceToString(colorSpace)
+ ", " + ", "
+ colorRange + colorRangeToString(colorRange)
+ ", " + ", "
+ colorTransfer + colorTransferToString(colorTransfer)
+ ", " + ", "
+ (hdrStaticInfo != null) + (hdrStaticInfo != null)
+ ")"; + ")";
} }
private static String colorSpaceToString(@C.ColorSpace int colorSpace) {
// LINT.IfChange(color_space)
switch (colorSpace) {
case Format.NO_VALUE:
return "Unset color space";
case C.COLOR_SPACE_BT601:
return "BT601";
case C.COLOR_SPACE_BT709:
return "BT709";
case C.COLOR_SPACE_BT2020:
return "BT2020";
default:
return "Undefined color space";
}
}
private static String colorTransferToString(@C.ColorTransfer int colorTransfer) {
// LINT.IfChange(color_transfer)
switch (colorTransfer) {
case Format.NO_VALUE:
return "Unset color transfer";
case C.COLOR_TRANSFER_LINEAR:
return "Linear";
case C.COLOR_TRANSFER_SDR:
return "SDR SMPTE 170M";
case C.COLOR_TRANSFER_SRGB:
return "sRGB";
case C.COLOR_TRANSFER_GAMMA_2_2:
return "Gamma 2.2";
case C.COLOR_TRANSFER_ST2084:
return "ST2084 PQ";
case C.COLOR_TRANSFER_HLG:
return "HLG";
default:
return "Undefined color transfer";
}
}
private static String colorRangeToString(@C.ColorRange int colorRange) {
// LINT.IfChange(color_range)
switch (colorRange) {
case Format.NO_VALUE:
return "Unset color range";
case C.COLOR_RANGE_LIMITED:
return "Limited range";
case C.COLOR_RANGE_FULL:
return "Full range";
default:
return "Undefined color range";
}
}
@Override @Override
public int hashCode() { public int hashCode() {
if (hashCode == 0) { if (hashCode == 0) {

View File

@ -19,7 +19,7 @@ import android.view.SurfaceView;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.media3.common.util.UnstableApi; import androidx.media3.common.util.UnstableApi;
/** Provider for views to show diagnostic information during a transformation, for debugging. */ /** Provider for views to show diagnostic information during an export, for debugging. */
@UnstableApi @UnstableApi
public interface DebugViewProvider { public interface DebugViewProvider {

View File

@ -17,11 +17,15 @@ package androidx.media3.common;
import static java.lang.annotation.ElementType.TYPE_USE; import static java.lang.annotation.ElementType.TYPE_USE;
import android.media.MediaRouter2;
import android.os.Bundle; import android.os.Bundle;
import androidx.annotation.IntDef; import androidx.annotation.IntDef;
import androidx.annotation.IntRange;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.media3.common.util.Assertions;
import androidx.media3.common.util.UnstableApi; import androidx.media3.common.util.UnstableApi;
import androidx.media3.common.util.Util; import androidx.media3.common.util.Util;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
import java.lang.annotation.Documented; import java.lang.annotation.Documented;
import java.lang.annotation.Retention; import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy; import java.lang.annotation.RetentionPolicy;
@ -45,22 +49,116 @@ public final class DeviceInfo implements Bundleable {
public static final int PLAYBACK_TYPE_REMOTE = 1; public static final int PLAYBACK_TYPE_REMOTE = 1;
/** Unknown DeviceInfo. */ /** Unknown DeviceInfo. */
public static final DeviceInfo UNKNOWN = public static final DeviceInfo UNKNOWN = new Builder(PLAYBACK_TYPE_LOCAL).build();
new DeviceInfo(PLAYBACK_TYPE_LOCAL, /* minVolume= */ 0, /* maxVolume= */ 0);
/** Builder for {@link DeviceInfo}. */
public static final class Builder {
private final @PlaybackType int playbackType;
private int minVolume;
private int maxVolume;
@Nullable private String routingControllerId;
/**
* Creates the builder.
*
* @param playbackType The {@link PlaybackType}.
*/
public Builder(@PlaybackType int playbackType) {
this.playbackType = playbackType;
}
/**
* Sets the minimum supported device volume.
*
* <p>The minimum will be set to {@code 0} if not specified.
*
* @param minVolume The minimum device volume.
* @return This builder.
*/
@CanIgnoreReturnValue
public Builder setMinVolume(@IntRange(from = 0) int minVolume) {
this.minVolume = minVolume;
return this;
}
/**
* Sets the maximum supported device volume.
*
* @param maxVolume The maximum device volume, or {@code 0} to leave the maximum unspecified.
* @return This builder.
*/
@CanIgnoreReturnValue
public Builder setMaxVolume(@IntRange(from = 0) int maxVolume) {
this.maxVolume = maxVolume;
return this;
}
/**
* Sets the {@linkplain MediaRouter2.RoutingController#getId() routing controller id} of the
* associated {@link MediaRouter2.RoutingController}.
*
* <p>This id allows mapping this device information to a routing controller, which provides
* information about the media route and allows controlling its volume.
*
* <p>The set value must be null if {@link DeviceInfo#playbackType} is {@link
* #PLAYBACK_TYPE_LOCAL}.
*
* @param routingControllerId The {@linkplain MediaRouter2.RoutingController#getId() routing
* controller id} of the associated {@link MediaRouter2.RoutingController}, or null to leave
* it unspecified.
* @return This builder.
*/
@CanIgnoreReturnValue
public Builder setRoutingControllerId(@Nullable String routingControllerId) {
Assertions.checkArgument(playbackType != PLAYBACK_TYPE_LOCAL || routingControllerId == null);
this.routingControllerId = routingControllerId;
return this;
}
/** Builds the {@link DeviceInfo}. */
public DeviceInfo build() {
Assertions.checkArgument(minVolume <= maxVolume);
return new DeviceInfo(this);
}
}
/** The type of playback. */ /** The type of playback. */
public final @PlaybackType int playbackType; public final @PlaybackType int playbackType;
/** The minimum volume that the device supports. */ /** The minimum volume that the device supports. */
@IntRange(from = 0)
public final int minVolume; public final int minVolume;
/** The maximum volume that the device supports. */ /** The maximum volume that the device supports, or {@code 0} if unspecified. */
@IntRange(from = 0)
public final int maxVolume; public final int maxVolume;
/**
* The {@linkplain MediaRouter2.RoutingController#getId() routing controller id} of the associated
* {@link MediaRouter2.RoutingController}, or null if unset or {@link #playbackType} is {@link
* #PLAYBACK_TYPE_LOCAL}.
*
* <p>This id allows mapping this device information to a routing controller, which provides
* information about the media route and allows controlling its volume.
*/
@Nullable public final String routingControllerId;
/** Creates device information. */ /**
* @deprecated Use {@link Builder} instead.
*/
@UnstableApi @UnstableApi
public DeviceInfo(@PlaybackType int playbackType, int minVolume, int maxVolume) { @Deprecated
this.playbackType = playbackType; public DeviceInfo(
this.minVolume = minVolume; @PlaybackType int playbackType,
this.maxVolume = maxVolume; @IntRange(from = 0) int minVolume,
@IntRange(from = 0) int maxVolume) {
this(new Builder(playbackType).setMinVolume(minVolume).setMaxVolume(maxVolume));
}
private DeviceInfo(Builder builder) {
this.playbackType = builder.playbackType;
this.minVolume = builder.minVolume;
this.maxVolume = builder.maxVolume;
this.routingControllerId = builder.routingControllerId;
} }
@Override @Override
@ -74,7 +172,8 @@ public final class DeviceInfo implements Bundleable {
DeviceInfo other = (DeviceInfo) obj; DeviceInfo other = (DeviceInfo) obj;
return playbackType == other.playbackType return playbackType == other.playbackType
&& minVolume == other.minVolume && minVolume == other.minVolume
&& maxVolume == other.maxVolume; && maxVolume == other.maxVolume
&& Util.areEqual(routingControllerId, other.routingControllerId);
} }
@Override @Override
@ -83,6 +182,7 @@ public final class DeviceInfo implements Bundleable {
result = 31 * result + playbackType; result = 31 * result + playbackType;
result = 31 * result + minVolume; result = 31 * result + minVolume;
result = 31 * result + maxVolume; result = 31 * result + maxVolume;
result = 31 * result + (routingControllerId == null ? 0 : routingControllerId.hashCode());
return result; return result;
} }
@ -91,14 +191,24 @@ public final class DeviceInfo implements Bundleable {
private static final String FIELD_PLAYBACK_TYPE = Util.intToStringMaxRadix(0); private static final String FIELD_PLAYBACK_TYPE = Util.intToStringMaxRadix(0);
private static final String FIELD_MIN_VOLUME = Util.intToStringMaxRadix(1); private static final String FIELD_MIN_VOLUME = Util.intToStringMaxRadix(1);
private static final String FIELD_MAX_VOLUME = Util.intToStringMaxRadix(2); private static final String FIELD_MAX_VOLUME = Util.intToStringMaxRadix(2);
private static final String FIELD_ROUTING_CONTROLLER_ID = Util.intToStringMaxRadix(3);
@UnstableApi @UnstableApi
@Override @Override
public Bundle toBundle() { public Bundle toBundle() {
Bundle bundle = new Bundle(); Bundle bundle = new Bundle();
if (playbackType != PLAYBACK_TYPE_LOCAL) {
bundle.putInt(FIELD_PLAYBACK_TYPE, playbackType); bundle.putInt(FIELD_PLAYBACK_TYPE, playbackType);
}
if (minVolume != 0) {
bundle.putInt(FIELD_MIN_VOLUME, minVolume); bundle.putInt(FIELD_MIN_VOLUME, minVolume);
}
if (maxVolume != 0) {
bundle.putInt(FIELD_MAX_VOLUME, maxVolume); bundle.putInt(FIELD_MAX_VOLUME, maxVolume);
}
if (routingControllerId != null) {
bundle.putString(FIELD_ROUTING_CONTROLLER_ID, routingControllerId);
}
return bundle; return bundle;
} }
@ -110,6 +220,11 @@ public final class DeviceInfo implements Bundleable {
bundle.getInt(FIELD_PLAYBACK_TYPE, /* defaultValue= */ PLAYBACK_TYPE_LOCAL); bundle.getInt(FIELD_PLAYBACK_TYPE, /* defaultValue= */ PLAYBACK_TYPE_LOCAL);
int minVolume = bundle.getInt(FIELD_MIN_VOLUME, /* defaultValue= */ 0); int minVolume = bundle.getInt(FIELD_MIN_VOLUME, /* defaultValue= */ 0);
int maxVolume = bundle.getInt(FIELD_MAX_VOLUME, /* defaultValue= */ 0); int maxVolume = bundle.getInt(FIELD_MAX_VOLUME, /* defaultValue= */ 0);
return new DeviceInfo(playbackType, minVolume, maxVolume); @Nullable String routingControllerId = bundle.getString(FIELD_ROUTING_CONTROLLER_ID);
return new DeviceInfo.Builder(playbackType)
.setMinVolume(minVolume)
.setMaxVolume(maxVolume)
.setRoutingControllerId(routingControllerId)
.build();
}; };
} }

View File

@ -134,7 +134,7 @@ public final class FileTypes {
/** /**
* Returns the {@link Type} corresponding to the MIME type provided. * Returns the {@link Type} corresponding to the MIME type provided.
* *
* <p>Returns {@link #UNKNOWN} if the mime type is {@code null}. * <p>Returns {@link #UNKNOWN} if the MIME type is {@code null}.
*/ */
public static @FileTypes.Type int inferFileTypeFromMimeType(@Nullable String mimeType) { public static @FileTypes.Type int inferFileTypeFromMimeType(@Nullable String mimeType) {
if (mimeType == null) { if (mimeType == null) {

View File

@ -748,12 +748,12 @@ public final class Format implements Bundleable {
// Container specific. // Container specific.
/** The mime type of the container, or null if unknown or not applicable. */ /** The MIME type of the container, or null if unknown or not applicable. */
@Nullable public final String containerMimeType; @Nullable public final String containerMimeType;
// Sample specific. // Sample specific.
/** The sample mime type, or null if unknown or not applicable. */ /** The sample MIME type, or null if unknown or not applicable. */
@Nullable public final String sampleMimeType; @Nullable public final String sampleMimeType;
/** /**
* The maximum size of a buffer of data (typically one sample), or {@link #NO_VALUE} if unknown or * The maximum size of a buffer of data (typically one sample), or {@link #NO_VALUE} if unknown or
@ -846,184 +846,6 @@ public final class Format implements Bundleable {
// Lazily initialized hashcode. // Lazily initialized hashcode.
private int hashCode; private int hashCode;
// Video.
/**
* @deprecated Use {@link Format.Builder}.
*/
@UnstableApi
@Deprecated
public static Format createVideoSampleFormat(
@Nullable String id,
@Nullable String sampleMimeType,
@Nullable String codecs,
int bitrate,
int maxInputSize,
int width,
int height,
float frameRate,
@Nullable List<byte[]> initializationData,
@Nullable DrmInitData drmInitData) {
return new Builder()
.setId(id)
.setAverageBitrate(bitrate)
.setPeakBitrate(bitrate)
.setCodecs(codecs)
.setSampleMimeType(sampleMimeType)
.setMaxInputSize(maxInputSize)
.setInitializationData(initializationData)
.setDrmInitData(drmInitData)
.setWidth(width)
.setHeight(height)
.setFrameRate(frameRate)
.build();
}
/**
* @deprecated Use {@link Format.Builder}.
*/
@UnstableApi
@Deprecated
public static Format createVideoSampleFormat(
@Nullable String id,
@Nullable String sampleMimeType,
@Nullable String codecs,
int bitrate,
int maxInputSize,
int width,
int height,
float frameRate,
@Nullable List<byte[]> initializationData,
int rotationDegrees,
float pixelWidthHeightRatio,
@Nullable DrmInitData drmInitData) {
return new Builder()
.setId(id)
.setAverageBitrate(bitrate)
.setPeakBitrate(bitrate)
.setCodecs(codecs)
.setSampleMimeType(sampleMimeType)
.setMaxInputSize(maxInputSize)
.setInitializationData(initializationData)
.setDrmInitData(drmInitData)
.setWidth(width)
.setHeight(height)
.setFrameRate(frameRate)
.setRotationDegrees(rotationDegrees)
.setPixelWidthHeightRatio(pixelWidthHeightRatio)
.build();
}
// Audio.
/**
* @deprecated Use {@link Format.Builder}.
*/
@UnstableApi
@Deprecated
public static Format createAudioSampleFormat(
@Nullable String id,
@Nullable String sampleMimeType,
@Nullable String codecs,
int bitrate,
int maxInputSize,
int channelCount,
int sampleRate,
@Nullable List<byte[]> initializationData,
@Nullable DrmInitData drmInitData,
@C.SelectionFlags int selectionFlags,
@Nullable String language) {
return new Builder()
.setId(id)
.setLanguage(language)
.setSelectionFlags(selectionFlags)
.setAverageBitrate(bitrate)
.setPeakBitrate(bitrate)
.setCodecs(codecs)
.setSampleMimeType(sampleMimeType)
.setMaxInputSize(maxInputSize)
.setInitializationData(initializationData)
.setDrmInitData(drmInitData)
.setChannelCount(channelCount)
.setSampleRate(sampleRate)
.build();
}
/**
* @deprecated Use {@link Format.Builder}.
*/
@UnstableApi
@Deprecated
public static Format createAudioSampleFormat(
@Nullable String id,
@Nullable String sampleMimeType,
@Nullable String codecs,
int bitrate,
int maxInputSize,
int channelCount,
int sampleRate,
@C.PcmEncoding int pcmEncoding,
@Nullable List<byte[]> initializationData,
@Nullable DrmInitData drmInitData,
@C.SelectionFlags int selectionFlags,
@Nullable String language) {
return new Builder()
.setId(id)
.setLanguage(language)
.setSelectionFlags(selectionFlags)
.setAverageBitrate(bitrate)
.setPeakBitrate(bitrate)
.setCodecs(codecs)
.setSampleMimeType(sampleMimeType)
.setMaxInputSize(maxInputSize)
.setInitializationData(initializationData)
.setDrmInitData(drmInitData)
.setChannelCount(channelCount)
.setSampleRate(sampleRate)
.setPcmEncoding(pcmEncoding)
.build();
}
// Generic.
/**
* @deprecated Use {@link Format.Builder}.
*/
@UnstableApi
@Deprecated
public static Format createContainerFormat(
@Nullable String id,
@Nullable String label,
@Nullable String containerMimeType,
@Nullable String sampleMimeType,
@Nullable String codecs,
int bitrate,
@C.SelectionFlags int selectionFlags,
@C.RoleFlags int roleFlags,
@Nullable String language) {
return new Builder()
.setId(id)
.setLabel(label)
.setLanguage(language)
.setSelectionFlags(selectionFlags)
.setRoleFlags(roleFlags)
.setAverageBitrate(bitrate)
.setPeakBitrate(bitrate)
.setCodecs(codecs)
.setContainerMimeType(containerMimeType)
.setSampleMimeType(sampleMimeType)
.build();
}
/**
* @deprecated Use {@link Format.Builder}.
*/
@UnstableApi
@Deprecated
public static Format createSampleFormat(@Nullable String id, @Nullable String sampleMimeType) {
return new Builder().setId(id).setSampleMimeType(sampleMimeType).build();
}
private Format(Builder builder) { private Format(Builder builder) {
id = builder.id; id = builder.id;
label = builder.label; label = builder.label;
@ -1080,42 +902,6 @@ public final class Format implements Bundleable {
return new Builder(this); return new Builder(this);
} }
/**
* @deprecated Use {@link #buildUpon()} and {@link Builder#setMaxInputSize(int)}.
*/
@UnstableApi
@Deprecated
public Format copyWithMaxInputSize(int maxInputSize) {
return buildUpon().setMaxInputSize(maxInputSize).build();
}
/**
* @deprecated Use {@link #buildUpon()} and {@link Builder#setSubsampleOffsetUs(long)}.
*/
@UnstableApi
@Deprecated
public Format copyWithSubsampleOffsetUs(long subsampleOffsetUs) {
return buildUpon().setSubsampleOffsetUs(subsampleOffsetUs).build();
}
/**
* @deprecated Use {@link #buildUpon()} and {@link Builder#setLabel(String)} .
*/
@UnstableApi
@Deprecated
public Format copyWithLabel(@Nullable String label) {
return buildUpon().setLabel(label).build();
}
/**
* @deprecated Use {@link #withManifestFormatInfo(Format)}.
*/
@UnstableApi
@Deprecated
public Format copyWithManifestFormatInfo(Format manifestFormat) {
return withManifestFormatInfo(manifestFormat);
}
@UnstableApi @UnstableApi
@SuppressWarnings("ReferenceEquality") @SuppressWarnings("ReferenceEquality")
public Format withManifestFormatInfo(Format manifestFormat) { public Format withManifestFormatInfo(Format manifestFormat) {
@ -1184,63 +970,6 @@ public final class Format implements Bundleable {
.build(); .build();
} }
/**
* @deprecated Use {@link #buildUpon()}, {@link Builder#setEncoderDelay(int)} and {@link
* Builder#setEncoderPadding(int)}.
*/
@UnstableApi
@Deprecated
public Format copyWithGaplessInfo(int encoderDelay, int encoderPadding) {
return buildUpon().setEncoderDelay(encoderDelay).setEncoderPadding(encoderPadding).build();
}
/**
* @deprecated Use {@link #buildUpon()} and {@link Builder#setFrameRate(float)}.
*/
@UnstableApi
@Deprecated
public Format copyWithFrameRate(float frameRate) {
return buildUpon().setFrameRate(frameRate).build();
}
/**
* @deprecated Use {@link #buildUpon()} and {@link Builder#setDrmInitData(DrmInitData)}.
*/
@UnstableApi
@Deprecated
public Format copyWithDrmInitData(@Nullable DrmInitData drmInitData) {
return buildUpon().setDrmInitData(drmInitData).build();
}
/**
* @deprecated Use {@link #buildUpon()} and {@link Builder#setMetadata(Metadata)}.
*/
@UnstableApi
@Deprecated
public Format copyWithMetadata(@Nullable Metadata metadata) {
return buildUpon().setMetadata(metadata).build();
}
/**
* @deprecated Use {@link #buildUpon()} and {@link Builder#setAverageBitrate(int)} and {@link
* Builder#setPeakBitrate(int)}.
*/
@UnstableApi
@Deprecated
public Format copyWithBitrate(int bitrate) {
return buildUpon().setAverageBitrate(bitrate).setPeakBitrate(bitrate).build();
}
/**
* @deprecated Use {@link #buildUpon()}, {@link Builder#setWidth(int)} and {@link
* Builder#setHeight(int)}.
*/
@UnstableApi
@Deprecated
public Format copyWithVideoSize(int width, int height) {
return buildUpon().setWidth(width).setHeight(height).build();
}
/** Returns a copy of this format with the specified {@link #cryptoType}. */ /** Returns a copy of this format with the specified {@link #cryptoType}. */
@UnstableApi @UnstableApi
public Format copyWithCryptoType(@C.CryptoType int cryptoType) { public Format copyWithCryptoType(@C.CryptoType int cryptoType) {
@ -1278,6 +1007,8 @@ public final class Format implements Bundleable {
+ height + height
+ ", " + ", "
+ frameRate + frameRate
+ ", "
+ colorInfo
+ "]" + "]"
+ ", [" + ", ["
+ channelCount + channelCount
@ -1444,6 +1175,9 @@ public final class Format implements Bundleable {
if (format.width != NO_VALUE && format.height != NO_VALUE) { if (format.width != NO_VALUE && format.height != NO_VALUE) {
builder.append(", res=").append(format.width).append("x").append(format.height); builder.append(", res=").append(format.width).append("x").append(format.height);
} }
if (format.colorInfo != null && format.colorInfo.isValid()) {
builder.append(", color=").append(format.colorInfo.toLogString());
}
if (format.frameRate != NO_VALUE) { if (format.frameRate != NO_VALUE) {
builder.append(", fps=").append(format.frameRate); builder.append(", fps=").append(format.frameRate);
} }

View File

@ -147,6 +147,18 @@ public class ForwardingPlayer implements Player {
player.moveMediaItems(fromIndex, toIndex, newIndex); player.moveMediaItems(fromIndex, toIndex, newIndex);
} }
/** Calls {@link Player#replaceMediaItem(int, MediaItem)} on the delegate. */
@Override
public void replaceMediaItem(int index, MediaItem mediaItem) {
player.replaceMediaItem(index, mediaItem);
}
/** Calls {@link Player#replaceMediaItems(int, int, List)} on the delegate. */
@Override
public void replaceMediaItems(int fromIndex, int toIndex, List<MediaItem> mediaItems) {
player.replaceMediaItems(fromIndex, toIndex, mediaItems);
}
/** Calls {@link Player#removeMediaItem(int)} on the delegate. */ /** Calls {@link Player#removeMediaItem(int)} on the delegate. */
@Override @Override
public void removeMediaItem(int index) { public void removeMediaItem(int index) {
@ -478,20 +490,6 @@ public class ForwardingPlayer implements Player {
player.stop(); player.stop();
} }
/**
* Calls {@link Player#stop(boolean)} on the delegate.
*
* @deprecated Use {@link #stop()} and {@link #clearMediaItems()} (if {@code reset} is true) or
* just {@link #stop()} (if {@code reset} is false). Any player error will be cleared when
* {@link #prepare() re-preparing} the player.
*/
@SuppressWarnings("deprecation") // Forwarding to deprecated method
@Deprecated
@Override
public void stop(boolean reset) {
player.stop(reset);
}
/** Calls {@link Player#release()} on the delegate. */ /** Calls {@link Player#release()} on the delegate. */
@Override @Override
public void release() { public void release() {
@ -860,30 +858,66 @@ public class ForwardingPlayer implements Player {
return player.isDeviceMuted(); return player.isDeviceMuted();
} }
/** Calls {@link Player#setDeviceVolume(int)} on the delegate. */ /**
* @deprecated Use {@link #setDeviceVolume(int, int)} instead.
*/
@Deprecated
@Override @Override
public void setDeviceVolume(int volume) { public void setDeviceVolume(int volume) {
player.setDeviceVolume(volume); player.setDeviceVolume(volume);
} }
/** Calls {@link Player#increaseDeviceVolume()} on the delegate. */ /** Calls {@link Player#setDeviceVolume(int, int)} on the delegate. */
@Override
public void setDeviceVolume(int volume, @C.VolumeFlags int flags) {
player.setDeviceVolume(volume, flags);
}
/**
* @deprecated Use {@link #increaseDeviceVolume(int)} instead.
*/
@Deprecated
@Override @Override
public void increaseDeviceVolume() { public void increaseDeviceVolume() {
player.increaseDeviceVolume(); player.increaseDeviceVolume();
} }
/** Calls {@link Player#decreaseDeviceVolume()} on the delegate. */ /** Calls {@link Player#increaseDeviceVolume(int)} on the delegate. */
@Override
public void increaseDeviceVolume(@C.VolumeFlags int flags) {
player.increaseDeviceVolume(flags);
}
/**
* @deprecated Use {@link #decreaseDeviceVolume(int)} instead.
*/
@Deprecated
@Override @Override
public void decreaseDeviceVolume() { public void decreaseDeviceVolume() {
player.decreaseDeviceVolume(); player.decreaseDeviceVolume();
} }
/** Calls {@link Player#setDeviceMuted(boolean)} on the delegate. */ /** Calls {@link Player#decreaseDeviceVolume(int)} on the delegate. */
@Override
public void decreaseDeviceVolume(@C.VolumeFlags int flags) {
player.decreaseDeviceVolume(flags);
}
/**
* @deprecated Use {@link #setDeviceMuted(boolean, int)} instead.
*/
@Deprecated
@Override @Override
public void setDeviceMuted(boolean muted) { public void setDeviceMuted(boolean muted) {
player.setDeviceMuted(muted); player.setDeviceMuted(muted);
} }
/** Calls {@link Player#setDeviceMuted(boolean, int)} on the delegate. */
@Override
public void setDeviceMuted(boolean muted, @C.VolumeFlags int flags) {
player.setDeviceMuted(muted, flags);
}
/** Returns the {@link Player} to which operations are forwarded. */ /** Returns the {@link Player} to which operations are forwarded. */
public Player getWrappedPlayer() { public Player getWrappedPlayer() {
return player; return player;
@ -1032,12 +1066,6 @@ public class ForwardingPlayer implements Player {
listener.onMaxSeekToPreviousPositionChanged(maxSeekToPreviousPositionMs); listener.onMaxSeekToPreviousPositionChanged(maxSeekToPreviousPositionMs);
} }
@Override
@SuppressWarnings("deprecation")
public void onSeekProcessed() {
listener.onSeekProcessed();
}
@Override @Override
public void onVideoSizeChanged(VideoSize videoSize) { public void onVideoSizeChanged(VideoSize videoSize) {
listener.onVideoSizeChanged(videoSize); listener.onVideoSizeChanged(videoSize);

View File

@ -18,10 +18,83 @@ package androidx.media3.common;
import static androidx.media3.common.util.Assertions.checkArgument; import static androidx.media3.common.util.Assertions.checkArgument;
import androidx.media3.common.util.UnstableApi; import androidx.media3.common.util.UnstableApi;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
/** Value class specifying information about a decoded video frame. */ /** Value class specifying information about a decoded video frame. */
@UnstableApi @UnstableApi
public class FrameInfo { public class FrameInfo {
/** A builder for {@link FrameInfo} instances. */
public static final class Builder {
private int width;
private int height;
private float pixelWidthHeightRatio;
private long offsetToAddUs;
/**
* Creates an instance with default values.
*
* @param width The frame width, in pixels.
* @param height The frame height, in pixels.
*/
public Builder(int width, int height) {
this.width = width;
this.height = height;
pixelWidthHeightRatio = 1;
}
/** Creates an instance with the values of the provided {@link FrameInfo}. */
public Builder(FrameInfo frameInfo) {
width = frameInfo.width;
height = frameInfo.height;
pixelWidthHeightRatio = frameInfo.pixelWidthHeightRatio;
offsetToAddUs = frameInfo.offsetToAddUs;
}
/** 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(width, height, pixelWidthHeightRatio, offsetToAddUs);
}
}
/** The width of the frame, in pixels. */ /** The width of the frame, in pixels. */
public final int width; public final int width;
/** The height of the frame, in pixels. */ /** The height of the frame, in pixels. */
@ -29,32 +102,22 @@ public class FrameInfo {
/** The ratio of width over height for each pixel. */ /** The ratio of width over height for each pixel. */
public final float pixelWidthHeightRatio; public final float pixelWidthHeightRatio;
/** /**
* An offset in microseconds that is part of the input timestamps and should be ignored for * The offset that must be added to the frame presentation timestamp, in microseconds.
* processing but added back to the output timestamps.
* *
* <p>The offset stays constant within a stream but changes in between streams to ensure that * <p>This offset is not part of the input timestamps. It is added to the frame timestamps before
* frame timestamps are always monotonically increasing. * processing, and is retained in the output timestamps.
*/ */
public final long streamOffsetUs; public final long offsetToAddUs;
// TODO(b/227624622): Add color space information for HDR. // TODO(b/227624622): Add color space information for HDR.
/** private FrameInfo(int width, int height, float pixelWidthHeightRatio, long offsetToAddUs) {
* Creates a new instance.
*
* @param width The width of the frame, in pixels.
* @param height The height of the frame, in pixels.
* @param pixelWidthHeightRatio The ratio of width over height for each pixel.
* @param streamOffsetUs An offset in microseconds that is part of the input timestamps and should
* be ignored for processing but added back to the output timestamps.
*/
public FrameInfo(int width, int height, float pixelWidthHeightRatio, long streamOffsetUs) {
checkArgument(width > 0, "width must be positive, but is: " + width); checkArgument(width > 0, "width must be positive, but is: " + width);
checkArgument(height > 0, "height must be positive, but is: " + height); checkArgument(height > 0, "height must be positive, but is: " + height);
this.width = width; this.width = width;
this.height = height; this.height = height;
this.pixelWidthHeightRatio = pixelWidthHeightRatio; this.pixelWidthHeightRatio = pixelWidthHeightRatio;
this.streamOffsetUs = streamOffsetUs; this.offsetToAddUs = offsetToAddUs;
} }
} }

View File

@ -1,205 +0,0 @@
/*
* Copyright 2022 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.content.Context;
import android.opengl.EGLExt;
import android.view.Surface;
import androidx.annotation.Nullable;
import androidx.media3.common.util.UnstableApi;
import java.util.List;
/**
* Interface for a frame processor that applies changes to individual video frames.
*
* <p>The changes are specified by {@link Effect} instances passed to {@link Factory#create}.
*
* <p>Manages its input {@link Surface}, which can be accessed via {@link #getInputSurface()}. The
* output {@link Surface} must be set by the caller using {@link
* #setOutputSurfaceInfo(SurfaceInfo)}.
*
* <p>The caller must {@linkplain #registerInputFrame() register} input frames before rendering them
* to the input {@link Surface}.
*/
@UnstableApi
public interface FrameProcessor {
// TODO(b/243036513): Allow effects to be replaced.
/** A factory for {@link FrameProcessor} instances. */
interface Factory {
/**
* Creates a new {@link FrameProcessor} instance.
*
* @param context A {@link Context}.
* @param listener A {@link Listener}.
* @param effects The {@link Effect} instances to apply to each frame.
* @param debugViewProvider A {@link DebugViewProvider}.
* @param colorInfo The {@link ColorInfo} for input and output frames.
* @param releaseFramesAutomatically If {@code true}, the {@link FrameProcessor} will render
* output frames to the {@linkplain #setOutputSurfaceInfo(SurfaceInfo) output surface}
* automatically as {@link FrameProcessor} is done processing them. If {@code false}, the
* {@link FrameProcessor} will block until {@link #releaseOutputFrame(long)} is called, to
* render or drop the frame.
* @return A new instance.
* @throws FrameProcessingException If a problem occurs while creating the {@link
* FrameProcessor}.
*/
FrameProcessor create(
Context context,
Listener listener,
List<Effect> effects,
DebugViewProvider debugViewProvider,
ColorInfo colorInfo,
boolean releaseFramesAutomatically)
throws FrameProcessingException;
}
/**
* Listener for asynchronous frame processing events.
*
* <p>All listener methods must be called from the same thread.
*/
interface Listener {
/**
* Called when the output size changes.
*
* <p>The output size is the frame size in pixels after applying all {@linkplain Effect
* effects}.
*
* <p>The output size may differ from the size specified using {@link
* #setOutputSurfaceInfo(SurfaceInfo)}.
*/
void onOutputSizeChanged(int width, int height);
/**
* Called when an output frame with the given {@code presentationTimeUs} becomes available.
*
* @param presentationTimeUs The presentation time of the frame, in microseconds.
*/
void onOutputFrameAvailable(long presentationTimeUs);
/**
* Called when an exception occurs during asynchronous frame processing.
*
* <p>If an error occurred, consuming and producing further frames will not work as expected and
* the {@link FrameProcessor} should be released.
*/
void onFrameProcessingError(FrameProcessingException exception);
/** Called after the {@link FrameProcessor} has produced its final output frame. */
void onFrameProcessingEnded();
}
/**
* Indicates the frame should be released immediately after {@link #releaseOutputFrame(long)} is
* invoked.
*/
long RELEASE_OUTPUT_FRAME_IMMEDIATELY = -1;
/** Indicates the frame should be dropped after {@link #releaseOutputFrame(long)} is invoked. */
long DROP_OUTPUT_FRAME = -2;
/** Returns the input {@link Surface}, where {@link FrameProcessor} consumes input frames from. */
Surface getInputSurface();
/**
* Sets information about the input frames.
*
* <p>The new input information is applied from the next frame {@linkplain #registerInputFrame()
* registered} onwards.
*
* <p>Pixels are expanded using the {@link FrameInfo#pixelWidthHeightRatio} so that the output
* frames' pixels have a ratio of 1.
*
* <p>The caller should update {@link FrameInfo#streamOffsetUs} when switching input streams to
* ensure that frame timestamps are always monotonically increasing.
*/
void setInputFrameInfo(FrameInfo inputFrameInfo);
/**
* Informs the {@code FrameProcessor} that a frame will be queued to its input surface.
*
* <p>Must be called before rendering a frame to the frame processor's input surface.
*
* @throws IllegalStateException If called after {@link #signalEndOfInput()} or before {@link
* #setInputFrameInfo(FrameInfo)}.
*/
void registerInputFrame();
/**
* Returns the number of input frames that have been {@linkplain #registerInputFrame() registered}
* but not processed off the {@linkplain #getInputSurface() input surface} yet.
*/
int getPendingInputFrameCount();
/**
* Sets the output surface and supporting information. When output frames are released and not
* dropped, they will be rendered to this output {@link SurfaceInfo}.
*
* <p>The new output {@link SurfaceInfo} is applied from the next output frame rendered onwards.
* If the output {@link SurfaceInfo} is {@code null}, the {@code FrameProcessor} will stop
* rendering pending frames and resume rendering once a non-null {@link SurfaceInfo} is set.
*
* <p>If the dimensions given in {@link SurfaceInfo} do not match the {@linkplain
* Listener#onOutputSizeChanged(int,int) output size after applying the final effect} the frames
* are resized before rendering to the surface and letter/pillar-boxing is applied.
*
* <p>The caller is responsible for tracking the lifecycle of the {@link SurfaceInfo#surface}
* including calling this method with a new surface if it is destroyed. When this method returns,
* the previous output surface is no longer being used and can safely be released by the caller.
*/
void setOutputSurfaceInfo(@Nullable SurfaceInfo outputSurfaceInfo);
/**
* Releases the oldest unreleased output frame that has become {@linkplain
* Listener#onOutputFrameAvailable(long) available} at the given {@code releaseTimeNs}.
*
* <p>This will either render the output frame to the {@linkplain #setOutputSurfaceInfo output
* surface}, or drop the frame, per {@code releaseTimeNs}.
*
* <p>This method must only be called if {@code releaseFramesAutomatically} was set to {@code
* false} using the {@link Factory} and should be called exactly once for each frame that becomes
* {@linkplain Listener#onOutputFrameAvailable(long) available}.
*
* <p>The {@code releaseTimeNs} may be passed to {@link EGLExt#eglPresentationTimeANDROID}
* depending on the implementation.
*
* @param releaseTimeNs The release time to use for the frame, in nanoseconds. The release time
* can be before of after the current system time. Use {@link #DROP_OUTPUT_FRAME} to drop the
* frame, or {@link #RELEASE_OUTPUT_FRAME_IMMEDIATELY} to release the frame immediately.
*/
void releaseOutputFrame(long releaseTimeNs);
/**
* Informs the {@code FrameProcessor} that no further input frames should be accepted.
*
* @throws IllegalStateException If called more than once.
*/
void signalEndOfInput();
/**
* Releases all resources.
*
* <p>If the frame processor is released before it has {@linkplain
* Listener#onFrameProcessingEnded() ended}, it will attempt to cancel processing any input frames
* that have already become available. Input frames that become available after release are
* ignored.
*
* <p>This method blocks until all resources are released or releasing times out.
*/
void release();
}

View File

@ -0,0 +1,131 @@
/*
* Copyright (C) 2023 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.opengl.EGL14;
import android.opengl.EGLContext;
import android.opengl.EGLDisplay;
import android.opengl.EGLSurface;
import androidx.annotation.IntRange;
import androidx.annotation.RequiresApi;
import androidx.media3.common.util.GlUtil;
import androidx.media3.common.util.GlUtil.GlException;
import androidx.media3.common.util.UnstableApi;
// TODO(271433904): Expand this class to cover more methods in GlUtil.
/** Provider to customize the creation and maintenance of GL objects. */
@UnstableApi
public interface GlObjectsProvider {
/**
* @deprecated Please use {@code DefaultGlObjectsProvider} in {@code androidx.media3.effect}.
*/
@Deprecated
GlObjectsProvider DEFAULT =
new GlObjectsProvider() {
@Override
@RequiresApi(17)
public EGLContext createEglContext(
EGLDisplay eglDisplay, int openGlVersion, int[] configAttributes) throws GlException {
return GlUtil.createEglContext(
EGL14.EGL_NO_CONTEXT, eglDisplay, openGlVersion, configAttributes);
}
@Override
@RequiresApi(17)
public EGLSurface createEglSurface(
EGLDisplay eglDisplay,
Object surface,
@C.ColorTransfer int colorTransfer,
boolean isEncoderInputSurface)
throws GlException {
return GlUtil.createEglSurface(eglDisplay, surface, colorTransfer, isEncoderInputSurface);
}
@Override
@RequiresApi(17)
public EGLSurface createFocusedPlaceholderEglSurface(
EGLContext eglContext, EGLDisplay eglDisplay, int[] configAttributes)
throws GlException {
return GlUtil.createFocusedPlaceholderEglSurface(
eglContext, eglDisplay, configAttributes);
}
@Override
public GlTextureInfo createBuffersForTexture(int texId, int width, int height)
throws GlException {
int fboId = GlUtil.createFboForTexture(texId);
return new GlTextureInfo(texId, fboId, /* rboId= */ C.INDEX_UNSET, width, height);
}
};
/**
* Creates a new {@link EGLContext} for the specified {@link EGLDisplay}.
*
* @param eglDisplay The {@link EGLDisplay} to create an {@link EGLContext} for.
* @param openGlVersion The version of OpenGL ES to configure. Accepts either {@code 2}, for
* OpenGL ES 2.0, or {@code 3}, for OpenGL ES 3.0.
* @param configAttributes The attributes to configure EGL with.
* @throws GlException If an error occurs during creation.
*/
@RequiresApi(17)
EGLContext createEglContext(
EGLDisplay eglDisplay, @IntRange(from = 2, to = 3) int openGlVersion, int[] configAttributes)
throws GlException;
/**
* Creates a new {@link EGLSurface} wrapping the specified {@code surface}.
*
* @param eglDisplay The {@link EGLDisplay} to attach the surface to.
* @param surface The surface to wrap; must be a surface, surface texture or surface holder.
* @param colorTransfer The {@linkplain C.ColorTransfer color transfer characteristics} to which
* the {@code surface} is configured.
* @param isEncoderInputSurface Whether the {@code surface} is the input surface of an encoder.
* @throws GlException If an error occurs during creation.
*/
@RequiresApi(17)
EGLSurface createEglSurface(
EGLDisplay eglDisplay,
Object surface,
@C.ColorTransfer int colorTransfer,
boolean isEncoderInputSurface)
throws GlException;
/**
* Creates and focuses a placeholder {@link EGLSurface}.
*
* @param eglContext The {@link EGLContext} to make current.
* @param eglDisplay The {@link EGLDisplay} to attach the surface to.
* @param configAttributes The attributes to configure EGL with.
* @return A placeholder {@link EGLSurface} that has been focused to allow rendering to take
* place, or {@link EGL14#EGL_NO_SURFACE} if the current context supports rendering without a
* surface.
* @throws GlException If an error occurs during creation.
*/
@RequiresApi(17)
EGLSurface createFocusedPlaceholderEglSurface(
EGLContext eglContext, EGLDisplay eglDisplay, int[] configAttributes) throws GlException;
/**
* Returns a {@link GlTextureInfo} containing the identifiers of the newly created buffers.
*
* @param texId The identifier of the texture to attach to the buffers.
* @param width The width of the texture in pixels.
* @param height The height of the texture in pixels.
* @throws GlException If an error occurs during creation.
*/
GlTextureInfo createBuffersForTexture(int texId, int width, int height) throws GlException;
}

View File

@ -0,0 +1,110 @@
/*
* Copyright 2022 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 static androidx.media3.common.util.Assertions.checkState;
import androidx.media3.common.util.GlUtil;
import androidx.media3.common.util.UnstableApi;
/** Contains information describing an OpenGL texture. */
@UnstableApi
public final class GlTextureInfo {
/** A {@link GlTextureInfo} instance with all fields unset. */
public static final GlTextureInfo UNSET =
new GlTextureInfo(
/* texId= */ C.INDEX_UNSET,
/* fboId= */ C.INDEX_UNSET,
/* rboId= */ C.INDEX_UNSET,
/* width= */ C.LENGTH_UNSET,
/* height= */ C.LENGTH_UNSET);
private final int texId;
private final int fboId;
private final int rboId;
private final int width;
private final int height;
private boolean isReleased;
/**
* Creates a new instance.
*
* @param texId The OpenGL texture identifier, or {@link C#INDEX_UNSET} if not specified.
* @param fboId Identifier of a framebuffer object associated with the texture, or {@link
* C#INDEX_UNSET} if not specified.
* @param rboId Identifier of a renderbuffer object associated with the texture, or {@link
* C#INDEX_UNSET} if not specified.
* @param width The width of the texture, in pixels, or {@link C#LENGTH_UNSET} if not specified.
* @param height The height of the texture, in pixels, or {@link C#LENGTH_UNSET} if not specified.
*/
public GlTextureInfo(int texId, int fboId, int rboId, int width, int height) {
this.texId = texId;
this.fboId = fboId;
this.rboId = rboId;
this.width = width;
this.height = height;
}
/** The OpenGL texture identifier, or {@link C#INDEX_UNSET} if not specified. */
public int getTexId() {
checkState(!isReleased);
return texId;
}
/**
* Identifier of a framebuffer object associated with the texture, or {@link C#INDEX_UNSET} if not
* specified.
*/
public int getFboId() {
checkState(!isReleased);
return fboId;
}
/**
* Identifier of a renderbuffer object attached with the framebuffer, or {@link C#INDEX_UNSET} if
* not specified.
*/
public int getRboId() {
checkState(!isReleased);
return rboId;
}
/** The width of the texture, in pixels, or {@link C#LENGTH_UNSET} if not specified. */
public int getWidth() {
checkState(!isReleased);
return width;
}
/** The height of the texture, in pixels, or {@link C#LENGTH_UNSET} if not specified. */
public int getHeight() {
checkState(!isReleased);
return height;
}
public void release() throws GlUtil.GlException {
isReleased = true;
if (texId != C.INDEX_UNSET) {
GlUtil.deleteTexture(texId);
}
if (fboId != C.INDEX_UNSET) {
GlUtil.deleteFbo(fboId);
}
if (rboId != C.INDEX_UNSET) {
GlUtil.deleteRbo(rboId);
}
}
}

View File

@ -1,61 +0,0 @@
/*
* Copyright 2022 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.media.MediaPlayer;
import android.os.Looper;
import androidx.media3.common.util.UnstableApi;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
/** A {@link Player} wrapper for the legacy Android platform {@link MediaPlayer}. */
@UnstableApi
public final class LegacyMediaPlayerWrapper extends SimpleBasePlayer {
private final MediaPlayer player;
private boolean playWhenReady;
/**
* Creates the {@link MediaPlayer} wrapper.
*
* @param looper The {@link Looper} used to call all methods on.
*/
public LegacyMediaPlayerWrapper(Looper looper) {
super(looper);
this.player = new MediaPlayer();
}
@Override
protected State getState() {
return new State.Builder()
.setAvailableCommands(new Commands.Builder().addAll(Player.COMMAND_PLAY_PAUSE).build())
.setPlayWhenReady(playWhenReady, Player.PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST)
.build();
}
@Override
protected ListenableFuture<?> handleSetPlayWhenReady(boolean playWhenReady) {
this.playWhenReady = playWhenReady;
// TODO: Only call these methods if the player is in Started or Paused state.
if (playWhenReady) {
player.start();
} else {
player.pause();
}
return Futures.immediateVoidFuture();
}
}

View File

@ -23,6 +23,7 @@ import android.os.Bundle;
import androidx.annotation.IntRange; import androidx.annotation.IntRange;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.media3.common.util.Assertions; import androidx.media3.common.util.Assertions;
import androidx.media3.common.util.BundleableUtil;
import androidx.media3.common.util.UnstableApi; import androidx.media3.common.util.UnstableApi;
import androidx.media3.common.util.Util; import androidx.media3.common.util.Util;
import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableList;
@ -569,15 +570,14 @@ public final class MediaItem implements Bundleable {
} }
/** Returns a new {@link MediaItem} instance with the current builder values. */ /** Returns a new {@link MediaItem} instance with the current builder values. */
@SuppressWarnings("deprecation") // Using PlaybackProperties while it exists.
public MediaItem build() { public MediaItem build() {
// TODO: remove this check once all the deprecated individual DRM setters are removed. // TODO: remove this check once all the deprecated individual DRM setters are removed.
checkState(drmConfiguration.licenseUri == null || drmConfiguration.scheme != null); checkState(drmConfiguration.licenseUri == null || drmConfiguration.scheme != null);
@Nullable PlaybackProperties localConfiguration = null; @Nullable LocalConfiguration localConfiguration = null;
@Nullable Uri uri = this.uri; @Nullable Uri uri = this.uri;
if (uri != null) { if (uri != null) {
localConfiguration = localConfiguration =
new PlaybackProperties( new LocalConfiguration(
uri, uri,
mimeType, mimeType,
drmConfiguration.scheme != null ? drmConfiguration.build() : null, drmConfiguration.scheme != null ? drmConfiguration.build() : null,
@ -598,7 +598,7 @@ public final class MediaItem implements Bundleable {
} }
/** DRM configuration for a media item. */ /** DRM configuration for a media item. */
public static final class DrmConfiguration { public static final class DrmConfiguration implements Bundleable {
/** Builder for {@link DrmConfiguration}. */ /** Builder for {@link DrmConfiguration}. */
public static final class Builder { public static final class Builder {
@ -773,7 +773,6 @@ public final class MediaItem implements Bundleable {
} }
public DrmConfiguration build() { public DrmConfiguration build() {
return new DrmConfiguration(this); return new DrmConfiguration(this);
} }
} }
@ -888,10 +887,87 @@ public final class MediaItem implements Bundleable {
result = 31 * result + Arrays.hashCode(keySetId); result = 31 * result + Arrays.hashCode(keySetId);
return result; return result;
} }
// Bundleable implementation.
private static final String FIELD_SCHEME = Util.intToStringMaxRadix(0);
private static final String FIELD_LICENSE_URI = Util.intToStringMaxRadix(1);
private static final String FIELD_LICENSE_REQUEST_HEADERS = Util.intToStringMaxRadix(2);
private static final String FIELD_MULTI_SESSION = Util.intToStringMaxRadix(3);
private static final String FIELD_PLAY_CLEAR_CONTENT_WITHOUT_KEY = Util.intToStringMaxRadix(4);
private static final String FIELD_FORCE_DEFAULT_LICENSE_URI = Util.intToStringMaxRadix(5);
private static final String FIELD_FORCED_SESSION_TRACK_TYPES = Util.intToStringMaxRadix(6);
private static final String FIELD_KEY_SET_ID = Util.intToStringMaxRadix(7);
/** An object that can restore {@link DrmConfiguration} from a {@link Bundle}. */
@UnstableApi
public static final Creator<DrmConfiguration> CREATOR = DrmConfiguration::fromBundle;
@UnstableApi
private static DrmConfiguration fromBundle(Bundle bundle) {
UUID scheme = UUID.fromString(checkNotNull(bundle.getString(FIELD_SCHEME)));
@Nullable Uri licenseUri = bundle.getParcelable(FIELD_LICENSE_URI);
Bundle licenseMapAsBundle =
BundleableUtil.getBundleWithDefault(bundle, FIELD_LICENSE_REQUEST_HEADERS, Bundle.EMPTY);
ImmutableMap<String, String> licenseRequestHeaders =
BundleableUtil.bundleToStringImmutableMap(licenseMapAsBundle);
boolean multiSession = bundle.getBoolean(FIELD_MULTI_SESSION, false);
boolean playClearContentWithoutKey =
bundle.getBoolean(FIELD_PLAY_CLEAR_CONTENT_WITHOUT_KEY, false);
boolean forceDefaultLicenseUri = bundle.getBoolean(FIELD_FORCE_DEFAULT_LICENSE_URI, false);
ArrayList<@C.TrackType Integer> forcedSessionTrackTypesArray =
BundleableUtil.getIntegerArrayListWithDefault(
bundle, FIELD_FORCED_SESSION_TRACK_TYPES, new ArrayList<>());
ImmutableList<@C.TrackType Integer> forcedSessionTrackTypes =
ImmutableList.copyOf(forcedSessionTrackTypesArray);
@Nullable byte[] keySetId = bundle.getByteArray(FIELD_KEY_SET_ID);
Builder builder = new Builder(scheme);
return builder
.setLicenseUri(licenseUri)
.setLicenseRequestHeaders(licenseRequestHeaders)
.setMultiSession(multiSession)
.setForceDefaultLicenseUri(forceDefaultLicenseUri)
.setPlayClearContentWithoutKey(playClearContentWithoutKey)
.setForcedSessionTrackTypes(forcedSessionTrackTypes)
.setKeySetId(keySetId)
.build();
}
@UnstableApi
@Override
public Bundle toBundle() {
Bundle bundle = new Bundle();
bundle.putString(FIELD_SCHEME, scheme.toString());
if (licenseUri != null) {
bundle.putParcelable(FIELD_LICENSE_URI, licenseUri);
}
if (!licenseRequestHeaders.isEmpty()) {
bundle.putBundle(
FIELD_LICENSE_REQUEST_HEADERS, BundleableUtil.stringMapToBundle(licenseRequestHeaders));
}
if (multiSession) {
bundle.putBoolean(FIELD_MULTI_SESSION, multiSession);
}
if (playClearContentWithoutKey) {
bundle.putBoolean(FIELD_PLAY_CLEAR_CONTENT_WITHOUT_KEY, playClearContentWithoutKey);
}
if (forceDefaultLicenseUri) {
bundle.putBoolean(FIELD_FORCE_DEFAULT_LICENSE_URI, forceDefaultLicenseUri);
}
if (!forcedSessionTrackTypes.isEmpty()) {
bundle.putIntegerArrayList(
FIELD_FORCED_SESSION_TRACK_TYPES, new ArrayList<>(forcedSessionTrackTypes));
}
if (keySetId != null) {
bundle.putByteArray(FIELD_KEY_SET_ID, keySetId);
}
return bundle;
}
} }
/** Configuration for playing back linear ads with a media item. */ /** Configuration for playing back linear ads with a media item. */
public static final class AdsConfiguration { public static final class AdsConfiguration implements Bundleable {
/** Builder for {@link AdsConfiguration} instances. */ /** Builder for {@link AdsConfiguration} instances. */
public static final class Builder { public static final class Builder {
@ -975,11 +1051,43 @@ public final class MediaItem implements Bundleable {
result = 31 * result + (adsId != null ? adsId.hashCode() : 0); result = 31 * result + (adsId != null ? adsId.hashCode() : 0);
return result; return result;
} }
// Bundleable implementation.
private static final String FIELD_AD_TAG_URI = Util.intToStringMaxRadix(0);
/**
* An object that can restore {@link AdsConfiguration} from a {@link Bundle}.
*
* <p>The {@link #adsId} of a restored instance will always be {@code null}.
*/
@UnstableApi
public static final Creator<AdsConfiguration> CREATOR = AdsConfiguration::fromBundle;
@UnstableApi
private static AdsConfiguration fromBundle(Bundle bundle) {
@Nullable Uri adTagUri = bundle.getParcelable(FIELD_AD_TAG_URI);
checkNotNull(adTagUri);
return new AdsConfiguration.Builder(adTagUri).build();
}
/**
* {@inheritDoc}
*
* <p>It omits the {@link #adsId} field. The {@link #adsId} of an instance restored from such a
* bundle by {@link #CREATOR} will be {@code null}.
*/
@UnstableApi
@Override
public Bundle toBundle() {
Bundle bundle = new Bundle();
bundle.putParcelable(FIELD_AD_TAG_URI, adTagUri);
return bundle;
}
} }
/** Properties for local playback. */ /** Properties for local playback. */
// TODO: Mark this final when PlaybackProperties is deleted. public static final class LocalConfiguration implements Bundleable {
public static class LocalConfiguration {
/** The {@link Uri}. */ /** The {@link Uri}. */
public final Uri uri; public final Uri uri;
@ -1075,33 +1183,84 @@ public final class MediaItem implements Bundleable {
result = 31 * result + (tag == null ? 0 : tag.hashCode()); result = 31 * result + (tag == null ? 0 : tag.hashCode());
return result; return result;
} }
}
// Bundleable implementation.
private static final String FIELD_URI = Util.intToStringMaxRadix(0);
private static final String FIELD_MIME_TYPE = Util.intToStringMaxRadix(1);
private static final String FIELD_DRM_CONFIGURATION = Util.intToStringMaxRadix(2);
private static final String FIELD_ADS_CONFIGURATION = Util.intToStringMaxRadix(3);
private static final String FIELD_STREAM_KEYS = Util.intToStringMaxRadix(4);
private static final String FIELD_CUSTOM_CACHE_KEY = Util.intToStringMaxRadix(5);
private static final String FIELD_SUBTITLE_CONFIGURATION = Util.intToStringMaxRadix(6);
/** /**
* @deprecated Use {@link LocalConfiguration}. * {@inheritDoc}
*
* <p>It omits the {@link #tag} field. The {@link #tag} of an instance restored from such a
* bundle by {@link #CREATOR} will be {@code null}.
*/ */
@UnstableApi @UnstableApi
@Deprecated @Override
public static final class PlaybackProperties extends LocalConfiguration { public Bundle toBundle() {
Bundle bundle = new Bundle();
bundle.putParcelable(FIELD_URI, uri);
if (mimeType != null) {
bundle.putString(FIELD_MIME_TYPE, mimeType);
}
if (drmConfiguration != null) {
bundle.putBundle(FIELD_DRM_CONFIGURATION, drmConfiguration.toBundle());
}
if (adsConfiguration != null) {
bundle.putBundle(FIELD_ADS_CONFIGURATION, adsConfiguration.toBundle());
}
if (!streamKeys.isEmpty()) {
bundle.putParcelableArrayList(
FIELD_STREAM_KEYS, BundleableUtil.toBundleArrayList(streamKeys));
}
if (customCacheKey != null) {
bundle.putString(FIELD_CUSTOM_CACHE_KEY, customCacheKey);
}
if (!subtitleConfigurations.isEmpty()) {
bundle.putParcelableArrayList(
FIELD_SUBTITLE_CONFIGURATION, BundleableUtil.toBundleArrayList(subtitleConfigurations));
}
return bundle;
}
private PlaybackProperties( /** Object that can restore {@link LocalConfiguration} from a {@link Bundle}. */
Uri uri, @UnstableApi
@Nullable String mimeType, public static final Creator<LocalConfiguration> CREATOR = LocalConfiguration::fromBundle;
@Nullable DrmConfiguration drmConfiguration,
@Nullable AdsConfiguration adsConfiguration, @UnstableApi
List<StreamKey> streamKeys, private static LocalConfiguration fromBundle(Bundle bundle) {
@Nullable String customCacheKey, @Nullable Bundle drmBundle = bundle.getBundle(FIELD_DRM_CONFIGURATION);
ImmutableList<SubtitleConfiguration> subtitleConfigurations, DrmConfiguration drmConfiguration =
@Nullable Object tag) { drmBundle == null ? null : DrmConfiguration.CREATOR.fromBundle(drmBundle);
super( @Nullable Bundle adsBundle = bundle.getBundle(FIELD_ADS_CONFIGURATION);
uri, AdsConfiguration adsConfiguration =
mimeType, adsBundle == null ? null : AdsConfiguration.CREATOR.fromBundle(adsBundle);
@Nullable List<Bundle> streamKeysBundles = bundle.getParcelableArrayList(FIELD_STREAM_KEYS);
List<StreamKey> streamKeys =
streamKeysBundles == null
? ImmutableList.of()
: BundleableUtil.fromBundleList(StreamKey::fromBundle, streamKeysBundles);
@Nullable
List<Bundle> subtitleBundles = bundle.getParcelableArrayList(FIELD_SUBTITLE_CONFIGURATION);
ImmutableList<SubtitleConfiguration> subtitleConfiguration =
subtitleBundles == null
? ImmutableList.of()
: BundleableUtil.fromBundleList(SubtitleConfiguration.CREATOR, subtitleBundles);
return new LocalConfiguration(
checkNotNull(bundle.getParcelable(FIELD_URI)),
bundle.getString(FIELD_MIME_TYPE),
drmConfiguration, drmConfiguration,
adsConfiguration, adsConfiguration,
streamKeys, streamKeys,
customCacheKey, bundle.getString(FIELD_CUSTOM_CACHE_KEY),
subtitleConfigurations, subtitleConfiguration,
tag); /* tag= */ null);
} }
} }
@ -1326,7 +1485,7 @@ public final class MediaItem implements Bundleable {
return bundle; return bundle;
} }
/** Object that can restore {@link LiveConfiguration} from a {@link Bundle}. */ /** An object that can restore {@link LiveConfiguration} from a {@link Bundle}. */
@UnstableApi @UnstableApi
public static final Creator<LiveConfiguration> CREATOR = public static final Creator<LiveConfiguration> CREATOR =
bundle -> bundle ->
@ -1342,7 +1501,7 @@ public final class MediaItem implements Bundleable {
/** Properties for a text track. */ /** Properties for a text track. */
// TODO: Mark this final when Subtitle is deleted. // TODO: Mark this final when Subtitle is deleted.
public static class SubtitleConfiguration { public static class SubtitleConfiguration implements Bundleable {
/** Builder for {@link SubtitleConfiguration} instances. */ /** Builder for {@link SubtitleConfiguration} instances. */
public static final class Builder { public static final class Builder {
@ -1513,6 +1672,67 @@ public final class MediaItem implements Bundleable {
result = 31 * result + (id == null ? 0 : id.hashCode()); result = 31 * result + (id == null ? 0 : id.hashCode());
return result; return result;
} }
// Bundleable implementation.
private static final String FIELD_URI = Util.intToStringMaxRadix(0);
private static final String FIELD_MIME_TYPE = Util.intToStringMaxRadix(1);
private static final String FIELD_LANGUAGE = Util.intToStringMaxRadix(2);
private static final String FIELD_SELECTION_FLAGS = Util.intToStringMaxRadix(3);
private static final String FIELD_ROLE_FLAGS = Util.intToStringMaxRadix(4);
private static final String FIELD_LABEL = Util.intToStringMaxRadix(5);
private static final String FIELD_ID = Util.intToStringMaxRadix(6);
/** An object that can restore {@link SubtitleConfiguration} from a {@link Bundle}. */
@UnstableApi
public static final Creator<SubtitleConfiguration> CREATOR = SubtitleConfiguration::fromBundle;
@UnstableApi
private static SubtitleConfiguration fromBundle(Bundle bundle) {
Uri uri = checkNotNull(bundle.getParcelable(FIELD_URI));
@Nullable String mimeType = bundle.getString(FIELD_MIME_TYPE);
@Nullable String language = bundle.getString(FIELD_LANGUAGE);
@C.SelectionFlags int selectionFlags = bundle.getInt(FIELD_SELECTION_FLAGS, 0);
@C.RoleFlags int roleFlags = bundle.getInt(FIELD_ROLE_FLAGS, 0);
@Nullable String label = bundle.getString(FIELD_LABEL);
@Nullable String id = bundle.getString(FIELD_ID);
SubtitleConfiguration.Builder builder = new SubtitleConfiguration.Builder(uri);
return builder
.setMimeType(mimeType)
.setLanguage(language)
.setSelectionFlags(selectionFlags)
.setRoleFlags(roleFlags)
.setLabel(label)
.setId(id)
.build();
}
@UnstableApi
@Override
public Bundle toBundle() {
Bundle bundle = new Bundle();
bundle.putParcelable(FIELD_URI, uri);
if (mimeType != null) {
bundle.putString(FIELD_MIME_TYPE, mimeType);
}
if (language != null) {
bundle.putString(FIELD_LANGUAGE, language);
}
if (selectionFlags != 0) {
bundle.putInt(FIELD_SELECTION_FLAGS, selectionFlags);
}
if (roleFlags != 0) {
bundle.putInt(FIELD_ROLE_FLAGS, roleFlags);
}
if (label != null) {
bundle.putString(FIELD_LABEL, label);
}
if (id != null) {
bundle.putString(FIELD_ID, id);
}
return bundle;
}
} }
/** /**
@ -1757,7 +1977,7 @@ public final class MediaItem implements Bundleable {
return bundle; return bundle;
} }
/** Object that can restore {@link ClippingConfiguration} from a {@link Bundle}. */ /** An object that can restore {@link ClippingConfiguration} from a {@link Bundle}. */
@UnstableApi @UnstableApi
public static final Creator<ClippingProperties> CREATOR = public static final Creator<ClippingProperties> CREATOR =
bundle -> bundle ->
@ -1917,7 +2137,7 @@ public final class MediaItem implements Bundleable {
return bundle; return bundle;
} }
/** Object that can restore {@link RequestMetadata} from a {@link Bundle}. */ /** An object that can restore {@link RequestMetadata} from a {@link Bundle}. */
@UnstableApi @UnstableApi
public static final Creator<RequestMetadata> CREATOR = public static final Creator<RequestMetadata> CREATOR =
bundle -> bundle ->
@ -1948,7 +2168,7 @@ public final class MediaItem implements Bundleable {
/** /**
* @deprecated Use {@link #localConfiguration} instead. * @deprecated Use {@link #localConfiguration} instead.
*/ */
@UnstableApi @Deprecated @Nullable public final PlaybackProperties playbackProperties; @UnstableApi @Deprecated @Nullable public final LocalConfiguration playbackProperties;
/** The live playback configuration. */ /** The live playback configuration. */
public final LiveConfiguration liveConfiguration; public final LiveConfiguration liveConfiguration;
@ -1966,12 +2186,12 @@ public final class MediaItem implements Bundleable {
/** The media {@link RequestMetadata}. */ /** The media {@link RequestMetadata}. */
public final RequestMetadata requestMetadata; public final RequestMetadata requestMetadata;
// Using PlaybackProperties and ClippingProperties until they're deleted. // Using ClippingProperties until they're deleted.
@SuppressWarnings("deprecation") @SuppressWarnings("deprecation")
private MediaItem( private MediaItem(
String mediaId, String mediaId,
ClippingProperties clippingConfiguration, ClippingProperties clippingConfiguration,
@Nullable PlaybackProperties localConfiguration, @Nullable LocalConfiguration localConfiguration,
LiveConfiguration liveConfiguration, LiveConfiguration liveConfiguration,
MediaMetadata mediaMetadata, MediaMetadata mediaMetadata,
RequestMetadata requestMetadata) { RequestMetadata requestMetadata) {
@ -2026,16 +2246,10 @@ public final class MediaItem implements Bundleable {
private static final String FIELD_MEDIA_METADATA = Util.intToStringMaxRadix(2); private static final String FIELD_MEDIA_METADATA = Util.intToStringMaxRadix(2);
private static final String FIELD_CLIPPING_PROPERTIES = Util.intToStringMaxRadix(3); private static final String FIELD_CLIPPING_PROPERTIES = Util.intToStringMaxRadix(3);
private static final String FIELD_REQUEST_METADATA = Util.intToStringMaxRadix(4); private static final String FIELD_REQUEST_METADATA = Util.intToStringMaxRadix(4);
private static final String FIELD_LOCAL_CONFIGURATION = Util.intToStringMaxRadix(5);
/**
* {@inheritDoc}
*
* <p>It omits the {@link #localConfiguration} field. The {@link #localConfiguration} of an
* instance restored by {@link #CREATOR} will always be {@code null}.
*/
@UnstableApi @UnstableApi
@Override private Bundle toBundle(boolean includeLocalConfiguration) {
public Bundle toBundle() {
Bundle bundle = new Bundle(); Bundle bundle = new Bundle();
if (!mediaId.equals(DEFAULT_MEDIA_ID)) { if (!mediaId.equals(DEFAULT_MEDIA_ID)) {
bundle.putString(FIELD_MEDIA_ID, mediaId); bundle.putString(FIELD_MEDIA_ID, mediaId);
@ -2052,11 +2266,35 @@ public final class MediaItem implements Bundleable {
if (!requestMetadata.equals(RequestMetadata.EMPTY)) { if (!requestMetadata.equals(RequestMetadata.EMPTY)) {
bundle.putBundle(FIELD_REQUEST_METADATA, requestMetadata.toBundle()); bundle.putBundle(FIELD_REQUEST_METADATA, requestMetadata.toBundle());
} }
if (includeLocalConfiguration && localConfiguration != null) {
bundle.putBundle(FIELD_LOCAL_CONFIGURATION, localConfiguration.toBundle());
}
return bundle; return bundle;
} }
/** /**
* Object that can restore {@link MediaItem} from a {@link Bundle}. * {@inheritDoc}
*
* <p>It omits the {@link #localConfiguration} field. The {@link #localConfiguration} of an
* instance restored from such a bundle by {@link #CREATOR} will be {@code null}.
*/
@UnstableApi
@Override
public Bundle toBundle() {
return toBundle(/* includeLocalConfiguration= */ false);
}
/**
* Returns a {@link Bundle} representing the information stored in this {@link #MediaItem} object,
* while including the {@link #localConfiguration} field if it is not null (otherwise skips it).
*/
@UnstableApi
public Bundle toBundleIncludeLocalConfiguration() {
return toBundle(/* includeLocalConfiguration= */ true);
}
/**
* An object that can restore {@link MediaItem} from a {@link Bundle}.
* *
* <p>The {@link #localConfiguration} of a restored instance will always be {@code null}. * <p>The {@link #localConfiguration} of a restored instance will always be {@code null}.
*/ */
@ -2093,10 +2331,17 @@ public final class MediaItem implements Bundleable {
} else { } else {
requestMetadata = RequestMetadata.CREATOR.fromBundle(requestMetadataBundle); requestMetadata = RequestMetadata.CREATOR.fromBundle(requestMetadataBundle);
} }
@Nullable Bundle localConfigurationBundle = bundle.getBundle(FIELD_LOCAL_CONFIGURATION);
LocalConfiguration localConfiguration;
if (localConfigurationBundle == null) {
localConfiguration = null;
} else {
localConfiguration = LocalConfiguration.CREATOR.fromBundle(localConfigurationBundle);
}
return new MediaItem( return new MediaItem(
mediaId, mediaId,
clippingConfiguration, clippingConfiguration,
/* localConfiguration= */ null, localConfiguration,
liveConfiguration, liveConfiguration,
mediaMetadata, mediaMetadata,
requestMetadata); requestMetadata);

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.3-beta01". */ /** The version of the library expressed as a string, for example "1.2.3" or "1.2.3-beta01". */
// Intentionally hardcoded. Do not derive from other constants (e.g. VERSION_INT) or vice versa. // Intentionally hardcoded. Do not derive from other constants (e.g. VERSION_INT) or vice versa.
public static final String VERSION = "1.0.2"; public static final String VERSION = "1.1.0";
/** The version of the library expressed as {@code TAG + "/" + VERSION}. */ /** The version of the library expressed as {@code TAG + "/" + VERSION}. */
// Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa. // Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa.
public static final String VERSION_SLASHY = "AndroidXMedia3/1.0.2"; public static final String VERSION_SLASHY = "AndroidXMedia3/1.1.0";
/** /**
* The version of the library expressed as an integer, for example 1002003300. * 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). * (123-045-006-3-00).
*/ */
// Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa. // Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa.
public static final int VERSION_INT = 1_000_002_3_00; public static final int VERSION_INT = 1_001_000_3_00;
/** Whether the library was compiled with {@link Assertions} checks enabled. */ /** Whether the library was compiled with {@link Assertions} checks enabled. */
public static final boolean ASSERTIONS_ENABLED = true; public static final boolean ASSERTIONS_ENABLED = true;

View File

@ -60,7 +60,11 @@ public final class MediaMetadata implements Bundleable {
@Nullable private Uri artworkUri; @Nullable private Uri artworkUri;
@Nullable private Integer trackNumber; @Nullable private Integer trackNumber;
@Nullable private Integer totalTrackCount; @Nullable private Integer totalTrackCount;
@Nullable private @FolderType Integer folderType;
@SuppressWarnings("deprecation") // Builder for deprecated field.
@Nullable
private @FolderType Integer folderType;
@Nullable private Boolean isBrowsable; @Nullable private Boolean isBrowsable;
@Nullable private Boolean isPlayable; @Nullable private Boolean isPlayable;
@Nullable private Integer recordingYear; @Nullable private Integer recordingYear;
@ -82,6 +86,7 @@ public final class MediaMetadata implements Bundleable {
public Builder() {} public Builder() {}
@SuppressWarnings("deprecation") // Assigning from deprecated fields.
private Builder(MediaMetadata mediaMetadata) { private Builder(MediaMetadata mediaMetadata) {
this.title = mediaMetadata.title; this.title = mediaMetadata.title;
this.artist = mediaMetadata.artist; this.artist = mediaMetadata.artist;
@ -251,9 +256,11 @@ public final class MediaMetadata implements Bundleable {
/** /**
* Sets the {@link FolderType}. * Sets the {@link FolderType}.
* *
* <p>This method will be deprecated. Use {@link #setIsBrowsable} to indicate if an item is a * @deprecated Use {@link #setIsBrowsable} to indicate if an item is a browsable folder and use
* browsable folder and use {@link #setMediaType} to indicate the type of the folder. * {@link #setMediaType} to indicate the type of the folder.
*/ */
@SuppressWarnings("deprecation") // Using deprecated type.
@Deprecated
@CanIgnoreReturnValue @CanIgnoreReturnValue
public Builder setFolderType(@Nullable @FolderType Integer folderType) { public Builder setFolderType(@Nullable @FolderType Integer folderType) {
this.folderType = folderType; this.folderType = folderType;
@ -261,7 +268,6 @@ public final class MediaMetadata implements Bundleable {
} }
/** Sets whether the media is a browsable folder. */ /** Sets whether the media is a browsable folder. */
@UnstableApi
@CanIgnoreReturnValue @CanIgnoreReturnValue
public Builder setIsBrowsable(@Nullable Boolean isBrowsable) { public Builder setIsBrowsable(@Nullable Boolean isBrowsable) {
this.isBrowsable = isBrowsable; this.isBrowsable = isBrowsable;
@ -402,7 +408,6 @@ public final class MediaMetadata implements Bundleable {
/** Sets the {@link MediaType}. */ /** Sets the {@link MediaType}. */
@CanIgnoreReturnValue @CanIgnoreReturnValue
@UnstableApi
public Builder setMediaType(@Nullable @MediaType Integer mediaType) { public Builder setMediaType(@Nullable @MediaType Integer mediaType) {
this.mediaType = mediaType; this.mediaType = mediaType;
return this; return this;
@ -458,6 +463,7 @@ public final class MediaMetadata implements Bundleable {
} }
/** Populates all the fields from {@code mediaMetadata}, provided they are non-null. */ /** Populates all the fields from {@code mediaMetadata}, provided they are non-null. */
@SuppressWarnings("deprecation") // Populating deprecated fields.
@CanIgnoreReturnValue @CanIgnoreReturnValue
@UnstableApi @UnstableApi
public Builder populate(@Nullable MediaMetadata mediaMetadata) { public Builder populate(@Nullable MediaMetadata mediaMetadata) {
@ -595,7 +601,6 @@ public final class MediaMetadata implements Bundleable {
@Documented @Documented
@Retention(RetentionPolicy.SOURCE) @Retention(RetentionPolicy.SOURCE)
@Target(TYPE_USE) @Target(TYPE_USE)
@UnstableApi
@IntDef({ @IntDef({
MEDIA_TYPE_MIXED, MEDIA_TYPE_MIXED,
MEDIA_TYPE_MUSIC, MEDIA_TYPE_MUSIC,
@ -637,111 +642,111 @@ public final class MediaMetadata implements Bundleable {
public @interface MediaType {} public @interface MediaType {}
/** Media of undetermined type or a mix of multiple {@linkplain MediaType media types}. */ /** Media of undetermined type or a mix of multiple {@linkplain MediaType media types}. */
@UnstableApi public static final int MEDIA_TYPE_MIXED = 0; public static final int MEDIA_TYPE_MIXED = 0;
/** {@link MediaType} for music. */ /** {@link MediaType} for music. */
@UnstableApi public static final int MEDIA_TYPE_MUSIC = 1; public static final int MEDIA_TYPE_MUSIC = 1;
/** {@link MediaType} for an audio book chapter. */ /** {@link MediaType} for an audio book chapter. */
@UnstableApi public static final int MEDIA_TYPE_AUDIO_BOOK_CHAPTER = 2; public static final int MEDIA_TYPE_AUDIO_BOOK_CHAPTER = 2;
/** {@link MediaType} for a podcast episode. */ /** {@link MediaType} for a podcast episode. */
@UnstableApi public static final int MEDIA_TYPE_PODCAST_EPISODE = 3; public static final int MEDIA_TYPE_PODCAST_EPISODE = 3;
/** {@link MediaType} for a radio station. */ /** {@link MediaType} for a radio station. */
@UnstableApi public static final int MEDIA_TYPE_RADIO_STATION = 4; public static final int MEDIA_TYPE_RADIO_STATION = 4;
/** {@link MediaType} for news. */ /** {@link MediaType} for news. */
@UnstableApi public static final int MEDIA_TYPE_NEWS = 5; public static final int MEDIA_TYPE_NEWS = 5;
/** {@link MediaType} for a video. */ /** {@link MediaType} for a video. */
@UnstableApi public static final int MEDIA_TYPE_VIDEO = 6; public static final int MEDIA_TYPE_VIDEO = 6;
/** {@link MediaType} for a movie trailer. */ /** {@link MediaType} for a movie trailer. */
@UnstableApi public static final int MEDIA_TYPE_TRAILER = 7; public static final int MEDIA_TYPE_TRAILER = 7;
/** {@link MediaType} for a movie. */ /** {@link MediaType} for a movie. */
@UnstableApi public static final int MEDIA_TYPE_MOVIE = 8; public static final int MEDIA_TYPE_MOVIE = 8;
/** {@link MediaType} for a TV show. */ /** {@link MediaType} for a TV show. */
@UnstableApi public static final int MEDIA_TYPE_TV_SHOW = 9; public static final int MEDIA_TYPE_TV_SHOW = 9;
/** /**
* {@link MediaType} for a group of items (e.g., {@link #MEDIA_TYPE_MUSIC music}) belonging to an * {@link MediaType} for a group of items (e.g., {@link #MEDIA_TYPE_MUSIC music}) belonging to an
* album. * album.
*/ */
@UnstableApi public static final int MEDIA_TYPE_ALBUM = 10; public static final int MEDIA_TYPE_ALBUM = 10;
/** /**
* {@link MediaType} for a group of items (e.g., {@link #MEDIA_TYPE_MUSIC music}) from the same * {@link MediaType} for a group of items (e.g., {@link #MEDIA_TYPE_MUSIC music}) from the same
* artist. * artist.
*/ */
@UnstableApi public static final int MEDIA_TYPE_ARTIST = 11; public static final int MEDIA_TYPE_ARTIST = 11;
/** /**
* {@link MediaType} for a group of items (e.g., {@link #MEDIA_TYPE_MUSIC music}) of the same * {@link MediaType} for a group of items (e.g., {@link #MEDIA_TYPE_MUSIC music}) of the same
* genre. * genre.
*/ */
@UnstableApi public static final int MEDIA_TYPE_GENRE = 12; public static final int MEDIA_TYPE_GENRE = 12;
/** /**
* {@link MediaType} for a group of items (e.g., {@link #MEDIA_TYPE_MUSIC music}) forming a * {@link MediaType} for a group of items (e.g., {@link #MEDIA_TYPE_MUSIC music}) forming a
* playlist. * playlist.
*/ */
@UnstableApi public static final int MEDIA_TYPE_PLAYLIST = 13; public static final int MEDIA_TYPE_PLAYLIST = 13;
/** /**
* {@link MediaType} for a group of items (e.g., {@link #MEDIA_TYPE_MUSIC music}) from the same * {@link MediaType} for a group of items (e.g., {@link #MEDIA_TYPE_MUSIC music}) from the same
* year. * year.
*/ */
@UnstableApi public static final int MEDIA_TYPE_YEAR = 14; public static final int MEDIA_TYPE_YEAR = 14;
/** /**
* {@link MediaType} for a group of items forming an audio book. Items in this group are typically * {@link MediaType} for a group of items forming an audio book. Items in this group are typically
* of type {@link #MEDIA_TYPE_AUDIO_BOOK_CHAPTER}. * of type {@link #MEDIA_TYPE_AUDIO_BOOK_CHAPTER}.
*/ */
@UnstableApi public static final int MEDIA_TYPE_AUDIO_BOOK = 15; public static final int MEDIA_TYPE_AUDIO_BOOK = 15;
/** /**
* {@link MediaType} for a group of items belonging to a podcast. Items in this group are * {@link MediaType} for a group of items belonging to a podcast. Items in this group are
* typically of type {@link #MEDIA_TYPE_PODCAST_EPISODE}. * typically of type {@link #MEDIA_TYPE_PODCAST_EPISODE}.
*/ */
@UnstableApi public static final int MEDIA_TYPE_PODCAST = 16; public static final int MEDIA_TYPE_PODCAST = 16;
/** /**
* {@link MediaType} for a group of items that are part of a TV channel. Items in this group are * {@link MediaType} for a group of items that are part of a TV channel. Items in this group are
* typically of type {@link #MEDIA_TYPE_TV_SHOW}, {@link #MEDIA_TYPE_TV_SERIES} or {@link * typically of type {@link #MEDIA_TYPE_TV_SHOW}, {@link #MEDIA_TYPE_TV_SERIES} or {@link
* #MEDIA_TYPE_MOVIE}. * #MEDIA_TYPE_MOVIE}.
*/ */
@UnstableApi public static final int MEDIA_TYPE_TV_CHANNEL = 17; public static final int MEDIA_TYPE_TV_CHANNEL = 17;
/** /**
* {@link MediaType} for a group of items that are part of a TV series. Items in this group are * {@link MediaType} for a group of items that are part of a TV series. Items in this group are
* typically of type {@link #MEDIA_TYPE_TV_SHOW} or {@link #MEDIA_TYPE_TV_SEASON}. * typically of type {@link #MEDIA_TYPE_TV_SHOW} or {@link #MEDIA_TYPE_TV_SEASON}.
*/ */
@UnstableApi public static final int MEDIA_TYPE_TV_SERIES = 18; public static final int MEDIA_TYPE_TV_SERIES = 18;
/** /**
* {@link MediaType} for a group of items that are part of a TV series. Items in this group are * {@link MediaType} for a group of items that are part of a TV series. Items in this group are
* typically of type {@link #MEDIA_TYPE_TV_SHOW}. * typically of type {@link #MEDIA_TYPE_TV_SHOW}.
*/ */
@UnstableApi public static final int MEDIA_TYPE_TV_SEASON = 19; public static final int MEDIA_TYPE_TV_SEASON = 19;
/** {@link MediaType} for a folder with mixed or undetermined content. */ /** {@link MediaType} for a folder with mixed or undetermined content. */
@UnstableApi public static final int MEDIA_TYPE_FOLDER_MIXED = 20; public static final int MEDIA_TYPE_FOLDER_MIXED = 20;
/** {@link MediaType} for a folder containing {@linkplain #MEDIA_TYPE_ALBUM albums}. */ /** {@link MediaType} for a folder containing {@linkplain #MEDIA_TYPE_ALBUM albums}. */
@UnstableApi public static final int MEDIA_TYPE_FOLDER_ALBUMS = 21; public static final int MEDIA_TYPE_FOLDER_ALBUMS = 21;
/** {@link MediaType} for a folder containing {@linkplain #FIELD_ARTIST artists}. */ /** {@link MediaType} for a folder containing {@linkplain #FIELD_ARTIST artists}. */
@UnstableApi public static final int MEDIA_TYPE_FOLDER_ARTISTS = 22; public static final int MEDIA_TYPE_FOLDER_ARTISTS = 22;
/** {@link MediaType} for a folder containing {@linkplain #MEDIA_TYPE_GENRE genres}. */ /** {@link MediaType} for a folder containing {@linkplain #MEDIA_TYPE_GENRE genres}. */
@UnstableApi public static final int MEDIA_TYPE_FOLDER_GENRES = 23; public static final int MEDIA_TYPE_FOLDER_GENRES = 23;
/** {@link MediaType} for a folder containing {@linkplain #MEDIA_TYPE_PLAYLIST playlists}. */ /** {@link MediaType} for a folder containing {@linkplain #MEDIA_TYPE_PLAYLIST playlists}. */
@UnstableApi public static final int MEDIA_TYPE_FOLDER_PLAYLISTS = 24; public static final int MEDIA_TYPE_FOLDER_PLAYLISTS = 24;
/** {@link MediaType} for a folder containing {@linkplain #MEDIA_TYPE_YEAR years}. */ /** {@link MediaType} for a folder containing {@linkplain #MEDIA_TYPE_YEAR years}. */
@UnstableApi public static final int MEDIA_TYPE_FOLDER_YEARS = 25; public static final int MEDIA_TYPE_FOLDER_YEARS = 25;
/** {@link MediaType} for a folder containing {@linkplain #MEDIA_TYPE_AUDIO_BOOK audio books}. */ /** {@link MediaType} for a folder containing {@linkplain #MEDIA_TYPE_AUDIO_BOOK audio books}. */
@UnstableApi public static final int MEDIA_TYPE_FOLDER_AUDIO_BOOKS = 26; public static final int MEDIA_TYPE_FOLDER_AUDIO_BOOKS = 26;
/** {@link MediaType} for a folder containing {@linkplain #MEDIA_TYPE_PODCAST podcasts}. */ /** {@link MediaType} for a folder containing {@linkplain #MEDIA_TYPE_PODCAST podcasts}. */
@UnstableApi public static final int MEDIA_TYPE_FOLDER_PODCASTS = 27; public static final int MEDIA_TYPE_FOLDER_PODCASTS = 27;
/** {@link MediaType} for a folder containing {@linkplain #MEDIA_TYPE_TV_CHANNEL TV channels}. */ /** {@link MediaType} for a folder containing {@linkplain #MEDIA_TYPE_TV_CHANNEL TV channels}. */
@UnstableApi public static final int MEDIA_TYPE_FOLDER_TV_CHANNELS = 28; public static final int MEDIA_TYPE_FOLDER_TV_CHANNELS = 28;
/** {@link MediaType} for a folder containing {@linkplain #MEDIA_TYPE_TV_SERIES TV series}. */ /** {@link MediaType} for a folder containing {@linkplain #MEDIA_TYPE_TV_SERIES TV series}. */
@UnstableApi public static final int MEDIA_TYPE_FOLDER_TV_SERIES = 29; public static final int MEDIA_TYPE_FOLDER_TV_SERIES = 29;
/** {@link MediaType} for a folder containing {@linkplain #MEDIA_TYPE_TV_SHOW TV shows}. */ /** {@link MediaType} for a folder containing {@linkplain #MEDIA_TYPE_TV_SHOW TV shows}. */
@UnstableApi public static final int MEDIA_TYPE_FOLDER_TV_SHOWS = 30; public static final int MEDIA_TYPE_FOLDER_TV_SHOWS = 30;
/** /**
* {@link MediaType} for a folder containing {@linkplain #MEDIA_TYPE_RADIO_STATION radio * {@link MediaType} for a folder containing {@linkplain #MEDIA_TYPE_RADIO_STATION radio
* stations}. * stations}.
*/ */
@UnstableApi public static final int MEDIA_TYPE_FOLDER_RADIO_STATIONS = 31; public static final int MEDIA_TYPE_FOLDER_RADIO_STATIONS = 31;
/** {@link MediaType} for a folder containing {@linkplain #MEDIA_TYPE_NEWS news}. */ /** {@link MediaType} for a folder containing {@linkplain #MEDIA_TYPE_NEWS news}. */
@UnstableApi public static final int MEDIA_TYPE_FOLDER_NEWS = 32; public static final int MEDIA_TYPE_FOLDER_NEWS = 32;
/** {@link MediaType} for a folder containing {@linkplain #MEDIA_TYPE_VIDEO videos}. */ /** {@link MediaType} for a folder containing {@linkplain #MEDIA_TYPE_VIDEO videos}. */
@UnstableApi public static final int MEDIA_TYPE_FOLDER_VIDEOS = 33; public static final int MEDIA_TYPE_FOLDER_VIDEOS = 33;
/** {@link MediaType} for a folder containing {@linkplain #MEDIA_TYPE_TRAILER movie trailers}. */ /** {@link MediaType} for a folder containing {@linkplain #MEDIA_TYPE_TRAILER movie trailers}. */
@UnstableApi public static final int MEDIA_TYPE_FOLDER_TRAILERS = 34; public static final int MEDIA_TYPE_FOLDER_TRAILERS = 34;
/** {@link MediaType} for a folder containing {@linkplain #MEDIA_TYPE_MOVIE movies}. */ /** {@link MediaType} for a folder containing {@linkplain #MEDIA_TYPE_MOVIE movies}. */
@UnstableApi public static final int MEDIA_TYPE_FOLDER_MOVIES = 35; public static final int MEDIA_TYPE_FOLDER_MOVIES = 35;
/** /**
* The folder type of the media item. * The folder type of the media item.
@ -753,12 +758,17 @@ public final class MediaMetadata implements Bundleable {
* <p>One of {@link #FOLDER_TYPE_NONE}, {@link #FOLDER_TYPE_MIXED}, {@link #FOLDER_TYPE_TITLES}, * <p>One of {@link #FOLDER_TYPE_NONE}, {@link #FOLDER_TYPE_MIXED}, {@link #FOLDER_TYPE_TITLES},
* {@link #FOLDER_TYPE_ALBUMS}, {@link #FOLDER_TYPE_ARTISTS}, {@link #FOLDER_TYPE_GENRES}, {@link * {@link #FOLDER_TYPE_ALBUMS}, {@link #FOLDER_TYPE_ARTISTS}, {@link #FOLDER_TYPE_GENRES}, {@link
* #FOLDER_TYPE_PLAYLISTS} or {@link #FOLDER_TYPE_YEARS}. * #FOLDER_TYPE_PLAYLISTS} or {@link #FOLDER_TYPE_YEARS}.
*
* @deprecated Use {@link #isBrowsable} to indicate if an item is a browsable folder and use
* {@link #mediaType} to indicate the type of the folder.
*/ */
// @Target list includes both 'default' targets and TYPE_USE, to ensure backwards compatibility // @Target list includes both 'default' targets and TYPE_USE, to ensure backwards compatibility
// with Kotlin usages from before TYPE_USE was added. // with Kotlin usages from before TYPE_USE was added.
@Documented @Documented
@Retention(RetentionPolicy.SOURCE) @Retention(RetentionPolicy.SOURCE)
@Target({FIELD, METHOD, PARAMETER, LOCAL_VARIABLE, TYPE_USE}) @Target({FIELD, METHOD, PARAMETER, LOCAL_VARIABLE, TYPE_USE})
@Deprecated
@SuppressWarnings("deprecation") // Defining deprecated constants.
@IntDef({ @IntDef({
FOLDER_TYPE_NONE, FOLDER_TYPE_NONE,
FOLDER_TYPE_MIXED, FOLDER_TYPE_MIXED,
@ -771,22 +781,60 @@ public final class MediaMetadata implements Bundleable {
}) })
public @interface FolderType {} public @interface FolderType {}
/** Type for an item that is not a folder. */ /**
public static final int FOLDER_TYPE_NONE = -1; * Type for an item that is not a folder.
/** Type for a folder containing media of mixed types. */ *
public static final int FOLDER_TYPE_MIXED = 0; * @deprecated Use {@link #isBrowsable} set to false instead.
/** Type for a folder containing only playable media. */ */
public static final int FOLDER_TYPE_TITLES = 1; @Deprecated public static final int FOLDER_TYPE_NONE = -1;
/** Type for a folder containing media categorized by album. */ /**
public static final int FOLDER_TYPE_ALBUMS = 2; * Type for a folder containing media of mixed types.
/** Type for a folder containing media categorized by artist. */ *
public static final int FOLDER_TYPE_ARTISTS = 3; * @deprecated Use {@link #isBrowsable} set to true and {@link #mediaType} set to {@link
/** Type for a folder containing media categorized by genre. */ * #MEDIA_TYPE_FOLDER_MIXED} instead.
public static final int FOLDER_TYPE_GENRES = 4; */
/** Type for a folder containing a playlist. */ @Deprecated public static final int FOLDER_TYPE_MIXED = 0;
public static final int FOLDER_TYPE_PLAYLISTS = 5; /**
/** Type for a folder containing media categorized by year. */ * Type for a folder containing only playable media.
public static final int FOLDER_TYPE_YEARS = 6; *
* @deprecated Use {@link #isBrowsable} set to true instead.
*/
@Deprecated public static final int FOLDER_TYPE_TITLES = 1;
/**
* Type for a folder containing media categorized by album.
*
* @deprecated Use {@link #isBrowsable} set to true and {@link #mediaType} set to {@link
* #MEDIA_TYPE_FOLDER_ALBUMS} instead.
*/
@Deprecated public static final int FOLDER_TYPE_ALBUMS = 2;
/**
* Type for a folder containing media categorized by artist.
*
* @deprecated Use {@link #isBrowsable} set to true and {@link #mediaType} set to {@link
* #MEDIA_TYPE_FOLDER_ARTISTS} instead.
*/
@Deprecated public static final int FOLDER_TYPE_ARTISTS = 3;
/**
* Type for a folder containing media categorized by genre.
*
* @deprecated Use {@link #isBrowsable} set to true and {@link #mediaType} set to {@link
* #MEDIA_TYPE_FOLDER_GENRES} instead.
*/
@Deprecated public static final int FOLDER_TYPE_GENRES = 4;
/**
* Type for a folder containing a playlist.
*
* @deprecated Use {@link #isBrowsable} set to true and {@link #mediaType} set to {@link
* #MEDIA_TYPE_FOLDER_PLAYLISTS} instead.
*/
@Deprecated public static final int FOLDER_TYPE_PLAYLISTS = 5;
/**
* Type for a folder containing media categorized by year.
*
* @deprecated Use {@link #isBrowsable} set to true and {@link #mediaType} set to {@link
* #MEDIA_TYPE_FOLDER_YEARS} instead.
*/
@Deprecated public static final int FOLDER_TYPE_YEARS = 6;
/** /**
* The picture type of the artwork. * The picture type of the artwork.
@ -895,12 +943,15 @@ public final class MediaMetadata implements Bundleable {
/** /**
* Optional {@link FolderType}. * Optional {@link FolderType}.
* *
* <p>This field will be deprecated. Use {@link #isBrowsable} to indicate if an item is a * @deprecated Use {@link #isBrowsable} to indicate if an item is a browsable folder and use
* browsable folder and use {@link #mediaType} to indicate the type of the folder. * {@link #mediaType} to indicate the type of the folder.
*/ */
@Nullable public final @FolderType Integer folderType; @SuppressWarnings("deprecation") // Defining field of deprecated type.
@Deprecated
@Nullable
public final @FolderType Integer folderType;
/** Optional boolean to indicate that the media is a browsable folder. */ /** Optional boolean to indicate that the media is a browsable folder. */
@UnstableApi @Nullable public final Boolean isBrowsable; @Nullable public final Boolean isBrowsable;
/** Optional boolean to indicate that the media is playable. */ /** Optional boolean to indicate that the media is playable. */
@Nullable public final Boolean isPlayable; @Nullable public final Boolean isPlayable;
/** /**
@ -953,7 +1004,7 @@ public final class MediaMetadata implements Bundleable {
/** Optional name of the station streaming the media. */ /** Optional name of the station streaming the media. */
@Nullable public final CharSequence station; @Nullable public final CharSequence station;
/** Optional {@link MediaType}. */ /** Optional {@link MediaType}. */
@UnstableApi @Nullable public final @MediaType Integer mediaType; @Nullable public final @MediaType Integer mediaType;
/** /**
* Optional extras {@link Bundle}. * Optional extras {@link Bundle}.
@ -963,6 +1014,7 @@ public final class MediaMetadata implements Bundleable {
*/ */
@Nullable public final Bundle extras; @Nullable public final Bundle extras;
@SuppressWarnings("deprecation") // Assigning deprecated fields.
private MediaMetadata(Builder builder) { private MediaMetadata(Builder builder) {
// Handle compatibility for deprecated fields. // Handle compatibility for deprecated fields.
@Nullable Boolean isBrowsable = builder.isBrowsable; @Nullable Boolean isBrowsable = builder.isBrowsable;
@ -1021,6 +1073,7 @@ public final class MediaMetadata implements Bundleable {
return new Builder(/* mediaMetadata= */ this); return new Builder(/* mediaMetadata= */ this);
} }
@SuppressWarnings("deprecation") // Comparing deprecated fields.
@Override @Override
public boolean equals(@Nullable Object obj) { public boolean equals(@Nullable Object obj) {
if (this == obj) { if (this == obj) {
@ -1064,6 +1117,7 @@ public final class MediaMetadata implements Bundleable {
&& Util.areEqual(mediaType, that.mediaType); && Util.areEqual(mediaType, that.mediaType);
} }
@SuppressWarnings("deprecation") // Hashing deprecated fields.
@Override @Override
public int hashCode() { public int hashCode() {
return Objects.hashCode( return Objects.hashCode(
@ -1138,6 +1192,7 @@ public final class MediaMetadata implements Bundleable {
private static final String FIELD_IS_BROWSABLE = Util.intToStringMaxRadix(32); private static final String FIELD_IS_BROWSABLE = Util.intToStringMaxRadix(32);
private static final String FIELD_EXTRAS = Util.intToStringMaxRadix(1000); private static final String FIELD_EXTRAS = Util.intToStringMaxRadix(1000);
@SuppressWarnings("deprecation") // Bundling deprecated fields.
@UnstableApi @UnstableApi
@Override @Override
public Bundle toBundle() { public Bundle toBundle() {
@ -1247,6 +1302,7 @@ public final class MediaMetadata implements Bundleable {
/** Object that can restore {@link MediaMetadata} from a {@link Bundle}. */ /** Object that can restore {@link MediaMetadata} from a {@link Bundle}. */
@UnstableApi public static final Creator<MediaMetadata> CREATOR = MediaMetadata::fromBundle; @UnstableApi public static final Creator<MediaMetadata> CREATOR = MediaMetadata::fromBundle;
@SuppressWarnings("deprecation") // Unbundling deprecated fields.
private static MediaMetadata fromBundle(Bundle bundle) { private static MediaMetadata fromBundle(Bundle bundle) {
Builder builder = new Builder(); Builder builder = new Builder();
builder builder
@ -1329,6 +1385,7 @@ public final class MediaMetadata implements Bundleable {
return builder.build(); return builder.build();
} }
@SuppressWarnings("deprecation") // Converting deprecated field.
private static @FolderType int getFolderTypeFromMediaType(@MediaType int mediaType) { private static @FolderType int getFolderTypeFromMediaType(@MediaType int mediaType) {
switch (mediaType) { switch (mediaType) {
case MEDIA_TYPE_ALBUM: case MEDIA_TYPE_ALBUM:
@ -1378,6 +1435,7 @@ public final class MediaMetadata implements Bundleable {
} }
} }
@SuppressWarnings("deprecation") // Converting deprecated field.
private static @MediaType int getMediaTypeFromFolderType(@FolderType int folderType) { private static @MediaType int getMediaTypeFromFolderType(@FolderType int folderType) {
switch (folderType) { switch (folderType) {
case FOLDER_TYPE_ALBUMS: case FOLDER_TYPE_ALBUMS:

View File

@ -60,6 +60,7 @@ public final class MimeTypes {
public static final String VIDEO_MJPEG = BASE_TYPE_VIDEO + "/mjpeg"; public static final String VIDEO_MJPEG = BASE_TYPE_VIDEO + "/mjpeg";
public static final String VIDEO_MP42 = BASE_TYPE_VIDEO + "/mp42"; public static final String VIDEO_MP42 = BASE_TYPE_VIDEO + "/mp42";
public static final String VIDEO_MP43 = BASE_TYPE_VIDEO + "/mp43"; public static final String VIDEO_MP43 = BASE_TYPE_VIDEO + "/mp43";
@UnstableApi public static final String VIDEO_RAW = BASE_TYPE_VIDEO + "/raw";
@UnstableApi public static final String VIDEO_UNKNOWN = BASE_TYPE_VIDEO + "/x-unknown"; @UnstableApi public static final String VIDEO_UNKNOWN = BASE_TYPE_VIDEO + "/x-unknown";
// audio/ MIME types // audio/ MIME types
@ -130,7 +131,12 @@ public final class MimeTypes {
public static final String APPLICATION_TX3G = BASE_TYPE_APPLICATION + "/x-quicktime-tx3g"; public static final String APPLICATION_TX3G = BASE_TYPE_APPLICATION + "/x-quicktime-tx3g";
public static final String APPLICATION_MP4VTT = BASE_TYPE_APPLICATION + "/x-mp4-vtt"; public static final String APPLICATION_MP4VTT = BASE_TYPE_APPLICATION + "/x-mp4-vtt";
public static final String APPLICATION_MP4CEA608 = BASE_TYPE_APPLICATION + "/x-mp4-cea-608"; public static final String APPLICATION_MP4CEA608 = BASE_TYPE_APPLICATION + "/x-mp4-cea-608";
public static final String APPLICATION_RAWCC = BASE_TYPE_APPLICATION + "/x-rawcc"; /**
* @deprecated RawCC is a Google-internal subtitle format that isn't supported by this version of
* Media3. There is no replacement for this value.
*/
@Deprecated public static final String APPLICATION_RAWCC = BASE_TYPE_APPLICATION + "/x-rawcc";
public static final String APPLICATION_VOBSUB = BASE_TYPE_APPLICATION + "/vobsub"; public static final String APPLICATION_VOBSUB = BASE_TYPE_APPLICATION + "/vobsub";
public static final String APPLICATION_PGS = BASE_TYPE_APPLICATION + "/pgs"; public static final String APPLICATION_PGS = BASE_TYPE_APPLICATION + "/pgs";
@UnstableApi public static final String APPLICATION_SCTE35 = BASE_TYPE_APPLICATION + "/x-scte35"; @UnstableApi public static final String APPLICATION_SCTE35 = BASE_TYPE_APPLICATION + "/x-scte35";
@ -147,7 +153,11 @@ public final class MimeTypes {
// image/ MIME types // image/ MIME types
public static final String IMAGE_PNG = BASE_TYPE_IMAGE + "/png";
public static final String IMAGE_WEBP = BASE_TYPE_IMAGE + "/webp";
public static final String IMAGE_JPEG = BASE_TYPE_IMAGE + "/jpeg"; public static final String IMAGE_JPEG = BASE_TYPE_IMAGE + "/jpeg";
public static final String IMAGE_HEIC = BASE_TYPE_IMAGE + "/heic";
public static final String IMAGE_HEIF = BASE_TYPE_IMAGE + "/heif";
/** /**
* A non-standard codec string for E-AC3-JOC. Use of this constant allows for disambiguation * A non-standard codec string for E-AC3-JOC. Use of this constant allows for disambiguation
@ -585,6 +595,10 @@ public final class MimeTypes {
return C.ENCODING_DTS; return C.ENCODING_DTS;
case MimeTypes.AUDIO_DTS_HD: case MimeTypes.AUDIO_DTS_HD:
return C.ENCODING_DTS_HD; return C.ENCODING_DTS_HD;
case MimeTypes.AUDIO_DTS_EXPRESS:
return C.ENCODING_DTS_HD;
case MimeTypes.AUDIO_DTS_X:
return C.ENCODING_DTS_UHD_P2;
case MimeTypes.AUDIO_TRUEHD: case MimeTypes.AUDIO_TRUEHD:
return C.ENCODING_DOLBY_TRUEHD; return C.ENCODING_DOLBY_TRUEHD;
case MimeTypes.AUDIO_OPUS: case MimeTypes.AUDIO_OPUS:

View File

@ -0,0 +1,26 @@
/*
* Copyright 2023 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 androidx.media3.common.util.UnstableApi;
/** A listener for processing input frames. */
@UnstableApi
public interface OnInputFrameProcessedListener {
/** Called when the given input frame has been processed. */
void onInputFrameProcessed(int textureId) throws VideoFrameProcessingException;
}

View File

@ -107,4 +107,15 @@ public class ParserException extends IOException {
this.contentIsMalformed = contentIsMalformed; this.contentIsMalformed = contentIsMalformed;
this.dataType = dataType; this.dataType = dataType;
} }
@Nullable
@Override
public String getMessage() {
return super.getMessage()
+ "{contentIsMalformed="
+ contentIsMalformed
+ ", dataType="
+ dataType
+ "}";
}
} }

View File

@ -229,6 +229,13 @@ public class PlaybackException extends Exception implements Bundleable {
/** Caused by an expired DRM license being loaded into an open DRM session. */ /** Caused by an expired DRM license being loaded into an open DRM session. */
public static final int ERROR_CODE_DRM_LICENSE_EXPIRED = 6008; public static final int ERROR_CODE_DRM_LICENSE_EXPIRED = 6008;
// Frame processing errors (7xxx).
/** Caused by a failure when initializing a {@link VideoFrameProcessor}. */
@UnstableApi public static final int ERROR_CODE_VIDEO_FRAME_PROCESSOR_INIT_FAILED = 7000;
/** Caused by a failure when processing a video frame. */
@UnstableApi public static final int ERROR_CODE_VIDEO_FRAME_PROCESSING_FAILED = 7001;
/** /**
* Player implementations that want to surface custom errors can use error codes greater than this * Player implementations that want to surface custom errors can use error codes greater than this
* value, so as to avoid collision with other error codes defined in this class. * value, so as to avoid collision with other error codes defined in this class.
@ -306,6 +313,10 @@ public class PlaybackException extends Exception implements Bundleable {
return "ERROR_CODE_DRM_DEVICE_REVOKED"; return "ERROR_CODE_DRM_DEVICE_REVOKED";
case ERROR_CODE_DRM_LICENSE_EXPIRED: case ERROR_CODE_DRM_LICENSE_EXPIRED:
return "ERROR_CODE_DRM_LICENSE_EXPIRED"; return "ERROR_CODE_DRM_LICENSE_EXPIRED";
case ERROR_CODE_VIDEO_FRAME_PROCESSOR_INIT_FAILED:
return "ERROR_CODE_VIDEO_FRAME_PROCESSOR_INIT_FAILED";
case ERROR_CODE_VIDEO_FRAME_PROCESSING_FAILED:
return "ERROR_CODE_VIDEO_FRAME_PROCESSING_FAILED";
default: default:
if (errorCode >= CUSTOM_ERROR_CODE_BASE) { if (errorCode >= CUSTOM_ERROR_CODE_BASE) {
return "custom error code"; return "custom error code";

View File

@ -43,7 +43,7 @@ public final class PlaybackParameters implements Bundleable {
* *
* @param speed The factor by which playback will be sped up. Must be greater than zero. * @param speed The factor by which playback will be sped up. Must be greater than zero.
*/ */
public PlaybackParameters(float speed) { public PlaybackParameters(@FloatRange(from = 0, fromInclusive = false) float speed) {
this(speed, /* pitch= */ 1f); this(speed, /* pitch= */ 1f);
} }

View File

@ -46,26 +46,125 @@ import java.util.ArrayList;
import java.util.List; import java.util.List;
/** /**
* A media player interface defining traditional high-level functionality, such as the ability to * A media player interface defining high-level functionality, such as the ability to play, pause,
* play, pause, seek and query properties of the currently playing media. * seek and query properties of the currently playing media.
* *
* <p>All methods must be called from a single {@linkplain #getApplicationLooper() application * <h2>Player features and usage</h2>
* thread} unless indicated otherwise. Callbacks in registered listeners are called on the same
* thread.
*
* <p>This interface includes some convenience methods that can be implemented by calling other
* methods in the interface. {@link BasePlayer} implements these convenience methods so inheriting
* {@link BasePlayer} is recommended when implementing the interface so that only the minimal set of
* required methods can be implemented.
* *
* <p>Some important properties of media players that implement this interface are: * <p>Some important properties of media players that implement this interface are:
* *
* <ul> * <ul>
* <li>They can provide a {@link Timeline} representing the structure of the media being played, * <li>All methods must be called from a single {@linkplain #getApplicationLooper() application
* which can be obtained by calling {@link #getCurrentTimeline()}. * thread} unless indicated otherwise. Callbacks in registered listeners are called on the
* <li>They can provide a {@link Tracks} defining the currently available tracks and which are * same thread.
* selected to be rendered, which can be obtained by calling {@link #getCurrentTracks()}. * <li>The available functionality can be limited. Player instances provide a set of {@link
* #getAvailableCommands() availabe commands} to signal feature support and users of the
* interface must only call methods if the corresponding {@link Command} is available.
* <li>Users can register {@link Player.Listener} callbacks that get informed about state changes.
* <li>Player instances need to update the visible state immediately after each method call, even
* if the actual changes are handled on background threads or even other devices. This
* simplifies the usage for callers of methods as no asynchronous handling needs to be
* considered.
* <li>Player instances can provide playlist operations, like 'set', 'add', 'remove', 'move' or
* 'replace' of {@link MediaItem} instances. The player can also support {@linkplain
* RepeatMode repeat modes} and shuffling within this playlist. The player provides a {@link
* Timeline} representing the structure of the playlist and all its items, which can be
* obtained by calling {@link #getCurrentTimeline()}
* <li>Player instances can provide seeking within the currently playing item and to other items,
* using the various {@code seek...} methods.
* <li>Player instances can provide {@link Tracks} defining the currently available and selected
* tracks, which can be obtained by calling {@link #getCurrentTracks()}. Users can also modify
* track selection behavior by setting {@link TrackSelectionParameters} with {@link
* #setTrackSelectionParameters}.
* <li>Player instances can provide {@link MediaMetadata} about the currently playing item, which
* can be obtained by calling {@link #getMediaMetadata()}.
* <li>Player instances can provide information about ads in its media structure, for example via
* {@link #isPlayingAd()}.
* <li>Player instances can accept different types of video outputs, like {@link
* #setVideoSurfaceView SurfaceView} or {@link #setVideoTextureView TextureView} for video
* rendering.
* <li>Player instances can handle {@linkplain #setPlaybackSpeed playback speed}, {@linkplain
* #getAudioAttributes audio attributes}, and {@linkplain #setVolume audio volume}.
* <li>Player instances can provide information about the {@linkplain #getDeviceInfo playback
* device}, which may be remote, and allow to change the device's volume.
* </ul> * </ul>
*
* <h2>API stability guarantees</h2>
*
* <p>The majority of the Player interface and its related classes are part of the stable API that
* guarantees backwards-compatibility for users of the API. Only more advances use cases may need to
* rely on {@link UnstableApi} classes and methods that are subject to incompatible changes or even
* removal in a future release. Implementors of the Player interface are not covered by these API
* stability guarantees.
*
* <h2>Player state</h2>
*
* <p>Users can listen to state changes by adding a {@link Player.Listener} with {@link
* #addListener}.
*
* <p>The main elements of the overall player state are:
*
* <ul>
* <li>Playlist
* <ul>
* <li>{@link MediaItem} instances can be added with methods like {@link #setMediaItem} to
* define what the player will be playing.
* <li>The current playlist can be obtained via {@link #getCurrentTimeline} and convenience
* methods like {@link #getMediaItemCount} or {@link #getCurrentMediaItem}.
* <li>With an empty playlist, the player can only be in {@link #STATE_IDLE} or {@link
* #STATE_ENDED}.
* </ul>
* <li>Playback state
* <ul>
* <li>{@link #STATE_IDLE}: This is the initial state, the state when the player is
* {@linkplain #stop stopped}, and when playback {@linkplain #getPlayerError failed}.
* The player will hold only limited resources in this state. {@link #prepare} must be
* called to transition away from this state.
* <li>{@link #STATE_BUFFERING}: The player is not able to immediately play from its current
* position. This mostly happens because more data needs to be loaded.
* <li>{@link #STATE_READY}: The player is able to immediately play from its current
* position.
* <li>{@link #STATE_ENDED}: The player finished playing all media, or there is no media to
* play.
* </ul>
* <li>Play/Pause, playback suppression and isPlaying
* <ul>
* <li>{@linkplain #getPlayWhenReady() playWhenReady}: Indicates the user intention to play.
* It can be set with {@link #play} or {@link #pause}.
* <li>{@linkplain #getPlaybackSuppressionReason() playback suppression}: Defines a reason
* for which playback will be suppressed even if {@linkplain #getPlayWhenReady()
* playWhenReady} is {@code true}.
* <li>{@link #isPlaying()}: Whether the player is playing (that is, its position is
* advancing and media is being presented). This will only be {@code true} if playback
* state is {@link #STATE_READY}, {@linkplain #getPlayWhenReady() playWhenReady} is
* {@code true}, and playback is not suppressed.
* </ul>
* <li>Playback position
* <ul>
* <li>{@linkplain #getCurrentMediaItemIndex() media item index}: The index in the playlist.
* <li>{@linkplain #isPlayingAd() ad insertion}: Whether an inserted ad is playing and which
* {@linkplain #getCurrentAdGroupIndex() ad group index} and {@linkplain
* #getCurrentAdIndexInAdGroup() ad index in the group} it belongs to
* <li>{@linkplain #getCurrentPosition() current position}: The current position of the
* playback. This is the same as the {@linkplain #getContentPosition() content position}
* unless an ad is playing, where this indicates the position in the inserted ad.
* </ul>
* </ul>
*
* <p>Note that there are no callbacks for normal playback progression, only for {@linkplain
* Listener#onMediaItemTransition transitions between media items} and other {@linkplain
* Listener#onPositionDiscontinuity position discontinuities}. Code that needs to monitor playback
* progress (for example, an UI progress bar) should query the current position in appropriate
* intervals.
*
* <h2>Implementing the Player interface</h2>
*
* <p>Implementing the Player interface is complex, as the interface includes many convenience
* methods that need to provide a consistent state and behavior, requires correct handling of
* listeners and available commands, and expects immediate state changes even if methods are
* internally handled asynchronously. For this reason, implementations are advised to inherit {@link
* SimpleBasePlayer} that handles all of these complexities and provides a simpler integration point
* for implementors of the interface.
*/ */
public interface Player { public interface Player {
@ -371,8 +470,8 @@ public interface Player {
COMMAND_SET_REPEAT_MODE, COMMAND_SET_REPEAT_MODE,
COMMAND_GET_CURRENT_MEDIA_ITEM, COMMAND_GET_CURRENT_MEDIA_ITEM,
COMMAND_GET_TIMELINE, COMMAND_GET_TIMELINE,
COMMAND_GET_MEDIA_ITEMS_METADATA, COMMAND_GET_METADATA,
COMMAND_SET_MEDIA_ITEMS_METADATA, COMMAND_SET_PLAYLIST_METADATA,
COMMAND_SET_MEDIA_ITEM, COMMAND_SET_MEDIA_ITEM,
COMMAND_CHANGE_MEDIA_ITEMS, COMMAND_CHANGE_MEDIA_ITEMS,
COMMAND_GET_AUDIO_ATTRIBUTES, COMMAND_GET_AUDIO_ATTRIBUTES,
@ -380,11 +479,14 @@ public interface Player {
COMMAND_GET_DEVICE_VOLUME, COMMAND_GET_DEVICE_VOLUME,
COMMAND_SET_VOLUME, COMMAND_SET_VOLUME,
COMMAND_SET_DEVICE_VOLUME, COMMAND_SET_DEVICE_VOLUME,
COMMAND_SET_DEVICE_VOLUME_WITH_FLAGS,
COMMAND_ADJUST_DEVICE_VOLUME, COMMAND_ADJUST_DEVICE_VOLUME,
COMMAND_ADJUST_DEVICE_VOLUME_WITH_FLAGS,
COMMAND_SET_VIDEO_SURFACE, COMMAND_SET_VIDEO_SURFACE,
COMMAND_GET_TEXT, COMMAND_GET_TEXT,
COMMAND_SET_TRACK_SELECTION_PARAMETERS, COMMAND_SET_TRACK_SELECTION_PARAMETERS,
COMMAND_GET_TRACKS, COMMAND_GET_TRACKS,
COMMAND_RELEASE
}; };
private final FlagSet.Builder flagsBuilder; private final FlagSet.Builder flagsBuilder;
@ -914,15 +1016,6 @@ public interface Player {
*/ */
default void onMaxSeekToPreviousPositionChanged(long maxSeekToPreviousPositionMs) {} default void onMaxSeekToPreviousPositionChanged(long maxSeekToPreviousPositionMs) {}
/**
* @deprecated Seeks are processed without delay. Listen to {@link
* #onPositionDiscontinuity(PositionInfo, PositionInfo, int)} with reason {@link
* #DISCONTINUITY_REASON_SEEK} instead.
*/
@Deprecated
@UnstableApi
default void onSeekProcessed() {}
/** /**
* Called when the audio session ID changes. * Called when the audio session ID changes.
* *
@ -986,7 +1079,7 @@ public interface Player {
default void onDeviceVolumeChanged(int volume, boolean muted) {} default void onDeviceVolumeChanged(int volume, boolean muted) {}
/** /**
* Called each time there's a change in the size of the video being rendered. * Called each time when {@link Player#getVideoSize()} changes.
* *
* <p>{@link #onEvents(Player, Events)} will also be called to report this event along with * <p>{@link #onEvents(Player, Events)} will also be called to report this event along with
* other events that happen in the same {@link Looper} message queue iteration. * other events that happen in the same {@link Looper} message queue iteration.
@ -1091,8 +1184,9 @@ public interface Player {
* #PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST}, {@link * #PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST}, {@link
* #PLAY_WHEN_READY_CHANGE_REASON_AUDIO_FOCUS_LOSS}, {@link * #PLAY_WHEN_READY_CHANGE_REASON_AUDIO_FOCUS_LOSS}, {@link
* #PLAY_WHEN_READY_CHANGE_REASON_AUDIO_BECOMING_NOISY}, {@link * #PLAY_WHEN_READY_CHANGE_REASON_AUDIO_BECOMING_NOISY}, {@link
* #PLAY_WHEN_READY_CHANGE_REASON_REMOTE} or {@link * #PLAY_WHEN_READY_CHANGE_REASON_REMOTE}, {@link
* #PLAY_WHEN_READY_CHANGE_REASON_END_OF_MEDIA_ITEM}. * #PLAY_WHEN_READY_CHANGE_REASON_END_OF_MEDIA_ITEM} or {@link
* #PLAY_WHEN_READY_CHANGE_REASON_SUPPRESSED_TOO_LONG}.
*/ */
// @Target list includes both 'default' targets and TYPE_USE, to ensure backwards compatibility // @Target list includes both 'default' targets and TYPE_USE, to ensure backwards compatibility
// with Kotlin usages from before TYPE_USE was added. // with Kotlin usages from before TYPE_USE was added.
@ -1104,7 +1198,8 @@ public interface Player {
PLAY_WHEN_READY_CHANGE_REASON_AUDIO_FOCUS_LOSS, PLAY_WHEN_READY_CHANGE_REASON_AUDIO_FOCUS_LOSS,
PLAY_WHEN_READY_CHANGE_REASON_AUDIO_BECOMING_NOISY, PLAY_WHEN_READY_CHANGE_REASON_AUDIO_BECOMING_NOISY,
PLAY_WHEN_READY_CHANGE_REASON_REMOTE, PLAY_WHEN_READY_CHANGE_REASON_REMOTE,
PLAY_WHEN_READY_CHANGE_REASON_END_OF_MEDIA_ITEM PLAY_WHEN_READY_CHANGE_REASON_END_OF_MEDIA_ITEM,
PLAY_WHEN_READY_CHANGE_REASON_SUPPRESSED_TOO_LONG
}) })
@interface PlayWhenReadyChangeReason {} @interface PlayWhenReadyChangeReason {}
/** Playback has been started or paused by a call to {@link #setPlayWhenReady(boolean)}. */ /** Playback has been started or paused by a call to {@link #setPlayWhenReady(boolean)}. */
@ -1117,11 +1212,17 @@ public interface Player {
int PLAY_WHEN_READY_CHANGE_REASON_REMOTE = 4; int PLAY_WHEN_READY_CHANGE_REASON_REMOTE = 4;
/** Playback has been paused at the end of a media item. */ /** Playback has been paused at the end of a media item. */
int PLAY_WHEN_READY_CHANGE_REASON_END_OF_MEDIA_ITEM = 5; int PLAY_WHEN_READY_CHANGE_REASON_END_OF_MEDIA_ITEM = 5;
/**
* Playback has been paused because playback has been {@linkplain #getPlaybackSuppressionReason()
* suppressed} too long.
*/
int PLAY_WHEN_READY_CHANGE_REASON_SUPPRESSED_TOO_LONG = 6;
/** /**
* Reason why playback is suppressed even though {@link #getPlayWhenReady()} is {@code true}. One * Reason why playback is suppressed even though {@link #getPlayWhenReady()} is {@code true}. One
* of {@link #PLAYBACK_SUPPRESSION_REASON_NONE} or {@link * of {@link #PLAYBACK_SUPPRESSION_REASON_NONE}, {@link
* #PLAYBACK_SUPPRESSION_REASON_TRANSIENT_AUDIO_FOCUS_LOSS}. * #PLAYBACK_SUPPRESSION_REASON_TRANSIENT_AUDIO_FOCUS_LOSS} or {@link
* #PLAYBACK_SUPPRESSION_REASON_UNSUITABLE_AUDIO_ROUTE}.
*/ */
// @Target list includes both 'default' targets and TYPE_USE, to ensure backwards compatibility // @Target list includes both 'default' targets and TYPE_USE, to ensure backwards compatibility
// with Kotlin usages from before TYPE_USE was added. // with Kotlin usages from before TYPE_USE was added.
@ -1130,13 +1231,19 @@ public interface Player {
@Target({FIELD, METHOD, PARAMETER, LOCAL_VARIABLE, TYPE_USE}) @Target({FIELD, METHOD, PARAMETER, LOCAL_VARIABLE, TYPE_USE})
@IntDef({ @IntDef({
PLAYBACK_SUPPRESSION_REASON_NONE, PLAYBACK_SUPPRESSION_REASON_NONE,
PLAYBACK_SUPPRESSION_REASON_TRANSIENT_AUDIO_FOCUS_LOSS PLAYBACK_SUPPRESSION_REASON_TRANSIENT_AUDIO_FOCUS_LOSS,
PLAYBACK_SUPPRESSION_REASON_UNSUITABLE_AUDIO_ROUTE
}) })
@interface PlaybackSuppressionReason {} @interface PlaybackSuppressionReason {}
/** Playback is not suppressed. */ /** Playback is not suppressed. */
int PLAYBACK_SUPPRESSION_REASON_NONE = 0; int PLAYBACK_SUPPRESSION_REASON_NONE = 0;
/** Playback is suppressed due to transient audio focus loss. */ /** Playback is suppressed due to transient audio focus loss. */
int PLAYBACK_SUPPRESSION_REASON_TRANSIENT_AUDIO_FOCUS_LOSS = 1; int PLAYBACK_SUPPRESSION_REASON_TRANSIENT_AUDIO_FOCUS_LOSS = 1;
/**
* Playback is suppressed due to no suitable audio route, such as an attempt to use an internal
* speaker instead of bluetooth headphones on Wear OS.
*/
int PLAYBACK_SUPPRESSION_REASON_UNSUITABLE_AUDIO_ROUTE = 2;
/** /**
* Repeat modes for playback. One of {@link #REPEAT_MODE_OFF}, {@link #REPEAT_MODE_ONE} or {@link * Repeat modes for playback. One of {@link #REPEAT_MODE_OFF}, {@link #REPEAT_MODE_ONE} or {@link
@ -1413,8 +1520,8 @@ public interface Player {
* <li>{@link #COMMAND_SET_REPEAT_MODE} * <li>{@link #COMMAND_SET_REPEAT_MODE}
* <li>{@link #COMMAND_GET_CURRENT_MEDIA_ITEM} * <li>{@link #COMMAND_GET_CURRENT_MEDIA_ITEM}
* <li>{@link #COMMAND_GET_TIMELINE} * <li>{@link #COMMAND_GET_TIMELINE}
* <li>{@link #COMMAND_GET_MEDIA_ITEMS_METADATA} * <li>{@link #COMMAND_GET_METADATA}
* <li>{@link #COMMAND_SET_MEDIA_ITEMS_METADATA} * <li>{@link #COMMAND_SET_PLAYLIST_METADATA}
* <li>{@link #COMMAND_SET_MEDIA_ITEM} * <li>{@link #COMMAND_SET_MEDIA_ITEM}
* <li>{@link #COMMAND_CHANGE_MEDIA_ITEMS} * <li>{@link #COMMAND_CHANGE_MEDIA_ITEMS}
* <li>{@link #COMMAND_GET_AUDIO_ATTRIBUTES} * <li>{@link #COMMAND_GET_AUDIO_ATTRIBUTES}
@ -1422,15 +1529,19 @@ public interface Player {
* <li>{@link #COMMAND_GET_DEVICE_VOLUME} * <li>{@link #COMMAND_GET_DEVICE_VOLUME}
* <li>{@link #COMMAND_SET_VOLUME} * <li>{@link #COMMAND_SET_VOLUME}
* <li>{@link #COMMAND_SET_DEVICE_VOLUME} * <li>{@link #COMMAND_SET_DEVICE_VOLUME}
* <li>{@link #COMMAND_SET_DEVICE_VOLUME_WITH_FLAGS}
* <li>{@link #COMMAND_ADJUST_DEVICE_VOLUME} * <li>{@link #COMMAND_ADJUST_DEVICE_VOLUME}
* <li>{@link #COMMAND_ADJUST_DEVICE_VOLUME_WITH_FLAGS}
* <li>{@link #COMMAND_SET_VIDEO_SURFACE} * <li>{@link #COMMAND_SET_VIDEO_SURFACE}
* <li>{@link #COMMAND_GET_TEXT} * <li>{@link #COMMAND_GET_TEXT}
* <li>{@link #COMMAND_SET_TRACK_SELECTION_PARAMETERS} * <li>{@link #COMMAND_SET_TRACK_SELECTION_PARAMETERS}
* <li>{@link #COMMAND_GET_TRACKS} * <li>{@link #COMMAND_GET_TRACKS}
* <li>{@link #COMMAND_RELEASE}
* </ul> * </ul>
*/ */
// @Target list includes both 'default' targets and TYPE_USE, to ensure backwards compatibility // @Target list includes both 'default' targets and TYPE_USE, to ensure backwards compatibility
// with Kotlin usages from before TYPE_USE was added. // with Kotlin usages from before TYPE_USE was added.
@SuppressWarnings("deprecation") // Listing deprecated constants.
@Documented @Documented
@Retention(RetentionPolicy.SOURCE) @Retention(RetentionPolicy.SOURCE)
@Target({FIELD, METHOD, PARAMETER, LOCAL_VARIABLE, TYPE_USE}) @Target({FIELD, METHOD, PARAMETER, LOCAL_VARIABLE, TYPE_USE})
@ -1454,7 +1565,9 @@ public interface Player {
COMMAND_GET_CURRENT_MEDIA_ITEM, COMMAND_GET_CURRENT_MEDIA_ITEM,
COMMAND_GET_TIMELINE, COMMAND_GET_TIMELINE,
COMMAND_GET_MEDIA_ITEMS_METADATA, COMMAND_GET_MEDIA_ITEMS_METADATA,
COMMAND_GET_METADATA,
COMMAND_SET_MEDIA_ITEMS_METADATA, COMMAND_SET_MEDIA_ITEMS_METADATA,
COMMAND_SET_PLAYLIST_METADATA,
COMMAND_SET_MEDIA_ITEM, COMMAND_SET_MEDIA_ITEM,
COMMAND_CHANGE_MEDIA_ITEMS, COMMAND_CHANGE_MEDIA_ITEMS,
COMMAND_GET_AUDIO_ATTRIBUTES, COMMAND_GET_AUDIO_ATTRIBUTES,
@ -1462,11 +1575,14 @@ public interface Player {
COMMAND_GET_DEVICE_VOLUME, COMMAND_GET_DEVICE_VOLUME,
COMMAND_SET_VOLUME, COMMAND_SET_VOLUME,
COMMAND_SET_DEVICE_VOLUME, COMMAND_SET_DEVICE_VOLUME,
COMMAND_SET_DEVICE_VOLUME_WITH_FLAGS,
COMMAND_ADJUST_DEVICE_VOLUME, COMMAND_ADJUST_DEVICE_VOLUME,
COMMAND_ADJUST_DEVICE_VOLUME_WITH_FLAGS,
COMMAND_SET_VIDEO_SURFACE, COMMAND_SET_VIDEO_SURFACE,
COMMAND_GET_TEXT, COMMAND_GET_TEXT,
COMMAND_SET_TRACK_SELECTION_PARAMETERS, COMMAND_SET_TRACK_SELECTION_PARAMETERS,
COMMAND_GET_TRACKS, COMMAND_GET_TRACKS,
COMMAND_RELEASE,
}) })
@interface Command {} @interface Command {}
/** /**
@ -1665,6 +1781,11 @@ public interface Player {
*/ */
int COMMAND_GET_TIMELINE = 17; int COMMAND_GET_TIMELINE = 17;
/**
* @deprecated Use {@link #COMMAND_GET_METADATA} instead.
*/
@Deprecated int COMMAND_GET_MEDIA_ITEMS_METADATA = 18;
/** /**
* Command to get metadata related to the playlist and current {@link MediaItem}. * Command to get metadata related to the playlist and current {@link MediaItem}.
* *
@ -1676,8 +1797,12 @@ public interface Player {
* <li>{@link #getPlaylistMetadata()} * <li>{@link #getPlaylistMetadata()}
* </ul> * </ul>
*/ */
// TODO(b/263132691): Rename this to COMMAND_GET_METADATA int COMMAND_GET_METADATA = 18;
int COMMAND_GET_MEDIA_ITEMS_METADATA = 18;
/**
* @deprecated Use {@link #COMMAND_SET_PLAYLIST_METADATA} instead.
*/
@Deprecated int COMMAND_SET_MEDIA_ITEMS_METADATA = 19;
/** /**
* Command to set the playlist metadata. * Command to set the playlist metadata.
@ -1685,8 +1810,7 @@ public interface Player {
* <p>The {@link #setPlaylistMetadata(MediaMetadata)} method must only be called if this command * <p>The {@link #setPlaylistMetadata(MediaMetadata)} method must only be called if this command
* is {@linkplain #isCommandAvailable(int) available}. * is {@linkplain #isCommandAvailable(int) available}.
*/ */
// TODO(b/263132691): Rename this to COMMAND_SET_PLAYLIST_METADATA int COMMAND_SET_PLAYLIST_METADATA = 19;
int COMMAND_SET_MEDIA_ITEMS_METADATA = 19;
/** /**
* Command to set a {@link MediaItem}. * Command to set a {@link MediaItem}.
@ -1720,6 +1844,8 @@ public interface Player {
* <li>{@link #setMediaItems(List)} * <li>{@link #setMediaItems(List)}
* <li>{@link #setMediaItems(List, boolean)} * <li>{@link #setMediaItems(List, boolean)}
* <li>{@link #setMediaItems(List, int, long)} * <li>{@link #setMediaItems(List, int, long)}
* <li>{@link #replaceMediaItem(int, MediaItem)}
* <li>{@link #replaceMediaItems(int, int, List)}
* </ul> * </ul>
*/ */
int COMMAND_CHANGE_MEDIA_ITEMS = 20; int COMMAND_CHANGE_MEDIA_ITEMS = 20;
@ -1760,27 +1886,36 @@ public interface Player {
* #isCommandAvailable(int) available}. * #isCommandAvailable(int) available}.
*/ */
int COMMAND_SET_VOLUME = 24; int COMMAND_SET_VOLUME = 24;
/**
* Command to set the device volume.
*
* <p>The {@link #setDeviceVolume(int)} method must only be called if this command is {@linkplain
* #isCommandAvailable(int) available}.
*/
int COMMAND_SET_DEVICE_VOLUME = 25;
/** /**
* Command to increase and decrease the device volume and mute it. * @deprecated Use {@link #COMMAND_SET_DEVICE_VOLUME_WITH_FLAGS} instead.
*/
@Deprecated int COMMAND_SET_DEVICE_VOLUME = 25;
/**
* Command to set the device volume with volume flags.
*
* <p>The {@link #setDeviceVolume(int, int)} method must only be called if this command is
* {@linkplain #isCommandAvailable(int) available}.
*/
int COMMAND_SET_DEVICE_VOLUME_WITH_FLAGS = 33;
/**
* @deprecated Use {@link #COMMAND_ADJUST_DEVICE_VOLUME_WITH_FLAGS} instead.
*/
@Deprecated int COMMAND_ADJUST_DEVICE_VOLUME = 26;
/**
* Command to increase and decrease the device volume and mute it with volume flags.
* *
* <p>The following methods must only be called if this command is {@linkplain * <p>The following methods must only be called if this command is {@linkplain
* #isCommandAvailable(int) available}: * #isCommandAvailable(int) available}:
* *
* <ul> * <ul>
* <li>{@link #increaseDeviceVolume()} * <li>{@link #increaseDeviceVolume(int)}
* <li>{@link #decreaseDeviceVolume()} * <li>{@link #decreaseDeviceVolume(int)}
* <li>{@link #setDeviceMuted(boolean)} * <li>{@link #setDeviceMuted(boolean, int)}
* </ul> * </ul>
*/ */
int COMMAND_ADJUST_DEVICE_VOLUME = 26; int COMMAND_ADJUST_DEVICE_VOLUME_WITH_FLAGS = 34;
/** /**
* Command to set and clear the surface on which to render the video. * Command to set and clear the surface on which to render the video.
@ -1823,6 +1958,13 @@ public interface Player {
* #isCommandAvailable(int) available}. * #isCommandAvailable(int) available}.
*/ */
int COMMAND_GET_TRACKS = 30; int COMMAND_GET_TRACKS = 30;
/**
* Command to release the player.
*
* <p>The {@link #release()} method must only be called if this command is {@linkplain
* #isCommandAvailable(int) available}.
*/
int COMMAND_RELEASE = 32;
/** Represents an invalid {@link Command}. */ /** Represents an invalid {@link Command}. */
int COMMAND_INVALID = -1; int COMMAND_INVALID = -1;
@ -2005,6 +2147,37 @@ public interface Player {
*/ */
void moveMediaItems(int fromIndex, int toIndex, int newIndex); void moveMediaItems(int fromIndex, int toIndex, int newIndex);
/**
* Replaces the media item at the given index of the playlist.
*
* <p>This method must only be called if {@link #COMMAND_CHANGE_MEDIA_ITEMS} is {@linkplain
* #getAvailableCommands() available}.
*
* @param index The index at which to replace the media item. If the index is larger than the size
* of the playlist, the request is ignored.
* @param mediaItem The new {@link MediaItem}.
*/
void replaceMediaItem(int index, MediaItem mediaItem);
/**
* Replaces the media items at the given range of the playlist.
*
* <p>This method must only be called if {@link #COMMAND_CHANGE_MEDIA_ITEMS} is {@linkplain
* #getAvailableCommands() available}.
*
* <p>Note that it is possible to replace a range with an arbitrary number of new items, so that
* the number of removed items defined by {@code fromIndex} and {@code toIndex} does not have to
* match the number of added items defined by {@code mediaItems}. As result, it may also change
* the index of subsequent items not touched by this operation.
*
* @param fromIndex The start of the range. If the index is larger than the size of the playlist,
* the request is ignored.
* @param toIndex The first item not to be included in the range (exclusive). If the index is
* larger than the size of the playlist, items up to the end of the playlist are replaced.
* @param mediaItems The {@linkplain MediaItem media items} to replace the range with.
*/
void replaceMediaItems(int fromIndex, int toIndex, List<MediaItem> mediaItems);
/** /**
* Removes the media item at the given index of the playlist. * Removes the media item at the given index of the playlist.
* *
@ -2500,20 +2673,13 @@ public interface Player {
*/ */
void stop(); void stop();
/**
* @deprecated Use {@link #stop()} and {@link #clearMediaItems()} (if {@code reset} is true) or
* just {@link #stop()} (if {@code reset} is false). Any player error will be cleared when
* {@link #prepare() re-preparing} the player.
*/
@UnstableApi
@Deprecated
void stop(boolean reset);
/** /**
* Releases the player. This method must be called when the player is no longer required. The * Releases the player. This method must be called when the player is no longer required. The
* player must not be used after calling this method. * player must not be used after calling this method.
*
* <p>This method must only be called if {@link #COMMAND_RELEASE} is {@linkplain
* #getAvailableCommands() available}.
*/ */
// TODO(b/261158047): Document that COMMAND_RELEASE must be available once it exists.
void release(); void release();
/** /**
@ -2565,7 +2731,7 @@ public interface Player {
* Listener#onMetadata(Metadata)}. If a field is populated in the {@link MediaItem#mediaMetadata}, * Listener#onMetadata(Metadata)}. If a field is populated in the {@link MediaItem#mediaMetadata},
* it will be prioritised above the same field coming from static or timed metadata. * it will be prioritised above the same field coming from static or timed metadata.
* *
* <p>This method must only be called if {@link #COMMAND_GET_MEDIA_ITEMS_METADATA} is {@linkplain * <p>This method must only be called if {@link #COMMAND_GET_METADATA} is {@linkplain
* #getAvailableCommands() available}. * #getAvailableCommands() available}.
*/ */
MediaMetadata getMediaMetadata(); MediaMetadata getMediaMetadata();
@ -2574,7 +2740,7 @@ public interface Player {
* Returns the playlist {@link MediaMetadata}, as set by {@link * Returns the playlist {@link MediaMetadata}, as set by {@link
* #setPlaylistMetadata(MediaMetadata)}, or {@link MediaMetadata#EMPTY} if not supported. * #setPlaylistMetadata(MediaMetadata)}, or {@link MediaMetadata#EMPTY} if not supported.
* *
* <p>This method must only be called if {@link #COMMAND_GET_MEDIA_ITEMS_METADATA} is {@linkplain * <p>This method must only be called if {@link #COMMAND_GET_METADATA} is {@linkplain
* #getAvailableCommands() available}. * #getAvailableCommands() available}.
*/ */
MediaMetadata getPlaylistMetadata(); MediaMetadata getPlaylistMetadata();
@ -2582,7 +2748,7 @@ public interface Player {
/** /**
* Sets the playlist {@link MediaMetadata}. * Sets the playlist {@link MediaMetadata}.
* *
* <p>This method must only be called if {@link #COMMAND_SET_MEDIA_ITEMS_METADATA} is {@linkplain * <p>This method must only be called if {@link #COMMAND_SET_PLAYLIST_METADATA} is {@linkplain
* #getAvailableCommands() available}. * #getAvailableCommands() available}.
*/ */
void setPlaylistMetadata(MediaMetadata mediaMetadata); void setPlaylistMetadata(MediaMetadata mediaMetadata);
@ -3015,8 +3181,8 @@ public interface Player {
/** /**
* Gets the size of the video. * Gets the size of the video.
* *
* <p>The video's width and height are {@code 0} if there is no video or its size has not been * <p>The video's width and height are {@code 0} if there is {@linkplain
* determined yet. * Tracks#isTypeSupported(int) no supported video track} or its size has not been determined yet.
* *
* @see Listener#onVideoSizeChanged(VideoSize) * @see Listener#onVideoSizeChanged(VideoSize)
*/ */
@ -3067,36 +3233,74 @@ public interface Player {
boolean isDeviceMuted(); boolean isDeviceMuted();
/** /**
* Sets the volume of the device. * @deprecated Use {@link #setDeviceVolume(int, int)} instead.
*/
@Deprecated
void setDeviceVolume(@IntRange(from = 0) int volume);
/**
* Sets the volume of the device with volume flags.
* *
* <p>This method must only be called if {@link #COMMAND_SET_DEVICE_VOLUME} is {@linkplain * <p>This method must only be called if {@link #COMMAND_SET_DEVICE_VOLUME_WITH_FLAGS} is
* #getAvailableCommands() available}. * {@linkplain #getAvailableCommands() available}.
* *
* @param volume The volume to set. * @param volume The volume to set.
* @param flags Either 0 or a bitwise combination of one or more {@link C.VolumeFlags}.
*/ */
void setDeviceVolume(@IntRange(from = 0) int volume); void setDeviceVolume(@IntRange(from = 0) int volume, int flags);
/**
* @deprecated Use {@link #increaseDeviceVolume(int)} instead.
*/
@Deprecated
void increaseDeviceVolume();
/** /**
* Increases the volume of the device. * Increases the volume of the device.
* *
* <p>This method must only be called if {@link #COMMAND_ADJUST_DEVICE_VOLUME} is {@linkplain * <p>The {@link #getDeviceVolume()} device volume cannot be increased above {@link
* #getAvailableCommands() available}. * DeviceInfo#maxVolume}, if defined.
*
* <p>This method must only be called if {@link #COMMAND_ADJUST_DEVICE_VOLUME_WITH_FLAGS} is
* {@linkplain #getAvailableCommands() available}.
*
* @param flags Either 0 or a bitwise combination of one or more {@link C.VolumeFlags}.
*/ */
void increaseDeviceVolume(); void increaseDeviceVolume(@C.VolumeFlags int flags);
/**
* @deprecated Use {@link #decreaseDeviceVolume(int)} instead.
*/
@Deprecated
void decreaseDeviceVolume();
/** /**
* Decreases the volume of the device. * Decreases the volume of the device.
* *
* <p>This method must only be called if {@link #COMMAND_ADJUST_DEVICE_VOLUME} is {@linkplain * <p>The {@link #getDeviceVolume()} device volume cannot be decreased below {@link
* #getAvailableCommands() available}. * DeviceInfo#minVolume}.
*
* <p>This method must only be called if {@link #COMMAND_ADJUST_DEVICE_VOLUME_WITH_FLAGS} is
* {@linkplain #getAvailableCommands() available}.
*
* @param flags Either 0 or a bitwise combination of one or more {@link C.VolumeFlags}.
*/ */
void decreaseDeviceVolume(); void decreaseDeviceVolume(@C.VolumeFlags int flags);
/**
* @deprecated Use {@link #setDeviceMuted(boolean, int)} instead.
*/
@Deprecated
void setDeviceMuted(boolean muted);
/** /**
* Sets the mute state of the device. * Sets the mute state of the device.
* *
* <p>This method must only be called if {@link #COMMAND_ADJUST_DEVICE_VOLUME} is {@linkplain * <p>This method must only be called if {@link #COMMAND_ADJUST_DEVICE_VOLUME_WITH_FLAGS} is
* #getAvailableCommands() available}. * {@linkplain #getAvailableCommands() available}.
*
* @param muted Whether to set the device to be muted or not
* @param flags Either 0 or a bitwise combination of one or more {@link C.VolumeFlags}.
*/ */
void setDeviceMuted(boolean muted); void setDeviceMuted(boolean muted, @C.VolumeFlags int flags);
} }

View File

@ -2101,7 +2101,16 @@ public abstract class SimpleBasePlayer extends BasePlayer {
placeholderPlaylist.add( placeholderPlaylist.add(
i + correctedIndex, getPlaceholderMediaItemData(mediaItems.get(i))); i + correctedIndex, getPlaceholderMediaItemData(mediaItems.get(i)));
} }
if (!state.playlist.isEmpty()) {
return getStateWithNewPlaylist(state, placeholderPlaylist, period); return getStateWithNewPlaylist(state, placeholderPlaylist, period);
} else {
// Handle initial position update when these are the first items added to the playlist.
return getStateWithNewPlaylistAndPosition(
state,
placeholderPlaylist,
state.currentMediaItemIndex,
state.contentPositionMsSupplier.get());
}
}); });
} }
@ -2132,6 +2141,45 @@ public abstract class SimpleBasePlayer extends BasePlayer {
}); });
} }
@Override
public final void replaceMediaItems(int fromIndex, int toIndex, List<MediaItem> mediaItems) {
verifyApplicationThreadAndInitState();
checkArgument(fromIndex >= 0 && fromIndex <= toIndex);
State state = this.state;
int playlistSize = state.playlist.size();
if (!shouldHandleCommand(Player.COMMAND_CHANGE_MEDIA_ITEMS) || fromIndex > playlistSize) {
return;
}
int correctedToIndex = min(toIndex, playlistSize);
updateStateForPendingOperation(
/* pendingOperation= */ handleReplaceMediaItems(fromIndex, correctedToIndex, mediaItems),
/* placeholderStateSupplier= */ () -> {
ArrayList<MediaItemData> placeholderPlaylist = new ArrayList<>(state.playlist);
for (int i = 0; i < mediaItems.size(); i++) {
placeholderPlaylist.add(
i + correctedToIndex, getPlaceholderMediaItemData(mediaItems.get(i)));
}
State updatedState;
if (!state.playlist.isEmpty()) {
updatedState = getStateWithNewPlaylist(state, placeholderPlaylist, period);
} else {
// Handle initial position update when these are the first items added to the playlist.
updatedState =
getStateWithNewPlaylistAndPosition(
state,
placeholderPlaylist,
state.currentMediaItemIndex,
state.contentPositionMsSupplier.get());
}
if (fromIndex < correctedToIndex) {
Util.removeRange(placeholderPlaylist, fromIndex, correctedToIndex);
return getStateWithNewPlaylist(updatedState, placeholderPlaylist, period);
} else {
return updatedState;
}
});
}
@Override @Override
public final void removeMediaItems(int fromIndex, int toIndex) { public final void removeMediaItems(int fromIndex, int toIndex) {
verifyApplicationThreadAndInitState(); verifyApplicationThreadAndInitState();
@ -2325,20 +2373,12 @@ public abstract class SimpleBasePlayer extends BasePlayer {
.build()); .build());
} }
@Override
public final void stop(boolean reset) {
stop();
if (reset) {
clearMediaItems();
}
}
@Override @Override
public final void release() { public final void release() {
verifyApplicationThreadAndInitState(); verifyApplicationThreadAndInitState();
// Use a local copy to ensure the lambda below uses the current state value. // Use a local copy to ensure the lambda below uses the current state value.
State state = this.state; State state = this.state;
if (released) { // TODO(b/261158047): Replace by !shouldHandleCommand(Player.COMMAND_RELEASE) if (!shouldHandleCommand(Player.COMMAND_RELEASE)) {
return; return;
} }
updateStateForPendingOperation( updateStateForPendingOperation(
@ -2401,7 +2441,7 @@ public abstract class SimpleBasePlayer extends BasePlayer {
verifyApplicationThreadAndInitState(); verifyApplicationThreadAndInitState();
// Use a local copy to ensure the lambda below uses the current state value. // Use a local copy to ensure the lambda below uses the current state value.
State state = this.state; State state = this.state;
if (!shouldHandleCommand(Player.COMMAND_SET_MEDIA_ITEMS_METADATA)) { if (!shouldHandleCommand(Player.COMMAND_SET_PLAYLIST_METADATA)) {
return; return;
} }
updateStateForPendingOperation( updateStateForPendingOperation(
@ -2669,6 +2709,10 @@ public abstract class SimpleBasePlayer extends BasePlayer {
return state.isDeviceMuted; return state.isDeviceMuted;
} }
/**
* @deprecated Use {@link #setDeviceVolume(int, int)} instead.
*/
@Deprecated
@Override @Override
public final void setDeviceVolume(int volume) { public final void setDeviceVolume(int volume) {
verifyApplicationThreadAndInitState(); verifyApplicationThreadAndInitState();
@ -2678,10 +2722,27 @@ public abstract class SimpleBasePlayer extends BasePlayer {
return; return;
} }
updateStateForPendingOperation( updateStateForPendingOperation(
/* pendingOperation= */ handleSetDeviceVolume(volume), /* pendingOperation= */ handleSetDeviceVolume(volume, C.VOLUME_FLAG_SHOW_UI),
/* placeholderStateSupplier= */ () -> state.buildUpon().setDeviceVolume(volume).build()); /* placeholderStateSupplier= */ () -> state.buildUpon().setDeviceVolume(volume).build());
} }
@Override
public final void setDeviceVolume(int volume, @C.VolumeFlags int flags) {
verifyApplicationThreadAndInitState();
// Use a local copy to ensure the lambda below uses the current state value.
State state = this.state;
if (!shouldHandleCommand(Player.COMMAND_SET_DEVICE_VOLUME_WITH_FLAGS)) {
return;
}
updateStateForPendingOperation(
/* pendingOperation= */ handleSetDeviceVolume(volume, flags),
/* placeholderStateSupplier= */ () -> state.buildUpon().setDeviceVolume(volume).build());
}
/**
* @deprecated Use {@link #increaseDeviceVolume(int)} instead.
*/
@Deprecated
@Override @Override
public final void increaseDeviceVolume() { public final void increaseDeviceVolume() {
verifyApplicationThreadAndInitState(); verifyApplicationThreadAndInitState();
@ -2691,11 +2752,29 @@ public abstract class SimpleBasePlayer extends BasePlayer {
return; return;
} }
updateStateForPendingOperation( updateStateForPendingOperation(
/* pendingOperation= */ handleIncreaseDeviceVolume(), /* pendingOperation= */ handleIncreaseDeviceVolume(C.VOLUME_FLAG_SHOW_UI),
/* placeholderStateSupplier= */ () -> /* placeholderStateSupplier= */ () ->
state.buildUpon().setDeviceVolume(state.deviceVolume + 1).build()); state.buildUpon().setDeviceVolume(state.deviceVolume + 1).build());
} }
@Override
public final void increaseDeviceVolume(@C.VolumeFlags int flags) {
verifyApplicationThreadAndInitState();
// Use a local copy to ensure the lambda below uses the current state value.
State state = this.state;
if (!shouldHandleCommand(Player.COMMAND_ADJUST_DEVICE_VOLUME_WITH_FLAGS)) {
return;
}
updateStateForPendingOperation(
/* pendingOperation= */ handleIncreaseDeviceVolume(flags),
/* placeholderStateSupplier= */ () ->
state.buildUpon().setDeviceVolume(state.deviceVolume + 1).build());
}
/**
* @deprecated Use {@link #decreaseDeviceVolume(int)} instead.
*/
@Deprecated
@Override @Override
public final void decreaseDeviceVolume() { public final void decreaseDeviceVolume() {
verifyApplicationThreadAndInitState(); verifyApplicationThreadAndInitState();
@ -2705,11 +2784,29 @@ public abstract class SimpleBasePlayer extends BasePlayer {
return; return;
} }
updateStateForPendingOperation( updateStateForPendingOperation(
/* pendingOperation= */ handleDecreaseDeviceVolume(), /* pendingOperation= */ handleDecreaseDeviceVolume(C.VOLUME_FLAG_SHOW_UI),
/* placeholderStateSupplier= */ () -> /* placeholderStateSupplier= */ () ->
state.buildUpon().setDeviceVolume(max(0, state.deviceVolume - 1)).build()); state.buildUpon().setDeviceVolume(max(0, state.deviceVolume - 1)).build());
} }
@Override
public final void decreaseDeviceVolume(@C.VolumeFlags int flags) {
verifyApplicationThreadAndInitState();
// Use a local copy to ensure the lambda below uses the current state value.
State state = this.state;
if (!shouldHandleCommand(Player.COMMAND_ADJUST_DEVICE_VOLUME_WITH_FLAGS)) {
return;
}
updateStateForPendingOperation(
/* pendingOperation= */ handleDecreaseDeviceVolume(flags),
/* placeholderStateSupplier= */ () ->
state.buildUpon().setDeviceVolume(max(0, state.deviceVolume - 1)).build());
}
/**
* @deprecated Use {@link #setDeviceMuted(boolean, int)} instead.
*/
@Deprecated
@Override @Override
public final void setDeviceMuted(boolean muted) { public final void setDeviceMuted(boolean muted) {
verifyApplicationThreadAndInitState(); verifyApplicationThreadAndInitState();
@ -2719,7 +2816,20 @@ public abstract class SimpleBasePlayer extends BasePlayer {
return; return;
} }
updateStateForPendingOperation( updateStateForPendingOperation(
/* pendingOperation= */ handleSetDeviceMuted(muted), /* pendingOperation= */ handleSetDeviceMuted(muted, C.VOLUME_FLAG_SHOW_UI),
/* placeholderStateSupplier= */ () -> state.buildUpon().setIsDeviceMuted(muted).build());
}
@Override
public final void setDeviceMuted(boolean muted, @C.VolumeFlags int flags) {
verifyApplicationThreadAndInitState();
// Use a local copy to ensure the lambda below uses the current state value.
State state = this.state;
if (!shouldHandleCommand(Player.COMMAND_ADJUST_DEVICE_VOLUME_WITH_FLAGS)) {
return;
}
updateStateForPendingOperation(
/* pendingOperation= */ handleSetDeviceMuted(muted, flags),
/* placeholderStateSupplier= */ () -> state.buildUpon().setIsDeviceMuted(muted).build()); /* placeholderStateSupplier= */ () -> state.buildUpon().setIsDeviceMuted(muted).build());
} }
@ -2837,10 +2947,11 @@ public abstract class SimpleBasePlayer extends BasePlayer {
/** /**
* Handles calls to {@link Player#release}. * Handles calls to {@link Player#release}.
* *
* <p>Will only be called if {@link Player#COMMAND_RELEASE} is available.
*
* @return A {@link ListenableFuture} indicating the completion of all immediate {@link State} * @return A {@link ListenableFuture} indicating the completion of all immediate {@link State}
* changes caused by this call. * changes caused by this call.
*/ */
// TODO(b/261158047): Add that this method will only be called if COMMAND_RELEASE is available.
@ForOverride @ForOverride
protected ListenableFuture<?> handleRelease() { protected ListenableFuture<?> handleRelease() {
throw new IllegalStateException("Missing implementation to handle COMMAND_RELEASE"); throw new IllegalStateException("Missing implementation to handle COMMAND_RELEASE");
@ -2907,7 +3018,7 @@ public abstract class SimpleBasePlayer extends BasePlayer {
/** /**
* Handles calls to {@link Player#setPlaylistMetadata}. * Handles calls to {@link Player#setPlaylistMetadata}.
* *
* <p>Will only be called if {@link Player#COMMAND_SET_MEDIA_ITEMS_METADATA} is available. * <p>Will only be called if {@link Player#COMMAND_SET_PLAYLIST_METADATA} is available.
* *
* @param playlistMetadata The requested {@linkplain MediaMetadata playlist metadata}. * @param playlistMetadata The requested {@linkplain MediaMetadata playlist metadata}.
* @return A {@link ListenableFuture} indicating the completion of all immediate {@link State} * @return A {@link ListenableFuture} indicating the completion of all immediate {@link State}
@ -2916,7 +3027,7 @@ public abstract class SimpleBasePlayer extends BasePlayer {
@ForOverride @ForOverride
protected ListenableFuture<?> handleSetPlaylistMetadata(MediaMetadata playlistMetadata) { protected ListenableFuture<?> handleSetPlaylistMetadata(MediaMetadata playlistMetadata) {
throw new IllegalStateException( throw new IllegalStateException(
"Missing implementation to handle COMMAND_SET_MEDIA_ITEMS_METADATA"); "Missing implementation to handle COMMAND_SET_PLAYLIST_METADATA");
} }
/** /**
@ -2935,60 +3046,78 @@ public abstract class SimpleBasePlayer extends BasePlayer {
} }
/** /**
* Handles calls to {@link Player#setDeviceVolume}. * Handles calls to {@link Player#setDeviceVolume(int)} and {@link Player#setDeviceVolume(int,
* int)}.
* *
* <p>Will only be called if {@link Player#COMMAND_SET_DEVICE_VOLUME} is available. * <p>Will only be called if {@link Player#COMMAND_SET_DEVICE_VOLUME} or {@link
* Player#COMMAND_SET_DEVICE_VOLUME_WITH_FLAGS} is available.
* *
* @param deviceVolume The requested device volume. * @param deviceVolume The requested device volume.
* @param flags Either 0 or a bitwise combination of one or more {@link C.VolumeFlags}.
* @return A {@link ListenableFuture} indicating the completion of all immediate {@link State} * @return A {@link ListenableFuture} indicating the completion of all immediate {@link State}
* changes caused by this call. * changes caused by this call.
*/ */
@ForOverride @ForOverride
protected ListenableFuture<?> handleSetDeviceVolume(@IntRange(from = 0) int deviceVolume) { protected ListenableFuture<?> handleSetDeviceVolume(
throw new IllegalStateException("Missing implementation to handle COMMAND_SET_DEVICE_VOLUME"); @IntRange(from = 0) int deviceVolume, int flags) {
}
/**
* Handles calls to {@link Player#increaseDeviceVolume()}.
*
* <p>Will only be called if {@link Player#COMMAND_ADJUST_DEVICE_VOLUME} is available.
*
* @return A {@link ListenableFuture} indicating the completion of all immediate {@link State}
* changes caused by this call.
*/
@ForOverride
protected ListenableFuture<?> handleIncreaseDeviceVolume() {
throw new IllegalStateException( throw new IllegalStateException(
"Missing implementation to handle COMMAND_ADJUST_DEVICE_VOLUME"); "Missing implementation to handle COMMAND_SET_DEVICE_VOLUME or"
+ " COMMAND_SET_DEVICE_VOLUME_WITH_FLAGS");
} }
/** /**
* Handles calls to {@link Player#decreaseDeviceVolume()}. * Handles calls to {@link Player#increaseDeviceVolume()} and {@link
* Player#increaseDeviceVolume(int)}.
* *
* <p>Will only be called if {@link Player#COMMAND_ADJUST_DEVICE_VOLUME} is available. * <p>Will only be called if {@link Player#COMMAND_ADJUST_DEVICE_VOLUME} or {@link
* Player#COMMAND_ADJUST_DEVICE_VOLUME_WITH_FLAGS} is available.
* *
* @param flags Either 0 or a bitwise combination of one or more {@link C.VolumeFlags}.
* @return A {@link ListenableFuture} indicating the completion of all immediate {@link State} * @return A {@link ListenableFuture} indicating the completion of all immediate {@link State}
* changes caused by this call. * changes caused by this call.
*/ */
@ForOverride @ForOverride
protected ListenableFuture<?> handleDecreaseDeviceVolume() { protected ListenableFuture<?> handleIncreaseDeviceVolume(@C.VolumeFlags int flags) {
throw new IllegalStateException( throw new IllegalStateException(
"Missing implementation to handle COMMAND_ADJUST_DEVICE_VOLUME"); "Missing implementation to handle COMMAND_ADJUST_DEVICE_VOLUME or"
+ " COMMAND_ADJUST_DEVICE_VOLUME_WITH_FLAGS");
} }
/** /**
* Handles calls to {@link Player#setDeviceMuted}. * Handles calls to {@link Player#decreaseDeviceVolume()} and {@link
* Player#decreaseDeviceVolume(int)}.
* *
* <p>Will only be called if {@link Player#COMMAND_ADJUST_DEVICE_VOLUME} is available. * <p>Will only be called if {@link Player#COMMAND_ADJUST_DEVICE_VOLUME} or {@link
* Player#COMMAND_ADJUST_DEVICE_VOLUME_WITH_FLAGS} is available.
*
* @param flags Either 0 or a bitwise combination of one or more {@link C.VolumeFlags}.
* @return A {@link ListenableFuture} indicating the completion of all immediate {@link State}
* changes caused by this call.
*/
@ForOverride
protected ListenableFuture<?> handleDecreaseDeviceVolume(@C.VolumeFlags int flags) {
throw new IllegalStateException(
"Missing implementation to handle COMMAND_ADJUST_DEVICE_VOLUME or"
+ " COMMAND_ADJUST_DEVICE_VOLUME_WITH_FLAGS");
}
/**
* Handles calls to {@link Player#setDeviceMuted(boolean)} and {@link
* Player#setDeviceMuted(boolean, int)}.
*
* <p>Will only be called if {@link Player#COMMAND_ADJUST_DEVICE_VOLUME} or {@link
* Player#COMMAND_ADJUST_DEVICE_VOLUME} is available.
* *
* @param muted Whether the device was requested to be muted. * @param muted Whether the device was requested to be muted.
* @param flags Either 0 or a bitwise combination of one or more {@link C.VolumeFlags}.
* @return A {@link ListenableFuture} indicating the completion of all immediate {@link State} * @return A {@link ListenableFuture} indicating the completion of all immediate {@link State}
* changes caused by this call. * changes caused by this call.
*/ */
@ForOverride @ForOverride
protected ListenableFuture<?> handleSetDeviceMuted(boolean muted) { protected ListenableFuture<?> handleSetDeviceMuted(boolean muted, @C.VolumeFlags int flags) {
throw new IllegalStateException( throw new IllegalStateException(
"Missing implementation to handle COMMAND_ADJUST_DEVICE_VOLUME"); "Missing implementation to handle COMMAND_ADJUST_DEVICE_VOLUME or"
+ " COMMAND_ADJUST_DEVICE_VOLUME_WITH_FLAGS");
} }
/** /**
@ -3080,6 +3209,27 @@ public abstract class SimpleBasePlayer extends BasePlayer {
throw new IllegalStateException("Missing implementation to handle COMMAND_CHANGE_MEDIA_ITEMS"); throw new IllegalStateException("Missing implementation to handle COMMAND_CHANGE_MEDIA_ITEMS");
} }
/**
* Handles calls to {@link Player#replaceMediaItem} and {@link Player#replaceMediaItems}.
*
* <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()}.
* @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()}.
* @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.
*/
@ForOverride
protected ListenableFuture<?> handleReplaceMediaItems(
int fromIndex, int toIndex, List<MediaItem> mediaItems) {
ListenableFuture<?> addFuture = handleAddMediaItems(toIndex, mediaItems);
ListenableFuture<?> removeFuture = handleRemoveMediaItems(fromIndex, toIndex);
return Util.transformFutureAsync(addFuture, unused -> removeFuture);
}
/** /**
* Handles calls to {@link Player#removeMediaItem} and {@link Player#removeMediaItems}. * Handles calls to {@link Player#removeMediaItem} and {@link Player#removeMediaItems}.
* *
@ -3336,9 +3486,6 @@ public abstract class SimpleBasePlayer extends BasePlayer {
listeners.queueEvent( listeners.queueEvent(
Player.EVENT_METADATA, listener -> listener.onMetadata(newState.timedMetadata)); Player.EVENT_METADATA, listener -> listener.onMetadata(newState.timedMetadata));
} }
if (positionDiscontinuityReason == Player.DISCONTINUITY_REASON_SEEK) {
listeners.queueEvent(/* eventFlag= */ C.INDEX_UNSET, Listener::onSeekProcessed);
}
if (!previousState.availableCommands.equals(newState.availableCommands)) { if (!previousState.availableCommands.equals(newState.availableCommands)) {
listeners.queueEvent( listeners.queueEvent(
Player.EVENT_AVAILABLE_COMMANDS_CHANGED, Player.EVENT_AVAILABLE_COMMANDS_CHANGED,
@ -3717,7 +3864,7 @@ public abstract class SimpleBasePlayer extends BasePlayer {
State.Builder stateBuilder = oldState.buildUpon(); State.Builder stateBuilder = oldState.buildUpon();
stateBuilder.setPlaylist(newPlaylist); stateBuilder.setPlaylist(newPlaylist);
if (oldState.playbackState != Player.STATE_IDLE) { if (oldState.playbackState != Player.STATE_IDLE) {
if (newPlaylist.isEmpty()) { if (newPlaylist.isEmpty() || (newIndex != C.INDEX_UNSET && newIndex >= newPlaylist.size())) {
stateBuilder.setPlaybackState(Player.STATE_ENDED).setIsLoading(false); stateBuilder.setPlaybackState(Player.STATE_ENDED).setIsLoading(false);
} else { } else {
stateBuilder.setPlaybackState(Player.STATE_BUFFERING); stateBuilder.setPlaybackState(Player.STATE_BUFFERING);

View File

@ -15,10 +15,12 @@
*/ */
package androidx.media3.common; package androidx.media3.common;
import android.os.Bundle;
import android.os.Parcel; import android.os.Parcel;
import android.os.Parcelable; import android.os.Parcelable;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.media3.common.util.UnstableApi; import androidx.media3.common.util.UnstableApi;
import androidx.media3.common.util.Util;
/** /**
* A key for a subset of media that can be separately loaded (a "stream"). * A key for a subset of media that can be separately loaded (a "stream").
@ -35,7 +37,7 @@ import androidx.media3.common.util.UnstableApi;
* particular track selection. * particular track selection.
*/ */
@UnstableApi @UnstableApi
public final class StreamKey implements Comparable<StreamKey>, Parcelable { public final class StreamKey implements Comparable<StreamKey>, Parcelable, Bundleable {
/** The period index. */ /** The period index. */
public final int periodIndex; public final int periodIndex;
@ -44,11 +46,6 @@ public final class StreamKey implements Comparable<StreamKey>, Parcelable {
/** The stream index. */ /** The stream index. */
public final int streamIndex; public final int streamIndex;
/**
* @deprecated Use {@link #streamIndex}.
*/
@Deprecated public final int trackIndex;
/** /**
* Creates an instance with {@link #periodIndex} set to 0. * Creates an instance with {@link #periodIndex} set to 0.
* *
@ -60,26 +57,22 @@ public final class StreamKey implements Comparable<StreamKey>, Parcelable {
} }
/** /**
* Creates an instance. * Creates an instance of {@link StreamKey} using 3 indices.
* *
* @param periodIndex The period index. * @param periodIndex The period index.
* @param groupIndex The group index. * @param groupIndex The group index.
* @param streamIndex The stream index. * @param streamIndex The stream index.
*/ */
@SuppressWarnings("deprecation")
public StreamKey(int periodIndex, int groupIndex, int streamIndex) { public StreamKey(int periodIndex, int groupIndex, int streamIndex) {
this.periodIndex = periodIndex; this.periodIndex = periodIndex;
this.groupIndex = groupIndex; this.groupIndex = groupIndex;
this.streamIndex = streamIndex; this.streamIndex = streamIndex;
trackIndex = streamIndex;
} }
@SuppressWarnings("deprecation")
/* package */ StreamKey(Parcel in) { /* package */ StreamKey(Parcel in) {
periodIndex = in.readInt(); periodIndex = in.readInt();
groupIndex = in.readInt(); groupIndex = in.readInt();
streamIndex = in.readInt(); streamIndex = in.readInt();
trackIndex = streamIndex;
} }
@Override @Override
@ -151,4 +144,36 @@ public final class StreamKey implements Comparable<StreamKey>, Parcelable {
return new StreamKey[size]; return new StreamKey[size];
} }
}; };
// Bundleable implementation.
private static final String FIELD_PERIOD_INDEX = Util.intToStringMaxRadix(0);
private static final String FIELD_GROUP_INDEX = Util.intToStringMaxRadix(1);
private static final String FIELD_STREAM_INDEX = Util.intToStringMaxRadix(2);
@Override
public Bundle toBundle() {
Bundle bundle = new Bundle();
if (periodIndex != 0) {
bundle.putInt(FIELD_PERIOD_INDEX, periodIndex);
}
if (groupIndex != 0) {
bundle.putInt(FIELD_GROUP_INDEX, groupIndex);
}
if (streamIndex != 0) {
bundle.putInt(FIELD_STREAM_INDEX, streamIndex);
}
return bundle;
}
/**
* Constructs an instance of {@link StreamKey} from a {@link Bundle} produced by {@link
* #toBundle()}.
*/
public static StreamKey fromBundle(Bundle bundle) {
return new StreamKey(
bundle.getInt(FIELD_PERIOD_INDEX, /* defaultValue= */ 0),
bundle.getInt(FIELD_GROUP_INDEX, /* defaultValue= */ 0),
bundle.getInt(FIELD_STREAM_INDEX, /* defaultValue= */ 0));
}
} }

View File

@ -37,6 +37,9 @@ import com.google.errorprone.annotations.InlineMe;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
// TODO(b/276289331): Revert to media3-hosted SVG links below once they're available on
// developer.android.com.
/** /**
* A flexible representation of the structure of media. A timeline is able to represent the * A flexible representation of the structure of media. A timeline is able to represent the
* structure of a wide variety of media, from simple cases like a single media file through to * structure of a wide variety of media, from simple cases like a single media file through to
@ -138,8 +141,6 @@ import java.util.List;
* <p>This case includes mid-roll ad groups, which are defined as part of the timeline's single * <p>This case includes mid-roll ad groups, which are defined as part of the timeline's single
* period. The period can be queried for information about the ad groups and the ads they contain. * period. The period can be queried for information about the ad groups and the ads they contain.
*/ */
// TODO(b/276289331): Revert to media3-hosted SVG links above once they're available on
// developer.android.com.
public abstract class Timeline implements Bundleable { public abstract class Timeline implements Bundleable {
/** /**
@ -149,8 +150,9 @@ public abstract class Timeline implements Bundleable {
* shows some of the information defined by a window, as well as how this information relates to * shows some of the information defined by a window, as well as how this information relates to
* corresponding {@link Period Periods} in the timeline. * corresponding {@link Period Periods} in the timeline.
* *
* <p style="align:center"><img src="doc-files/timeline-window.svg" alt="Information defined by a * <p style="align:center"><img
* timeline window"> * src="https://exoplayer.dev/doc/reference/com/google/android/exoplayer2/doc-files/timeline-window.svg"
* alt="Information defined by a timeline window">
*/ */
public static final class Window implements Bundleable { public static final class Window implements Bundleable {
@ -557,8 +559,9 @@ public abstract class Timeline implements Bundleable {
* <p>The figure below shows some of the information defined by a period, as well as how this * <p>The figure below shows some of the information defined by a period, as well as how this
* information relates to a corresponding {@link Window} in the timeline. * information relates to a corresponding {@link Window} in the timeline.
* *
* <p style="align:center"><img src="doc-files/timeline-period.svg" alt="Information defined by a * <p style="align:center"><img
* period"> * src="https://exoplayer.dev/doc/reference/com/google/android/exoplayer2/doc-files/timeline-period.svg"
* alt="Information defined by a period">
*/ */
public static final class Period implements Bundleable { public static final class Period implements Bundleable {
@ -834,6 +837,18 @@ public abstract class Timeline implements Bundleable {
: AD_STATE_UNAVAILABLE; : AD_STATE_UNAVAILABLE;
} }
/**
* Returns whether the ad group 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.
*/
@UnstableApi
public boolean isLivePostrollPlaceholder(int adGroupIndex) {
return adGroupIndex == getAdGroupCount() - 1
&& adPlaybackState.isLivePostrollPlaceholder(adGroupIndex);
}
/** /**
* Returns the position offset in the first unplayed ad at which to begin playback, in * Returns the position offset in the first unplayed ad at which to begin playback, in
* microseconds. * microseconds.

View File

@ -17,27 +17,31 @@ package androidx.media3.common;
import androidx.media3.common.util.UnstableApi; import androidx.media3.common.util.UnstableApi;
/** Thrown when an exception occurs while applying effects to video frames. */ /**
* Thrown when an exception occurs while preparing an {@link Effect}, or applying an {@link Effect}
* to video frames.
*/
@UnstableApi @UnstableApi
public final class FrameProcessingException extends Exception { public final class VideoFrameProcessingException extends Exception {
/** /**
* Wraps the given exception in a {@code FrameProcessingException} if it is not already a {@code * Wraps the given exception in a {@code VideoFrameProcessingException} if it is not already a
* FrameProcessingException} and returns the exception otherwise. * {@code VideoFrameProcessingException} and returns the exception otherwise.
*/ */
public static FrameProcessingException from(Exception exception) { public static VideoFrameProcessingException from(Exception exception) {
return from(exception, /* presentationTimeUs= */ C.TIME_UNSET); return from(exception, /* presentationTimeUs= */ C.TIME_UNSET);
} }
/** /**
* Wraps the given exception in a {@code FrameProcessingException} with the given timestamp if it * Wraps the given exception in a {@code VideoFrameProcessingException} with the given timestamp
* is not already a {@code FrameProcessingException} and returns the exception otherwise. * if it is not already a {@code VideoFrameProcessingException} and returns the exception
* otherwise.
*/ */
public static FrameProcessingException from(Exception exception, long presentationTimeUs) { public static VideoFrameProcessingException from(Exception exception, long presentationTimeUs) {
if (exception instanceof FrameProcessingException) { if (exception instanceof VideoFrameProcessingException) {
return (FrameProcessingException) exception; return (VideoFrameProcessingException) exception;
} else { } else {
return new FrameProcessingException(exception, presentationTimeUs); return new VideoFrameProcessingException(exception, presentationTimeUs);
} }
} }
@ -52,7 +56,7 @@ public final class FrameProcessingException extends Exception {
* *
* @param message The detail message for this exception. * @param message The detail message for this exception.
*/ */
public FrameProcessingException(String message) { public VideoFrameProcessingException(String message) {
this(message, /* presentationTimeUs= */ C.TIME_UNSET); this(message, /* presentationTimeUs= */ C.TIME_UNSET);
} }
@ -62,7 +66,7 @@ public final class FrameProcessingException extends Exception {
* @param message The detail message for this exception. * @param message The detail message for this exception.
* @param presentationTimeUs The timestamp of the frame for which the exception occurred. * @param presentationTimeUs The timestamp of the frame for which the exception occurred.
*/ */
public FrameProcessingException(String message, long presentationTimeUs) { public VideoFrameProcessingException(String message, long presentationTimeUs) {
super(message); super(message);
this.presentationTimeUs = presentationTimeUs; this.presentationTimeUs = presentationTimeUs;
} }
@ -73,7 +77,7 @@ public final class FrameProcessingException extends Exception {
* @param message The detail message for this exception. * @param message The detail message for this exception.
* @param cause The cause of this exception. * @param cause The cause of this exception.
*/ */
public FrameProcessingException(String message, Throwable cause) { public VideoFrameProcessingException(String message, Throwable cause) {
this(message, cause, /* presentationTimeUs= */ C.TIME_UNSET); this(message, cause, /* presentationTimeUs= */ C.TIME_UNSET);
} }
@ -84,7 +88,7 @@ public final class FrameProcessingException extends Exception {
* @param cause The cause of this exception. * @param cause The cause of this exception.
* @param presentationTimeUs The timestamp of the frame for which the exception occurred. * @param presentationTimeUs The timestamp of the frame for which the exception occurred.
*/ */
public FrameProcessingException(String message, Throwable cause, long presentationTimeUs) { public VideoFrameProcessingException(String message, Throwable cause, long presentationTimeUs) {
super(message, cause); super(message, cause);
this.presentationTimeUs = presentationTimeUs; this.presentationTimeUs = presentationTimeUs;
} }
@ -94,7 +98,7 @@ public final class FrameProcessingException extends Exception {
* *
* @param cause The cause of this exception. * @param cause The cause of this exception.
*/ */
public FrameProcessingException(Throwable cause) { public VideoFrameProcessingException(Throwable cause) {
this(cause, /* presentationTimeUs= */ C.TIME_UNSET); this(cause, /* presentationTimeUs= */ C.TIME_UNSET);
} }
@ -104,7 +108,7 @@ public final class FrameProcessingException extends Exception {
* @param cause The cause of this exception. * @param cause The cause of this exception.
* @param presentationTimeUs The timestamp of the frame for which the exception occurred. * @param presentationTimeUs The timestamp of the frame for which the exception occurred.
*/ */
public FrameProcessingException(Throwable cause, long presentationTimeUs) { public VideoFrameProcessingException(Throwable cause, long presentationTimeUs) {
super(cause); super(cause);
this.presentationTimeUs = presentationTimeUs; this.presentationTimeUs = presentationTimeUs;
} }

View File

@ -0,0 +1,320 @@
/*
* Copyright 2022 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 static java.lang.annotation.ElementType.TYPE_USE;
import android.content.Context;
import android.graphics.Bitmap;
import android.opengl.EGLExt;
import android.view.Surface;
import androidx.annotation.IntDef;
import androidx.annotation.Nullable;
import androidx.media3.common.util.UnstableApi;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.List;
import java.util.concurrent.Executor;
/**
* Interface for a video frame processor that applies changes to individual video frames.
*
* <p>The changes are specified by {@link Effect} instances passed to {@link Factory#create}.
*
* <p>Manages its input {@link Surface}, which can be accessed via {@link #getInputSurface()}. The
* output {@link Surface} must be set by the caller using {@link
* #setOutputSurfaceInfo(SurfaceInfo)}.
*
* <p>The caller must {@linkplain #registerInputFrame() register} input frames before rendering them
* to the input {@link Surface}.
*/
@UnstableApi
public interface VideoFrameProcessor {
// TODO(b/243036513): Allow effects to be replaced.
/**
* Specifies how the input frames are made available to the {@link VideoFrameProcessor}. One of
* {@link #INPUT_TYPE_SURFACE}, {@link #INPUT_TYPE_BITMAP} or {@link #INPUT_TYPE_TEXTURE_ID}.
*/
@Documented
@Retention(RetentionPolicy.SOURCE)
@Target(TYPE_USE)
@IntDef({INPUT_TYPE_SURFACE, INPUT_TYPE_BITMAP, INPUT_TYPE_TEXTURE_ID})
@interface InputType {}
/** Input frames come from a {@link #getInputSurface surface}. */
int INPUT_TYPE_SURFACE = 1;
/** Input frames come from a {@link Bitmap}. */
int INPUT_TYPE_BITMAP = 2;
/**
* Input frames come from a {@linkplain android.opengl.GLES10#GL_TEXTURE_2D traditional GLES
* texture}.
*/
int INPUT_TYPE_TEXTURE_ID = 3;
/** A factory for {@link VideoFrameProcessor} instances. */
interface Factory {
// TODO(271433904): Turn parameters with default values into setters.
/**
* Creates a new {@link VideoFrameProcessor} instance.
*
* @param context A {@link Context}.
* @param effects The {@link Effect} instances to apply to each frame. Applied on the {@code
* outputColorInfo}'s color space.
* @param debugViewProvider A {@link DebugViewProvider}.
* @param inputColorInfo The {@link ColorInfo} for the input frames.
* @param outputColorInfo The {@link ColorInfo} for the output frames.
* @param renderFramesAutomatically If {@code true}, the instance will render output frames to
* the {@linkplain #setOutputSurfaceInfo(SurfaceInfo) output surface} automatically as
* {@link VideoFrameProcessor} is done processing them. If {@code false}, the {@link
* VideoFrameProcessor} will block until {@link #renderOutputFrame(long)} is called, to
* render or drop the frame.
* @param listenerExecutor The {@link Executor} on which the {@code listener} is invoked.
* @param listener A {@link Listener}.
* @return A new instance.
* @throws VideoFrameProcessingException If a problem occurs while creating the {@link
* VideoFrameProcessor}.
*/
VideoFrameProcessor create(
Context context,
List<Effect> effects,
DebugViewProvider debugViewProvider,
ColorInfo inputColorInfo,
ColorInfo outputColorInfo,
boolean renderFramesAutomatically,
Executor listenerExecutor,
Listener listener)
throws VideoFrameProcessingException;
}
/**
* Listener for asynchronous frame processing events.
*
* <p>All listener methods must be called from the {@link Executor} passed in at {@linkplain
* Factory#create creation}.
*/
interface Listener {
/**
* Called when the output size changes.
*
* <p>The output size is the frame size in pixels after applying all {@linkplain Effect
* effects}.
*
* <p>The output size may differ from the size specified using {@link
* #setOutputSurfaceInfo(SurfaceInfo)}.
*/
void onOutputSizeChanged(int width, int height);
/**
* Called when an output frame with the given {@code presentationTimeUs} becomes available for
* rendering.
*
* @param presentationTimeUs The presentation time of the frame, in microseconds.
*/
void onOutputFrameAvailableForRendering(long presentationTimeUs);
/**
* Called when an exception occurs during asynchronous video frame processing.
*
* <p>If an error occurred, consuming and producing further frames will not work as expected and
* the {@link VideoFrameProcessor} should be released.
*/
void onError(VideoFrameProcessingException exception);
/** Called after the {@link VideoFrameProcessor} has rendered its final output frame. */
void onEnded();
}
/**
* Indicates the frame should be rendered immediately after {@link #renderOutputFrame(long)} is
* invoked.
*/
long RENDER_OUTPUT_FRAME_IMMEDIATELY = -1;
/** Indicates the frame should be dropped after {@link #renderOutputFrame(long)} is invoked. */
long DROP_OUTPUT_FRAME = -2;
/**
* Provides an input {@link Bitmap} to the {@link VideoFrameProcessor}.
*
* <p>Can be called on any thread.
*
* @param inputBitmap The {@link Bitmap} queued to the {@code VideoFrameProcessor}.
* @param durationUs The duration for which to display the {@code inputBitmap}, in microseconds.
* @param frameRate The frame rate at which to display the {@code inputBitmap}, in frames per
* second.
* @throws UnsupportedOperationException If the {@code VideoFrameProcessor} does not accept
* {@linkplain #INPUT_TYPE_BITMAP bitmap input}.
*/
// TODO(b/262693274): Remove duration and frameRate parameters when EditedMediaItem can be
// signalled down to the processors.
void queueInputBitmap(Bitmap inputBitmap, long durationUs, float frameRate);
/**
* Provides an input texture ID to the {@code VideoFrameProcessor}.
*
* <p>It must be called after the {@link #setOnInputFrameProcessedListener
* onInputFrameProcessedListener} and the {@link #setInputFrameInfo frameInfo} have been set.
*
* <p>Can be called on any thread.
*
* @param textureId The ID of the texture queued to the {@code VideoFrameProcessor}.
* @param presentationTimeUs The presentation time of the queued texture, in microseconds.
*/
void queueInputTexture(int textureId, long presentationTimeUs);
/**
* Sets the {@link OnInputFrameProcessedListener}.
*
* <p>Can be called on any thread.
*
* @param listener The {@link OnInputFrameProcessedListener}.
*/
void setOnInputFrameProcessedListener(OnInputFrameProcessedListener listener);
/**
* Returns the input {@link Surface}, where {@link VideoFrameProcessor} consumes input frames
* from.
*
* <p>Can be called on any thread.
*
* @throws UnsupportedOperationException If the {@code VideoFrameProcessor} does not accept
* {@linkplain #INPUT_TYPE_SURFACE surface input}.
*/
Surface getInputSurface();
/**
* Informs the {@code VideoFrameProcessor} that a new input stream will be queued.
*
* <p>Call {@link #setInputFrameInfo} before this method if the {@link FrameInfo} of the new input
* stream differs from that of the current input stream.
*/
// TODO(b/274109008) Merge this and setInputFrameInfo.
void registerInputStream(@InputType int inputType);
/**
* Sets information about the input frames.
*
* <p>The new input information is applied from the next frame {@linkplain #registerInputFrame()
* registered} or {@linkplain #queueInputTexture} queued} onwards.
*
* <p>Pixels are expanded using the {@link FrameInfo#pixelWidthHeightRatio} so that the output
* frames' pixels have a ratio of 1.
*
* <p>Can be called on any thread.
*/
void setInputFrameInfo(FrameInfo inputFrameInfo);
/**
* Informs the {@code VideoFrameProcessor} that a frame will be queued to its {@linkplain
* #getInputSurface() input surface}.
*
* <p>Must be called before rendering a frame to the input surface.
*
* <p>Can be called on any thread.
*
* @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
* #setInputFrameInfo(FrameInfo)}.
*/
void registerInputFrame();
/**
* Returns the number of input frames that have been made available to the {@code
* VideoFrameProcessor} but have not been processed yet.
*
* <p>Can be called on any thread.
*/
int getPendingInputFrameCount();
/**
* Sets the output surface and supporting information. When output frames are rendered and not
* dropped, they will be rendered to this output {@link SurfaceInfo}.
*
* <p>The new output {@link SurfaceInfo} is applied from the next output frame rendered onwards.
* If the output {@link SurfaceInfo} is {@code null}, the {@code VideoFrameProcessor} will stop
* rendering pending frames and resume rendering once a non-null {@link SurfaceInfo} is set.
*
* <p>If the dimensions given in {@link SurfaceInfo} do not match the {@linkplain
* Listener#onOutputSizeChanged(int,int) output size after applying the final effect} the frames
* are resized before rendering to the surface and letter/pillar-boxing is applied.
*
* <p>The caller is responsible for tracking the lifecycle of the {@link SurfaceInfo#surface}
* including calling this method with a new surface if it is destroyed. When this method returns,
* the previous output surface is no longer being used and can safely be released by the caller.
*/
void setOutputSurfaceInfo(@Nullable SurfaceInfo outputSurfaceInfo);
/**
* Renders the oldest unrendered output frame that has become {@linkplain
* Listener#onOutputFrameAvailableForRendering(long) available for rendering} at the given {@code
* renderTimeNs}.
*
* <p>This will either render the output frame to the {@linkplain #setOutputSurfaceInfo output
* surface}, or drop the frame, per {@code renderTimeNs}.
*
* <p>This method must only be called if {@code renderFramesAutomatically} was set to {@code
* false} using the {@link Factory} and should be called exactly once for each frame that becomes
* {@linkplain Listener#onOutputFrameAvailableForRendering(long) available for rendering}.
*
* <p>The {@code renderTimeNs} may be passed to {@link EGLExt#eglPresentationTimeANDROID}
* depending on the implementation.
*
* @param renderTimeNs The render time to use for the frame, in nanoseconds. The render time can
* be before or after the current system time. Use {@link #DROP_OUTPUT_FRAME} to drop the
* frame, or {@link #RENDER_OUTPUT_FRAME_IMMEDIATELY} to render the frame immediately.
*/
void renderOutputFrame(long renderTimeNs);
/**
* Informs the {@code VideoFrameProcessor} that no further input frames should be accepted.
*
* <p>Can be called on any thread.
*
* @throws IllegalStateException If called more than once.
*/
void signalEndOfInput();
/**
* Flushes the {@code VideoFrameProcessor}.
*
* <p>All the frames that are {@linkplain #registerInputFrame() registered} prior to calling this
* method are no longer considered to be registered when this method returns.
*
* <p>{@link Listener} methods invoked prior to calling this method should be ignored.
*
* @throws UnsupportedOperationException If the {@code VideoFrameProcessor} does not accept
* {@linkplain #INPUT_TYPE_SURFACE surface input}.
*/
void flush();
/**
* Releases all resources.
*
* <p>If the {@code VideoFrameProcessor} is released before it has {@linkplain Listener#onEnded()
* ended}, it will attempt to cancel processing any input frames that have already become
* available. Input frames that become available after release are ignored.
*
* <p>This method blocks until all resources are released or releasing times out.
*
* <p>Can be called on any thread.
*/
void release();
}

View File

@ -77,7 +77,7 @@ public final class VideoSize implements Bundleable {
} }
/** /**
* Creates a VideoSize. * Creates a new instance.
* *
* @param width The video width in pixels. * @param width The video width in pixels.
* @param height The video height in pixels. * @param height The video height in pixels.

View File

@ -0,0 +1,337 @@
/*
* Copyright 2022 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.audio;
import static androidx.media3.common.audio.AudioProcessor.EMPTY_BUFFER;
import static androidx.media3.common.util.Assertions.checkState;
import androidx.annotation.Nullable;
import androidx.media3.common.audio.AudioProcessor.AudioFormat;
import androidx.media3.common.util.UnstableApi;
import com.google.common.collect.ImmutableList;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;
/**
* Handles passing buffers through multiple {@link AudioProcessor} instances.
*
* <p>Two instances of {@link AudioProcessingPipeline} are considered {@linkplain #equals(Object)
* equal} if they have the same underlying {@link AudioProcessor} references, in the same order.
*
* <p>To make use of this class, the caller must:
*
* <ul>
* <li>Initialize an instance, passing in all audio processors that may be used for processing.
* <li>Call {@link #configure(AudioFormat)} with the {@link AudioFormat} of the input data. This
* method will give back the {@link AudioFormat} that will be output from the pipeline when
* this configuration is in use.
* <li>Call {@link #flush()} to apply the pending configuration.
* <li>Check if the pipeline {@link #isOperational()}. If not, then the pipeline can not be used
* to process buffers in the current configuration. This is because none of the underlying
* {@link AudioProcessor} instances are {@linkplain AudioProcessor#isActive active}.
* <li>If the pipeline {@link #isOperational()}, {@link #queueInput(ByteBuffer)} then {@link
* #getOutput()} to process buffers.
* <li>{@link #queueEndOfStream()} to inform the pipeline the current input stream is at an end.
* <li>Repeatedly call {@link #getOutput()} and handle those buffers until {@link #isEnded()}
* returns true.
* <li>When finished with the pipeline, call {@link #reset()} to release underlying resources.
* </ul>
*
* <p>If underlying {@link AudioProcessor} instances have pending configuration changes, or the
* {@link AudioFormat} of the input is changing:
*
* <ul>
* <li>Call {@link #configure(AudioFormat)} to configure the pipeline for the new input stream.
* You can still {@link #queueInput(ByteBuffer)} and {@link #getOutput()} in the old setup at
* this time.
* <li>{@link #queueEndOfStream()} to inform the pipeline the current input stream is at an end.
* <li>Repeatedly call {@link #getOutput()} until {@link #isEnded()} returns true.
* <li>Call {@link #flush()} to apply the new configuration and flush the pipeline.
* <li>Begin {@linkplain #queueInput(ByteBuffer) queuing input} and handling the {@linkplain
* #getOutput() output} in the new configuration.
* </ul>
*/
@UnstableApi
public final class AudioProcessingPipeline {
/** The {@link AudioProcessor} instances passed to {@link AudioProcessingPipeline}. */
private final ImmutableList<AudioProcessor> audioProcessors;
/**
* The processors that are {@linkplain AudioProcessor#isActive() active} based on the current
* configuration.
*/
private final List<AudioProcessor> activeAudioProcessors;
/**
* The buffers output by the {@link #activeAudioProcessors}. This has the same number of elements
* as {@link #activeAudioProcessors}.
*/
private ByteBuffer[] outputBuffers;
/** The {@link AudioFormat} currently being output by the pipeline. */
private AudioFormat outputAudioFormat;
/** The {@link AudioFormat} that will be output following a {@link #flush()}. */
private AudioFormat pendingOutputAudioFormat;
/** Whether input has ended, either due to configuration change or end of stream. */
private boolean inputEnded;
/**
* Creates an instance.
*
* @param audioProcessors The {@link AudioProcessor} instances to be used for processing buffers.
*/
public AudioProcessingPipeline(ImmutableList<AudioProcessor> audioProcessors) {
this.audioProcessors = audioProcessors;
activeAudioProcessors = new ArrayList<>();
outputBuffers = new ByteBuffer[0];
outputAudioFormat = AudioFormat.NOT_SET;
pendingOutputAudioFormat = AudioFormat.NOT_SET;
inputEnded = false;
}
/**
* Configures the pipeline to process input audio with the specified format. Returns the
* configured output audio format.
*
* <p>To apply the new configuration for use, the pipeline must be {@linkplain #flush() flushed}.
* Before applying the new configuration, it is safe to queue input and get output in the old
* input/output formats/configuration. Call {@link #queueEndOfStream()} when no more input will be
* supplied for processing in the old configuration.
*
* @param inputAudioFormat The format of audio that will be queued after the next call to {@link
* #flush()}.
* @return The configured output audio format.
* @throws AudioProcessor.UnhandledAudioFormatException If the specified format is not supported
* by the pipeline.
*/
@CanIgnoreReturnValue
public AudioFormat configure(AudioFormat inputAudioFormat)
throws AudioProcessor.UnhandledAudioFormatException {
if (inputAudioFormat.equals(AudioFormat.NOT_SET)) {
throw new AudioProcessor.UnhandledAudioFormatException(inputAudioFormat);
}
AudioFormat intermediateAudioFormat = inputAudioFormat;
for (int i = 0; i < audioProcessors.size(); i++) {
AudioProcessor audioProcessor = audioProcessors.get(i);
AudioFormat nextFormat = audioProcessor.configure(intermediateAudioFormat);
if (audioProcessor.isActive()) {
checkState(!nextFormat.equals(AudioFormat.NOT_SET));
intermediateAudioFormat = nextFormat;
}
}
return pendingOutputAudioFormat = intermediateAudioFormat;
}
/**
* Clears any buffered data and pending output. If any underlying audio processors are {@linkplain
* AudioProcessor#isActive() active}, this also prepares them to receive a new stream of input in
* the last {@linkplain #configure(AudioFormat) configured} (pending) format.
*
* <p>{@link #configure(AudioFormat)} must have been called at least once since the last call to
* {@link #reset()} before calling this.
*/
public void flush() {
activeAudioProcessors.clear();
outputAudioFormat = pendingOutputAudioFormat;
inputEnded = false;
for (int i = 0; i < audioProcessors.size(); i++) {
AudioProcessor audioProcessor = audioProcessors.get(i);
audioProcessor.flush();
if (audioProcessor.isActive()) {
activeAudioProcessors.add(audioProcessor);
}
}
outputBuffers = new ByteBuffer[activeAudioProcessors.size()];
for (int i = 0; i <= getFinalOutputBufferIndex(); i++) {
outputBuffers[i] = activeAudioProcessors.get(i).getOutput();
}
}
/** Returns the {@link AudioFormat} currently being output. */
public AudioFormat getOutputAudioFormat() {
return outputAudioFormat;
}
/**
* Whether the pipeline can be used for processing buffers.
*
* <p>For this to happen the pipeline must be {@linkplain #configure(AudioFormat) configured},
* {@linkplain #flush() flushed} and have {@linkplain AudioProcessor#isActive() active}
* {@linkplain AudioProcessor underlying audio processors} that are ready to process buffers with
* the current configuration.
*/
public boolean isOperational() {
return !activeAudioProcessors.isEmpty();
}
/**
* Queues audio data between the position and limit of the {@code inputBuffer} for processing.
* After calling this method, processed output may be available via {@link #getOutput()}.
*
* @param inputBuffer The input buffer to process. It must be a direct {@link ByteBuffer} with
* native byte order. Its contents are treated as read-only. Its position will be advanced by
* the number of bytes consumed (which may be zero). The caller retains ownership of the
* provided buffer.
*/
public void queueInput(ByteBuffer inputBuffer) {
if (!isOperational() || inputEnded) {
return;
}
processData(inputBuffer);
}
/**
* Returns a {@link ByteBuffer} containing processed output data between its position and limit.
* The buffer will be empty if no output is available.
*
* <p>Buffers returned from this method are retained by pipeline, and it is necessary to consume
* the data (or copy it into another buffer) to allow the pipeline to progress.
*
* @return A buffer containing processed output data between its position and limit.
*/
public ByteBuffer getOutput() {
if (!isOperational()) {
return EMPTY_BUFFER;
}
ByteBuffer outputBuffer = outputBuffers[getFinalOutputBufferIndex()];
if (!outputBuffer.hasRemaining()) {
processData(EMPTY_BUFFER);
}
return outputBuffer;
}
/**
* Queues an end of stream signal. After this method has been called, {@link
* #queueInput(ByteBuffer)} should not be called until after the next call to {@link #flush()}.
* Calling {@link #getOutput()} will return any remaining output data. Multiple calls may be
* required to read all of the remaining output data. {@link #isEnded()} will return {@code true}
* once all remaining output data has been read.
*/
public void queueEndOfStream() {
if (!isOperational() || inputEnded) {
return;
}
inputEnded = true;
activeAudioProcessors.get(0).queueEndOfStream();
}
/**
* Returns whether the pipeline has ended.
*
* <p>The pipeline is considered ended when:
*
* <ul>
* <li>End of stream has been {@linkplain #queueEndOfStream() queued}.
* <li>Every {@linkplain #queueInput(ByteBuffer) input buffer} has been processed.
* <li>Every {@linkplain #getOutput() output buffer} has been fully consumed.
* </ul>
*/
public boolean isEnded() {
return inputEnded
&& activeAudioProcessors.get(getFinalOutputBufferIndex()).isEnded()
&& !outputBuffers[getFinalOutputBufferIndex()].hasRemaining();
}
/**
* Resets the pipeline and its underlying {@link AudioProcessor} instances to their unconfigured
* state, releasing any resources.
*/
public void reset() {
for (int i = 0; i < audioProcessors.size(); i++) {
AudioProcessor audioProcessor = audioProcessors.get(i);
audioProcessor.flush();
audioProcessor.reset();
}
outputBuffers = new ByteBuffer[0];
outputAudioFormat = AudioFormat.NOT_SET;
pendingOutputAudioFormat = AudioFormat.NOT_SET;
inputEnded = false;
}
/**
* Indicates whether some other object is "equal to" this one.
*
* <p>Two instances of {@link AudioProcessingPipeline} are considered equal if they have the same
* underlying {@link AudioProcessor} references in the same order.
*/
@Override
public boolean equals(@Nullable Object o) {
if (this == o) {
return true;
}
if (!(o instanceof AudioProcessingPipeline)) {
return false;
}
AudioProcessingPipeline that = (AudioProcessingPipeline) o;
if (this.audioProcessors.size() != that.audioProcessors.size()) {
return false;
}
for (int i = 0; i < this.audioProcessors.size(); i++) {
if (this.audioProcessors.get(i) != that.audioProcessors.get(i)) {
return false;
}
}
return true;
}
@Override
public int hashCode() {
return audioProcessors.hashCode();
}
private void processData(ByteBuffer inputBuffer) {
boolean progressMade = true;
while (progressMade) {
progressMade = false;
for (int index = 0; index <= getFinalOutputBufferIndex(); index++) {
if (outputBuffers[index].hasRemaining()) {
// Processor at this index has output that has not been consumed. Do not queue input.
continue;
}
AudioProcessor audioProcessor = activeAudioProcessors.get(index);
if (audioProcessor.isEnded()) {
if (!outputBuffers[index].hasRemaining() && index < getFinalOutputBufferIndex()) {
activeAudioProcessors.get(index + 1).queueEndOfStream();
}
continue;
}
ByteBuffer input =
index > 0
? outputBuffers[index - 1]
: inputBuffer.hasRemaining() ? inputBuffer : EMPTY_BUFFER;
long inputBytes = input.remaining();
audioProcessor.queueInput(input);
outputBuffers[index] = audioProcessor.getOutput();
progressMade |= (inputBytes - input.remaining()) > 0 || outputBuffers[index].hasRemaining();
}
}
}
private int getFinalOutputBufferIndex() {
return outputBuffers.length - 1;
}
}

View File

@ -37,6 +37,13 @@ public interface AudioProcessor {
/** PCM audio format that may be handled by an audio processor. */ /** PCM audio format that may be handled by an audio processor. */
final class AudioFormat { final class AudioFormat {
/**
* An {@link AudioFormat} instance to represent an unset {@link AudioFormat}. This should not be
* returned by {@link #configure(AudioFormat)} if the processor {@link #isActive()}.
*
* <p>Typically used to represent an inactive {@link AudioProcessor} {@linkplain
* #configure(AudioFormat) output format}.
*/
public static final AudioFormat NOT_SET = public static final AudioFormat NOT_SET =
new AudioFormat( new AudioFormat(
/* sampleRate= */ Format.NO_VALUE, /* sampleRate= */ Format.NO_VALUE,
@ -94,11 +101,15 @@ public interface AudioProcessor {
} }
} }
/** Exception thrown when a processor can't be configured for a given input audio format. */ /** Exception thrown when the given {@link AudioFormat} can not be handled. */
final class UnhandledAudioFormatException extends Exception { final class UnhandledAudioFormatException extends Exception {
public UnhandledAudioFormatException(AudioFormat inputAudioFormat) { public UnhandledAudioFormatException(AudioFormat inputAudioFormat) {
super("Unhandled format: " + inputAudioFormat); this("Unhandled input format:", inputAudioFormat);
}
public UnhandledAudioFormatException(String message, AudioFormat audioFormat) {
super(message + " " + audioFormat);
} }
} }

View File

@ -0,0 +1,148 @@
/*
* Copyright (C) 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.media3.common.audio;
import androidx.annotation.CallSuper;
import androidx.media3.common.util.UnstableApi;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
/**
* Base class for audio processors that keep an output buffer and an internal buffer that is reused
* whenever input is queued. Subclasses should override {@link #onConfigure(AudioFormat)} to return
* the output audio format for the processor if it's active.
*/
@UnstableApi
public abstract class BaseAudioProcessor implements AudioProcessor {
/** The current input audio format. */
protected AudioFormat inputAudioFormat;
/** The current output audio format. */
protected AudioFormat outputAudioFormat;
private AudioFormat pendingInputAudioFormat;
private AudioFormat pendingOutputAudioFormat;
private ByteBuffer buffer;
private ByteBuffer outputBuffer;
private boolean inputEnded;
public BaseAudioProcessor() {
buffer = EMPTY_BUFFER;
outputBuffer = EMPTY_BUFFER;
pendingInputAudioFormat = AudioFormat.NOT_SET;
pendingOutputAudioFormat = AudioFormat.NOT_SET;
inputAudioFormat = AudioFormat.NOT_SET;
outputAudioFormat = AudioFormat.NOT_SET;
}
@Override
@CanIgnoreReturnValue
public final AudioFormat configure(AudioFormat inputAudioFormat)
throws UnhandledAudioFormatException {
pendingInputAudioFormat = inputAudioFormat;
pendingOutputAudioFormat = onConfigure(inputAudioFormat);
return isActive() ? pendingOutputAudioFormat : AudioFormat.NOT_SET;
}
@Override
public boolean isActive() {
return pendingOutputAudioFormat != AudioFormat.NOT_SET;
}
@Override
public final void queueEndOfStream() {
inputEnded = true;
onQueueEndOfStream();
}
@CallSuper
@Override
public ByteBuffer getOutput() {
ByteBuffer outputBuffer = this.outputBuffer;
this.outputBuffer = EMPTY_BUFFER;
return outputBuffer;
}
@CallSuper
@SuppressWarnings("ReferenceEquality")
@Override
public boolean isEnded() {
return inputEnded && outputBuffer == EMPTY_BUFFER;
}
@Override
public final void flush() {
outputBuffer = EMPTY_BUFFER;
inputEnded = false;
inputAudioFormat = pendingInputAudioFormat;
outputAudioFormat = pendingOutputAudioFormat;
onFlush();
}
@Override
public final void reset() {
flush();
buffer = EMPTY_BUFFER;
pendingInputAudioFormat = AudioFormat.NOT_SET;
pendingOutputAudioFormat = AudioFormat.NOT_SET;
inputAudioFormat = AudioFormat.NOT_SET;
outputAudioFormat = AudioFormat.NOT_SET;
onReset();
}
/**
* Replaces the current output buffer with a buffer of at least {@code size} bytes and returns it.
* Callers should write to the returned buffer then {@link ByteBuffer#flip()} it so it can be read
* via {@link #getOutput()}.
*/
protected final ByteBuffer replaceOutputBuffer(int size) {
if (buffer.capacity() < size) {
buffer = ByteBuffer.allocateDirect(size).order(ByteOrder.nativeOrder());
} else {
buffer.clear();
}
outputBuffer = buffer;
return buffer;
}
/** Returns whether the current output buffer has any data remaining. */
protected final boolean hasPendingOutput() {
return outputBuffer.hasRemaining();
}
/** Called when the processor is configured for a new input format. */
@CanIgnoreReturnValue
protected AudioFormat onConfigure(AudioFormat inputAudioFormat)
throws UnhandledAudioFormatException {
return AudioFormat.NOT_SET;
}
/** Called when the end-of-stream is queued to the processor. */
protected void onQueueEndOfStream() {
// Do nothing.
}
/** Called when the processor is flushed, directly or as part of resetting. */
protected void onFlush() {
// Do nothing.
}
/** Called when the processor is reset. */
protected void onReset() {
// Do nothing.
}
}

View File

@ -0,0 +1,110 @@
/*
* Copyright 2023 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.audio;
import static androidx.media3.common.util.Assertions.checkStateNotNull;
import android.util.SparseArray;
import androidx.annotation.Nullable;
import androidx.media3.common.C;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.common.util.Util;
import java.nio.ByteBuffer;
/**
* An {@link AudioProcessor} that handles mixing and scaling audio channels. Call {@link
* #putChannelMixingMatrix(ChannelMixingMatrix)} specifying mixing matrices to apply for each
* possible input channel count before using the audio processor. Input and output are 16-bit PCM.
*/
@UnstableApi
public final class ChannelMixingAudioProcessor extends BaseAudioProcessor {
private final SparseArray<ChannelMixingMatrix> matrixByInputChannelCount;
/** Creates a new audio processor for mixing and scaling audio channels. */
public ChannelMixingAudioProcessor() {
matrixByInputChannelCount = new SparseArray<>();
}
/**
* Stores a channel mixing matrix for processing audio with a given {@link
* ChannelMixingMatrix#getInputChannelCount() channel count}. Overwrites any previously stored
* matrix for the same input channel count.
*/
public void putChannelMixingMatrix(ChannelMixingMatrix matrix) {
int inputChannelCount = matrix.getInputChannelCount();
matrixByInputChannelCount.put(inputChannelCount, matrix);
}
@Override
protected AudioFormat onConfigure(AudioFormat inputAudioFormat)
throws UnhandledAudioFormatException {
if (inputAudioFormat.encoding != C.ENCODING_PCM_16BIT) {
throw new UnhandledAudioFormatException(inputAudioFormat);
}
@Nullable
ChannelMixingMatrix channelMixingMatrix =
matrixByInputChannelCount.get(inputAudioFormat.channelCount);
if (channelMixingMatrix == null) {
throw new UnhandledAudioFormatException(
"No mixing matrix for input channel count", inputAudioFormat);
}
if (channelMixingMatrix.isIdentity()) {
return AudioFormat.NOT_SET;
}
return new AudioFormat(
inputAudioFormat.sampleRate,
channelMixingMatrix.getOutputChannelCount(),
C.ENCODING_PCM_16BIT);
}
@Override
public void queueInput(ByteBuffer inputBuffer) {
ChannelMixingMatrix channelMixingMatrix =
checkStateNotNull(matrixByInputChannelCount.get(inputAudioFormat.channelCount));
int inputFramesToMix = inputBuffer.remaining() / inputAudioFormat.bytesPerFrame;
ByteBuffer outputBuffer =
replaceOutputBuffer(inputFramesToMix * outputAudioFormat.bytesPerFrame);
int inputChannelCount = channelMixingMatrix.getInputChannelCount();
int outputChannelCount = channelMixingMatrix.getOutputChannelCount();
float[] outputFrame = new float[outputChannelCount];
while (inputBuffer.hasRemaining()) {
for (int inputChannelIndex = 0; inputChannelIndex < inputChannelCount; inputChannelIndex++) {
short inputValue = inputBuffer.getShort();
for (int outputChannelIndex = 0;
outputChannelIndex < outputChannelCount;
outputChannelIndex++) {
outputFrame[outputChannelIndex] +=
channelMixingMatrix.getMixingCoefficient(inputChannelIndex, outputChannelIndex)
* inputValue;
}
}
for (int outputChannelIndex = 0;
outputChannelIndex < outputChannelCount;
outputChannelIndex++) {
short shortValue =
(short)
Util.constrainValue(
outputFrame[outputChannelIndex], Short.MIN_VALUE, Short.MAX_VALUE);
outputBuffer.put((byte) (shortValue & 0xFF));
outputBuffer.put((byte) ((shortValue >> 8) & 0xFF));
outputFrame[outputChannelIndex] = 0;
}
}
outputBuffer.flip();
}
}

View File

@ -0,0 +1,194 @@
/*
* Copyright 2022 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.audio;
import static androidx.media3.common.util.Assertions.checkArgument;
import androidx.media3.common.util.UnstableApi;
/**
* An immutable matrix that describes the mapping of input channels to output channels.
*
* <p>The matrix coefficients define the scaling factor to use when mixing samples from the input
* channel (row) to the output channel (column).
*
* <p>Examples:
*
* <ul>
* <li>Stereo to mono with each channel at half volume:
* <pre>
* [0.5 0.5]</pre>
* <li>Stereo to stereo with no mixing or scaling:
* <pre>
* [1 0
* 0 1]</pre>
* <li>Stereo to stereo with 0.7 volume:
* <pre>
* [0.7 0
* 0 0.7]</pre>
* </ul>
*/
@UnstableApi
public final class ChannelMixingMatrix {
private final int inputChannelCount;
private final int outputChannelCount;
private final float[] coefficients;
private final boolean isZero;
private final boolean isDiagonal;
private final boolean isIdentity;
/**
* Creates a standard channel mixing matrix that converts from {@code inputChannelCount} channels
* to {@code outputChannelCount} channels.
*
* <p>If the input and output channel counts match then a simple identity matrix will be returned.
* Otherwise, default matrix coefficients will be used to best match channel locations and overall
* power level.
*
* @param inputChannelCount Number of input channels.
* @param outputChannelCount Number of output channels.
* @return New channel mixing matrix.
* @throws UnsupportedOperationException If no default matrix coefficients are implemented for the
* given input and output channel counts.
*/
public static ChannelMixingMatrix create(int inputChannelCount, int outputChannelCount) {
return new ChannelMixingMatrix(
inputChannelCount,
outputChannelCount,
createMixingCoefficients(inputChannelCount, outputChannelCount));
}
/**
* Creates a matrix with the given coefficients in row-major order.
*
* @param inputChannelCount Number of input channels (rows in the matrix).
* @param outputChannelCount Number of output channels (columns in the matrix).
* @param coefficients Non-negative matrix coefficients in row-major order.
*/
public ChannelMixingMatrix(int inputChannelCount, int outputChannelCount, float[] coefficients) {
checkArgument(inputChannelCount > 0, "Input channel count must be positive.");
checkArgument(outputChannelCount > 0, "Output channel count must be positive.");
checkArgument(
coefficients.length == inputChannelCount * outputChannelCount,
"Coefficient array length is invalid.");
this.inputChannelCount = inputChannelCount;
this.outputChannelCount = outputChannelCount;
this.coefficients = checkCoefficientsValid(coefficients);
// Calculate matrix properties.
boolean allDiagonalCoefficientsAreOne = true;
boolean allCoefficientsAreZero = true;
boolean allNonDiagonalCoefficientsAreZero = true;
for (int row = 0; row < inputChannelCount; row++) {
for (int col = 0; col < outputChannelCount; col++) {
float coefficient = getMixingCoefficient(row, col);
boolean onDiagonal = row == col;
if (coefficient != 1f && onDiagonal) {
allDiagonalCoefficientsAreOne = false;
}
if (coefficient != 0f) {
allCoefficientsAreZero = false;
if (!onDiagonal) {
allNonDiagonalCoefficientsAreZero = false;
}
}
}
}
isZero = allCoefficientsAreZero;
isDiagonal = isSquare() && allNonDiagonalCoefficientsAreZero;
isIdentity = isDiagonal && allDiagonalCoefficientsAreOne;
}
public int getInputChannelCount() {
return inputChannelCount;
}
public int getOutputChannelCount() {
return outputChannelCount;
}
/** Gets the scaling factor for the given input and output channel. */
public float getMixingCoefficient(int inputChannel, int outputChannel) {
return coefficients[inputChannel * outputChannelCount + outputChannel];
}
/** Returns whether all mixing coefficients are zero. */
public boolean isZero() {
return isZero;
}
/** Returns whether the input and output channel count is the same. */
public boolean isSquare() {
return inputChannelCount == outputChannelCount;
}
/** Returns whether the matrix is square and all non-diagonal coefficients are zero. */
public boolean isDiagonal() {
return isDiagonal;
}
/** Returns whether this is an identity matrix. */
public boolean isIdentity() {
return isIdentity;
}
/** Returns a new matrix with the given scaling factor applied to all coefficients. */
public ChannelMixingMatrix scaleBy(float scale) {
float[] scaledCoefficients = new float[coefficients.length];
for (int i = 0; i < coefficients.length; i++) {
scaledCoefficients[i] = scale * coefficients[i];
}
return new ChannelMixingMatrix(inputChannelCount, outputChannelCount, scaledCoefficients);
}
private static float[] createMixingCoefficients(int inputChannelCount, int outputChannelCount) {
if (inputChannelCount == outputChannelCount) {
return initializeIdentityMatrix(outputChannelCount);
}
if (inputChannelCount == 1 && outputChannelCount == 2) {
// Mono -> stereo.
return new float[] {1f, 1f};
}
if (inputChannelCount == 2 && outputChannelCount == 1) {
// Stereo -> mono.
return new float[] {0.5f, 0.5f};
}
throw new UnsupportedOperationException(
"Default channel mixing coefficients for "
+ inputChannelCount
+ "->"
+ outputChannelCount
+ " are not yet implemented.");
}
private static float[] initializeIdentityMatrix(int channelCount) {
float[] coefficients = new float[channelCount * channelCount];
for (int c = 0; c < channelCount; c++) {
coefficients[channelCount * c + c] = 1f;
}
return coefficients;
}
private static float[] checkCoefficientsValid(float[] coefficients) {
for (int i = 0; i < coefficients.length; i++) {
if (coefficients[i] < 0f) {
throw new IllegalArgumentException("Coefficient at index " + i + " is negative.");
}
}
return coefficients;
}
}

View File

@ -14,7 +14,7 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package androidx.media3.exoplayer.audio; package androidx.media3.common.audio;
import static java.lang.Math.min; import static java.lang.Math.min;

View File

@ -0,0 +1,263 @@
/*
* Copyright (C) 2017 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.audio;
import static androidx.media3.common.util.Assertions.checkNotNull;
import androidx.annotation.Nullable;
import androidx.media3.common.C;
import androidx.media3.common.Format;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.common.util.Util;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.ShortBuffer;
/**
* An {@link AudioProcessor} that uses the Sonic library to modify audio speed/pitch/sample rate.
*/
@UnstableApi
public class SonicAudioProcessor implements AudioProcessor {
/** Indicates that the output sample rate should be the same as the input. */
public static final int SAMPLE_RATE_NO_CHANGE = -1;
/** The threshold below which the difference between two pitch/speed factors is negligible. */
private static final float CLOSE_THRESHOLD = 0.0001f;
/**
* The minimum number of output bytes required for duration scaling to be calculated using the
* input and output byte counts, rather than using the current playback speed.
*/
private static final int MIN_BYTES_FOR_DURATION_SCALING_CALCULATION = 1024;
private int pendingOutputSampleRate;
private float speed;
private float pitch;
private AudioFormat pendingInputAudioFormat;
private AudioFormat pendingOutputAudioFormat;
private AudioFormat inputAudioFormat;
private AudioFormat outputAudioFormat;
private boolean pendingSonicRecreation;
@Nullable private Sonic sonic;
private ByteBuffer buffer;
private ShortBuffer shortBuffer;
private ByteBuffer outputBuffer;
private long inputBytes;
private long outputBytes;
private boolean inputEnded;
/** Creates a new Sonic audio processor. */
public SonicAudioProcessor() {
speed = 1f;
pitch = 1f;
pendingInputAudioFormat = AudioFormat.NOT_SET;
pendingOutputAudioFormat = AudioFormat.NOT_SET;
inputAudioFormat = AudioFormat.NOT_SET;
outputAudioFormat = AudioFormat.NOT_SET;
buffer = EMPTY_BUFFER;
shortBuffer = buffer.asShortBuffer();
outputBuffer = EMPTY_BUFFER;
pendingOutputSampleRate = SAMPLE_RATE_NO_CHANGE;
}
/**
* Sets the target playback speed. This method may only be called after draining data through the
* processor. The value returned by {@link #isActive()} may change, and the processor must be
* {@link #flush() flushed} before queueing more data.
*
* @param speed The target factor by which playback should be sped up.
*/
public final void setSpeed(float speed) {
if (this.speed != speed) {
this.speed = speed;
pendingSonicRecreation = true;
}
}
/**
* Sets the target playback pitch. This method may only be called after draining data through the
* processor. The value returned by {@link #isActive()} may change, and the processor must be
* {@link #flush() flushed} before queueing more data.
*
* @param pitch The target pitch.
*/
public final void setPitch(float pitch) {
if (this.pitch != pitch) {
this.pitch = pitch;
pendingSonicRecreation = true;
}
}
/**
* Sets the sample rate for output audio, in Hertz. Pass {@link #SAMPLE_RATE_NO_CHANGE} to output
* audio at the same sample rate as the input. After calling this method, call {@link
* #configure(AudioFormat)} to configure the processor with the new sample rate.
*
* @param sampleRateHz The sample rate for output audio, in Hertz.
* @see #configure(AudioFormat)
*/
public final void setOutputSampleRateHz(int sampleRateHz) {
pendingOutputSampleRate = sampleRateHz;
}
/**
* Returns the media duration corresponding to the specified playout duration, taking speed
* adjustment into account.
*
* <p>The scaling performed by this method will use the actual playback speed achieved by the
* audio processor, on average, since it was last flushed. This may differ very slightly from the
* target playback speed.
*
* @param playoutDuration The playout duration to scale.
* @return The corresponding media duration, in the same units as {@code duration}.
*/
public final long getMediaDuration(long playoutDuration) {
if (outputBytes >= MIN_BYTES_FOR_DURATION_SCALING_CALCULATION) {
long processedInputBytes = inputBytes - checkNotNull(sonic).getPendingInputBytes();
return outputAudioFormat.sampleRate == inputAudioFormat.sampleRate
? Util.scaleLargeTimestamp(playoutDuration, processedInputBytes, outputBytes)
: Util.scaleLargeTimestamp(
playoutDuration,
processedInputBytes * outputAudioFormat.sampleRate,
outputBytes * inputAudioFormat.sampleRate);
} else {
return (long) ((double) speed * playoutDuration);
}
}
@Override
@CanIgnoreReturnValue
public final AudioFormat configure(AudioFormat inputAudioFormat)
throws UnhandledAudioFormatException {
if (inputAudioFormat.encoding != C.ENCODING_PCM_16BIT) {
throw new UnhandledAudioFormatException(inputAudioFormat);
}
int outputSampleRateHz =
pendingOutputSampleRate == SAMPLE_RATE_NO_CHANGE
? inputAudioFormat.sampleRate
: pendingOutputSampleRate;
pendingInputAudioFormat = inputAudioFormat;
pendingOutputAudioFormat =
new AudioFormat(outputSampleRateHz, inputAudioFormat.channelCount, C.ENCODING_PCM_16BIT);
pendingSonicRecreation = true;
return pendingOutputAudioFormat;
}
@Override
public final boolean isActive() {
return pendingOutputAudioFormat.sampleRate != Format.NO_VALUE
&& (Math.abs(speed - 1f) >= CLOSE_THRESHOLD
|| Math.abs(pitch - 1f) >= CLOSE_THRESHOLD
|| pendingOutputAudioFormat.sampleRate != pendingInputAudioFormat.sampleRate);
}
@Override
public final void queueInput(ByteBuffer inputBuffer) {
if (!inputBuffer.hasRemaining()) {
return;
}
Sonic sonic = checkNotNull(this.sonic);
ShortBuffer shortBuffer = inputBuffer.asShortBuffer();
int inputSize = inputBuffer.remaining();
inputBytes += inputSize;
sonic.queueInput(shortBuffer);
inputBuffer.position(inputBuffer.position() + inputSize);
}
@Override
public final void queueEndOfStream() {
// TODO(internal b/174554082): assert sonic is non-null here and in getOutput.
if (sonic != null) {
sonic.queueEndOfStream();
}
inputEnded = true;
}
@Override
public final ByteBuffer getOutput() {
@Nullable Sonic sonic = this.sonic;
if (sonic != null) {
int outputSize = sonic.getOutputSize();
if (outputSize > 0) {
if (buffer.capacity() < outputSize) {
buffer = ByteBuffer.allocateDirect(outputSize).order(ByteOrder.nativeOrder());
shortBuffer = buffer.asShortBuffer();
} else {
buffer.clear();
shortBuffer.clear();
}
sonic.getOutput(shortBuffer);
outputBytes += outputSize;
buffer.limit(outputSize);
outputBuffer = buffer;
}
}
ByteBuffer outputBuffer = this.outputBuffer;
this.outputBuffer = EMPTY_BUFFER;
return outputBuffer;
}
@Override
public final boolean isEnded() {
return inputEnded && (sonic == null || sonic.getOutputSize() == 0);
}
@Override
public final void flush() {
if (isActive()) {
inputAudioFormat = pendingInputAudioFormat;
outputAudioFormat = pendingOutputAudioFormat;
if (pendingSonicRecreation) {
sonic =
new Sonic(
inputAudioFormat.sampleRate,
inputAudioFormat.channelCount,
speed,
pitch,
outputAudioFormat.sampleRate);
} else if (sonic != null) {
sonic.flush();
}
}
outputBuffer = EMPTY_BUFFER;
inputBytes = 0;
outputBytes = 0;
inputEnded = false;
}
@Override
public final void reset() {
speed = 1f;
pitch = 1f;
pendingInputAudioFormat = AudioFormat.NOT_SET;
pendingOutputAudioFormat = AudioFormat.NOT_SET;
inputAudioFormat = AudioFormat.NOT_SET;
outputAudioFormat = AudioFormat.NOT_SET;
buffer = EMPTY_BUFFER;
shortBuffer = buffer.asShortBuffer();
outputBuffer = EMPTY_BUFFER;
pendingOutputSampleRate = SAMPLE_RATE_NO_CHANGE;
pendingSonicRecreation = false;
sonic = null;
inputBytes = 0;
outputBytes = 0;
inputEnded = false;
}
}

View File

@ -13,11 +13,11 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package androidx.media3.exoplayer.audio; package androidx.media3.common.audio;
import androidx.media3.common.C; import androidx.media3.common.C;
import androidx.media3.common.Format; import androidx.media3.common.Format;
import androidx.media3.common.audio.AudioProcessor; import androidx.media3.common.util.UnstableApi;
import androidx.media3.common.util.Util; import androidx.media3.common.util.Util;
import com.google.errorprone.annotations.CanIgnoreReturnValue; import com.google.errorprone.annotations.CanIgnoreReturnValue;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
@ -35,7 +35,8 @@ import java.nio.ByteBuffer;
* <li>{@link C#ENCODING_PCM_FLOAT} * <li>{@link C#ENCODING_PCM_FLOAT}
* </ul> * </ul>
*/ */
/* package */ final class ResamplingAudioProcessor extends BaseAudioProcessor { @UnstableApi
public final class ToInt16PcmAudioProcessor extends BaseAudioProcessor {
@Override @Override
@CanIgnoreReturnValue @CanIgnoreReturnValue

View File

@ -300,162 +300,6 @@ public final class Cue implements Bundleable {
*/ */
public final float shearDegrees; public final float shearDegrees;
/**
* Creates a text cue whose {@link #textAlignment} is null, whose type parameters are set to
* {@link #TYPE_UNSET} and whose dimension parameters are set to {@link #DIMEN_UNSET}.
*
* @param text See {@link #text}.
* @deprecated Use {@link Builder}.
*/
@UnstableApi
@SuppressWarnings("deprecation")
@Deprecated
public Cue(CharSequence text) {
this(
text,
/* textAlignment= */ null,
/* line= */ DIMEN_UNSET,
/* lineType= */ TYPE_UNSET,
/* lineAnchor= */ TYPE_UNSET,
/* position= */ DIMEN_UNSET,
/* positionAnchor= */ TYPE_UNSET,
/* size= */ DIMEN_UNSET);
}
/**
* Creates a text cue.
*
* @param text See {@link #text}.
* @param textAlignment See {@link #textAlignment}.
* @param line See {@link #line}.
* @param lineType See {@link #lineType}.
* @param lineAnchor See {@link #lineAnchor}.
* @param position See {@link #position}.
* @param positionAnchor See {@link #positionAnchor}.
* @param size See {@link #size}.
* @deprecated Use {@link Builder}.
*/
@UnstableApi
@SuppressWarnings("deprecation")
@Deprecated
public Cue(
CharSequence text,
@Nullable Alignment textAlignment,
float line,
@LineType int lineType,
@AnchorType int lineAnchor,
float position,
@AnchorType int positionAnchor,
float size) {
this(
text,
textAlignment,
line,
lineType,
lineAnchor,
position,
positionAnchor,
size,
/* windowColorSet= */ false,
/* windowColor= */ Color.BLACK);
}
/**
* Creates a text cue.
*
* @param text See {@link #text}.
* @param textAlignment See {@link #textAlignment}.
* @param line See {@link #line}.
* @param lineType See {@link #lineType}.
* @param lineAnchor See {@link #lineAnchor}.
* @param position See {@link #position}.
* @param positionAnchor See {@link #positionAnchor}.
* @param size See {@link #size}.
* @param textSizeType See {@link #textSizeType}.
* @param textSize See {@link #textSize}.
* @deprecated Use {@link Builder}.
*/
@UnstableApi
@Deprecated
public Cue(
CharSequence text,
@Nullable Alignment textAlignment,
float line,
@LineType int lineType,
@AnchorType int lineAnchor,
float position,
@AnchorType int positionAnchor,
float size,
@TextSizeType int textSizeType,
float textSize) {
this(
text,
textAlignment,
/* multiRowAlignment= */ null,
/* bitmap= */ null,
line,
lineType,
lineAnchor,
position,
positionAnchor,
textSizeType,
textSize,
size,
/* bitmapHeight= */ DIMEN_UNSET,
/* windowColorSet= */ false,
/* windowColor= */ Color.BLACK,
/* verticalType= */ TYPE_UNSET,
/* shearDegrees= */ 0f);
}
/**
* Creates a text cue.
*
* @param text See {@link #text}.
* @param textAlignment See {@link #textAlignment}.
* @param line See {@link #line}.
* @param lineType See {@link #lineType}.
* @param lineAnchor See {@link #lineAnchor}.
* @param position See {@link #position}.
* @param positionAnchor See {@link #positionAnchor}.
* @param size See {@link #size}.
* @param windowColorSet See {@link #windowColorSet}.
* @param windowColor See {@link #windowColor}.
* @deprecated Use {@link Builder}.
*/
@UnstableApi
@Deprecated
public Cue(
CharSequence text,
@Nullable Alignment textAlignment,
float line,
@LineType int lineType,
@AnchorType int lineAnchor,
float position,
@AnchorType int positionAnchor,
float size,
boolean windowColorSet,
int windowColor) {
this(
text,
textAlignment,
/* multiRowAlignment= */ null,
/* bitmap= */ null,
line,
lineType,
lineAnchor,
position,
positionAnchor,
/* textSizeType= */ TYPE_UNSET,
/* textSize= */ DIMEN_UNSET,
size,
/* bitmapHeight= */ DIMEN_UNSET,
windowColorSet,
windowColor,
/* verticalType= */ TYPE_UNSET,
/* shearDegrees= */ 0f);
}
private Cue( private Cue(
@Nullable CharSequence text, @Nullable CharSequence text,
@Nullable Alignment textAlignment, @Nullable Alignment textAlignment,

View File

@ -0,0 +1,54 @@
/*
* Copyright 2022 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.util;
import android.graphics.Bitmap;
import android.net.Uri;
import androidx.annotation.Nullable;
import androidx.media3.common.MediaMetadata;
import com.google.common.util.concurrent.ListenableFuture;
/** Loads images. */
@UnstableApi
public interface BitmapLoader {
/** Decodes an image from compressed binary data. */
ListenableFuture<Bitmap> decodeBitmap(byte[] data);
/** Loads an image from {@code uri}. */
ListenableFuture<Bitmap> loadBitmap(Uri uri);
/**
* Loads an image from {@link MediaMetadata}. Returns null if {@code metadata} doesn't contain
* bitmap information.
*
* <p>By default, the method will try to decode an image from {@link MediaMetadata#artworkData} if
* it is present. Otherwise, the method will try to load an image from {@link
* MediaMetadata#artworkUri} if it is present. The method will return null if neither {@link
* MediaMetadata#artworkData} nor {@link MediaMetadata#artworkUri} is present.
*/
@Nullable
default ListenableFuture<Bitmap> loadBitmapFromMetadata(MediaMetadata metadata) {
@Nullable ListenableFuture<Bitmap> future;
if (metadata.artworkData != null) {
future = decodeBitmap(metadata.artworkData);
} else if (metadata.artworkUri != null) {
future = loadBitmap(metadata.artworkUri);
} else {
future = null;
}
return future;
}
}

View File

@ -22,10 +22,14 @@ import android.os.Bundle;
import android.util.SparseArray; import android.util.SparseArray;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.media3.common.Bundleable; import androidx.media3.common.Bundleable;
import com.google.common.base.Function;
import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map;
/** Utilities for {@link Bundleable}. */ /** Utilities for {@link Bundleable}. */
@UnstableApi @UnstableApi
@ -33,10 +37,21 @@ public final class BundleableUtil {
/** Converts a list of {@link Bundleable} to a list {@link Bundle}. */ /** Converts a list of {@link Bundleable} to a list {@link Bundle}. */
public static <T extends Bundleable> ImmutableList<Bundle> toBundleList(List<T> bundleableList) { public static <T extends Bundleable> ImmutableList<Bundle> toBundleList(List<T> bundleableList) {
return toBundleList(bundleableList, Bundleable::toBundle);
}
/**
* Converts a list of {@link Bundleable} to a list {@link Bundle}
*
* @param bundleableList list of Bundleable items to be converted
* @param customToBundleFunc function that specifies how to bundle up each {@link Bundleable}
*/
public static <T extends Bundleable> ImmutableList<Bundle> toBundleList(
List<T> bundleableList, Function<T, Bundle> customToBundleFunc) {
ImmutableList.Builder<Bundle> builder = ImmutableList.builder(); ImmutableList.Builder<Bundle> builder = ImmutableList.builder();
for (int i = 0; i < bundleableList.size(); i++) { for (int i = 0; i < bundleableList.size(); i++) {
Bundleable bundleable = bundleableList.get(i); T bundleable = bundleableList.get(i);
builder.add(bundleable.toBundle()); builder.add(customToBundleFunc.apply(bundleable));
} }
return builder.build(); return builder.build();
} }
@ -94,6 +109,47 @@ public final class BundleableUtil {
return sparseArray; return sparseArray;
} }
public static Bundle stringMapToBundle(Map<String, String> bundleableMap) {
Bundle bundle = new Bundle();
for (Map.Entry<String, String> entry : bundleableMap.entrySet()) {
bundle.putString(entry.getKey(), entry.getValue());
}
return bundle;
}
public static HashMap<String, String> bundleToStringHashMap(Bundle bundle) {
HashMap<String, String> map = new HashMap<>();
if (bundle == Bundle.EMPTY) {
return map;
}
for (String key : bundle.keySet()) {
@Nullable String value = bundle.getString(key);
if (value != null) {
map.put(key, value);
}
}
return map;
}
public static ImmutableMap<String, String> bundleToStringImmutableMap(Bundle bundle) {
if (bundle == Bundle.EMPTY) {
return ImmutableMap.of();
}
HashMap<String, String> map = bundleToStringHashMap(bundle);
return ImmutableMap.copyOf(map);
}
public static Bundle getBundleWithDefault(Bundle bundle, String field, Bundle defaultValue) {
@Nullable Bundle result = bundle.getBundle(field);
return result != null ? result : defaultValue;
}
public static ArrayList<Integer> getIntegerArrayListWithDefault(
Bundle bundle, String field, ArrayList<Integer> defaultValue) {
@Nullable ArrayList<Integer> result = bundle.getIntegerArrayList(field);
return result != null ? result : defaultValue;
}
/** /**
* Sets the application class loader to the given {@link Bundle} if no class loader is present. * Sets the application class loader to the given {@link Bundle} if no class loader is present.
* *

View File

@ -67,7 +67,7 @@ public final class GlProgram {
* @return The content of the file to load. * @return The content of the file to load.
* @throws IOException If the file couldn't be read. * @throws IOException If the file couldn't be read.
*/ */
public static String loadAsset(Context context, String assetPath) throws IOException { private static String loadAsset(Context context, String assetPath) throws IOException {
@Nullable InputStream inputStream = null; @Nullable InputStream inputStream = null;
try { try {
inputStream = context.getAssets().open(assetPath); inputStream = context.getAssets().open(assetPath);
@ -328,7 +328,7 @@ public final class GlProgram {
/* unusedLength */ new int[1], /* unusedLength */ new int[1],
/* lengthOffset= */ 0, /* lengthOffset= */ 0,
/* unusedSize */ new int[1], /* unusedSize */ new int[1],
/*sizeOffset= */ 0, /* sizeOffset= */ 0,
type, type,
/* typeOffset= */ 0, /* typeOffset= */ 0,
nameBytes, nameBytes,

View File

@ -31,6 +31,7 @@ import android.opengl.GLES20;
import android.opengl.GLES30; import android.opengl.GLES30;
import android.opengl.Matrix; import android.opengl.Matrix;
import androidx.annotation.DoNotInline; import androidx.annotation.DoNotInline;
import androidx.annotation.IntRange;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi; import androidx.annotation.RequiresApi;
import androidx.media3.common.C; import androidx.media3.common.C;
@ -89,7 +90,16 @@ public final class GlUtil {
private static final String EXTENSION_SURFACELESS_CONTEXT = "EGL_KHR_surfaceless_context"; private static final String EXTENSION_SURFACELESS_CONTEXT = "EGL_KHR_surfaceless_context";
// https://www.khronos.org/registry/OpenGL/extensions/EXT/EXT_YUV_target.txt // https://www.khronos.org/registry/OpenGL/extensions/EXT/EXT_YUV_target.txt
private static final String EXTENSION_YUV_TARGET = "GL_EXT_YUV_target"; private static final String EXTENSION_YUV_TARGET = "GL_EXT_YUV_target";
// https://registry.khronos.org/EGL/extensions/EXT/EGL_EXT_gl_colorspace_bt2020_linear.txt
private static final String EXTENSION_COLORSPACE_BT2020_PQ = "EGL_EXT_gl_colorspace_bt2020_pq";
// https://registry.khronos.org/EGL/extensions/KHR/EGL_KHR_gl_colorspace.txt
private static final int EGL_GL_COLORSPACE_KHR = 0x309D;
// https://registry.khronos.org/EGL/extensions/EXT/EGL_EXT_gl_colorspace_bt2020_linear.txt
private static final int EGL_GL_COLORSPACE_BT2020_PQ_EXT = 0x3340;
private static final int[] EGL_WINDOW_SURFACE_ATTRIBUTES_BT2020_PQ =
new int[] {
EGL_GL_COLORSPACE_KHR, EGL_GL_COLORSPACE_BT2020_PQ_EXT, EGL14.EGL_NONE, EGL14.EGL_NONE
};
private static final int[] EGL_WINDOW_SURFACE_ATTRIBUTES_NONE = new int[] {EGL14.EGL_NONE}; private static final int[] EGL_WINDOW_SURFACE_ATTRIBUTES_NONE = new int[] {EGL14.EGL_NONE};
/** Class only contains static methods. */ /** Class only contains static methods. */
@ -190,7 +200,7 @@ public final class GlUtil {
* Returns whether the {@link #EXTENSION_YUV_TARGET} extension is supported. * Returns whether the {@link #EXTENSION_YUV_TARGET} extension is supported.
* *
* <p>This extension allows sampling raw YUV values from an external texture, which is required * <p>This extension allows sampling raw YUV values from an external texture, which is required
* for HDR. * for HDR input.
*/ */
public static boolean isYuvTargetExtensionSupported() { public static boolean isYuvTargetExtensionSupported() {
if (Util.SDK_INT < 17) { if (Util.SDK_INT < 17) {
@ -216,6 +226,13 @@ public final class GlUtil {
return glExtensions != null && glExtensions.contains(EXTENSION_YUV_TARGET); return glExtensions != null && glExtensions.contains(EXTENSION_YUV_TARGET);
} }
/** Returns whether {@link #EXTENSION_COLORSPACE_BT2020_PQ} is supported. */
public static boolean isBt2020PqExtensionSupported() {
EGLDisplay display = EGL14.eglGetDisplay(EGL14.EGL_DEFAULT_DISPLAY);
@Nullable String eglExtensions = EGL14.eglQueryString(display, EGL10.EGL_EXTENSIONS);
return eglExtensions != null && eglExtensions.contains(EXTENSION_COLORSPACE_BT2020_PQ);
}
/** Returns an initialized default {@link EGLDisplay}. */ /** Returns an initialized default {@link EGLDisplay}. */
@RequiresApi(17) @RequiresApi(17)
public static EGLDisplay createEglDisplay() throws GlException { public static EGLDisplay createEglDisplay() throws GlException {
@ -232,57 +249,78 @@ public final class GlUtil {
*/ */
@RequiresApi(17) @RequiresApi(17)
public static EGLContext createEglContext(EGLDisplay eglDisplay) throws GlException { public static EGLContext createEglContext(EGLDisplay eglDisplay) throws GlException {
return createEglContext(eglDisplay, EGL_CONFIG_ATTRIBUTES_RGBA_8888); return createEglContext(
EGL14.EGL_NO_CONTEXT, eglDisplay, /* openGlVersion= */ 2, EGL_CONFIG_ATTRIBUTES_RGBA_8888);
} }
/** /**
* Creates a new {@link EGLContext} for the specified {@link EGLDisplay}. * Creates a new {@link EGLContext} for the specified {@link EGLDisplay}.
* *
* @param sharedContext The {@link EGLContext} with which to share data.
* @param eglDisplay The {@link EGLDisplay} to create an {@link EGLContext} for. * @param eglDisplay The {@link EGLDisplay} to create an {@link EGLContext} for.
* @param openGlVersion The version of OpenGL ES to configure. Accepts either {@code 2}, for
* OpenGL ES 2.0, or {@code 3}, for OpenGL ES 3.0.
* @param configAttributes The attributes to configure EGL with. Accepts either {@link * @param configAttributes The attributes to configure EGL with. Accepts either {@link
* #EGL_CONFIG_ATTRIBUTES_RGBA_1010102}, which will request OpenGL ES 3.0, or {@link * #EGL_CONFIG_ATTRIBUTES_RGBA_1010102}, or {@link #EGL_CONFIG_ATTRIBUTES_RGBA_8888}.
* #EGL_CONFIG_ATTRIBUTES_RGBA_8888}, which will request OpenGL ES 2.0.
*/ */
@RequiresApi(17) @RequiresApi(17)
public static EGLContext createEglContext(EGLDisplay eglDisplay, int[] configAttributes) public static EGLContext createEglContext(
EGLContext sharedContext,
EGLDisplay eglDisplay,
@IntRange(from = 2, to = 3) int openGlVersion,
int[] configAttributes)
throws GlException { throws GlException {
checkArgument( checkArgument(
Arrays.equals(configAttributes, EGL_CONFIG_ATTRIBUTES_RGBA_8888) Arrays.equals(configAttributes, EGL_CONFIG_ATTRIBUTES_RGBA_8888)
|| Arrays.equals(configAttributes, EGL_CONFIG_ATTRIBUTES_RGBA_1010102)); || Arrays.equals(configAttributes, EGL_CONFIG_ATTRIBUTES_RGBA_1010102));
return Api17.createEglContext( checkArgument(openGlVersion == 2 || openGlVersion == 3);
eglDisplay, return Api17.createEglContext(sharedContext, eglDisplay, openGlVersion, configAttributes);
/* version= */ Arrays.equals(configAttributes, EGL_CONFIG_ATTRIBUTES_RGBA_1010102) ? 3 : 2,
configAttributes);
} }
/** /**
* Returns a new {@link EGLSurface} wrapping the specified {@code surface}. * Creates a new {@link EGLSurface} wrapping the specified {@code surface}.
* *
* <p>The {@link EGLSurface} will configure with {@link #EGL_CONFIG_ATTRIBUTES_RGBA_8888} and * <p>The {@link EGLSurface} will configure with OpenGL ES 2.0.
* OpenGL ES 2.0.
* *
* @param eglDisplay The {@link EGLDisplay} to attach the surface to. * @param eglDisplay The {@link EGLDisplay} to attach the surface to.
* @param surface The surface to wrap; must be a surface, surface texture or surface holder. * @param surface The surface to wrap; must be a surface, surface texture or surface holder.
* @param colorTransfer The {@linkplain C.ColorTransfer color transfer characteristics} to which
* the {@code surface} is configured. The only accepted values are {@link
* C#COLOR_TRANSFER_SDR}, {@link C#COLOR_TRANSFER_HLG} and {@link C#COLOR_TRANSFER_ST2084}.
* @param isEncoderInputSurface Whether the {@code surface} is the input surface of an encoder.
*/ */
@RequiresApi(17) @RequiresApi(17)
public static EGLSurface getEglSurface(EGLDisplay eglDisplay, Object surface) throws GlException { public static EGLSurface createEglSurface(
return Api17.getEglSurface( EGLDisplay eglDisplay,
eglDisplay, surface, EGL_CONFIG_ATTRIBUTES_RGBA_8888, EGL_WINDOW_SURFACE_ATTRIBUTES_NONE); Object surface,
@C.ColorTransfer int colorTransfer,
boolean isEncoderInputSurface)
throws GlException {
int[] configAttributes;
int[] windowAttributes;
if (colorTransfer == C.COLOR_TRANSFER_SDR || colorTransfer == C.COLOR_TRANSFER_GAMMA_2_2) {
configAttributes = EGL_CONFIG_ATTRIBUTES_RGBA_8888;
windowAttributes = EGL_WINDOW_SURFACE_ATTRIBUTES_NONE;
} else if (colorTransfer == C.COLOR_TRANSFER_ST2084) {
configAttributes = EGL_CONFIG_ATTRIBUTES_RGBA_1010102;
if (isEncoderInputSurface) {
// Outputting BT2020 PQ with EGL_WINDOW_SURFACE_ATTRIBUTES_BT2020_PQ to an encoder causes
// the encoder to incorrectly switch to full range color, even if the encoder is configured
// with limited range color, because EGL_WINDOW_SURFACE_ATTRIBUTES_BT2020_PQ sets full range
// color output, and GL windowAttributes overrides encoder settings.
windowAttributes = EGL_WINDOW_SURFACE_ATTRIBUTES_NONE;
} else {
// TODO(b/262259999) HDR10 PQ content looks dark on the screen.
windowAttributes = EGL_WINDOW_SURFACE_ATTRIBUTES_BT2020_PQ;
} }
} else if (colorTransfer == C.COLOR_TRANSFER_HLG) {
/** checkArgument(isEncoderInputSurface, "Outputting HLG to the screen is not supported.");
* Returns a new {@link EGLSurface} wrapping the specified {@code surface}. configAttributes = EGL_CONFIG_ATTRIBUTES_RGBA_1010102;
* windowAttributes = EGL_WINDOW_SURFACE_ATTRIBUTES_NONE;
* @param eglDisplay The {@link EGLDisplay} to attach the surface to. } else {
* @param surface The surface to wrap; must be a surface, surface texture or surface holder. throw new IllegalArgumentException("Unsupported color transfer: " + colorTransfer);
* @param configAttributes The attributes to configure EGL with. Accepts {@link }
* #EGL_CONFIG_ATTRIBUTES_RGBA_1010102} and {@link #EGL_CONFIG_ATTRIBUTES_RGBA_8888}. return Api17.createEglSurface(eglDisplay, surface, configAttributes, windowAttributes);
*/
@RequiresApi(17)
public static EGLSurface getEglSurface(
EGLDisplay eglDisplay, Object surface, int[] configAttributes) throws GlException {
return Api17.getEglSurface(
eglDisplay, surface, configAttributes, EGL_WINDOW_SURFACE_ATTRIBUTES_NONE);
} }
/** /**
@ -349,6 +387,11 @@ public final class GlUtil {
return eglSurface; return eglSurface;
} }
/** Gets the current {@link EGLContext context}. */
public static EGLContext getCurrentContext() {
return EGL14.eglGetCurrentContext();
}
/** /**
* Collects all OpenGL errors that occurred since this method was last called and throws a {@link * Collects all OpenGL errors that occurred since this method was last called and throws a {@link
* GlException} with the combined error message. * GlException} with the combined error message.
@ -378,7 +421,7 @@ public final class GlUtil {
*/ */
private static void assertValidTextureSize(int width, int height) throws GlException { private static void assertValidTextureSize(int width, int height) throws GlException {
// TODO(b/201293185): Consider handling adjustments for sizes > GL_MAX_TEXTURE_SIZE // TODO(b/201293185): Consider handling adjustments for sizes > GL_MAX_TEXTURE_SIZE
// (ex. downscaling appropriately) in a texture processor instead of asserting incorrect // (ex. downscaling appropriately) in a shader program instead of asserting incorrect
// values. // values.
// For valid GL sizes, see: // For valid GL sizes, see:
// https://www.khronos.org/registry/OpenGL-Refpages/es2.0/xhtml/glTexImage2D.xml // https://www.khronos.org/registry/OpenGL-Refpages/es2.0/xhtml/glTexImage2D.xml
@ -401,7 +444,8 @@ public final class GlUtil {
/** Fills the pixels in the current output render target with (r=0, g=0, b=0, a=0). */ /** Fills the pixels in the current output render target with (r=0, g=0, b=0, a=0). */
public static void clearOutputFrame() throws GlException { public static void clearOutputFrame() throws GlException {
GLES20.glClearColor(/* red= */ 0, /* green= */ 0, /* blue= */ 0, /* alpha= */ 0); GLES20.glClearColor(/* red= */ 0, /* green= */ 0, /* blue= */ 0, /* alpha= */ 0);
GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT); GLES20.glClearDepthf(1.0f);
GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT | GLES20.GL_DEPTH_BUFFER_BIT);
GlUtil.checkGlError(); GlUtil.checkGlError();
} }
@ -450,26 +494,6 @@ public final class GlUtil {
Api17.focusFramebufferUsingCurrentContext(framebuffer, width, height); Api17.focusFramebufferUsingCurrentContext(framebuffer, width, height);
} }
/**
* Deletes a GL texture.
*
* @param textureId The ID of the texture to delete.
*/
public static void deleteTexture(int textureId) throws GlException {
GLES20.glDeleteTextures(/* n= */ 1, new int[] {textureId}, /* offset= */ 0);
checkGlError();
}
/**
* Destroys the {@link EGLContext} identified by the provided {@link EGLDisplay} and {@link
* EGLContext}.
*/
@RequiresApi(17)
public static void destroyEglContext(
@Nullable EGLDisplay eglDisplay, @Nullable EGLContext eglContext) throws GlException {
Api17.destroyEglContext(eglDisplay, eglContext);
}
/** /**
* Allocates a FloatBuffer with the given data. * Allocates a FloatBuffer with the given data.
* *
@ -504,8 +528,8 @@ public final class GlUtil {
* *
* @param width The width of the new texture in pixels. * @param width The width of the new texture in pixels.
* @param height The height of the new texture in pixels. * @param height The height of the new texture in pixels.
* @param useHighPrecisionColorComponents If {@code false}, uses 8-bit unsigned bytes. If {@code * @param useHighPrecisionColorComponents If {@code false}, uses colors with 8-bit unsigned bytes.
* true}, use 16-bit (half-precision) floating-point. * If {@code true}, use 16-bit (half-precision) floating-point.
* @throws GlException If the texture allocation fails. * @throws GlException If the texture allocation fails.
* @return The texture identifier for the newly-allocated texture. * @return The texture identifier for the newly-allocated texture.
*/ */
@ -602,6 +626,49 @@ public final class GlUtil {
return fboId[0]; return fboId[0];
} }
/**
* Deletes a GL texture.
*
* @param textureId The ID of the texture to delete.
*/
public static void deleteTexture(int textureId) throws GlException {
GLES20.glDeleteTextures(/* n= */ 1, new int[] {textureId}, /* offset= */ 0);
checkGlError();
}
/**
* Destroys the {@link EGLContext} identified by the provided {@link EGLDisplay} and {@link
* EGLContext}.
*/
@RequiresApi(17)
public static void destroyEglContext(
@Nullable EGLDisplay eglDisplay, @Nullable EGLContext eglContext) throws GlException {
Api17.destroyEglContext(eglDisplay, eglContext);
}
/**
* Destroys the {@link EGLSurface} identified by the provided {@link EGLDisplay} and {@link
* EGLSurface}.
*/
@RequiresApi(17)
public static void destroyEglSurface(
@Nullable EGLDisplay eglDisplay, @Nullable EGLSurface eglSurface) throws GlException {
Api17.destroyEglSurface(eglDisplay, eglSurface);
}
/** Deletes a framebuffer, or silently ignores the method call if {@code fboId} is unused. */
public static void deleteFbo(int fboId) throws GlException {
GLES20.glDeleteFramebuffers(/* n= */ 1, new int[] {fboId}, /* offset= */ 0);
checkGlError();
}
/** Deletes a renderbuffer, or silently ignores the method call if {@code rboId} is unused. */
public static void deleteRbo(int rboId) throws GlException {
GLES20.glDeleteRenderbuffers(
/* n= */ 1, /* renderbuffers= */ new int[] {rboId}, /* offset= */ 0);
checkGlError();
}
/** /**
* Throws a {@link GlException} with the given message if {@code expression} evaluates to {@code * Throws a {@link GlException} with the given message if {@code expression} evaluates to {@code
* false}. * false}.
@ -639,13 +706,14 @@ public final class GlUtil {
@DoNotInline @DoNotInline
public static EGLContext createEglContext( public static EGLContext createEglContext(
EGLDisplay eglDisplay, int version, int[] configAttributes) throws GlException { EGLContext sharedContext, EGLDisplay eglDisplay, int version, int[] configAttributes)
throws GlException {
int[] contextAttributes = {EGL14.EGL_CONTEXT_CLIENT_VERSION, version, EGL14.EGL_NONE}; int[] contextAttributes = {EGL14.EGL_CONTEXT_CLIENT_VERSION, version, EGL14.EGL_NONE};
EGLContext eglContext = EGLContext eglContext =
EGL14.eglCreateContext( EGL14.eglCreateContext(
eglDisplay, eglDisplay,
getEglConfig(eglDisplay, configAttributes), getEglConfig(eglDisplay, configAttributes),
EGL14.EGL_NO_CONTEXT, sharedContext,
contextAttributes, contextAttributes,
/* offset= */ 0); /* offset= */ 0);
if (eglContext == null) { if (eglContext == null) {
@ -660,18 +728,15 @@ public final class GlUtil {
} }
@DoNotInline @DoNotInline
public static EGLSurface getEglSurface( public static EGLSurface createEglSurface(
EGLDisplay eglDisplay, EGLDisplay eglDisplay, Object surface, int[] configAttributes, int[] windowAttributes)
Object surface,
int[] configAttributes,
int[] windowSurfaceAttributes)
throws GlException { throws GlException {
EGLSurface eglSurface = EGLSurface eglSurface =
EGL14.eglCreateWindowSurface( EGL14.eglCreateWindowSurface(
eglDisplay, eglDisplay,
getEglConfig(eglDisplay, configAttributes), getEglConfig(eglDisplay, configAttributes),
surface, surface,
windowSurfaceAttributes, windowAttributes,
/* offset= */ 0); /* offset= */ 0);
checkEglException("Error creating surface"); checkEglException("Error creating surface");
return eglSurface; return eglSurface;
@ -739,6 +804,16 @@ public final class GlUtil {
checkEglException("Error terminating display"); checkEglException("Error terminating display");
} }
@DoNotInline
public static void destroyEglSurface(
@Nullable EGLDisplay eglDisplay, @Nullable EGLSurface eglSurface) throws GlException {
if (eglDisplay == null || eglSurface == null) {
return;
}
EGL14.eglDestroySurface(eglDisplay, eglSurface);
checkEglException("Error destroying surface");
}
@DoNotInline @DoNotInline
private static EGLConfig getEglConfig(EGLDisplay eglDisplay, int[] attributes) private static EGLConfig getEglConfig(EGLDisplay eglDisplay, int[] attributes)
throws GlException { throws GlException {

View File

@ -99,14 +99,20 @@ public final class ListenerSet<T extends @NonNull Object> {
* during one {@link Looper} message queue iteration were handled by the listeners. * during one {@link Looper} message queue iteration were handled by the listeners.
*/ */
public ListenerSet(Looper looper, Clock clock, IterationFinishedEvent<T> iterationFinishedEvent) { public ListenerSet(Looper looper, Clock clock, IterationFinishedEvent<T> iterationFinishedEvent) {
this(/* listeners= */ new CopyOnWriteArraySet<>(), looper, clock, iterationFinishedEvent); this(
/* listeners= */ new CopyOnWriteArraySet<>(),
looper,
clock,
iterationFinishedEvent,
/* throwsWhenUsingWrongThread= */ true);
} }
private ListenerSet( private ListenerSet(
CopyOnWriteArraySet<ListenerHolder<T>> listeners, CopyOnWriteArraySet<ListenerHolder<T>> listeners,
Looper looper, Looper looper,
Clock clock, Clock clock,
IterationFinishedEvent<T> iterationFinishedEvent) { IterationFinishedEvent<T> iterationFinishedEvent,
boolean throwsWhenUsingWrongThread) {
this.clock = clock; this.clock = clock;
this.listeners = listeners; this.listeners = listeners;
this.iterationFinishedEvent = iterationFinishedEvent; this.iterationFinishedEvent = iterationFinishedEvent;
@ -117,7 +123,7 @@ public final class ListenerSet<T extends @NonNull Object> {
@SuppressWarnings("nullness:methodref.receiver.bound") @SuppressWarnings("nullness:methodref.receiver.bound")
HandlerWrapper handler = clock.createHandler(looper, this::handleMessage); HandlerWrapper handler = clock.createHandler(looper, this::handleMessage);
this.handler = handler; this.handler = handler;
throwsWhenUsingWrongThread = true; this.throwsWhenUsingWrongThread = throwsWhenUsingWrongThread;
} }
/** /**
@ -149,7 +155,8 @@ public final class ListenerSet<T extends @NonNull Object> {
@CheckResult @CheckResult
public ListenerSet<T> copy( public ListenerSet<T> copy(
Looper looper, Clock clock, IterationFinishedEvent<T> iterationFinishedEvent) { Looper looper, Clock clock, IterationFinishedEvent<T> iterationFinishedEvent) {
return new ListenerSet<>(listeners, looper, clock, iterationFinishedEvent); return new ListenerSet<>(
listeners, looper, clock, iterationFinishedEvent, throwsWhenUsingWrongThread);
} }
/** /**

View File

@ -24,6 +24,8 @@ import androidx.annotation.Nullable;
import androidx.media3.common.C; import androidx.media3.common.C;
import androidx.media3.common.ColorInfo; import androidx.media3.common.ColorInfo;
import androidx.media3.common.Format; import androidx.media3.common.Format;
import androidx.media3.common.MimeTypes;
import com.google.common.collect.ImmutableList;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.util.List; import java.util.List;
@ -64,6 +66,70 @@ public final class MediaFormatUtil {
private static final int MAX_POWER_OF_TWO_INT = 1 << 30; private static final int MAX_POWER_OF_TWO_INT = 1 << 30;
/** Returns a {@link Format} representing the given {@link MediaFormat}. */
@SuppressLint("InlinedApi") // Inlined MediaFormat keys.
public static Format createFormatFromMediaFormat(MediaFormat mediaFormat) {
Format.Builder formatBuilder =
new Format.Builder()
.setSampleMimeType(mediaFormat.getString(MediaFormat.KEY_MIME))
.setLanguage(mediaFormat.getString(MediaFormat.KEY_LANGUAGE))
.setPeakBitrate(
getInteger(mediaFormat, KEY_MAX_BIT_RATE, /* defaultValue= */ Format.NO_VALUE))
.setAverageBitrate(
getInteger(
mediaFormat, MediaFormat.KEY_BIT_RATE, /* defaultValue= */ Format.NO_VALUE))
.setCodecs(mediaFormat.getString(MediaFormat.KEY_CODECS_STRING))
.setFrameRate(getFrameRate(mediaFormat, /* defaultValue= */ Format.NO_VALUE))
.setWidth(
getInteger(mediaFormat, MediaFormat.KEY_WIDTH, /* defaultValue= */ Format.NO_VALUE))
.setHeight(
getInteger(
mediaFormat, MediaFormat.KEY_HEIGHT, /* defaultValue= */ Format.NO_VALUE))
.setPixelWidthHeightRatio(
getPixelWidthHeightRatio(mediaFormat, /* defaultValue= */ 1.0f))
.setMaxInputSize(
getInteger(
mediaFormat,
MediaFormat.KEY_MAX_INPUT_SIZE,
/* defaultValue= */ Format.NO_VALUE))
.setRotationDegrees(
getInteger(mediaFormat, MediaFormat.KEY_ROTATION, /* defaultValue= */ 0))
// TODO(b/278101856): Disallow invalid values after confirming.
.setColorInfo(getColorInfo(mediaFormat, /* allowInvalidValues= */ true))
.setSampleRate(
getInteger(
mediaFormat, MediaFormat.KEY_SAMPLE_RATE, /* defaultValue= */ Format.NO_VALUE))
.setChannelCount(
getInteger(
mediaFormat,
MediaFormat.KEY_CHANNEL_COUNT,
/* defaultValue= */ Format.NO_VALUE))
.setPcmEncoding(
getInteger(
mediaFormat,
MediaFormat.KEY_PCM_ENCODING,
/* defaultValue= */ Format.NO_VALUE));
ImmutableList.Builder<byte[]> csdBuffers = new ImmutableList.Builder<>();
int csdIndex = 0;
while (true) {
@Nullable ByteBuffer csdByteBuffer = mediaFormat.getByteBuffer("csd-" + csdIndex);
if (csdByteBuffer == null) {
break;
}
byte[] csdBufferData = new byte[csdByteBuffer.remaining()];
csdByteBuffer.get(csdBufferData);
csdByteBuffer.rewind();
csdBuffers.add(csdBufferData);
csdIndex++;
}
formatBuilder.setInitializationData(csdBuffers.build());
return formatBuilder.build();
}
/** /**
* Returns a {@link MediaFormat} representing the given ExoPlayer {@link Format}. * Returns a {@link MediaFormat} representing the given ExoPlayer {@link Format}.
* *
@ -196,23 +262,38 @@ public final class MediaFormatUtil {
/** /**
* Creates and returns a {@code ColorInfo}, if a valid instance is described in the {@link * Creates and returns a {@code ColorInfo}, if a valid instance is described in the {@link
* MediaFormat}. * MediaFormat}.
*
* <p>Under API 24, {@code null} will always be returned, because {@link MediaFormat} color keys
* like {@link MediaFormat#KEY_COLOR_STANDARD} were only added in API 24.
*/ */
@Nullable @Nullable
public static ColorInfo getColorInfo(MediaFormat mediaFormat) { public static ColorInfo getColorInfo(MediaFormat mediaFormat) {
if (SDK_INT < 29) { return getColorInfo(mediaFormat, /* allowInvalidValues= */ false);
}
// Internal methods.
@Nullable
private static ColorInfo getColorInfo(MediaFormat mediaFormat, boolean allowInvalidValues) {
if (SDK_INT < 24) {
// MediaFormat KEY_COLOR_TRANSFER and other KEY_COLOR values available from API 24.
return null; return null;
} }
int colorSpace = int colorSpace =
mediaFormat.getInteger(MediaFormat.KEY_COLOR_STANDARD, /* defaultValue= */ Format.NO_VALUE); getInteger(
mediaFormat, MediaFormat.KEY_COLOR_STANDARD, /* defaultValue= */ Format.NO_VALUE);
int colorRange = int colorRange =
mediaFormat.getInteger(MediaFormat.KEY_COLOR_RANGE, /* defaultValue= */ Format.NO_VALUE); getInteger(mediaFormat, MediaFormat.KEY_COLOR_RANGE, /* defaultValue= */ Format.NO_VALUE);
int colorTransfer = int colorTransfer =
mediaFormat.getInteger(MediaFormat.KEY_COLOR_TRANSFER, /* defaultValue= */ Format.NO_VALUE); getInteger(
mediaFormat, MediaFormat.KEY_COLOR_TRANSFER, /* defaultValue= */ Format.NO_VALUE);
@Nullable @Nullable
ByteBuffer hdrStaticInfoByteBuffer = mediaFormat.getByteBuffer(MediaFormat.KEY_HDR_STATIC_INFO); ByteBuffer hdrStaticInfoByteBuffer = mediaFormat.getByteBuffer(MediaFormat.KEY_HDR_STATIC_INFO);
@Nullable @Nullable
byte[] hdrStaticInfo = byte[] hdrStaticInfo =
hdrStaticInfoByteBuffer != null ? getArray(hdrStaticInfoByteBuffer) : null; hdrStaticInfoByteBuffer != null ? getArray(hdrStaticInfoByteBuffer) : null;
if (!allowInvalidValues) {
// Some devices may produce invalid values from MediaFormat#getInteger. // Some devices may produce invalid values from MediaFormat#getInteger.
// See b/239435670 for more information. // See b/239435670 for more information.
if (!isValidColorSpace(colorSpace)) { if (!isValidColorSpace(colorSpace)) {
@ -224,22 +305,91 @@ public final class MediaFormatUtil {
if (!isValidColorTransfer(colorTransfer)) { if (!isValidColorTransfer(colorTransfer)) {
colorTransfer = Format.NO_VALUE; colorTransfer = Format.NO_VALUE;
} }
}
if (colorSpace != Format.NO_VALUE if (colorSpace != Format.NO_VALUE
|| colorRange != Format.NO_VALUE || colorRange != Format.NO_VALUE
|| colorTransfer != Format.NO_VALUE || colorTransfer != Format.NO_VALUE
|| hdrStaticInfo != null) { || hdrStaticInfo != null) {
return new ColorInfo(colorSpace, colorRange, colorTransfer, hdrStaticInfo); return new ColorInfo.Builder()
.setColorSpace(colorSpace)
.setColorRange(colorRange)
.setColorTransfer(colorTransfer)
.setHdrStaticInfo(hdrStaticInfo)
.build();
} }
return null; return null;
} }
/** Supports {@link MediaFormat#getInteger(String, int)} for {@code API < 29}. */
public static int getInteger(MediaFormat mediaFormat, String name, int defaultValue) {
return mediaFormat.containsKey(name) ? mediaFormat.getInteger(name) : defaultValue;
}
/** Supports {@link MediaFormat#getFloat(String, float)} for {@code API < 29}. */
public static float getFloat(MediaFormat mediaFormat, String name, float defaultValue) {
return mediaFormat.containsKey(name) ? mediaFormat.getFloat(name) : defaultValue;
}
/**
* Returns the frame rate from a {@link MediaFormat}.
*
* <p>The {@link MediaFormat#KEY_FRAME_RATE} can have both integer and float value so it returns
* which ever value is set.
*/
private static float getFrameRate(MediaFormat mediaFormat, float defaultValue) {
float frameRate = defaultValue;
if (mediaFormat.containsKey(MediaFormat.KEY_FRAME_RATE)) {
try {
frameRate = mediaFormat.getFloat(MediaFormat.KEY_FRAME_RATE);
} catch (ClassCastException ex) {
frameRate = mediaFormat.getInteger(MediaFormat.KEY_FRAME_RATE);
}
}
return frameRate;
}
/** Returns the ratio between a pixel's width and height for a {@link MediaFormat}. */
// Inlined MediaFormat.KEY_PIXEL_ASPECT_RATIO_WIDTH and MediaFormat.KEY_PIXEL_ASPECT_RATIO_HEIGHT.
@SuppressLint("InlinedApi")
private static float getPixelWidthHeightRatio(MediaFormat mediaFormat, float defaultValue) {
if (mediaFormat.containsKey(MediaFormat.KEY_PIXEL_ASPECT_RATIO_WIDTH)
&& mediaFormat.containsKey(MediaFormat.KEY_PIXEL_ASPECT_RATIO_HEIGHT)) {
return (float) mediaFormat.getInteger(MediaFormat.KEY_PIXEL_ASPECT_RATIO_WIDTH)
/ (float) mediaFormat.getInteger(MediaFormat.KEY_PIXEL_ASPECT_RATIO_HEIGHT);
}
return defaultValue;
}
public static byte[] getArray(ByteBuffer byteBuffer) { public static byte[] getArray(ByteBuffer byteBuffer) {
byte[] array = new byte[byteBuffer.remaining()]; byte[] array = new byte[byteBuffer.remaining()];
byteBuffer.get(array); byteBuffer.get(array);
return array; return array;
} }
/** Returns whether a {@link MediaFormat} is a video format. */
public static boolean isVideoFormat(MediaFormat mediaFormat) {
return MimeTypes.isVideo(mediaFormat.getString(MediaFormat.KEY_MIME));
}
/** Returns whether a {@link MediaFormat} is an audio format. */
public static boolean isAudioFormat(MediaFormat mediaFormat) {
return MimeTypes.isAudio(mediaFormat.getString(MediaFormat.KEY_MIME));
}
/** Returns the time lapse capture FPS from the given {@link MediaFormat} if it was set. */
@Nullable
public static Integer getTimeLapseFrameRate(MediaFormat format) {
if (format.containsKey("time-lapse-enable")
&& format.getInteger("time-lapse-enable") > 0
&& format.containsKey("time-lapse-fps")) {
return format.getInteger("time-lapse-fps");
} else {
return null;
}
}
// Internal methods. // Internal methods.
private static void setBooleanAsInt(MediaFormat format, String key, int value) { private static void setBooleanAsInt(MediaFormat format, String key, int value) {
@ -322,7 +472,10 @@ public final class MediaFormatUtil {
/** Whether this is a valid {@link C.ColorTransfer} instance. */ /** Whether this is a valid {@link C.ColorTransfer} instance. */
private static boolean isValidColorTransfer(int colorTransfer) { private static boolean isValidColorTransfer(int colorTransfer) {
// LINT.IfChange(color_transfer) // LINT.IfChange(color_transfer)
return colorTransfer == C.COLOR_TRANSFER_SDR // C.COLOR_TRANSFER_GAMMA_2_2 & C.COLOR_TRANSFER_SRGB aren't valid because MediaCodec, and
// hence MediaFormat, do not support them.
return colorTransfer == C.COLOR_TRANSFER_LINEAR
|| colorTransfer == C.COLOR_TRANSFER_SDR
|| colorTransfer == C.COLOR_TRANSFER_ST2084 || colorTransfer == C.COLOR_TRANSFER_ST2084
|| colorTransfer == C.COLOR_TRANSFER_HLG || colorTransfer == C.COLOR_TRANSFER_HLG
|| colorTransfer == Format.NO_VALUE; || colorTransfer == Format.NO_VALUE;

View File

@ -15,8 +15,13 @@
*/ */
package androidx.media3.common.util; package androidx.media3.common.util;
import static androidx.media3.common.util.Assertions.checkNotNull;
import static androidx.media3.common.util.Assertions.checkState;
import android.os.SystemClock;
import androidx.annotation.GuardedBy; import androidx.annotation.GuardedBy;
import androidx.media3.common.C; import androidx.media3.common.C;
import java.util.concurrent.TimeoutException;
/** /**
* Adjusts and offsets sample timestamps. MPEG-2 TS timestamps scaling and adjustment is supported, * Adjusts and offsets sample timestamps. MPEG-2 TS timestamps scaling and adjustment is supported,
@ -100,21 +105,40 @@ public final class TimestampAdjuster {
* @param canInitialize Whether the caller is able to initialize the adjuster, if needed. * @param canInitialize Whether the caller is able to initialize the adjuster, if needed.
* @param nextSampleTimestampUs The desired timestamp for the next sample loaded by the calling * @param nextSampleTimestampUs The desired timestamp for the next sample loaded by the calling
* thread, in microseconds. Only used if {@code canInitialize} is {@code true}. * thread, in microseconds. Only used if {@code canInitialize} is {@code true}.
* @param timeoutMs The timeout for the thread to wait for the timestamp adjuster to initialize,
* in milliseconds. A timeout of zero is interpreted as an infinite timeout.
* @throws InterruptedException If the thread is interrupted whilst blocked waiting for * @throws InterruptedException If the thread is interrupted whilst blocked waiting for
* initialization to complete. * initialization to complete.
* @throws TimeoutException If the thread is timeout whilst blocked waiting for initialization to
* complete.
*/ */
public synchronized void sharedInitializeOrWait(boolean canInitialize, long nextSampleTimestampUs) public synchronized void sharedInitializeOrWait(
throws InterruptedException { boolean canInitialize, long nextSampleTimestampUs, long timeoutMs)
Assertions.checkState(firstSampleTimestampUs == MODE_SHARED); throws InterruptedException, TimeoutException {
if (timestampOffsetUs != C.TIME_UNSET) { checkState(firstSampleTimestampUs == MODE_SHARED);
// Already initialized. if (isInitialized()) {
return; return;
} else if (canInitialize) { } else if (canInitialize) {
this.nextSampleTimestampUs.set(nextSampleTimestampUs); this.nextSampleTimestampUs.set(nextSampleTimestampUs);
} else { } else {
// Wait for another calling thread to complete initialization. // Wait for another calling thread to complete initialization.
while (timestampOffsetUs == C.TIME_UNSET) { long totalWaitDurationMs = 0;
long remainingTimeoutMs = timeoutMs;
while (!isInitialized()) {
if (timeoutMs == 0) {
wait(); wait();
} else {
checkState(remainingTimeoutMs > 0);
long waitStartingTimeMs = SystemClock.elapsedRealtime();
wait(remainingTimeoutMs);
totalWaitDurationMs += SystemClock.elapsedRealtime() - waitStartingTimeMs;
if (totalWaitDurationMs >= timeoutMs && !isInitialized()) {
String message =
"TimestampAdjuster failed to initialize in " + timeoutMs + " milliseconds";
throw new TimeoutException(message);
}
remainingTimeoutMs = timeoutMs - totalWaitDurationMs;
}
} }
} }
} }
@ -195,10 +219,10 @@ public final class TimestampAdjuster {
if (timeUs == C.TIME_UNSET) { if (timeUs == C.TIME_UNSET) {
return C.TIME_UNSET; return C.TIME_UNSET;
} }
if (timestampOffsetUs == C.TIME_UNSET) { if (!isInitialized()) {
long desiredSampleTimestampUs = long desiredSampleTimestampUs =
firstSampleTimestampUs == MODE_SHARED firstSampleTimestampUs == MODE_SHARED
? Assertions.checkNotNull(nextSampleTimestampUs.get()) ? checkNotNull(nextSampleTimestampUs.get())
: firstSampleTimestampUs; : firstSampleTimestampUs;
timestampOffsetUs = desiredSampleTimestampUs - timeUs; timestampOffsetUs = desiredSampleTimestampUs - timeUs;
// Notify threads waiting for the timestamp offset to be determined. // Notify threads waiting for the timestamp offset to be determined.
@ -208,6 +232,11 @@ public final class TimestampAdjuster {
return timeUs + timestampOffsetUs; return timeUs + timestampOffsetUs;
} }
/** Returns whether the instance is initialized with a timestamp offset. */
public synchronized boolean isInitialized() {
return timestampOffsetUs != C.TIME_UNSET;
}
/** /**
* Converts a 90 kHz clock timestamp to a timestamp in microseconds. * Converts a 90 kHz clock timestamp to a timestamp in microseconds.
* *

View File

@ -16,6 +16,8 @@
package androidx.media3.common.util; package androidx.media3.common.util;
import static android.content.Context.UI_MODE_SERVICE; import static android.content.Context.UI_MODE_SERVICE;
import static androidx.media3.common.Player.COMMAND_PLAY_PAUSE;
import static androidx.media3.common.Player.COMMAND_PREPARE;
import static androidx.media3.common.Player.COMMAND_SEEK_BACK; import static androidx.media3.common.Player.COMMAND_SEEK_BACK;
import static androidx.media3.common.Player.COMMAND_SEEK_FORWARD; import static androidx.media3.common.Player.COMMAND_SEEK_FORWARD;
import static androidx.media3.common.Player.COMMAND_SEEK_IN_CURRENT_MEDIA_ITEM; import static androidx.media3.common.Player.COMMAND_SEEK_IN_CURRENT_MEDIA_ITEM;
@ -25,6 +27,7 @@ import static androidx.media3.common.Player.COMMAND_SEEK_TO_NEXT;
import static androidx.media3.common.Player.COMMAND_SEEK_TO_NEXT_MEDIA_ITEM; import static androidx.media3.common.Player.COMMAND_SEEK_TO_NEXT_MEDIA_ITEM;
import static androidx.media3.common.Player.COMMAND_SEEK_TO_PREVIOUS; import static androidx.media3.common.Player.COMMAND_SEEK_TO_PREVIOUS;
import static androidx.media3.common.Player.COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM; import static androidx.media3.common.Player.COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM;
import static androidx.media3.common.util.Assertions.checkArgument;
import static androidx.media3.common.util.Assertions.checkNotNull; import static androidx.media3.common.util.Assertions.checkNotNull;
import static java.lang.Math.abs; import static java.lang.Math.abs;
import static java.lang.Math.max; import static java.lang.Math.max;
@ -51,6 +54,7 @@ import android.graphics.drawable.Drawable;
import android.hardware.display.DisplayManager; import android.hardware.display.DisplayManager;
import android.media.AudioFormat; import android.media.AudioFormat;
import android.media.AudioManager; import android.media.AudioManager;
import android.media.MediaCodec;
import android.media.MediaDrm; import android.media.MediaDrm;
import android.net.Uri; import android.net.Uri;
import android.os.Build; import android.os.Build;
@ -114,6 +118,7 @@ import java.util.concurrent.CancellationException;
import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService; import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors; import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.regex.Matcher; import java.util.regex.Matcher;
import java.util.regex.Pattern; import java.util.regex.Pattern;
import java.util.zip.DataFormatException; import java.util.zip.DataFormatException;
@ -122,6 +127,7 @@ import java.util.zip.Inflater;
import org.checkerframework.checker.initialization.qual.UnknownInitialization; import org.checkerframework.checker.initialization.qual.UnknownInitialization;
import org.checkerframework.checker.nullness.compatqual.NullableType; import org.checkerframework.checker.nullness.compatqual.NullableType;
import org.checkerframework.checker.nullness.qual.EnsuresNonNull; import org.checkerframework.checker.nullness.qual.EnsuresNonNull;
import org.checkerframework.checker.nullness.qual.EnsuresNonNullIf;
import org.checkerframework.checker.nullness.qual.PolyNull; import org.checkerframework.checker.nullness.qual.PolyNull;
/** Miscellaneous utility methods. */ /** Miscellaneous utility methods. */
@ -201,6 +207,55 @@ public final class Util {
return outputStream.toByteArray(); return outputStream.toByteArray();
} }
/** Converts an integer into an equivalent byte array. */
@UnstableApi
public static byte[] toByteArray(int value) {
return new byte[] {
(byte) (value >> 24), (byte) (value >> 16), (byte) (value >> 8), (byte) value
};
}
/**
* Converts an array of integers into an equivalent byte array.
*
* <p>Each integer is converted into 4 sequential bytes.
*/
@UnstableApi
public static byte[] toByteArray(int... values) {
byte[] array = new byte[values.length * 4];
int index = 0;
for (int value : values) {
byte[] byteArray = toByteArray(value);
array[index++] = byteArray[0];
array[index++] = byteArray[1];
array[index++] = byteArray[2];
array[index++] = byteArray[3];
}
return array;
}
/** Converts a float into an equivalent byte array. */
@UnstableApi
public static byte[] toByteArray(float value) {
return toByteArray(Float.floatToIntBits(value));
}
/** Converts a byte array into a float. */
@UnstableApi
public static float toFloat(byte[] bytes) {
checkArgument(bytes.length == 4);
int intBits =
bytes[0] << 24 | (bytes[1] & 0xFF) << 16 | (bytes[2] & 0xFF) << 8 | (bytes[3] & 0xFF);
return Float.intBitsToFloat(intBits);
}
/** Converts a byte array into an integer. */
@UnstableApi
public static int toInteger(byte[] bytes) {
checkArgument(bytes.length == 4);
return bytes[0] << 24 | bytes[1] << 16 | bytes[2] << 8 | bytes[3];
}
/** /**
* Registers a {@link BroadcastReceiver} that's not intended to receive broadcasts from other * Registers a {@link BroadcastReceiver} that's not intended to receive broadcasts from other
* apps. This will be enforced by specifying {@link Context#RECEIVER_NOT_EXPORTED} if {@link * apps. This will be enforced by specifying {@link Context#RECEIVER_NOT_EXPORTED} if {@link
@ -440,7 +495,7 @@ public final class Util {
@UnstableApi @UnstableApi
@SuppressWarnings({"nullness:argument", "nullness:return"}) @SuppressWarnings({"nullness:argument", "nullness:return"})
public static <T> T[] nullSafeArrayCopy(T[] input, int length) { public static <T> T[] nullSafeArrayCopy(T[] input, int length) {
Assertions.checkArgument(length <= input.length); checkArgument(length <= input.length);
return Arrays.copyOf(input, length); return Arrays.copyOf(input, length);
} }
@ -455,8 +510,8 @@ public final class Util {
@UnstableApi @UnstableApi
@SuppressWarnings({"nullness:argument", "nullness:return"}) @SuppressWarnings({"nullness:argument", "nullness:return"})
public static <T> T[] nullSafeArrayCopyOfRange(T[] input, int from, int to) { public static <T> T[] nullSafeArrayCopyOfRange(T[] input, int from, int to) {
Assertions.checkArgument(0 <= from); checkArgument(0 <= from);
Assertions.checkArgument(to <= input.length); checkArgument(to <= input.length);
return Arrays.copyOfRange(input, from, to); return Arrays.copyOfRange(input, from, to);
} }
@ -720,6 +775,17 @@ public final class Util {
return Executors.newSingleThreadExecutor(runnable -> new Thread(runnable, threadName)); return Executors.newSingleThreadExecutor(runnable -> new Thread(runnable, threadName));
} }
/**
* Instantiates a new single threaded scheduled executor whose thread has the specified name.
*
* @param threadName The name of the thread.
* @return The executor.
*/
@UnstableApi
public static ScheduledExecutorService newSingleThreadScheduledExecutor(String threadName) {
return Executors.newSingleThreadScheduledExecutor(runnable -> new Thread(runnable, threadName));
}
/** /**
* Closes a {@link Closeable}, suppressing any {@link IOException} that may occur. Both {@link * Closes a {@link Closeable}, suppressing any {@link IOException} that may occur. Both {@link
* java.io.OutputStream} and {@link InputStream} are {@code Closeable}. * java.io.OutputStream} and {@link InputStream} are {@code Closeable}.
@ -1575,7 +1641,7 @@ public final class Util {
@UnstableApi @UnstableApi
public static int getIntegerCodeForString(String string) { public static int getIntegerCodeForString(String string) {
int length = string.length(); int length = string.length();
Assertions.checkArgument(length <= 4); checkArgument(length <= 4);
int result = 0; int result = 0;
for (int i = 0; i < length; i++) { for (int i = 0; i < length; i++) {
result <<= 8; result <<= 8;
@ -1862,6 +1928,14 @@ public final class Util {
return AudioFormat.CHANNEL_OUT_5POINT1 | AudioFormat.CHANNEL_OUT_BACK_CENTER; return AudioFormat.CHANNEL_OUT_5POINT1 | AudioFormat.CHANNEL_OUT_BACK_CENTER;
case 8: case 8:
return AudioFormat.CHANNEL_OUT_7POINT1_SURROUND; return AudioFormat.CHANNEL_OUT_7POINT1_SURROUND;
case 10:
if (Util.SDK_INT >= 32) {
return AudioFormat.CHANNEL_OUT_5POINT1POINT4;
} else {
// Before API 32, height channel masks are not available. For those 10-channel streams
// supported on the audio output devices (e.g. DTS:X P2), we use 7.1-surround instead.
return AudioFormat.CHANNEL_OUT_7POINT1_SURROUND;
}
case 12: case 12:
return AudioFormat.CHANNEL_OUT_7POINT1POINT4; return AudioFormat.CHANNEL_OUT_7POINT1POINT4;
default: default:
@ -2768,6 +2842,40 @@ public final class Util {
} }
} }
/**
* Returns the number of maximum pending output frames that are allowed on a {@link MediaCodec}
* decoder.
*/
@UnstableApi
public static int getMaxPendingFramesCountForMediaCodecDecoders(
Context context, String codecName, boolean requestedHdrToneMapping) {
if (SDK_INT < 29
|| context.getApplicationContext().getApplicationInfo().targetSdkVersion < 29) {
// Prior to API 29, decoders may drop frames to keep their output surface from growing out of
// bounds. From API 29, if the app targets API 29 or later, the {@link
// MediaFormat#KEY_ALLOW_FRAME_DROP} key prevents frame dropping even when the surface is
// full.
// Frame dropping is never desired, so a workaround is needed for older API levels.
// Allow a maximum of one frame to be pending at a time to prevent frame dropping.
// TODO(b/226330223): Investigate increasing this limit.
return 1;
}
// Limit the maximum amount of frames for all decoders. This is a tentative value that should be
// large enough to avoid significant performance degradation, but small enough to bypass decoder
// issues.
//
// TODO: b/278234847 - Evaluate whether this reduces decoder timeouts, and consider restoring
// prior higher limits as appropriate.
//
// Some OMX decoders don't correctly track their number of output buffers available, and get
// stuck if too many frames are rendered without being processed. This value is experimentally
// determined. See also
// b/213455700, b/230097284, b/229978305, and b/245491744.
//
// OMX video codecs should no longer exist from android.os.Build.DEVICE_INITIAL_SDK_INT 31+.
return 5;
}
/** /**
* Returns string representation of a {@link C.FormatSupport} flag. * Returns string representation of a {@link C.FormatSupport} flag.
* *
@ -2872,6 +2980,87 @@ public final class Util {
return Integer.toString(i, Character.MAX_RADIX); return Integer.toString(i, Character.MAX_RADIX);
} }
/**
* Returns whether a play button should be presented on a UI element for playback control. If
* {@code false}, a pause button should be shown instead.
*
* <p>Use {@link #handlePlayPauseButtonAction}, {@link #handlePlayButtonAction} or {@link
* #handlePauseButtonAction} to handle the interaction with the play or pause button UI element.
*
* @param player The {@link Player}. May be null.
*/
@EnsuresNonNullIf(result = false, expression = "#1")
public static boolean shouldShowPlayButton(@Nullable Player player) {
return player == null
|| !player.getPlayWhenReady()
|| player.getPlaybackState() == Player.STATE_IDLE
|| player.getPlaybackState() == Player.STATE_ENDED;
}
/**
* Updates the player to handle an interaction with a play button.
*
* <p>This method assumes the play button is enabled if {@link #shouldShowPlayButton} returns
* true.
*
* @param player The {@link Player}. May be null.
* @return Whether a player method was triggered to handle this action.
*/
public static boolean handlePlayButtonAction(@Nullable Player player) {
if (player == null) {
return false;
}
@Player.State int state = player.getPlaybackState();
boolean methodTriggered = false;
if (state == Player.STATE_IDLE && player.isCommandAvailable(COMMAND_PREPARE)) {
player.prepare();
methodTriggered = true;
} else if (state == Player.STATE_ENDED
&& player.isCommandAvailable(COMMAND_SEEK_TO_DEFAULT_POSITION)) {
player.seekToDefaultPosition();
methodTriggered = true;
}
if (player.isCommandAvailable(COMMAND_PLAY_PAUSE)) {
player.play();
methodTriggered = true;
}
return methodTriggered;
}
/**
* Updates the player to handle an interaction with a pause button.
*
* <p>This method assumes the pause button is enabled if {@link #shouldShowPlayButton} returns
* false.
*
* @param player The {@link Player}. May be null.
* @return Whether a player method was triggered to handle this action.
*/
public static boolean handlePauseButtonAction(@Nullable Player player) {
if (player != null && player.isCommandAvailable(COMMAND_PLAY_PAUSE)) {
player.pause();
return true;
}
return false;
}
/**
* Updates the player to handle an interaction with a play or pause button.
*
* <p>This method assumes that the UI element enables a play button if {@link
* #shouldShowPlayButton} returns true and a pause button otherwise.
*
* @param player The {@link Player}. May be null.
* @return Whether a player method was triggered to handle this action.
*/
public static boolean handlePlayPauseButtonAction(@Nullable Player player) {
if (shouldShowPlayButton(player)) {
return handlePlayButtonAction(player);
} else {
return handlePauseButtonAction(player);
}
}
@Nullable @Nullable
private static String getSystemProperty(String name) { private static String getSystemProperty(String name) {
try { try {

View File

@ -487,6 +487,65 @@ public class AdPlaybackStateTest {
assertThat(AdPlaybackState.AdGroup.CREATOR.fromBundle(adGroup.toBundle())).isEqualTo(adGroup); assertThat(AdPlaybackState.AdGroup.CREATOR.fromBundle(adGroup.toBundle())).isEqualTo(adGroup);
} }
@Test
public void withLivePostrollPlaceholderAppended_emptyAdPlaybackState_insertsPlaceholder() {
AdPlaybackState adPlaybackState =
new AdPlaybackState("adsId").withLivePostrollPlaceholderAppended();
assertThat(adPlaybackState.adGroupCount).isEqualTo(1);
assertThat(adPlaybackState.getAdGroup(/* adGroupIndex= */ 0).timeUs)
.isEqualTo(C.TIME_END_OF_SOURCE);
assertThat(adPlaybackState.getAdGroup(/* adGroupIndex= */ 0).count).isEqualTo(C.LENGTH_UNSET);
assertThat(adPlaybackState.getAdGroup(/* adGroupIndex= */ 0).isServerSideInserted).isTrue();
}
@Test
public void withLivePostrollPlaceholderAppended_withExistingAdGroups_appendsPlaceholder() {
AdPlaybackState adPlaybackState =
new AdPlaybackState("state", /* adGroupTimesUs...= */ 0L, 10_000_000L)
.withIsServerSideInserted(/* adGroupIndex= */ 0, true)
.withIsServerSideInserted(/* adGroupIndex= */ 1, true)
.withAdCount(/* adGroupIndex= */ 0, /* adCount= */ 1)
.withAdCount(/* adGroupIndex= */ 1, /* adCount= */ 1)
.withAdDurationsUs(/* adGroupIndex= */ 0, /* adDurationsUs...= */ 10_000_000L)
.withAdDurationsUs(/* adGroupIndex= */ 1, /* adDurationsUs...= */ 5_000_000L);
adPlaybackState = adPlaybackState.withLivePostrollPlaceholderAppended();
assertThat(adPlaybackState.adGroupCount).isEqualTo(3);
assertThat(adPlaybackState.getAdGroup(/* adGroupIndex= */ 2).timeUs)
.isEqualTo(C.TIME_END_OF_SOURCE);
assertThat(adPlaybackState.getAdGroup(/* adGroupIndex= */ 2).count).isEqualTo(C.LENGTH_UNSET);
assertThat(adPlaybackState.getAdGroup(/* adGroupIndex= */ 2).isServerSideInserted).isTrue();
}
@Test
public void endsWithLivePostrollPlaceHolder_withExistingAdGroups_postrollDetected() {
AdPlaybackState adPlaybackState =
new AdPlaybackState("adsId", /* adGroupTimesUs...= */ 0L, 10_000_000L)
.withIsServerSideInserted(/* adGroupIndex= */ 0, true)
.withIsServerSideInserted(/* adGroupIndex= */ 1, true)
.withAdCount(/* adGroupIndex= */ 0, /* adCount= */ 1)
.withAdCount(/* adGroupIndex= */ 1, /* adCount= */ 1)
.withAdDurationsUs(/* adGroupIndex= */ 0, /* adDurationsUs...= */ 10_000_000L)
.withAdDurationsUs(/* adGroupIndex= */ 1, /* adDurationsUs...= */ 5_000_000L);
boolean endsWithLivePostrollPlaceHolder = adPlaybackState.endsWithLivePostrollPlaceHolder();
assertThat(endsWithLivePostrollPlaceHolder).isFalse();
adPlaybackState = adPlaybackState.withLivePostrollPlaceholderAppended();
endsWithLivePostrollPlaceHolder = adPlaybackState.endsWithLivePostrollPlaceHolder();
assertThat(endsWithLivePostrollPlaceHolder).isTrue();
}
@Test
public void endsWithLivePostrollPlaceHolder_emptyAdPlaybackState_postrollNotDetected() {
assertThat(AdPlaybackState.NONE.endsWithLivePostrollPlaceHolder()).isFalse();
assertThat(new AdPlaybackState("adsId").endsWithLivePostrollPlaceHolder()).isFalse();
}
@Test @Test
public void public void
getAdGroupIndexAfterPositionUs_withClientSideInsertedAds_returnsNextAdGroupWithUnplayedAds() { getAdGroupIndexAfterPositionUs_withClientSideInsertedAds_returnsNextAdGroupWithUnplayedAds() {
@ -634,4 +693,103 @@ public class AdPlaybackStateTest {
/* positionUs= */ C.TIME_END_OF_SOURCE, /* periodDurationUs= */ 5000)) /* positionUs= */ C.TIME_END_OF_SOURCE, /* periodDurationUs= */ 5000))
.isEqualTo(C.INDEX_UNSET); .isEqualTo(C.INDEX_UNSET);
} }
@Test
public void
getAdGroupIndexAfterPositionUs_withServerSidePostrollPlaceholderForLive_placeholderAsNextAdGroupIndex() {
AdPlaybackState state =
new AdPlaybackState(/* adsId= */ new Object(), /* adGroupTimesUs...= */ 2000)
.withIsServerSideInserted(/* adGroupIndex= */ 0, /* isServerSideInserted= */ true)
.withAdCount(/* adGroupIndex= */ 0, /* adCount= */ 1)
.withPlayedAd(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0)
.withLivePostrollPlaceholderAppended();
assertThat(
state.getAdGroupIndexAfterPositionUs(
/* positionUs= */ 1999, /* periodDurationUs= */ 5000))
.isEqualTo(0);
assertThat(
state.getAdGroupIndexAfterPositionUs(
/* positionUs= */ 2000, /* periodDurationUs= */ C.TIME_UNSET))
.isEqualTo(1);
assertThat(
state.getAdGroupIndexAfterPositionUs(
/* positionUs= */ 2000, /* periodDurationUs= */ 5000))
.isEqualTo(1);
assertThat(
state.getAdGroupIndexAfterPositionUs(
/* positionUs= */ C.TIME_END_OF_SOURCE, /* periodDurationUs= */ C.TIME_UNSET))
.isEqualTo(C.INDEX_UNSET);
assertThat(
state.getAdGroupIndexAfterPositionUs(
/* positionUs= */ C.TIME_END_OF_SOURCE, /* periodDurationUs= */ 5000))
.isEqualTo(C.INDEX_UNSET);
}
@Test
public void
getAdGroupIndexForPositionUs_withServerSidePostrollPlaceholderForLive_ignoresPlaceholder() {
AdPlaybackState state =
new AdPlaybackState("adsId", /* adGroupTimesUs...= */ 0L, 5_000_000L, C.TIME_END_OF_SOURCE)
.withIsServerSideInserted(/* adGroupIndex= */ 0, /* isServerSideInserted= */ true)
.withIsServerSideInserted(/* adGroupIndex= */ 1, /* isServerSideInserted= */ true)
.withIsServerSideInserted(/* adGroupIndex= */ 2, /* isServerSideInserted= */ true)
.withAdCount(/* adGroupIndex= */ 0, /* adCount= */ 1)
.withAdCount(/* adGroupIndex= */ 1, /* adCount= */ 1)
.withPlayedAd(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0);
assertThat(
state.getAdGroupIndexForPositionUs(
/* positionUs= */ 4_999_999L, /* periodDurationUs= */ 10_000_000L))
.isEqualTo(C.INDEX_UNSET);
assertThat(
state.getAdGroupIndexForPositionUs(
/* positionUs= */ 4_999_999L, /* periodDurationUs= */ C.TIME_UNSET))
.isEqualTo(C.INDEX_UNSET);
assertThat(
state.getAdGroupIndexForPositionUs(
/* positionUs= */ 5_000_000L, /* periodDurationUs= */ 10_000_000L))
.isEqualTo(1);
assertThat(
state.getAdGroupIndexForPositionUs(
/* positionUs= */ 5_000_000L, /* periodDurationUs= */ C.TIME_UNSET))
.isEqualTo(1);
assertThat(
state.getAdGroupIndexForPositionUs(
/* positionUs= */ C.TIME_END_OF_SOURCE, /* periodDurationUs= */ 10_000_000L))
.isEqualTo(1);
assertThat(
state.getAdGroupIndexForPositionUs(
/* positionUs= */ C.TIME_END_OF_SOURCE, /* periodDurationUs= */ C.TIME_UNSET))
.isEqualTo(1);
}
@Test
public void
getAdGroupIndexForPositionUs_withOnlyServerSidePostrollPlaceholderForLive_ignoresPlaceholder() {
AdPlaybackState state =
new AdPlaybackState("adsId", /* adGroupTimesUs...= */ C.TIME_END_OF_SOURCE)
.withIsServerSideInserted(/* adGroupIndex= */ 0, /* isServerSideInserted= */ true);
assertThat(
state.getAdGroupIndexForPositionUs(
/* positionUs= */ 5_000_000L, /* periodDurationUs= */ 10_000_000L))
.isEqualTo(C.INDEX_UNSET);
assertThat(
state.getAdGroupIndexForPositionUs(
/* positionUs= */ 5_000_000L, /* periodDurationUs= */ C.TIME_UNSET))
.isEqualTo(C.INDEX_UNSET);
assertThat(
state.getAdGroupIndexForPositionUs(
/* positionUs= */ 10_000_001L, /* periodDurationUs= */ 10_000_000L))
.isEqualTo(C.INDEX_UNSET);
assertThat(
state.getAdGroupIndexForPositionUs(
/* positionUs= */ C.TIME_END_OF_SOURCE, /* periodDurationUs= */ 10_000_000L))
.isEqualTo(C.INDEX_UNSET);
assertThat(
state.getAdGroupIndexForPositionUs(
/* positionUs= */ C.TIME_END_OF_SOURCE, /* periodDurationUs= */ C.TIME_UNSET))
.isEqualTo(C.INDEX_UNSET);
}
} }

View File

@ -28,7 +28,11 @@ public class DeviceInfoTest {
@Test @Test
public void roundTripViaBundle_yieldsEqualInstance() { public void roundTripViaBundle_yieldsEqualInstance() {
DeviceInfo deviceInfo = DeviceInfo deviceInfo =
new DeviceInfo(DeviceInfo.PLAYBACK_TYPE_REMOTE, /* minVolume= */ 1, /* maxVolume= */ 9); new DeviceInfo.Builder(DeviceInfo.PLAYBACK_TYPE_REMOTE)
.setMinVolume(1)
.setMaxVolume(9)
.setRoutingControllerId("route")
.build();
assertThat(DeviceInfo.CREATOR.fromBundle(deviceInfo.toBundle())).isEqualTo(deviceInfo); assertThat(DeviceInfo.CREATOR.fromBundle(deviceInfo.toBundle())).isEqualTo(deviceInfo);
} }

View File

@ -247,6 +247,25 @@ public class MediaItemTest {
.build()); .build());
} }
@Test
public void createDrmConfigurationInstance_roundTripViaBundle_yieldsEqualInstance() {
MediaItem.DrmConfiguration drmConfiguration =
new MediaItem.DrmConfiguration.Builder(C.WIDEVINE_UUID)
.setLicenseUri(URI_STRING + "/license")
.setLicenseRequestHeaders(ImmutableMap.of("Referer", "http://www.google.com"))
.setMultiSession(true)
.setForceDefaultLicenseUri(true)
.setPlayClearContentWithoutKey(true)
.setForcedSessionTrackTypes(ImmutableList.of(C.TRACK_TYPE_AUDIO))
.setKeySetId(new byte[] {1, 2, 3})
.build();
MediaItem.DrmConfiguration drmConfigurationFromBundle =
MediaItem.DrmConfiguration.CREATOR.fromBundle(drmConfiguration.toBundle());
assertThat(drmConfigurationFromBundle).isEqualTo(drmConfiguration);
}
@Test @Test
public void builderSetCustomCacheKey_setsCustomCacheKey() { public void builderSetCustomCacheKey_setsCustomCacheKey() {
MediaItem mediaItem = MediaItem mediaItem =
@ -319,6 +338,42 @@ public class MediaItemTest {
assertThat(mediaItem.localConfiguration.subtitles).isEqualTo(subtitles); assertThat(mediaItem.localConfiguration.subtitles).isEqualTo(subtitles);
} }
@Test
public void
createDefaultSubtitleConfigurationInstance_toBundleSkipsDefaultValues_fromBundleRestoresThem() {
MediaItem.SubtitleConfiguration subtitleConfiguration =
new MediaItem.SubtitleConfiguration.Builder(Uri.parse(URI_STRING + "/en")).build();
Bundle subtitleConfigurationBundle = subtitleConfiguration.toBundle();
// Check that default values are skipped when bundling, only Uri field (="0") is present
assertThat(subtitleConfigurationBundle.keySet()).containsExactly("0");
MediaItem.SubtitleConfiguration subtitleConfigurationFromBundle =
MediaItem.SubtitleConfiguration.CREATOR.fromBundle(subtitleConfigurationBundle);
assertThat(subtitleConfigurationFromBundle).isEqualTo(subtitleConfiguration);
}
@Test
public void createSubtitleConfigurationInstance_roundTripViaBundle_yieldsEqualInstance() {
// Creates instance by setting some non-default values
MediaItem.SubtitleConfiguration subtitleConfiguration =
new MediaItem.SubtitleConfiguration.Builder(Uri.parse(URI_STRING + "/en"))
.setMimeType(MimeTypes.APPLICATION_TTML)
.setLanguage("en")
.setSelectionFlags(C.SELECTION_FLAG_FORCED)
.setRoleFlags(C.ROLE_FLAG_ALTERNATE)
.setLabel("label")
.setId("id")
.build();
MediaItem.SubtitleConfiguration subtitleConfigurationFromBundle =
MediaItem.SubtitleConfiguration.CREATOR.fromBundle(subtitleConfiguration.toBundle());
assertThat(subtitleConfigurationFromBundle).isEqualTo(subtitleConfiguration);
}
@Test @Test
public void builderSetTag_isNullByDefault() { public void builderSetTag_isNullByDefault() {
MediaItem mediaItem = new MediaItem.Builder().setUri(URI_STRING).build(); MediaItem mediaItem = new MediaItem.Builder().setUri(URI_STRING).build();
@ -538,6 +593,21 @@ public class MediaItemTest {
assertThat(mediaItem.localConfiguration.adsConfiguration.adsId).isEqualTo(adsId); assertThat(mediaItem.localConfiguration.adsConfiguration.adsId).isEqualTo(adsId);
} }
@Test
public void createAdsConfigurationInstance_roundTripViaBundle_yieldsEqualInstanceExceptAdsId() {
Uri adTagUri = Uri.parse(URI_STRING + "/ad");
MediaItem.AdsConfiguration adsConfiguration =
new MediaItem.AdsConfiguration.Builder(adTagUri)
.setAdsId("Something that will be lost")
.build();
MediaItem.AdsConfiguration adsConfigurationFromBundle =
MediaItem.AdsConfiguration.CREATOR.fromBundle(adsConfiguration.toBundle());
assertThat(adsConfigurationFromBundle.adTagUri).isEqualTo(adsConfiguration.adTagUri);
assertThat(adsConfigurationFromBundle.adsId).isNull();
}
@Test @Test
public void builderSetMediaMetadata_setsMetadata() { public void builderSetMediaMetadata_setsMetadata() {
MediaMetadata mediaMetadata = new MediaMetadata.Builder().setTitle("title").build(); MediaMetadata mediaMetadata = new MediaMetadata.Builder().setTitle("title").build();
@ -595,6 +665,68 @@ public class MediaItemTest {
assertThat(liveConfigurationFromBundle).isEqualTo(liveConfiguration); assertThat(liveConfigurationFromBundle).isEqualTo(liveConfiguration);
} }
@Test
public void
createDefaultLocalConfigurationInstance_toBundleSkipsDefaultValues_fromBundleRestoresThem() {
MediaItem mediaItem = new MediaItem.Builder().setUri(URI_STRING).build();
Bundle localConfigurationBundle = mediaItem.localConfiguration.toBundle();
// Check that default values are skipped when bundling, only Uri field (="0") is present
assertThat(localConfigurationBundle.keySet()).containsExactly("0");
MediaItem.LocalConfiguration restoredLocalConfiguration =
MediaItem.LocalConfiguration.CREATOR.fromBundle(localConfigurationBundle);
assertThat(restoredLocalConfiguration).isEqualTo(mediaItem.localConfiguration);
assertThat(restoredLocalConfiguration.streamKeys).isEmpty();
assertThat(restoredLocalConfiguration.subtitleConfigurations).isEmpty();
}
@Test
public void createLocalConfigurationInstance_roundTripViaBundle_yieldsEqualInstance() {
Map<String, String> requestHeaders = new HashMap<>();
requestHeaders.put("Referer", "http://www.google.com");
MediaItem mediaItem =
new MediaItem.Builder()
.setUri(URI_STRING)
.setMimeType(MimeTypes.APPLICATION_MP4)
.setCustomCacheKey("key")
.setSubtitleConfigurations(
ImmutableList.of(
new MediaItem.SubtitleConfiguration.Builder(Uri.parse(URI_STRING + "/en"))
.setMimeType(MimeTypes.APPLICATION_TTML)
.setLanguage("en")
.setSelectionFlags(C.SELECTION_FLAG_FORCED)
.setRoleFlags(C.ROLE_FLAG_ALTERNATE)
.setLabel("label")
.setId("id")
.build()))
.setDrmConfiguration(
new MediaItem.DrmConfiguration.Builder(C.WIDEVINE_UUID)
.setLicenseUri(Uri.parse(URI_STRING))
.setLicenseRequestHeaders(requestHeaders)
.setMultiSession(true)
.setForceDefaultLicenseUri(true)
.setPlayClearContentWithoutKey(true)
.setForcedSessionTrackTypes(ImmutableList.of(C.TRACK_TYPE_AUDIO))
.setKeySetId(new byte[] {1, 2, 3})
.build())
.setAdsConfiguration(
new MediaItem.AdsConfiguration.Builder(Uri.parse(URI_STRING)).build())
.build();
MediaItem.LocalConfiguration localConfiguration = mediaItem.localConfiguration;
MediaItem.LocalConfiguration localConfigurationFromBundle =
MediaItem.LocalConfiguration.CREATOR.fromBundle(localConfiguration.toBundle());
MediaItem.LocalConfiguration localConfigurationFromMediaItemBundle =
MediaItem.CREATOR.fromBundle(mediaItem.toBundleIncludeLocalConfiguration())
.localConfiguration;
assertThat(localConfigurationFromBundle).isEqualTo(localConfiguration);
assertThat(localConfigurationFromMediaItemBundle).isEqualTo(localConfiguration);
}
@Test @Test
public void builderSetLiveConfiguration() { public void builderSetLiveConfiguration() {
MediaItem mediaItem = MediaItem mediaItem =
@ -727,7 +859,7 @@ public class MediaItemTest {
MediaItem copy = mediaItem.buildUpon().build(); MediaItem copy = mediaItem.buildUpon().build();
assertThat(copy).isEqualTo(mediaItem); assertThat(copy).isEqualTo(mediaItem);
assertThat(copy.localConfiguration).isEqualTo(mediaItem.playbackProperties); assertThat(copy.localConfiguration).isEqualTo(mediaItem.localConfiguration);
} }
@Test @Test
@ -792,7 +924,7 @@ public class MediaItemTest {
} }
@Test @Test
public void roundTripViaBundle_withoutPlaybackProperties_yieldsEqualInstance() { public void roundTripViaBundle_withoutLocalConfiguration_yieldsEqualInstance() {
MediaItem mediaItem = MediaItem mediaItem =
new MediaItem.Builder() new MediaItem.Builder()
.setMediaId("mediaId") .setMediaId("mediaId")
@ -822,13 +954,25 @@ public class MediaItemTest {
} }
@Test @Test
public void roundTripViaBundle_withPlaybackProperties_dropsPlaybackProperties() { public void
roundTripViaDefaultBundle_mediaItemContainsLocalConfiguration_dropsLocalConfiguration() {
MediaItem mediaItem = new MediaItem.Builder().setUri(URI_STRING).build(); MediaItem mediaItem = new MediaItem.Builder().setUri(URI_STRING).build();
assertThat(mediaItem.localConfiguration).isNotNull(); assertThat(mediaItem.localConfiguration).isNotNull();
assertThat(MediaItem.CREATOR.fromBundle(mediaItem.toBundle()).localConfiguration).isNull(); assertThat(MediaItem.CREATOR.fromBundle(mediaItem.toBundle()).localConfiguration).isNull();
} }
@Test
public void
roundTripViaBundleIncludeLocalConfiguration_mediaItemContainsLocalConfiguration_restoresLocalConfiguration() {
MediaItem mediaItem = new MediaItem.Builder().setUri(URI_STRING).build();
MediaItem restoredMediaItem =
MediaItem.CREATOR.fromBundle(mediaItem.toBundleIncludeLocalConfiguration());
assertThat(mediaItem.localConfiguration).isNotNull();
assertThat(restoredMediaItem.localConfiguration).isEqualTo(mediaItem.localConfiguration);
}
@Test @Test
public void createDefaultMediaItemInstance_checksDefaultValues() { public void createDefaultMediaItemInstance_checksDefaultValues() {
MediaItem mediaItem = new MediaItem.Builder().build(); MediaItem mediaItem = new MediaItem.Builder().build();

View File

@ -30,6 +30,7 @@ public class MediaMetadataTest {
private static final String EXTRAS_KEY = "exampleKey"; private static final String EXTRAS_KEY = "exampleKey";
private static final String EXTRAS_VALUE = "exampleValue"; private static final String EXTRAS_VALUE = "exampleValue";
@SuppressWarnings("deprecation") // Testing deprecated field.
@Test @Test
public void builder_minimal_correctDefaults() { public void builder_minimal_correctDefaults() {
MediaMetadata mediaMetadata = new MediaMetadata.Builder().build(); MediaMetadata mediaMetadata = new MediaMetadata.Builder().build();
@ -134,6 +135,7 @@ public class MediaMetadataTest {
assertThat(mediaMetadataFromBundle.extras.getString(EXTRAS_KEY)).isEqualTo(EXTRAS_VALUE); assertThat(mediaMetadataFromBundle.extras.getString(EXTRAS_KEY)).isEqualTo(EXTRAS_VALUE);
} }
@SuppressWarnings("deprecation") // Testing deprecated setter.
@Test @Test
public void builderSetFolderType_toNone_setsIsBrowsableToFalse() { public void builderSetFolderType_toNone_setsIsBrowsableToFalse() {
MediaMetadata mediaMetadata = MediaMetadata mediaMetadata =
@ -142,6 +144,7 @@ public class MediaMetadataTest {
assertThat(mediaMetadata.isBrowsable).isFalse(); assertThat(mediaMetadata.isBrowsable).isFalse();
} }
@SuppressWarnings("deprecation") // Testing deprecated setter.
@Test @Test
public void builderSetFolderType_toNotNone_setsIsBrowsableToTrueAndMatchingMediaType() { public void builderSetFolderType_toNotNone_setsIsBrowsableToTrueAndMatchingMediaType() {
MediaMetadata mediaMetadata = MediaMetadata mediaMetadata =
@ -151,6 +154,7 @@ public class MediaMetadataTest {
assertThat(mediaMetadata.mediaType).isEqualTo(MediaMetadata.MEDIA_TYPE_FOLDER_PLAYLISTS); assertThat(mediaMetadata.mediaType).isEqualTo(MediaMetadata.MEDIA_TYPE_FOLDER_PLAYLISTS);
} }
@SuppressWarnings("deprecation") // Testing deprecated setter.
@Test @Test
public void public void
builderSetFolderType_toNotNoneWithManualMediaType_setsIsBrowsableToTrueAndDoesNotOverrideMediaType() { builderSetFolderType_toNotNoneWithManualMediaType_setsIsBrowsableToTrueAndDoesNotOverrideMediaType() {
@ -164,6 +168,7 @@ public class MediaMetadataTest {
assertThat(mediaMetadata.mediaType).isEqualTo(MediaMetadata.MEDIA_TYPE_FOLDER_PODCASTS); assertThat(mediaMetadata.mediaType).isEqualTo(MediaMetadata.MEDIA_TYPE_FOLDER_PODCASTS);
} }
@SuppressWarnings("deprecation") // Testing deprecated field.
@Test @Test
public void builderSetIsBrowsable_toTrueWithoutMediaType_setsFolderTypeToMixed() { public void builderSetIsBrowsable_toTrueWithoutMediaType_setsFolderTypeToMixed() {
MediaMetadata mediaMetadata = new MediaMetadata.Builder().setIsBrowsable(true).build(); MediaMetadata mediaMetadata = new MediaMetadata.Builder().setIsBrowsable(true).build();
@ -171,6 +176,7 @@ public class MediaMetadataTest {
assertThat(mediaMetadata.folderType).isEqualTo(MediaMetadata.FOLDER_TYPE_MIXED); assertThat(mediaMetadata.folderType).isEqualTo(MediaMetadata.FOLDER_TYPE_MIXED);
} }
@SuppressWarnings("deprecation") // Testing deprecated field.
@Test @Test
public void builderSetIsBrowsable_toTrueWithMediaType_setsFolderTypeToMatchMediaType() { public void builderSetIsBrowsable_toTrueWithMediaType_setsFolderTypeToMatchMediaType() {
MediaMetadata mediaMetadata = MediaMetadata mediaMetadata =
@ -182,6 +188,7 @@ public class MediaMetadataTest {
assertThat(mediaMetadata.folderType).isEqualTo(MediaMetadata.FOLDER_TYPE_ARTISTS); assertThat(mediaMetadata.folderType).isEqualTo(MediaMetadata.FOLDER_TYPE_ARTISTS);
} }
@SuppressWarnings("deprecation") // Testing deprecated field.
@Test @Test
public void builderSetFolderType_toFalse_setsFolderTypeToNone() { public void builderSetFolderType_toFalse_setsFolderTypeToNone() {
MediaMetadata mediaMetadata = new MediaMetadata.Builder().setIsBrowsable(false).build(); MediaMetadata mediaMetadata = new MediaMetadata.Builder().setIsBrowsable(false).build();
@ -189,6 +196,7 @@ public class MediaMetadataTest {
assertThat(mediaMetadata.folderType).isEqualTo(MediaMetadata.FOLDER_TYPE_NONE); assertThat(mediaMetadata.folderType).isEqualTo(MediaMetadata.FOLDER_TYPE_NONE);
} }
@SuppressWarnings("deprecation") // Setting deprecated fields.
private static MediaMetadata getFullyPopulatedMediaMetadata() { private static MediaMetadata getFullyPopulatedMediaMetadata() {
Bundle extras = new Bundle(); Bundle extras = new Bundle();
extras.putString(EXTRAS_KEY, EXTRAS_VALUE); extras.putString(EXTRAS_KEY, EXTRAS_VALUE);

View File

@ -0,0 +1,41 @@
/*
* Copyright 2023 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.common;
import static com.google.common.truth.Truth.assertThat;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import java.lang.reflect.Method;
import org.junit.Test;
import org.junit.runner.RunWith;
/** Tests for {@link Player}. */
@RunWith(AndroidJUnit4.class)
public class PlayerTest {
/**
* This test picks a method on the {@link Player} interface that is known will never be
* stabilised, and asserts that it is required to be implemented (therefore enforcing that {@link
* Player} is unstable-for-implementors). If this test fails because the {@link Player#next()}
* method is removed, it should be replaced with an equivalent unstable, unimplemented method.
*/
@Test
public void testAtLeastOneUnstableUnimplementedMethodExists() throws Exception {
Method nextMethod = Player.class.getMethod("next");
assertThat(nextMethod.isDefault()).isFalse();
}
}

View File

@ -17,6 +17,7 @@ package androidx.media3.common;
import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertThat;
import android.os.Bundle;
import android.os.Parcel; import android.os.Parcel;
import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.ext.junit.runners.AndroidJUnit4;
import org.junit.Test; import org.junit.Test;
@ -28,7 +29,8 @@ public class StreamKeyTest {
@Test @Test
public void parcelable() { public void parcelable() {
StreamKey streamKeyToParcel = new StreamKey(1, 2, 3); StreamKey streamKeyToParcel =
new StreamKey(/* periodIndex= */ 1, /* groupIndex= */ 2, /* streamIndex= */ 3);
Parcel parcel = Parcel.obtain(); Parcel parcel = Parcel.obtain();
streamKeyToParcel.writeToParcel(parcel, 0); streamKeyToParcel.writeToParcel(parcel, 0);
parcel.setDataPosition(0); parcel.setDataPosition(0);
@ -38,4 +40,36 @@ public class StreamKeyTest {
parcel.recycle(); parcel.recycle();
} }
@Test
public void roundTripViaBundle_withDefaultPeriodIndex_yieldsEqualInstance() {
StreamKey originalStreamKey = new StreamKey(/* groupIndex= */ 1, /* streamIndex= */ 2);
StreamKey streamKeyFromBundle = StreamKey.fromBundle(originalStreamKey.toBundle());
assertThat(originalStreamKey).isEqualTo(streamKeyFromBundle);
}
@Test
public void roundTripViaBundle_toBundleSkipsDefaultValues_fromBundleRestoresThem() {
StreamKey originalStreamKey = new StreamKey(/* groupIndex= */ 0, /* streamIndex= */ 0);
Bundle streamKeyBundle = originalStreamKey.toBundle();
assertThat(streamKeyBundle.keySet()).isEmpty();
StreamKey streamKeyFromBundle = StreamKey.fromBundle(streamKeyBundle);
assertThat(originalStreamKey).isEqualTo(streamKeyFromBundle);
}
@Test
public void roundTripViaBundle_yieldsEqualInstance() {
StreamKey originalStreamKey =
new StreamKey(/* periodIndex= */ 10, /* groupIndex= */ 11, /* streamIndex= */ 12);
StreamKey streamKeyFromBundle = StreamKey.fromBundle(originalStreamKey.toBundle());
assertThat(originalStreamKey).isEqualTo(streamKeyFromBundle);
}
} }

View File

@ -15,12 +15,15 @@
*/ */
package androidx.media3.common; package androidx.media3.common;
import static androidx.media3.test.utils.FakeMultiPeriodLiveTimeline.AD_PERIOD_DURATION_MS;
import static androidx.media3.test.utils.FakeMultiPeriodLiveTimeline.PERIOD_DURATION_MS;
import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertThat;
import android.os.Bundle; import android.os.Bundle;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.media3.common.MediaItem.LiveConfiguration; import androidx.media3.common.MediaItem.LiveConfiguration;
import androidx.media3.exoplayer.source.ShuffleOrder.DefaultShuffleOrder; import androidx.media3.exoplayer.source.ShuffleOrder.DefaultShuffleOrder;
import androidx.media3.test.utils.FakeMultiPeriodLiveTimeline;
import androidx.media3.test.utils.FakeTimeline; import androidx.media3.test.utils.FakeTimeline;
import androidx.media3.test.utils.FakeTimeline.TimelineWindowDefinition; import androidx.media3.test.utils.FakeTimeline.TimelineWindowDefinition;
import androidx.media3.test.utils.TimelineAsserts; import androidx.media3.test.utils.TimelineAsserts;
@ -431,6 +434,34 @@ public class TimelineTest {
/* expectedPeriod= */ period, /* actualPeriod= */ restoredPeriod); /* expectedPeriod= */ period, /* actualPeriod= */ restoredPeriod);
} }
@Test
public void periodIsLivePostrollPlaceholder_recognizesLivePostrollPlaceholder() {
FakeMultiPeriodLiveTimeline timeline =
new FakeMultiPeriodLiveTimeline(
/* availabilityStartTimeMs= */ 0,
/* liveWindowDurationUs= */ 60_000_000,
/* nowUs= */ 60_000_000,
/* adSequencePattern= */ new boolean[] {false, true, true},
/* periodDurationMsPattern= */ new long[] {
PERIOD_DURATION_MS, AD_PERIOD_DURATION_MS, AD_PERIOD_DURATION_MS
},
/* isContentTimeline= */ false,
/* populateAds= */ true,
/* playedAds= */ false);
assertThat(timeline.getPeriodCount()).isEqualTo(4);
assertThat(
timeline
.getPeriod(/* periodIndex= */ 1, new Timeline.Period())
.isLivePostrollPlaceholder(/* adGroupIndex= */ 0))
.isFalse();
assertThat(
timeline
.getPeriod(/* periodIndex= */ 1, new Timeline.Period())
.isLivePostrollPlaceholder(/* adGroupIndex= */ 1))
.isTrue();
}
@SuppressWarnings("deprecation") // Populates the deprecated window.tag property. @SuppressWarnings("deprecation") // Populates the deprecated window.tag property.
private static Timeline.Window populateWindow( private static Timeline.Window populateWindow(
@Nullable MediaItem mediaItem, @Nullable Object tag) { @Nullable MediaItem mediaItem, @Nullable Object tag) {

View File

@ -181,9 +181,9 @@ public final class TrackSelectionParametersTest {
TrackSelectionParameters parameters = TrackSelectionParameters parameters =
new TrackSelectionParameters.Builder(getApplicationContext()) new TrackSelectionParameters.Builder(getApplicationContext())
.setViewportSize( .setViewportSize(
/*viewportWidth=*/ 1, /* viewportWidth= */ 1,
/*viewportHeight=*/ 2, /* viewportHeight= */ 2,
/*viewportOrientationMayChange=*/ false) /* viewportOrientationMayChange= */ false)
.clearViewportSizeConstraints() .clearViewportSizeConstraints()
.build(); .build();

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