mirror of
https://github.com/androidx/media.git
synced 2025-04-30 06:46:50 +08:00
commit
5328d6464a
14
.github/ISSUE_TEMPLATE/bug.yml
vendored
14
.github/ISSUE_TEMPLATE/bug.yml
vendored
@ -17,10 +17,10 @@ body:
|
||||
tracker: https://github.com/google/ExoPlayer/issues?q=is%3Aissue
|
||||
- type: dropdown
|
||||
attributes:
|
||||
label: Media3 Version
|
||||
label: Version
|
||||
description: What version of Media3 (or ExoPlayer) are you using?
|
||||
options:
|
||||
- Media3 1.1.0-alpha01
|
||||
- Media3 1.1.0
|
||||
- Media3 1.0.2
|
||||
- Media3 1.0.1
|
||||
- Media3 1.0.0
|
||||
@ -33,6 +33,8 @@ body:
|
||||
- Media3 1.0.0-alpha02
|
||||
- Media3 1.0.0-alpha01
|
||||
- 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.6
|
||||
- ExoPlayer 2.18.5
|
||||
@ -54,6 +56,12 @@ body:
|
||||
- Older (unsupported)
|
||||
validations:
|
||||
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
|
||||
attributes:
|
||||
label: Devices that reproduce the issue
|
||||
@ -114,7 +122,7 @@ body:
|
||||
* Attach a file here
|
||||
* Include a media URL
|
||||
* 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.
|
||||
|
||||
For DRM-protected media please also include the scheme and license server URL.
|
||||
|
6
.github/ISSUE_TEMPLATE/question.md
vendored
6
.github/ISSUE_TEMPLATE/question.md
vendored
@ -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).
|
||||
|
||||
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
|
||||
format "Issue #1234", where #1234 is your issue number (we don't reply to
|
||||
emails).
|
||||
then email the link/bug report to android-media-github@google.com using a
|
||||
subject in the format "Issue #1234", where #1234 is your issue number (we don't
|
||||
reply to emails).
|
||||
|
10
.idea/icon.svg
generated
Normal file
10
.idea/icon.svg
generated
Normal 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 |
@ -38,6 +38,24 @@ you made on top of `main` using
|
||||
$ 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
|
||||
|
||||
Contributions to any Google project must be accompanied by a Contributor
|
||||
|
295
RELEASENOTES.md
295
RELEASENOTES.md
@ -1,5 +1,300 @@
|
||||
# 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)
|
||||
|
||||
This release corresponds to the
|
||||
|
407
api.txt
407
api.txt
@ -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_SIGNALLING = 3; // 0x3
|
||||
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_NETWORK = 2; // 0x2
|
||||
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(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 {
|
||||
}
|
||||
|
||||
@ -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_REMOTE = 1; // 0x1
|
||||
field public static final androidx.media3.common.DeviceInfo UNKNOWN;
|
||||
field public final int maxVolume;
|
||||
field public final int minVolume;
|
||||
field @IntRange(from=0) public final int maxVolume;
|
||||
field @IntRange(from=0) public final int minVolume;
|
||||
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 {
|
||||
@ -318,7 +335,7 @@ package androidx.media3.common {
|
||||
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.DrmConfiguration drmConfiguration;
|
||||
field @Nullable public final String mimeType;
|
||||
@ -369,14 +386,50 @@ package androidx.media3.common {
|
||||
public final class MediaMetadata {
|
||||
method public androidx.media3.common.MediaMetadata.Builder buildUpon();
|
||||
field public static final androidx.media3.common.MediaMetadata EMPTY;
|
||||
field public static final int FOLDER_TYPE_ALBUMS = 2; // 0x2
|
||||
field public static final int FOLDER_TYPE_ARTISTS = 3; // 0x3
|
||||
field public static final int FOLDER_TYPE_GENRES = 4; // 0x4
|
||||
field public static final int FOLDER_TYPE_MIXED = 0; // 0x0
|
||||
field public static final int FOLDER_TYPE_NONE = -1; // 0xffffffff
|
||||
field public static final int FOLDER_TYPE_PLAYLISTS = 5; // 0x5
|
||||
field 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_ALBUMS = 2; // 0x2
|
||||
field @Deprecated public static final int FOLDER_TYPE_ARTISTS = 3; // 0x3
|
||||
field @Deprecated public static final int FOLDER_TYPE_GENRES = 4; // 0x4
|
||||
field @Deprecated public static final int FOLDER_TYPE_MIXED = 0; // 0x0
|
||||
field @Deprecated public static final int FOLDER_TYPE_NONE = -1; // 0xffffffff
|
||||
field @Deprecated public static final int FOLDER_TYPE_PLAYLISTS = 5; // 0x5
|
||||
field @Deprecated public static final int FOLDER_TYPE_TITLES = 1; // 0x1
|
||||
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_A_BRIGHT_COLORED_FISH = 17; // 0x11
|
||||
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 CharSequence displayTitle;
|
||||
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 Boolean isBrowsable;
|
||||
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 Integer recordingDay;
|
||||
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 setDisplayTitle(@Nullable CharSequence);
|
||||
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 setIsBrowsable(@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 setRecordingDay(@IntRange(from=1, to=31) @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);
|
||||
}
|
||||
|
||||
@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 {
|
||||
@ -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_MPD = "application/dash+xml";
|
||||
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_SS = "application/vnd.ms-sstr+xml";
|
||||
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_WAV = "audio/wav";
|
||||
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_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_VTT = "text/vtt";
|
||||
field public static final String VIDEO_AV1 = "video/av01";
|
||||
@ -602,7 +666,7 @@ package androidx.media3.common {
|
||||
}
|
||||
|
||||
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);
|
||||
method @CheckResult public androidx.media3.common.PlaybackParameters withSpeed(@FloatRange(from=0, fromInclusive=false) float);
|
||||
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 clearVideoSurfaceView(@Nullable android.view.SurfaceView);
|
||||
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 androidx.media3.common.AudioAttributes getAudioAttributes();
|
||||
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 public boolean hasNextMediaItem();
|
||||
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 isCurrentMediaItemDynamic();
|
||||
method public boolean isCurrentMediaItemLive();
|
||||
@ -685,6 +751,8 @@ package androidx.media3.common {
|
||||
method public void removeListener(androidx.media3.common.Player.Listener);
|
||||
method public void removeMediaItem(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 seekForward();
|
||||
method public void seekTo(long);
|
||||
@ -695,8 +763,10 @@ package androidx.media3.common {
|
||||
method public void seekToNextMediaItem();
|
||||
method public void seekToPrevious();
|
||||
method public void seekToPreviousMediaItem();
|
||||
method public void setDeviceMuted(boolean);
|
||||
method public void setDeviceVolume(@IntRange(from=0) int);
|
||||
method @Deprecated public void setDeviceMuted(boolean);
|
||||
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, long);
|
||||
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 setVolume(@FloatRange(from=0, to=1.0) float);
|
||||
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_GET_AUDIO_ATTRIBUTES = 21; // 0x15
|
||||
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_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_TIMELINE = 17; // 0x11
|
||||
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_PLAY_PAUSE = 1; // 0x1
|
||||
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_FORWARD = 12; // 0xc
|
||||
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_PREVIOUS = 7; // 0x7
|
||||
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_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_SHUFFLE_MODE = 14; // 0xe
|
||||
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 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_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_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_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 REPEAT_MODE_ALL = 2; // 0x2
|
||||
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
|
||||
}
|
||||
|
||||
@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 {
|
||||
@ -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.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 {
|
||||
@ -1162,11 +1239,15 @@ package androidx.media3.common.util {
|
||||
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 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 inferContentTypeForExtension(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, 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";
|
||||
}
|
||||
|
||||
public class MediaController implements androidx.media3.common.Player {
|
||||
method public void addListener(androidx.media3.common.Player.Listener);
|
||||
method public void addMediaItem(androidx.media3.common.MediaItem);
|
||||
method public void addMediaItem(int, androidx.media3.common.MediaItem);
|
||||
method public void addMediaItems(java.util.List<androidx.media3.common.MediaItem>);
|
||||
method public void addMediaItems(int, java.util.List<androidx.media3.common.MediaItem>);
|
||||
method public boolean canAdvertiseSession();
|
||||
method public void clearMediaItems();
|
||||
method public void clearVideoSurface();
|
||||
method public void clearVideoSurface(@Nullable android.view.Surface);
|
||||
method public void clearVideoSurfaceHolder(@Nullable android.view.SurfaceHolder);
|
||||
method public void clearVideoSurfaceView(@Nullable android.view.SurfaceView);
|
||||
method public void clearVideoTextureView(@Nullable android.view.TextureView);
|
||||
method public void decreaseDeviceVolume();
|
||||
method public android.os.Looper getApplicationLooper();
|
||||
method public androidx.media3.common.AudioAttributes getAudioAttributes();
|
||||
method public androidx.media3.common.Player.Commands getAvailableCommands();
|
||||
method public androidx.media3.session.SessionCommands getAvailableSessionCommands();
|
||||
method @IntRange(from=0, to=100) public int getBufferedPercentage();
|
||||
method public long getBufferedPosition();
|
||||
method @Nullable public androidx.media3.session.SessionToken getConnectedToken();
|
||||
method public long getContentBufferedPosition();
|
||||
method public long getContentDuration();
|
||||
method public long getContentPosition();
|
||||
method public int getCurrentAdGroupIndex();
|
||||
method public int getCurrentAdIndexInAdGroup();
|
||||
method public androidx.media3.common.text.CueGroup getCurrentCues();
|
||||
method public long getCurrentLiveOffset();
|
||||
method @Nullable public androidx.media3.common.MediaItem getCurrentMediaItem();
|
||||
method public int getCurrentMediaItemIndex();
|
||||
method public int getCurrentPeriodIndex();
|
||||
method public long getCurrentPosition();
|
||||
method public androidx.media3.common.Timeline getCurrentTimeline();
|
||||
method public androidx.media3.common.Tracks getCurrentTracks();
|
||||
method public androidx.media3.common.DeviceInfo getDeviceInfo();
|
||||
method @IntRange(from=0) public int getDeviceVolume();
|
||||
method public long getDuration();
|
||||
method public long getMaxSeekToPreviousPosition();
|
||||
method public androidx.media3.common.MediaItem getMediaItemAt(int);
|
||||
method public int getMediaItemCount();
|
||||
method public androidx.media3.common.MediaMetadata getMediaMetadata();
|
||||
method public int getNextMediaItemIndex();
|
||||
method public boolean getPlayWhenReady();
|
||||
method public androidx.media3.common.PlaybackParameters getPlaybackParameters();
|
||||
method @androidx.media3.common.Player.State public int getPlaybackState();
|
||||
method @androidx.media3.common.Player.PlaybackSuppressionReason public int getPlaybackSuppressionReason();
|
||||
method @Nullable public androidx.media3.common.PlaybackException getPlayerError();
|
||||
method public androidx.media3.common.MediaMetadata getPlaylistMetadata();
|
||||
method public int getPreviousMediaItemIndex();
|
||||
method @androidx.media3.common.Player.RepeatMode public int getRepeatMode();
|
||||
method public long getSeekBackIncrement();
|
||||
method public long getSeekForwardIncrement();
|
||||
method @Nullable public android.app.PendingIntent getSessionActivity();
|
||||
method public boolean getShuffleModeEnabled();
|
||||
method public long getTotalBufferedDuration();
|
||||
method public androidx.media3.common.TrackSelectionParameters getTrackSelectionParameters();
|
||||
method public androidx.media3.common.VideoSize getVideoSize();
|
||||
method @FloatRange(from=0, to=1) public float getVolume();
|
||||
method public boolean hasNextMediaItem();
|
||||
method public boolean hasPreviousMediaItem();
|
||||
method public void increaseDeviceVolume();
|
||||
method public boolean isCommandAvailable(@androidx.media3.common.Player.Command int);
|
||||
method public boolean isConnected();
|
||||
method public boolean isCurrentMediaItemDynamic();
|
||||
method public boolean isCurrentMediaItemLive();
|
||||
method public boolean isCurrentMediaItemSeekable();
|
||||
method public boolean isDeviceMuted();
|
||||
method public boolean isLoading();
|
||||
method public boolean isPlaying();
|
||||
method public boolean isPlayingAd();
|
||||
method public boolean isSessionCommandAvailable(@androidx.media3.session.SessionCommand.CommandCode int);
|
||||
method public boolean isSessionCommandAvailable(androidx.media3.session.SessionCommand);
|
||||
method public void moveMediaItem(int, int);
|
||||
method public void moveMediaItems(int, int, int);
|
||||
method public void pause();
|
||||
method public void play();
|
||||
method public void prepare();
|
||||
method public void release();
|
||||
@com.google.errorprone.annotations.DoNotMock public class MediaController implements androidx.media3.common.Player {
|
||||
method public final void addListener(androidx.media3.common.Player.Listener);
|
||||
method public final void addMediaItem(androidx.media3.common.MediaItem);
|
||||
method public final void addMediaItem(int, androidx.media3.common.MediaItem);
|
||||
method public final void addMediaItems(java.util.List<androidx.media3.common.MediaItem>);
|
||||
method public final void addMediaItems(int, java.util.List<androidx.media3.common.MediaItem>);
|
||||
method public final boolean canAdvertiseSession();
|
||||
method public final void clearMediaItems();
|
||||
method public final void clearVideoSurface();
|
||||
method public final void clearVideoSurface(@Nullable android.view.Surface);
|
||||
method public final void clearVideoSurfaceHolder(@Nullable android.view.SurfaceHolder);
|
||||
method public final void clearVideoSurfaceView(@Nullable android.view.SurfaceView);
|
||||
method public final void clearVideoTextureView(@Nullable android.view.TextureView);
|
||||
method @Deprecated public final void decreaseDeviceVolume();
|
||||
method public final void decreaseDeviceVolume(@androidx.media3.common.C.VolumeFlags int);
|
||||
method public final android.os.Looper getApplicationLooper();
|
||||
method public final androidx.media3.common.AudioAttributes getAudioAttributes();
|
||||
method public final androidx.media3.common.Player.Commands getAvailableCommands();
|
||||
method public final androidx.media3.session.SessionCommands getAvailableSessionCommands();
|
||||
method @IntRange(from=0, to=100) public final int getBufferedPercentage();
|
||||
method public final long getBufferedPosition();
|
||||
method @Nullable public final androidx.media3.session.SessionToken getConnectedToken();
|
||||
method public final long getContentBufferedPosition();
|
||||
method public final long getContentDuration();
|
||||
method public final long getContentPosition();
|
||||
method public final int getCurrentAdGroupIndex();
|
||||
method public final int getCurrentAdIndexInAdGroup();
|
||||
method public final androidx.media3.common.text.CueGroup getCurrentCues();
|
||||
method public final long getCurrentLiveOffset();
|
||||
method @Nullable public final androidx.media3.common.MediaItem getCurrentMediaItem();
|
||||
method public final int getCurrentMediaItemIndex();
|
||||
method public final int getCurrentPeriodIndex();
|
||||
method public final long getCurrentPosition();
|
||||
method public final androidx.media3.common.Timeline getCurrentTimeline();
|
||||
method public final androidx.media3.common.Tracks getCurrentTracks();
|
||||
method public final androidx.media3.common.DeviceInfo getDeviceInfo();
|
||||
method @IntRange(from=0) public final int getDeviceVolume();
|
||||
method public final long getDuration();
|
||||
method public final long getMaxSeekToPreviousPosition();
|
||||
method public final androidx.media3.common.MediaItem getMediaItemAt(int);
|
||||
method public final int getMediaItemCount();
|
||||
method public final androidx.media3.common.MediaMetadata getMediaMetadata();
|
||||
method public final int getNextMediaItemIndex();
|
||||
method public final boolean getPlayWhenReady();
|
||||
method public final androidx.media3.common.PlaybackParameters getPlaybackParameters();
|
||||
method @androidx.media3.common.Player.State public final int getPlaybackState();
|
||||
method @androidx.media3.common.Player.PlaybackSuppressionReason public final int getPlaybackSuppressionReason();
|
||||
method @Nullable public final androidx.media3.common.PlaybackException getPlayerError();
|
||||
method public final androidx.media3.common.MediaMetadata getPlaylistMetadata();
|
||||
method public final int getPreviousMediaItemIndex();
|
||||
method @androidx.media3.common.Player.RepeatMode public final int getRepeatMode();
|
||||
method public final long getSeekBackIncrement();
|
||||
method public final long getSeekForwardIncrement();
|
||||
method @Nullable public final android.app.PendingIntent getSessionActivity();
|
||||
method public final boolean getShuffleModeEnabled();
|
||||
method public final long getTotalBufferedDuration();
|
||||
method public final androidx.media3.common.TrackSelectionParameters getTrackSelectionParameters();
|
||||
method public final androidx.media3.common.VideoSize getVideoSize();
|
||||
method @FloatRange(from=0, to=1) public final float getVolume();
|
||||
method public final boolean hasNextMediaItem();
|
||||
method public final boolean hasPreviousMediaItem();
|
||||
method @Deprecated public final void increaseDeviceVolume();
|
||||
method public final void increaseDeviceVolume(@androidx.media3.common.C.VolumeFlags int);
|
||||
method public final boolean isCommandAvailable(@androidx.media3.common.Player.Command int);
|
||||
method public final boolean isConnected();
|
||||
method public final boolean isCurrentMediaItemDynamic();
|
||||
method public final boolean isCurrentMediaItemLive();
|
||||
method public final boolean isCurrentMediaItemSeekable();
|
||||
method public final boolean isDeviceMuted();
|
||||
method public final boolean isLoading();
|
||||
method public final boolean isPlaying();
|
||||
method public final boolean isPlayingAd();
|
||||
method public final boolean isSessionCommandAvailable(@androidx.media3.session.SessionCommand.CommandCode int);
|
||||
method public final boolean isSessionCommandAvailable(androidx.media3.session.SessionCommand);
|
||||
method public final void moveMediaItem(int, int);
|
||||
method public final void moveMediaItems(int, int, int);
|
||||
method public final void pause();
|
||||
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 void removeListener(androidx.media3.common.Player.Listener);
|
||||
method public void removeMediaItem(int);
|
||||
method public void removeMediaItems(int, int);
|
||||
method public void seekBack();
|
||||
method public void seekForward();
|
||||
method public void seekTo(long);
|
||||
method public void seekTo(int, long);
|
||||
method public void seekToDefaultPosition();
|
||||
method public void seekToDefaultPosition(int);
|
||||
method public void seekToNext();
|
||||
method public void seekToNextMediaItem();
|
||||
method public void seekToPrevious();
|
||||
method public void seekToPreviousMediaItem();
|
||||
method public com.google.common.util.concurrent.ListenableFuture<androidx.media3.session.SessionResult> sendCustomCommand(androidx.media3.session.SessionCommand, android.os.Bundle);
|
||||
method public void setDeviceMuted(boolean);
|
||||
method public void setDeviceVolume(@IntRange(from=0) int);
|
||||
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, boolean);
|
||||
method public void setMediaItems(java.util.List<androidx.media3.common.MediaItem>);
|
||||
method public void setMediaItems(java.util.List<androidx.media3.common.MediaItem>, boolean);
|
||||
method public void setMediaItems(java.util.List<androidx.media3.common.MediaItem>, int, long);
|
||||
method public void setPlayWhenReady(boolean);
|
||||
method public void setPlaybackParameters(androidx.media3.common.PlaybackParameters);
|
||||
method public void setPlaybackSpeed(float);
|
||||
method public void setPlaylistMetadata(androidx.media3.common.MediaMetadata);
|
||||
method public com.google.common.util.concurrent.ListenableFuture<androidx.media3.session.SessionResult> setRating(String, androidx.media3.common.Rating);
|
||||
method public com.google.common.util.concurrent.ListenableFuture<androidx.media3.session.SessionResult> setRating(androidx.media3.common.Rating);
|
||||
method public void setRepeatMode(@androidx.media3.common.Player.RepeatMode int);
|
||||
method public void setShuffleModeEnabled(boolean);
|
||||
method public void setTrackSelectionParameters(androidx.media3.common.TrackSelectionParameters);
|
||||
method public void setVideoSurface(@Nullable android.view.Surface);
|
||||
method public void setVideoSurfaceHolder(@Nullable android.view.SurfaceHolder);
|
||||
method public void setVideoSurfaceView(@Nullable android.view.SurfaceView);
|
||||
method public void setVideoTextureView(@Nullable android.view.TextureView);
|
||||
method public void setVolume(@FloatRange(from=0, to=1) float);
|
||||
method public void stop();
|
||||
method public final void removeListener(androidx.media3.common.Player.Listener);
|
||||
method public final void removeMediaItem(int);
|
||||
method public final void removeMediaItems(int, int);
|
||||
method public final void replaceMediaItem(int, androidx.media3.common.MediaItem);
|
||||
method public final void replaceMediaItems(int, int, java.util.List<androidx.media3.common.MediaItem>);
|
||||
method public final void seekBack();
|
||||
method public final void seekForward();
|
||||
method public final void seekTo(long);
|
||||
method public final void seekTo(int, long);
|
||||
method public final void seekToDefaultPosition();
|
||||
method public final void seekToDefaultPosition(int);
|
||||
method public final void seekToNext();
|
||||
method public final void seekToNextMediaItem();
|
||||
method public final void seekToPrevious();
|
||||
method public final void seekToPreviousMediaItem();
|
||||
method public final com.google.common.util.concurrent.ListenableFuture<androidx.media3.session.SessionResult> sendCustomCommand(androidx.media3.session.SessionCommand, android.os.Bundle);
|
||||
method @Deprecated public final void setDeviceMuted(boolean);
|
||||
method public final void setDeviceMuted(boolean, @androidx.media3.common.C.VolumeFlags int);
|
||||
method @Deprecated public final void setDeviceVolume(@IntRange(from=0) int);
|
||||
method public final void setDeviceVolume(@IntRange(from=0) int, @androidx.media3.common.C.VolumeFlags int);
|
||||
method public final void setMediaItem(androidx.media3.common.MediaItem);
|
||||
method public final void setMediaItem(androidx.media3.common.MediaItem, long);
|
||||
method public final void setMediaItem(androidx.media3.common.MediaItem, boolean);
|
||||
method public final void setMediaItems(java.util.List<androidx.media3.common.MediaItem>);
|
||||
method public final void setMediaItems(java.util.List<androidx.media3.common.MediaItem>, boolean);
|
||||
method public final void setMediaItems(java.util.List<androidx.media3.common.MediaItem>, int, long);
|
||||
method public final void setPlayWhenReady(boolean);
|
||||
method public final void setPlaybackParameters(androidx.media3.common.PlaybackParameters);
|
||||
method public final void setPlaybackSpeed(float);
|
||||
method public final void setPlaylistMetadata(androidx.media3.common.MediaMetadata);
|
||||
method public final com.google.common.util.concurrent.ListenableFuture<androidx.media3.session.SessionResult> setRating(String, androidx.media3.common.Rating);
|
||||
method public final com.google.common.util.concurrent.ListenableFuture<androidx.media3.session.SessionResult> setRating(androidx.media3.common.Rating);
|
||||
method public final void setRepeatMode(@androidx.media3.common.Player.RepeatMode int);
|
||||
method public final void setShuffleModeEnabled(boolean);
|
||||
method public final void setTrackSelectionParameters(androidx.media3.common.TrackSelectionParameters);
|
||||
method public final void setVideoSurface(@Nullable android.view.Surface);
|
||||
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 {
|
||||
@ -1622,21 +1709,22 @@ package androidx.media3.session {
|
||||
field @IntRange(from=1) public final int notificationId;
|
||||
}
|
||||
|
||||
public class MediaSession {
|
||||
method public void broadcastCustomCommand(androidx.media3.session.SessionCommand, android.os.Bundle);
|
||||
method public java.util.List<androidx.media3.session.MediaSession.ControllerInfo> getConnectedControllers();
|
||||
method public String getId();
|
||||
method public androidx.media3.common.Player getPlayer();
|
||||
method @Nullable public android.app.PendingIntent getSessionActivity();
|
||||
method public androidx.media3.session.SessionToken getToken();
|
||||
method public void release();
|
||||
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 void setAvailableCommands(androidx.media3.session.MediaSession.ControllerInfo, androidx.media3.session.SessionCommands, androidx.media3.common.Player.Commands);
|
||||
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 void setCustomLayout(java.util.List<androidx.media3.session.CommandButton>);
|
||||
method public void setPlayer(androidx.media3.common.Player);
|
||||
method public void setSessionExtras(android.os.Bundle);
|
||||
method public void setSessionExtras(androidx.media3.session.MediaSession.ControllerInfo, android.os.Bundle);
|
||||
@com.google.errorprone.annotations.DoNotMock public class MediaSession {
|
||||
method public final void broadcastCustomCommand(androidx.media3.session.SessionCommand, android.os.Bundle);
|
||||
method public final java.util.List<androidx.media3.session.MediaSession.ControllerInfo> getConnectedControllers();
|
||||
method @Nullable public final androidx.media3.session.MediaSession.ControllerInfo getControllerForCurrentRequest();
|
||||
method public final String getId();
|
||||
method public final androidx.media3.common.Player getPlayer();
|
||||
method @Nullable public final android.app.PendingIntent getSessionActivity();
|
||||
method public final androidx.media3.session.SessionToken getToken();
|
||||
method public final void release();
|
||||
method public final com.google.common.util.concurrent.ListenableFuture<androidx.media3.session.SessionResult> sendCustomCommand(androidx.media3.session.MediaSession.ControllerInfo, androidx.media3.session.SessionCommand, android.os.Bundle);
|
||||
method public final 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> setCustomLayout(androidx.media3.session.MediaSession.ControllerInfo, java.util.List<androidx.media3.session.CommandButton>);
|
||||
method public final void setCustomLayout(java.util.List<androidx.media3.session.CommandButton>);
|
||||
method public final void setPlayer(androidx.media3.common.Player);
|
||||
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 {
|
||||
@ -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 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 @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 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);
|
||||
@ -1682,7 +1770,8 @@ package androidx.media3.session {
|
||||
method public final boolean isSessionAdded(androidx.media3.session.MediaSession);
|
||||
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 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);
|
||||
field public static final String SERVICE_INTERFACE = "androidx.media3.session.MediaSessionService";
|
||||
}
|
||||
|
@ -17,9 +17,9 @@ buildscript {
|
||||
mavenCentral()
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:7.2.1'
|
||||
classpath 'com.google.android.gms:strict-version-matcher-plugin:1.2.2'
|
||||
classpath 'org.jetbrains.kotlin:kotlin-gradle-plugin:1.5.21'
|
||||
classpath 'com.android.tools.build:gradle:7.2.2'
|
||||
classpath 'com.google.android.gms:strict-version-matcher-plugin:1.2.4'
|
||||
classpath 'org.jetbrains.kotlin:kotlin-gradle-plugin:1.8.20'
|
||||
}
|
||||
}
|
||||
allprojects {
|
||||
|
@ -25,9 +25,11 @@ android {
|
||||
aarMetadata {
|
||||
minCompileSdk = project.ext.compileSdkVersion
|
||||
}
|
||||
multiDexEnabled true
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
coreLibraryDesugaringEnabled true
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
@ -39,3 +41,8 @@ android {
|
||||
unitTests.includeAndroidResources true
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation 'androidx.multidex:multidex:' + androidxMultidexVersion
|
||||
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5'
|
||||
}
|
||||
|
@ -12,8 +12,8 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
project.ext {
|
||||
releaseVersion = '1.0.2'
|
||||
releaseVersionCode = 1_000_002_3_00
|
||||
releaseVersion = '1.1.0'
|
||||
releaseVersionCode = 1_001_000_3_00
|
||||
minSdkVersion = 16
|
||||
appTargetSdkVersion = 33
|
||||
// API version before restricting local file access.
|
||||
@ -27,35 +27,38 @@ project.ext {
|
||||
junitVersion = '4.13.2'
|
||||
// Use the same Guava version as the Android repo:
|
||||
// https://cs.android.com/android/platform/superproject/+/master:external/guava/METADATA
|
||||
guavaVersion = '31.0.1-android'
|
||||
guavaVersion = '31.1-android'
|
||||
mockitoVersion = '3.12.4'
|
||||
robolectricVersion = '4.8.1'
|
||||
// Keep this in sync with Google's internal Checker Framework version.
|
||||
checkerframeworkVersion = '3.13.0'
|
||||
checkerframeworkCompatVersion = '2.5.5'
|
||||
errorProneVersion = '2.10.0'
|
||||
errorProneVersion = '2.18.0'
|
||||
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'
|
||||
// Updating this to 1.3.0+ will import Kotlin stdlib [internal ref: b/277891049].
|
||||
androidxAnnotationExperimentalVersion = '1.2.0'
|
||||
androidxAppCompatVersion = '1.3.1'
|
||||
androidxCollectionVersion = '1.1.0'
|
||||
androidxConstraintLayoutVersion = '2.0.4'
|
||||
androidxCoreVersion = '1.7.0'
|
||||
androidxAppCompatVersion = '1.6.1'
|
||||
androidxCollectionVersion = '1.2.0'
|
||||
androidxConstraintLayoutVersion = '2.1.4'
|
||||
// Updating this to 1.9.0+ will import Kotlin stdlib [internal ref: b/277891049].
|
||||
androidxCoreVersion = '1.8.0'
|
||||
androidxFuturesVersion = '1.1.0'
|
||||
androidxMediaVersion = '1.6.0'
|
||||
androidxMedia2Version = '1.2.0'
|
||||
androidxMedia2Version = '1.2.1'
|
||||
androidxMultidexVersion = '2.0.1'
|
||||
androidxRecyclerViewVersion = '1.2.1'
|
||||
androidxMaterialVersion = '1.4.0'
|
||||
androidxTestCoreVersion = '1.4.0'
|
||||
androidxTestJUnitVersion = '1.1.3'
|
||||
androidxTestRunnerVersion = '1.4.0'
|
||||
androidxTestRulesVersion = '1.4.0'
|
||||
androidxTestServicesStorageVersion = '1.4.0'
|
||||
androidxTestTruthVersion = '1.4.0'
|
||||
androidxRecyclerViewVersion = '1.3.0'
|
||||
androidxMaterialVersion = '1.8.0'
|
||||
androidxTestCoreVersion = '1.5.0'
|
||||
androidxTestJUnitVersion = '1.1.5'
|
||||
androidxTestRunnerVersion = '1.5.2'
|
||||
androidxTestRulesVersion = '1.5.0'
|
||||
androidxTestServicesStorageVersion = '1.4.2'
|
||||
androidxTestTruthVersion = '1.5.0'
|
||||
truthVersion = '1.1.3'
|
||||
okhttpVersion = '4.9.2'
|
||||
okhttpVersion = '4.11.0'
|
||||
modulePrefix = ':'
|
||||
if (gradle.ext.has('androidxMediaModulePrefix')) {
|
||||
modulePrefix += gradle.ext.androidxMediaModulePrefix
|
||||
|
@ -21,11 +21,12 @@ if (gradle.ext.has('androidxMediaModulePrefix')) {
|
||||
modulePrefix += gradle.ext.androidxMediaModulePrefix
|
||||
}
|
||||
|
||||
rootProject.name = gradle.ext.androidxMediaProjectName
|
||||
|
||||
include modulePrefix + 'lib-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'
|
||||
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'
|
||||
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'
|
||||
project(modulePrefix + 'lib-transformer').projectDir = new File(rootDir, 'libraries/transformer')
|
||||
|
||||
|
@ -1,7 +1,116 @@
|
||||
# Cast demo
|
||||
|
||||
This app demonstrates integration with Google Cast, as well as switching between
|
||||
Google Cast and local playback using ExoPlayer.
|
||||
This app demonstrates switching between Google Cast and local playback by using
|
||||
`CastPlayer` and `ExoPlayer`.
|
||||
|
||||
## Building the demo app
|
||||
|
||||
See the [demos README](../README.md) for instructions on how to build and run
|
||||
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.
|
||||
|
@ -14,6 +14,7 @@
|
||||
|
||||
apply from: '../../constants.gradle'
|
||||
apply plugin: 'com.android.application'
|
||||
apply plugin: 'kotlin-android'
|
||||
|
||||
android {
|
||||
compileSdkVersion project.ext.compileSdkVersion
|
||||
|
@ -52,6 +52,7 @@
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<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.BROWSABLE"/>
|
||||
<data android:scheme="http"/>
|
||||
|
@ -406,6 +406,18 @@
|
||||
"name": "DASH VOD: Tears of Steel (11 periods, pre/mid/post), 2/5/2 ads [5/10s]",
|
||||
"uri": "ssai://dai.google.com/?contentSourceId=2559737&videoId=tos-dash&format=0&adsId=1"
|
||||
},
|
||||
{
|
||||
"name": "DASH live: Tears of Steel (mid), 3 ads each [10 s]",
|
||||
"uri": "ssai://dai.google.com/?assetKey=jNVjPZwzSkyeGiaNQTPqiQ&format=0&adsId=1"
|
||||
},
|
||||
{
|
||||
"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",
|
||||
"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",
|
||||
"playlist": [
|
||||
@ -494,7 +492,7 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Audio -> Video -> Audio",
|
||||
"name": "Audio -> Video (MKV) -> Video (MKV) -> Audio -> Video (MKV) -> Video (DASH) -> Audio",
|
||||
"playlist": [
|
||||
{
|
||||
"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/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"
|
||||
}
|
||||
|
@ -94,7 +94,7 @@ public class IntentUtil {
|
||||
if (mediaItem.mediaMetadata.title != null) {
|
||||
intent.putExtra(TITLE_EXTRA, mediaItem.mediaMetadata.title);
|
||||
}
|
||||
addPlaybackPropertiesToIntent(localConfiguration, intent, /* extrasKeySuffix= */ "");
|
||||
addLocalConfigurationToIntent(localConfiguration, intent, /* extrasKeySuffix= */ "");
|
||||
addClippingConfigurationToIntent(
|
||||
mediaItem.clippingConfiguration, intent, /* extrasKeySuffix= */ "");
|
||||
} else {
|
||||
@ -104,7 +104,7 @@ public class IntentUtil {
|
||||
MediaItem.LocalConfiguration localConfiguration =
|
||||
checkNotNull(mediaItem.localConfiguration);
|
||||
intent.putExtra(URI_EXTRA + ("_" + i), localConfiguration.uri.toString());
|
||||
addPlaybackPropertiesToIntent(localConfiguration, intent, /* extrasKeySuffix= */ "_" + i);
|
||||
addLocalConfigurationToIntent(localConfiguration, intent, /* extrasKeySuffix= */ "_" + i);
|
||||
addClippingConfigurationToIntent(
|
||||
mediaItem.clippingConfiguration, intent, /* extrasKeySuffix= */ "_" + i);
|
||||
if (mediaItem.mediaMetadata.title != null) {
|
||||
@ -195,7 +195,7 @@ public class IntentUtil {
|
||||
return builder;
|
||||
}
|
||||
|
||||
private static void addPlaybackPropertiesToIntent(
|
||||
private static void addLocalConfigurationToIntent(
|
||||
MediaItem.LocalConfiguration localConfiguration, Intent intent, String extrasKeySuffix) {
|
||||
intent
|
||||
.putExtra(MIME_TYPE_EXTRA + extrasKeySuffix, localConfiguration.mimeType)
|
||||
|
@ -57,6 +57,8 @@ android {
|
||||
}
|
||||
|
||||
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.appcompat:appcompat:' + androidxAppCompatVersion
|
||||
implementation 'androidx.multidex:multidex:' + androidxMultidexVersion
|
||||
|
@ -40,7 +40,9 @@
|
||||
|
||||
<activity
|
||||
android:name=".PlayerActivity"
|
||||
android:exported="true"/>
|
||||
android:exported="true"
|
||||
android:launchMode="singleTop"
|
||||
android:theme="@style/Theme.AppCompat.NoActionBar"/>
|
||||
|
||||
<activity
|
||||
android:name=".PlayableFolderActivity"
|
||||
|
@ -501,6 +501,94 @@
|
||||
"totalTrackCount": 2,
|
||||
"duration": 160,
|
||||
"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"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -17,7 +17,6 @@ package androidx.media3.demo.session
|
||||
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.MenuItem
|
||||
@ -70,13 +69,13 @@ class MainActivity : AppCompatActivity() {
|
||||
|
||||
findViewById<ExtendedFloatingActionButton>(R.id.open_player_floating_button)
|
||||
.setOnClickListener {
|
||||
// display the playing media items
|
||||
val intent = Intent(this, PlayerActivity::class.java)
|
||||
startActivity(intent)
|
||||
// Start the session activity that shows the playback activity. The System UI uses the same
|
||||
// intent in the same way to start the activity from the notification.
|
||||
browser?.sessionActivity?.send()
|
||||
}
|
||||
|
||||
onBackPressedDispatcher.addCallback(
|
||||
object : OnBackPressedCallback(/* enabled= */ true) {
|
||||
object : OnBackPressedCallback(true) {
|
||||
override fun handleOnBackPressed() {
|
||||
popPathStack()
|
||||
}
|
||||
|
@ -20,8 +20,8 @@ import android.net.Uri
|
||||
import androidx.media3.common.MediaItem
|
||||
import androidx.media3.common.MediaItem.SubtitleConfiguration
|
||||
import androidx.media3.common.MediaMetadata
|
||||
import androidx.media3.common.util.Util
|
||||
import com.google.common.collect.ImmutableList
|
||||
import java.io.BufferedReader
|
||||
import org.json.JSONObject
|
||||
|
||||
/**
|
||||
@ -91,10 +91,8 @@ object MediaItemTree {
|
||||
.build()
|
||||
}
|
||||
|
||||
private fun loadJSONFromAsset(assets: AssetManager): String {
|
||||
val buffer = assets.open("catalog.json").use { Util.toByteArray(it) }
|
||||
return String(buffer, Charsets.UTF_8)
|
||||
}
|
||||
private fun loadJSONFromAsset(assets: AssetManager): String =
|
||||
assets.open("catalog.json").bufferedReader().use(BufferedReader::readText)
|
||||
|
||||
fun initialize(assets: AssetManager) {
|
||||
if (isInitialized) return
|
||||
|
@ -51,6 +51,7 @@ class PlayableFolderActivity : AppCompatActivity() {
|
||||
|
||||
companion object {
|
||||
private const val MEDIA_ITEM_ID_KEY = "MEDIA_ITEM_ID_KEY"
|
||||
|
||||
fun createIntent(context: Context, mediaItemID: String): Intent {
|
||||
val intent = Intent(context, PlayableFolderActivity::class.java)
|
||||
intent.putExtra(MEDIA_ITEM_ID_KEY, mediaItemID)
|
||||
@ -77,8 +78,7 @@ class PlayableFolderActivity : AppCompatActivity() {
|
||||
browser.shuffleModeEnabled = false
|
||||
browser.prepare()
|
||||
browser.play()
|
||||
val intent = Intent(this, PlayerActivity::class.java)
|
||||
startActivity(intent)
|
||||
browser.sessionActivity?.send()
|
||||
}
|
||||
}
|
||||
|
||||
@ -88,8 +88,7 @@ class PlayableFolderActivity : AppCompatActivity() {
|
||||
browser.shuffleModeEnabled = true
|
||||
browser.prepare()
|
||||
browser.play()
|
||||
val intent = Intent(this, PlayerActivity::class.java)
|
||||
startActivity(intent)
|
||||
browser?.sessionActivity?.send()
|
||||
}
|
||||
|
||||
findViewById<Button>(R.id.play_button).setOnClickListener {
|
||||
@ -104,9 +103,9 @@ class PlayableFolderActivity : AppCompatActivity() {
|
||||
|
||||
findViewById<ExtendedFloatingActionButton>(R.id.open_player_floating_button)
|
||||
.setOnClickListener {
|
||||
// display the playing media items
|
||||
val intent = Intent(this, PlayerActivity::class.java)
|
||||
startActivity(intent)
|
||||
// Start the session activity that shows the playback activity. The System UI uses the same
|
||||
// intent in the same way to start the activity from the notification.
|
||||
browser?.sessionActivity?.send()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -18,6 +18,7 @@ package androidx.media3.demo.session
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.NotificationChannel
|
||||
import android.app.NotificationManager
|
||||
import android.app.PendingIntent
|
||||
import android.app.PendingIntent.*
|
||||
import android.app.TaskStackBuilder
|
||||
import android.content.Intent
|
||||
@ -28,6 +29,7 @@ import androidx.core.app.NotificationManagerCompat
|
||||
import androidx.media3.common.AudioAttributes
|
||||
import androidx.media3.common.MediaItem
|
||||
import androidx.media3.common.util.Util
|
||||
import androidx.media3.datasource.DataSourceBitmapLoader
|
||||
import androidx.media3.exoplayer.ExoPlayer
|
||||
import androidx.media3.session.*
|
||||
import androidx.media3.session.LibraryResult.RESULT_ERROR_NOT_SUPPORTED
|
||||
@ -54,6 +56,7 @@ class PlaybackService : MediaLibraryService() {
|
||||
"android.media3.session.demo.SHUFFLE_OFF"
|
||||
private const val NOTIFICATION_ID = 123
|
||||
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() {
|
||||
@ -77,14 +80,15 @@ class PlaybackService : MediaLibraryService() {
|
||||
}
|
||||
|
||||
override fun onTaskRemoved(rootIntent: Intent?) {
|
||||
if (!player.playWhenReady) {
|
||||
if (!player.playWhenReady || player.mediaItemCount == 0) {
|
||||
stopSelf()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
player.release()
|
||||
mediaLibrarySession.setSessionActivity(getBackStackedActivity())
|
||||
mediaLibrarySession.release()
|
||||
player.release()
|
||||
clearListener()
|
||||
super.onDestroy()
|
||||
}
|
||||
@ -234,18 +238,10 @@ class PlaybackService : MediaLibraryService() {
|
||||
.build()
|
||||
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.Builder(this, player, librarySessionCallback)
|
||||
.setSessionActivity(sessionActivityPendingIntent)
|
||||
.setSessionActivity(getSingleTopActivity())
|
||||
.setBitmapLoader(CacheBitmapLoader(DataSourceBitmapLoader(/* context= */ this)))
|
||||
.build()
|
||||
if (!customLayout.isEmpty()) {
|
||||
// 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 {
|
||||
val isOn = sessionCommand.customAction == CUSTOM_COMMAND_TOGGLE_SHUFFLE_MODE_ON
|
||||
return CommandButton.Builder()
|
||||
@ -285,8 +298,6 @@ class PlaybackService : MediaLibraryService() {
|
||||
val pendingIntent =
|
||||
TaskStackBuilder.create(this@PlaybackService).run {
|
||||
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)
|
||||
}
|
||||
val builder =
|
||||
|
@ -19,7 +19,6 @@ import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ArrayAdapter
|
||||
@ -46,41 +45,33 @@ class PlayerActivity : AppCompatActivity() {
|
||||
get() = if (controllerFuture.isDone) controllerFuture.get() else null
|
||||
|
||||
private lateinit var playerView: PlayerView
|
||||
private lateinit var mediaList: ListView
|
||||
private lateinit var mediaListAdapter: PlayingMediaItemArrayAdapter
|
||||
private val subItemMediaList: MutableList<MediaItem> = mutableListOf()
|
||||
private lateinit var mediaItemListView: ListView
|
||||
private lateinit var mediaItemListAdapter: MediaItemListAdapter
|
||||
private val mediaItemList: MutableList<MediaItem> = mutableListOf()
|
||||
private var lastMediaItemId: String? = null
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.activity_player)
|
||||
playerView = findViewById(R.id.player_view)
|
||||
|
||||
mediaList = findViewById(R.id.current_playing_list)
|
||||
mediaListAdapter = PlayingMediaItemArrayAdapter(this, R.layout.folder_items, subItemMediaList)
|
||||
mediaList.adapter = mediaListAdapter
|
||||
mediaList.setOnItemClickListener { _, _, position, _ ->
|
||||
mediaItemListView = findViewById(R.id.current_playing_list)
|
||||
mediaItemListAdapter = MediaItemListAdapter(this, R.layout.folder_items, mediaItemList)
|
||||
mediaItemListView.adapter = mediaItemListAdapter
|
||||
mediaItemListView.setOnItemClickListener { _, _, position, _ ->
|
||||
run {
|
||||
val controller = this.controller ?: return@run
|
||||
controller.seekToDefaultPosition(/* windowIndex= */ position)
|
||||
mediaListAdapter.notifyDataSetChanged()
|
||||
if (controller.currentMediaItemIndex == position) {
|
||||
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() {
|
||||
@ -94,14 +85,6 @@ class PlayerActivity : AppCompatActivity() {
|
||||
releaseController()
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
if (item.itemId == android.R.id.home) {
|
||||
onBackPressed()
|
||||
return true
|
||||
}
|
||||
return super.onOptionsItemSelected(item)
|
||||
}
|
||||
|
||||
private fun initializeController() {
|
||||
controllerFuture =
|
||||
MediaController.Builder(
|
||||
@ -123,8 +106,6 @@ class PlayerActivity : AppCompatActivity() {
|
||||
|
||||
updateCurrentPlaylistUI()
|
||||
updateMediaMetadataUI(controller.mediaMetadata)
|
||||
updateShuffleSwitchUI(controller.shuffleModeEnabled)
|
||||
updateRepeatSwitchUI(controller.repeatMode)
|
||||
playerView.setShowSubtitleButton(controller.currentTracks.isTypeSupported(TRACK_TYPE_TEXT))
|
||||
|
||||
controller.addListener(
|
||||
@ -133,14 +114,6 @@ class PlayerActivity : AppCompatActivity() {
|
||||
updateMediaMetadataUI(mediaItem?.mediaMetadata ?: MediaMetadata.EMPTY)
|
||||
}
|
||||
|
||||
override fun onShuffleModeEnabledChanged(shuffleModeEnabled: Boolean) {
|
||||
updateShuffleSwitchUI(shuffleModeEnabled)
|
||||
}
|
||||
|
||||
override fun onRepeatModeChanged(repeatMode: Int) {
|
||||
updateRepeatSwitchUI(repeatMode)
|
||||
}
|
||||
|
||||
override fun onTracksChanged(tracks: Tracks) {
|
||||
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) {
|
||||
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.video_album).text = mediaMetadata.albumTitle
|
||||
findViewById<TextView>(R.id.video_artist).text = mediaMetadata.artist
|
||||
findViewById<TextView>(R.id.video_genre).text = mediaMetadata.genre
|
||||
findViewById<TextView>(R.id.media_title).text = title
|
||||
findViewById<TextView>(R.id.media_artist).text = mediaMetadata.artist
|
||||
|
||||
// Trick to update playlist UI
|
||||
mediaListAdapter.notifyDataSetChanged()
|
||||
mediaItemListAdapter.notifyDataSetChanged()
|
||||
}
|
||||
|
||||
private fun updateCurrentPlaylistUI() {
|
||||
val controller = this.controller ?: return
|
||||
subItemMediaList.clear()
|
||||
mediaItemList.clear()
|
||||
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,
|
||||
viewID: Int,
|
||||
mediaItemList: List<MediaItem>
|
||||
@ -201,23 +152,31 @@ class PlayerActivity : AppCompatActivity() {
|
||||
|
||||
returnConvertView.findViewById<TextView>(R.id.media_item).text = mediaItem.mediaMetadata.title
|
||||
|
||||
val deleteButton = returnConvertView.findViewById<Button>(R.id.delete_button)
|
||||
if (position == controller?.currentMediaItemIndex) {
|
||||
returnConvertView.setBackgroundColor(ContextCompat.getColor(context, R.color.white))
|
||||
returnConvertView
|
||||
.findViewById<TextView>(R.id.media_item)
|
||||
.setTextColor(ContextCompat.getColor(context, R.color.black))
|
||||
} else {
|
||||
returnConvertView.setBackgroundColor(ContextCompat.getColor(context, R.color.black))
|
||||
// Styles for the current media item list item.
|
||||
returnConvertView.setBackgroundColor(
|
||||
ContextCompat.getColor(context, R.color.playlist_item_background)
|
||||
)
|
||||
returnConvertView
|
||||
.findViewById<TextView>(R.id.media_item)
|
||||
.setTextColor(ContextCompat.getColor(context, R.color.white))
|
||||
}
|
||||
|
||||
returnConvertView.findViewById<Button>(R.id.delete_button).setOnClickListener {
|
||||
deleteButton.visibility = View.GONE
|
||||
} else {
|
||||
// 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
|
||||
controller.removeMediaItem(position)
|
||||
updateCurrentPlaylistUI()
|
||||
}
|
||||
}
|
||||
|
||||
return returnConvertView
|
||||
}
|
||||
|
BIN
demos/session/src/main/res/drawable/artwork_placeholder.png
Normal file
BIN
demos/session/src/main/res/drawable/artwork_placeholder.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 40 KiB |
@ -19,7 +19,7 @@
|
||||
android:orientation="vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@color/black"
|
||||
android:background="@color/player_background"
|
||||
tools:context=".PlayerActivity">
|
||||
|
||||
<androidx.media3.ui.AspectRatioFrameLayout
|
||||
@ -28,77 +28,48 @@
|
||||
>
|
||||
<androidx.media3.ui.PlayerView
|
||||
android:id="@+id/player_view"
|
||||
android:background="@color/player_background"
|
||||
android:layout_width="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>
|
||||
|
||||
|
||||
<TextView
|
||||
android:id="@+id/video_title"
|
||||
android:id="@+id/media_artist"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
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:paddingStart="10dp"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="20sp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/video_album"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
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>
|
||||
<View
|
||||
android:background="@color/divider"
|
||||
android:layout_height="1dp"
|
||||
android:layout_width="match_parent" />
|
||||
|
||||
<ListView
|
||||
android:id="@+id/current_playing_list"
|
||||
android:layout_width="match_parent"
|
||||
android:divider="@drawable/divider"
|
||||
android:dividerHeight="1px"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
</LinearLayout>
|
||||
|
@ -25,6 +25,8 @@
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center_vertical"
|
||||
android:paddingStart="10dp"
|
||||
android:maxLines="1"
|
||||
android:ellipsize="end"
|
||||
android:textColor="@color/white"
|
||||
android:paddingEnd="10dp"
|
||||
android:minHeight="50dp" />
|
||||
@ -35,6 +37,7 @@
|
||||
android:layout_width="50dp"
|
||||
android:layout_height="match_parent"
|
||||
android:id="@+id/delete_button"
|
||||
android:backgroundTint="@color/playlist_item_foreground"
|
||||
android:background="@drawable/baseline_playlist_remove_white_48"
|
||||
/>
|
||||
|
||||
|
@ -22,4 +22,9 @@
|
||||
<color name="black">#FF000000</color>
|
||||
<color name="white">#FFFFFFFF</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>
|
||||
|
@ -22,6 +22,7 @@
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
<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_MEDIA_VIDEO"/>
|
||||
|
||||
<application
|
||||
android:allowBackup="false"
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
@ -15,20 +15,24 @@
|
||||
*/
|
||||
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.checkState;
|
||||
import static androidx.media3.common.util.Util.SDK_INT;
|
||||
|
||||
import android.Manifest;
|
||||
import android.app.Activity;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.graphics.Color;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.view.View;
|
||||
import android.widget.ArrayAdapter;
|
||||
import android.widget.Button;
|
||||
import android.widget.CheckBox;
|
||||
import android.widget.EditText;
|
||||
import android.widget.Spinner;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
@ -41,22 +45,24 @@ import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.core.app.ActivityCompat;
|
||||
import androidx.media3.common.C;
|
||||
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.Slider;
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
||||
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}.
|
||||
*/
|
||||
public final class ConfigurationActivity extends AppCompatActivity {
|
||||
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_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 VIDEO_MIME_TYPE = "video_mime_type";
|
||||
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 ENABLE_FALLBACK = "enable_fallback";
|
||||
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 FORCE_INTERPRET_HDR_VIDEO_AS_SDR = "force_interpret_hdr_video_as_sdr";
|
||||
public static final String ENABLE_HDR_EDITING = "enable_hdr_editing";
|
||||
public static final String DEMO_EFFECTS_SELECTIONS = "demo_effects_selections";
|
||||
public static final String ABORT_SLOW_EXPORT = "abort_slow_export";
|
||||
public static final String HDR_MODE = "hdr_mode";
|
||||
public static final String AUDIO_EFFECTS_SELECTIONS = "audio_effects_selections";
|
||||
public static final String VIDEO_EFFECTS_SELECTIONS = "video_effects_selections";
|
||||
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_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_SATURATION = "hsl_adjustments_saturation";
|
||||
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_INVERTED = 1;
|
||||
public static final int COLOR_FILTER_SEPIA = 2;
|
||||
|
||||
public static final int FILE_PERMISSION_REQUEST_CODE = 1;
|
||||
private static final String[] PRESET_FILE_URIS = {
|
||||
"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-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/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/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/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
|
||||
"720p H264 video and AAC audio",
|
||||
@ -111,13 +149,20 @@ public final class ConfigurationActivity extends AppCompatActivity {
|
||||
"8k H265 video and AAC audio",
|
||||
"Short 1080p H265 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, 90\u00B0)",
|
||||
"H264 video and AAC audio (portrait, H > W, 0°)",
|
||||
"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",
|
||||
"480p DASH (non-square pixels)",
|
||||
"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",
|
||||
"Edge detector (Media Pipe)",
|
||||
"Color filters",
|
||||
@ -127,24 +172,52 @@ public final class ConfigurationActivity extends AppCompatActivity {
|
||||
"Contrast",
|
||||
"Periodic vignette",
|
||||
"3D spin",
|
||||
"Overlay logo & timer",
|
||||
"Zoom in start",
|
||||
"Overlay logo & timer",
|
||||
"Custom Bitmap Overlay",
|
||||
"Custom Text Overlay",
|
||||
};
|
||||
private static final int COLOR_FILTERS_INDEX = 2;
|
||||
private static final int RGB_ADJUSTMENTS_INDEX = 4;
|
||||
private static final int HSL_ADJUSTMENT_INDEX = 5;
|
||||
private static final int CONTRAST_INDEX = 6;
|
||||
private static final int PERIODIC_VIGNETTE_INDEX = 7;
|
||||
private static final ImmutableMap<String, @TransformationRequest.HdrMode Integer>
|
||||
HDR_MODE_DESCRIPTIONS =
|
||||
new ImmutableMap.Builder<String, @TransformationRequest.HdrMode Integer>()
|
||||
.put("Keep HDR", TransformationRequest.HDR_MODE_KEEP_HDR)
|
||||
.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 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 selectLocalFileButton;
|
||||
private @MonotonicNonNull TextView selectedFileTextView;
|
||||
private @MonotonicNonNull CheckBox removeAudioCheckbox;
|
||||
private @MonotonicNonNull CheckBox removeVideoCheckbox;
|
||||
private @MonotonicNonNull CheckBox flattenForSlowMotionCheckbox;
|
||||
private @MonotonicNonNull CheckBox forceAudioTrackCheckbox;
|
||||
private @MonotonicNonNull Spinner audioMimeSpinner;
|
||||
private @MonotonicNonNull Spinner videoMimeSpinner;
|
||||
private @MonotonicNonNull Spinner resolutionHeightSpinner;
|
||||
@ -153,11 +226,12 @@ public final class ConfigurationActivity extends AppCompatActivity {
|
||||
private @MonotonicNonNull CheckBox trimCheckBox;
|
||||
private @MonotonicNonNull CheckBox enableFallbackCheckBox;
|
||||
private @MonotonicNonNull CheckBox enableDebugPreviewCheckBox;
|
||||
private @MonotonicNonNull CheckBox enableRequestSdrToneMappingCheckBox;
|
||||
private @MonotonicNonNull CheckBox forceInterpretHdrVideoAsSdrCheckBox;
|
||||
private @MonotonicNonNull CheckBox enableHdrEditingCheckBox;
|
||||
private @MonotonicNonNull Button selectDemoEffectsButton;
|
||||
private boolean @MonotonicNonNull [] demoEffectsSelections;
|
||||
private @MonotonicNonNull CheckBox abortSlowExportCheckBox;
|
||||
private @MonotonicNonNull Spinner hdrModeSpinner;
|
||||
private @MonotonicNonNull Button selectAudioEffectsButton;
|
||||
private @MonotonicNonNull Button selectVideoEffectsButton;
|
||||
private boolean @MonotonicNonNull [] audioEffectsSelections;
|
||||
private boolean @MonotonicNonNull [] videoEffectsSelections;
|
||||
private @Nullable Uri localFileUri;
|
||||
private int inputUriPosition;
|
||||
private long trimStartMs;
|
||||
@ -174,15 +248,36 @@ public final class ConfigurationActivity extends AppCompatActivity {
|
||||
private float periodicVignetteCenterY;
|
||||
private float periodicVignetteInnerRadius;
|
||||
private float periodicVignetteOuterRadius;
|
||||
private @MonotonicNonNull String bitmapOverlayUri;
|
||||
private float bitmapOverlayAlpha;
|
||||
private @MonotonicNonNull String textOverlayText;
|
||||
private int textOverlayTextColor;
|
||||
private float textOverlayAlpha;
|
||||
|
||||
@Override
|
||||
protected void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
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.setText(PRESET_FILE_URI_DESCRIPTIONS[inputUriPosition]);
|
||||
@ -193,11 +288,9 @@ public final class ConfigurationActivity extends AppCompatActivity {
|
||||
removeVideoCheckbox = findViewById(R.id.remove_video_checkbox);
|
||||
removeVideoCheckbox.setOnClickListener(this::onRemoveVideo);
|
||||
|
||||
selectPresetFileButton = findViewById(R.id.select_preset_file_button);
|
||||
selectPresetFileButton.setOnClickListener(this::selectPresetFile);
|
||||
flattenForSlowMotionCheckbox = findViewById(R.id.flatten_for_slow_motion_checkbox);
|
||||
|
||||
selectLocalFileButton = findViewById(R.id.select_local_file_button);
|
||||
selectLocalFileButton.setOnClickListener(this::selectLocalFile);
|
||||
forceAudioTrackCheckbox = findViewById(R.id.force_audio_track_checkbox);
|
||||
|
||||
ArrayAdapter<String> audioMimeAdapter =
|
||||
new ArrayAdapter<>(/* context= */ this, R.layout.spinner_item);
|
||||
@ -214,7 +307,7 @@ public final class ConfigurationActivity extends AppCompatActivity {
|
||||
videoMimeSpinner.setAdapter(videoMimeAdapter);
|
||||
videoMimeAdapter.addAll(
|
||||
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);
|
||||
}
|
||||
|
||||
@ -247,21 +340,23 @@ public final class ConfigurationActivity extends AppCompatActivity {
|
||||
|
||||
enableFallbackCheckBox = findViewById(R.id.enable_fallback_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];
|
||||
selectDemoEffectsButton = findViewById(R.id.select_demo_effects_button);
|
||||
selectDemoEffectsButton.setOnClickListener(this::selectDemoEffects);
|
||||
abortSlowExportCheckBox = findViewById(R.id.abort_slow_export_checkbox);
|
||||
|
||||
localFilePickerLauncher =
|
||||
registerForActivityResult(
|
||||
new ActivityResultContracts.StartActivityForResult(),
|
||||
this::localFilePickerLauncherResult);
|
||||
ArrayAdapter<String> hdrModeAdapter =
|
||||
new ArrayAdapter<>(/* context= */ this, R.layout.spinner_item);
|
||||
hdrModeAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
|
||||
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
|
||||
@ -272,7 +367,7 @@ public final class ConfigurationActivity extends AppCompatActivity {
|
||||
if (requestCode == FILE_PERMISSION_REQUEST_CODE
|
||||
&& grantResults.length == 1
|
||||
&& grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
||||
launchLocalFilePicker();
|
||||
checkNotNull(onPermissionsGranted).run();
|
||||
} else {
|
||||
Toast.makeText(
|
||||
getApplicationContext(), getString(R.string.permission_denied), Toast.LENGTH_LONG)
|
||||
@ -301,6 +396,7 @@ public final class ConfigurationActivity extends AppCompatActivity {
|
||||
"removeAudioCheckbox",
|
||||
"removeVideoCheckbox",
|
||||
"flattenForSlowMotionCheckbox",
|
||||
"forceAudioTrackCheckbox",
|
||||
"audioMimeSpinner",
|
||||
"videoMimeSpinner",
|
||||
"resolutionHeightSpinner",
|
||||
@ -309,17 +405,18 @@ public final class ConfigurationActivity extends AppCompatActivity {
|
||||
"trimCheckBox",
|
||||
"enableFallbackCheckBox",
|
||||
"enableDebugPreviewCheckBox",
|
||||
"enableRequestSdrToneMappingCheckBox",
|
||||
"forceInterpretHdrVideoAsSdrCheckBox",
|
||||
"enableHdrEditingCheckBox",
|
||||
"demoEffectsSelections"
|
||||
"abortSlowExportCheckBox",
|
||||
"hdrModeSpinner",
|
||||
"audioEffectsSelections",
|
||||
"videoEffectsSelections"
|
||||
})
|
||||
private void startTransformation(View view) {
|
||||
private void startExport(View view) {
|
||||
Intent transformerIntent = new Intent(/* packageContext= */ this, TransformerActivity.class);
|
||||
Bundle bundle = new Bundle();
|
||||
bundle.putBoolean(SHOULD_REMOVE_AUDIO, removeAudioCheckbox.isChecked());
|
||||
bundle.putBoolean(SHOULD_REMOVE_VIDEO, removeVideoCheckbox.isChecked());
|
||||
bundle.putBoolean(SHOULD_FLATTEN_FOR_SLOW_MOTION, flattenForSlowMotionCheckbox.isChecked());
|
||||
bundle.putBoolean(FORCE_AUDIO_TRACK, forceAudioTrackCheckbox.isChecked());
|
||||
String selectedAudioMimeType = String.valueOf(audioMimeSpinner.getSelectedItem());
|
||||
if (!SAME_AS_INPUT_OPTION.equals(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_DEBUG_PREVIEW, enableDebugPreviewCheckBox.isChecked());
|
||||
bundle.putBoolean(
|
||||
ENABLE_REQUEST_SDR_TONE_MAPPING, enableRequestSdrToneMappingCheckBox.isChecked());
|
||||
bundle.putBoolean(
|
||||
FORCE_INTERPRET_HDR_VIDEO_AS_SDR, forceInterpretHdrVideoAsSdrCheckBox.isChecked());
|
||||
bundle.putBoolean(ENABLE_HDR_EDITING, enableHdrEditingCheckBox.isChecked());
|
||||
bundle.putBooleanArray(DEMO_EFFECTS_SELECTIONS, demoEffectsSelections);
|
||||
bundle.putBoolean(ABORT_SLOW_EXPORT, abortSlowExportCheckBox.isChecked());
|
||||
String selectedhdrMode = String.valueOf(hdrModeSpinner.getSelectedItem());
|
||||
bundle.putInt(HDR_MODE, checkNotNull(HDR_MODE_DESCRIPTIONS.get(selectedhdrMode)));
|
||||
bundle.putBooleanArray(AUDIO_EFFECTS_SELECTIONS, audioEffectsSelections);
|
||||
bundle.putBooleanArray(VIDEO_EFFECTS_SELECTIONS, videoEffectsSelections);
|
||||
bundle.putInt(COLOR_FILTER_SELECTION, colorFilterSelection);
|
||||
bundle.putFloat(CONTRAST_VALUE, contrastValue);
|
||||
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_INNER_RADIUS, periodicVignetteInnerRadius);
|
||||
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);
|
||||
|
||||
@Nullable Uri intentUri;
|
||||
@ -399,39 +500,69 @@ public final class ConfigurationActivity extends AppCompatActivity {
|
||||
selectedFileTextView.setText(PRESET_FILE_URI_DESCRIPTIONS[inputUriPosition]);
|
||||
}
|
||||
|
||||
private void selectLocalFile(View view) {
|
||||
int permissionStatus =
|
||||
ActivityCompat.checkSelfPermission(
|
||||
ConfigurationActivity.this, Manifest.permission.READ_EXTERNAL_STORAGE);
|
||||
if (permissionStatus != PackageManager.PERMISSION_GRANTED) {
|
||||
String[] neededPermissions = {Manifest.permission.READ_EXTERNAL_STORAGE};
|
||||
private void selectLocalFile(
|
||||
View view, ActivityResultLauncher<Intent> localFilePickerLauncher, String mimeType) {
|
||||
String permission = SDK_INT >= 33 ? READ_MEDIA_VIDEO : READ_EXTERNAL_STORAGE;
|
||||
if (ActivityCompat.checkSelfPermission(/* context= */ this, permission)
|
||||
!= PackageManager.PERMISSION_GRANTED) {
|
||||
onPermissionsGranted = () -> launchLocalFilePicker(localFilePickerLauncher, mimeType);
|
||||
ActivityCompat.requestPermissions(
|
||||
ConfigurationActivity.this, neededPermissions, FILE_PERMISSION_REQUEST_CODE);
|
||||
/* activity= */ this, new String[] {permission}, FILE_PERMISSION_REQUEST_CODE);
|
||||
} 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.setType("video/*");
|
||||
intent.setType(mimeType);
|
||||
checkNotNull(localFilePickerLauncher).launch(intent);
|
||||
}
|
||||
|
||||
@RequiresNonNull("selectedFileTextView")
|
||||
private void localFilePickerLauncherResult(ActivityResult result) {
|
||||
private void videoLocalFilePickerLauncherResult(ActivityResult result) {
|
||||
Intent data = result.getData();
|
||||
if (data != null) {
|
||||
localFileUri = checkNotNull(data.getData());
|
||||
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)
|
||||
.setTitle(R.string.select_demo_effects)
|
||||
.setTitle(R.string.select_audio_effects)
|
||||
.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)
|
||||
.create()
|
||||
.show();
|
||||
@ -444,7 +575,7 @@ public final class ConfigurationActivity extends AppCompatActivity {
|
||||
View dialogView = getLayoutInflater().inflate(R.layout.trim_options, /* root= */ null);
|
||||
RangeSlider trimRangeSlider =
|
||||
checkNotNull(dialogView.findViewById(R.id.trim_bounds_range_slider));
|
||||
trimRangeSlider.setValues(0f, 10f); // seconds
|
||||
trimRangeSlider.setValues(0f, 1f); // seconds
|
||||
new AlertDialog.Builder(/* context= */ this)
|
||||
.setView(dialogView)
|
||||
.setPositiveButton(
|
||||
@ -458,9 +589,14 @@ public final class ConfigurationActivity extends AppCompatActivity {
|
||||
.show();
|
||||
}
|
||||
|
||||
@RequiresNonNull("demoEffectsSelections")
|
||||
private void selectDemoEffect(DialogInterface dialog, int which, boolean isChecked) {
|
||||
demoEffectsSelections[which] = isChecked;
|
||||
@RequiresNonNull("audioEffectsSelections")
|
||||
private void selectAudioEffect(DialogInterface dialog, int which, boolean isChecked) {
|
||||
audioEffectsSelections[which] = isChecked;
|
||||
}
|
||||
|
||||
@RequiresNonNull("videoEffectsSelections")
|
||||
private void selectVideoEffect(DialogInterface dialog, int which, boolean isChecked) {
|
||||
videoEffectsSelections[which] = isChecked;
|
||||
if (!isChecked) {
|
||||
return;
|
||||
}
|
||||
@ -481,6 +617,12 @@ public final class ConfigurationActivity extends AppCompatActivity {
|
||||
case PERIODIC_VIGNETTE_INDEX:
|
||||
controlPeriodicVignetteSettings();
|
||||
break;
|
||||
case BITMAP_OVERLAY_INDEX:
|
||||
controlBitmapOverlaySettings();
|
||||
break;
|
||||
case TEXT_OVERLAY_INDEX:
|
||||
controlTextOverlaySettings();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@ -583,18 +725,66 @@ public final class ConfigurationActivity extends AppCompatActivity {
|
||||
.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({
|
||||
"removeVideoCheckbox",
|
||||
"forceAudioTrackCheckbox",
|
||||
"audioMimeSpinner",
|
||||
"videoMimeSpinner",
|
||||
"resolutionHeightSpinner",
|
||||
"scaleSpinner",
|
||||
"rotateSpinner",
|
||||
"enableDebugPreviewCheckBox",
|
||||
"enableRequestSdrToneMappingCheckBox",
|
||||
"forceInterpretHdrVideoAsSdrCheckBox",
|
||||
"enableHdrEditingCheckBox",
|
||||
"selectDemoEffectsButton"
|
||||
"hdrModeSpinner",
|
||||
"selectAudioEffectsButton",
|
||||
"selectVideoEffectsButton"
|
||||
})
|
||||
private void onRemoveAudio(View view) {
|
||||
if (((CheckBox) view).isChecked()) {
|
||||
@ -607,16 +797,16 @@ public final class ConfigurationActivity extends AppCompatActivity {
|
||||
|
||||
@RequiresNonNull({
|
||||
"removeAudioCheckbox",
|
||||
"forceAudioTrackCheckbox",
|
||||
"audioMimeSpinner",
|
||||
"videoMimeSpinner",
|
||||
"resolutionHeightSpinner",
|
||||
"scaleSpinner",
|
||||
"rotateSpinner",
|
||||
"enableDebugPreviewCheckBox",
|
||||
"enableRequestSdrToneMappingCheckBox",
|
||||
"forceInterpretHdrVideoAsSdrCheckBox",
|
||||
"enableHdrEditingCheckBox",
|
||||
"selectDemoEffectsButton"
|
||||
"hdrModeSpinner",
|
||||
"selectAudioEffectsButton",
|
||||
"selectVideoEffectsButton"
|
||||
})
|
||||
private void onRemoveVideo(View view) {
|
||||
if (((CheckBox) view).isChecked()) {
|
||||
@ -628,42 +818,34 @@ public final class ConfigurationActivity extends AppCompatActivity {
|
||||
}
|
||||
|
||||
@RequiresNonNull({
|
||||
"forceAudioTrackCheckbox",
|
||||
"audioMimeSpinner",
|
||||
"videoMimeSpinner",
|
||||
"resolutionHeightSpinner",
|
||||
"scaleSpinner",
|
||||
"rotateSpinner",
|
||||
"enableDebugPreviewCheckBox",
|
||||
"enableRequestSdrToneMappingCheckBox",
|
||||
"forceInterpretHdrVideoAsSdrCheckBox",
|
||||
"enableHdrEditingCheckBox",
|
||||
"selectDemoEffectsButton"
|
||||
"hdrModeSpinner",
|
||||
"selectAudioEffectsButton",
|
||||
"selectVideoEffectsButton"
|
||||
})
|
||||
private void enableTrackSpecificOptions(boolean isAudioEnabled, boolean isVideoEnabled) {
|
||||
forceAudioTrackCheckbox.setEnabled(isVideoEnabled);
|
||||
audioMimeSpinner.setEnabled(isAudioEnabled);
|
||||
videoMimeSpinner.setEnabled(isVideoEnabled);
|
||||
resolutionHeightSpinner.setEnabled(isVideoEnabled);
|
||||
scaleSpinner.setEnabled(isVideoEnabled);
|
||||
rotateSpinner.setEnabled(isVideoEnabled);
|
||||
enableDebugPreviewCheckBox.setEnabled(isVideoEnabled);
|
||||
enableRequestSdrToneMappingCheckBox.setEnabled(
|
||||
isRequestSdrToneMappingSupported() && isVideoEnabled);
|
||||
forceInterpretHdrVideoAsSdrCheckBox.setEnabled(isVideoEnabled);
|
||||
enableHdrEditingCheckBox.setEnabled(isVideoEnabled);
|
||||
selectDemoEffectsButton.setEnabled(isVideoEnabled);
|
||||
hdrModeSpinner.setEnabled(isVideoEnabled);
|
||||
selectAudioEffectsButton.setEnabled(isAudioEnabled);
|
||||
selectVideoEffectsButton.setEnabled(isVideoEnabled);
|
||||
|
||||
findViewById(R.id.audio_mime_text_view).setEnabled(isAudioEnabled);
|
||||
findViewById(R.id.video_mime_text_view).setEnabled(isVideoEnabled);
|
||||
findViewById(R.id.resolution_height_text_view).setEnabled(isVideoEnabled);
|
||||
findViewById(R.id.scale).setEnabled(isVideoEnabled);
|
||||
findViewById(R.id.rotate).setEnabled(isVideoEnabled);
|
||||
findViewById(R.id.request_sdr_tone_mapping)
|
||||
.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;
|
||||
findViewById(R.id.hdr_mode).setEnabled(isVideoEnabled);
|
||||
}
|
||||
}
|
||||
|
@ -19,18 +19,18 @@ import static androidx.media3.common.util.Assertions.checkArgument;
|
||||
|
||||
import android.content.Context;
|
||||
import android.opengl.GLES20;
|
||||
import android.util.Pair;
|
||||
import androidx.media3.common.FrameProcessingException;
|
||||
import androidx.media3.common.VideoFrameProcessingException;
|
||||
import androidx.media3.common.util.GlProgram;
|
||||
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;
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
/* 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 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 maxInnerRadius The upper bound of the radius that is unaffected by the effect.
|
||||
* @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,
|
||||
boolean useHdr,
|
||||
float centerX,
|
||||
@ -69,7 +69,7 @@ import java.io.IOException;
|
||||
float minInnerRadius,
|
||||
float maxInnerRadius,
|
||||
float outerRadius)
|
||||
throws FrameProcessingException {
|
||||
throws VideoFrameProcessingException {
|
||||
super(useHdr);
|
||||
checkArgument(minInnerRadius <= maxInnerRadius);
|
||||
checkArgument(maxInnerRadius <= outerRadius);
|
||||
@ -78,7 +78,7 @@ import java.io.IOException;
|
||||
try {
|
||||
glProgram = new GlProgram(context, VERTEX_SHADER_PATH, FRAGMENT_SHADER_PATH);
|
||||
} catch (IOException | GlUtil.GlException e) {
|
||||
throw new FrameProcessingException(e);
|
||||
throw new VideoFrameProcessingException(e);
|
||||
}
|
||||
glProgram.setFloatsUniform("uCenter", new float[] {centerX, centerY});
|
||||
glProgram.setFloatsUniform("uOuterRadius", new float[] {outerRadius});
|
||||
@ -90,12 +90,13 @@ import java.io.IOException;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Pair<Integer, Integer> configure(int inputWidth, int inputHeight) {
|
||||
return Pair.create(inputWidth, inputHeight);
|
||||
public Size configure(int inputWidth, int inputHeight) {
|
||||
return new Size(inputWidth, inputHeight);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void drawFrame(int inputTexId, long presentationTimeUs) throws FrameProcessingException {
|
||||
public void drawFrame(int inputTexId, long presentationTimeUs)
|
||||
throws VideoFrameProcessingException {
|
||||
try {
|
||||
glProgram.use();
|
||||
glProgram.setSamplerTexIdUniform("uTexSampler", inputTexId, /* texUnitIndex= */ 0);
|
||||
@ -107,17 +108,17 @@ import java.io.IOException;
|
||||
// The four-vertex triangle strip forms a quad.
|
||||
GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, /* first= */ 0, /* count= */ 4);
|
||||
} catch (GlUtil.GlException e) {
|
||||
throw new FrameProcessingException(e, presentationTimeUs);
|
||||
throw new VideoFrameProcessingException(e, presentationTimeUs);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void release() throws FrameProcessingException {
|
||||
public void release() throws VideoFrameProcessingException {
|
||||
super.release();
|
||||
try {
|
||||
glProgram.delete();
|
||||
} catch (GlUtil.GlException e) {
|
||||
throw new FrameProcessingException(e);
|
||||
throw new VideoFrameProcessingException(e);
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -16,48 +16,78 @@
|
||||
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.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.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.net.Uri;
|
||||
import android.opengl.Matrix;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.text.Spannable;
|
||||
import android.text.SpannableString;
|
||||
import android.text.style.ForegroundColorSpan;
|
||||
import android.view.SurfaceHolder;
|
||||
import android.view.SurfaceView;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.Button;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.StringRes;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.core.app.ActivityCompat;
|
||||
import androidx.media3.common.C;
|
||||
import androidx.media3.common.DebugViewProvider;
|
||||
import androidx.media3.common.Effect;
|
||||
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.Util;
|
||||
import androidx.media3.datasource.DataSourceBitmapLoader;
|
||||
import androidx.media3.effect.BitmapOverlay;
|
||||
import androidx.media3.effect.Contrast;
|
||||
import androidx.media3.effect.DrawableOverlay;
|
||||
import androidx.media3.effect.GlEffect;
|
||||
import androidx.media3.effect.GlTextureProcessor;
|
||||
import androidx.media3.effect.GlShaderProgram;
|
||||
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.RgbFilter;
|
||||
import androidx.media3.effect.RgbMatrix;
|
||||
import androidx.media3.effect.ScaleAndRotateTransformation;
|
||||
import androidx.media3.effect.SingleColorLut;
|
||||
import androidx.media3.effect.TextOverlay;
|
||||
import androidx.media3.effect.TextureOverlay;
|
||||
import androidx.media3.exoplayer.ExoPlayer;
|
||||
import androidx.media3.exoplayer.audio.SilenceSkippingAudioProcessor;
|
||||
import androidx.media3.exoplayer.util.DebugTextViewHelper;
|
||||
import androidx.media3.transformer.Composition;
|
||||
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.TransformationException;
|
||||
import androidx.media3.transformer.TransformationRequest;
|
||||
import androidx.media3.transformer.TransformationResult;
|
||||
import androidx.media3.transformer.Transformer;
|
||||
import androidx.media3.ui.AspectRatioFrameLayout;
|
||||
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.Ticker;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.util.concurrent.ListenableFuture;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.lang.reflect.Constructor;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
||||
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 {
|
||||
private static final String TAG = "TransformerActivity";
|
||||
|
||||
private @MonotonicNonNull Button displayInputButton;
|
||||
private @MonotonicNonNull MaterialCardView inputCardView;
|
||||
private @MonotonicNonNull TextView inputTextView;
|
||||
private @MonotonicNonNull ImageView inputImageView;
|
||||
private @MonotonicNonNull PlayerView inputPlayerView;
|
||||
private @MonotonicNonNull PlayerView outputPlayerView;
|
||||
private @MonotonicNonNull TextView outputVideoTextView;
|
||||
private @MonotonicNonNull TextView debugTextView;
|
||||
private @MonotonicNonNull TextView informationTextView;
|
||||
private @MonotonicNonNull ViewGroup progressViewGroup;
|
||||
private @MonotonicNonNull LinearProgressIndicator progressIndicator;
|
||||
private @MonotonicNonNull Stopwatch transformationStopwatch;
|
||||
private @MonotonicNonNull Stopwatch exportStopwatch;
|
||||
private @MonotonicNonNull AspectRatioFrameLayout debugFrame;
|
||||
|
||||
@Nullable private DebugTextViewHelper debugTextViewHelper;
|
||||
@ -101,8 +138,11 @@ public final class TransformerActivity extends AppCompatActivity {
|
||||
setContentView(R.layout.transformer_activity);
|
||||
|
||||
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);
|
||||
outputPlayerView = findViewById(R.id.output_player_view);
|
||||
outputVideoTextView = findViewById(R.id.output_video_text_view);
|
||||
debugTextView = findViewById(R.id.debug_text_view);
|
||||
informationTextView = findViewById(R.id.information_text_view);
|
||||
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.setOnClickListener(this::toggleInputVideoDisplay);
|
||||
|
||||
transformationStopwatch =
|
||||
exportStopwatch =
|
||||
Stopwatch.createUnstarted(
|
||||
new Ticker() {
|
||||
@Override
|
||||
@ -127,15 +167,18 @@ public final class TransformerActivity extends AppCompatActivity {
|
||||
|
||||
checkNotNull(progressIndicator);
|
||||
checkNotNull(informationTextView);
|
||||
checkNotNull(transformationStopwatch);
|
||||
checkNotNull(exportStopwatch);
|
||||
checkNotNull(inputCardView);
|
||||
checkNotNull(inputTextView);
|
||||
checkNotNull(inputImageView);
|
||||
checkNotNull(inputPlayerView);
|
||||
checkNotNull(outputPlayerView);
|
||||
checkNotNull(outputVideoTextView);
|
||||
checkNotNull(debugTextView);
|
||||
checkNotNull(progressViewGroup);
|
||||
checkNotNull(debugFrame);
|
||||
checkNotNull(displayInputButton);
|
||||
startTransformation();
|
||||
startExport();
|
||||
|
||||
inputPlayerView.onResume();
|
||||
outputPlayerView.onResume();
|
||||
@ -148,9 +191,9 @@ public final class TransformerActivity extends AppCompatActivity {
|
||||
checkNotNull(transformer).cancel();
|
||||
transformer = null;
|
||||
|
||||
// The stop watch is reset after cancelling the transformation, in case cancelling causes the
|
||||
// stop watch to be stopped in a transformer callback.
|
||||
checkNotNull(transformationStopwatch).reset();
|
||||
// The stop watch is reset after cancelling the export, in case cancelling causes the stop watch
|
||||
// to be stopped in a transformer callback.
|
||||
checkNotNull(exportStopwatch).reset();
|
||||
|
||||
checkNotNull(inputPlayerView).onPause();
|
||||
checkNotNull(outputPlayerView).onPause();
|
||||
@ -161,37 +204,49 @@ public final class TransformerActivity extends AppCompatActivity {
|
||||
}
|
||||
|
||||
@RequiresNonNull({
|
||||
"displayInputButton",
|
||||
"inputCardView",
|
||||
"inputTextView",
|
||||
"inputImageView",
|
||||
"inputPlayerView",
|
||||
"outputPlayerView",
|
||||
"displayInputButton",
|
||||
"outputVideoTextView",
|
||||
"debugTextView",
|
||||
"informationTextView",
|
||||
"progressIndicator",
|
||||
"transformationStopwatch",
|
||||
"exportStopwatch",
|
||||
"progressViewGroup",
|
||||
"debugFrame",
|
||||
})
|
||||
private void startTransformation() {
|
||||
requestTransformerPermission();
|
||||
private void startExport() {
|
||||
requestReadVideoPermission(/* activity= */ this);
|
||||
|
||||
Intent intent = getIntent();
|
||||
Uri uri = checkNotNull(intent.getData());
|
||||
Uri inputUri = checkNotNull(intent.getData());
|
||||
try {
|
||||
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) {
|
||||
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);
|
||||
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());
|
||||
ProgressHolder progressHolder = new ProgressHolder();
|
||||
mainHandler.post(
|
||||
@ -199,13 +254,10 @@ public final class TransformerActivity extends AppCompatActivity {
|
||||
@Override
|
||||
public void run() {
|
||||
if (transformer != null
|
||||
&& transformer.getProgress(progressHolder)
|
||||
!= Transformer.PROGRESS_STATE_NO_TRANSFORMATION) {
|
||||
&& transformer.getProgress(progressHolder) != PROGRESS_STATE_NOT_STARTED) {
|
||||
progressIndicator.setProgress(progressHolder.progress);
|
||||
informationTextView.setText(
|
||||
getString(
|
||||
R.string.transformation_timer,
|
||||
transformationStopwatch.elapsed(TimeUnit.SECONDS)));
|
||||
getString(R.string.export_timer, exportStopwatch.elapsed(TimeUnit.SECONDS)));
|
||||
mainHandler.postDelayed(/* r= */ this, /* delayMillis= */ 500);
|
||||
}
|
||||
}
|
||||
@ -232,21 +284,22 @@ public final class TransformerActivity extends AppCompatActivity {
|
||||
|
||||
@RequiresNonNull({
|
||||
"inputCardView",
|
||||
"inputTextView",
|
||||
"inputImageView",
|
||||
"inputPlayerView",
|
||||
"outputPlayerView",
|
||||
"outputVideoTextView",
|
||||
"displayInputButton",
|
||||
"debugTextView",
|
||||
"informationTextView",
|
||||
"transformationStopwatch",
|
||||
"exportStopwatch",
|
||||
"progressViewGroup",
|
||||
"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);
|
||||
if (bundle != null) {
|
||||
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);
|
||||
if (audioMimeType != null) {
|
||||
requestBuilder.setAudioMimeType(audioMimeType);
|
||||
@ -255,55 +308,38 @@ public final class TransformerActivity extends AppCompatActivity {
|
||||
if (videoMimeType != null) {
|
||||
requestBuilder.setVideoMimeType(videoMimeType);
|
||||
}
|
||||
int resolutionHeight =
|
||||
bundle.getInt(
|
||||
ConfigurationActivity.RESOLUTION_HEIGHT, /* defaultValue= */ C.LENGTH_UNSET);
|
||||
if (resolutionHeight != C.LENGTH_UNSET) {
|
||||
requestBuilder.setResolution(resolutionHeight);
|
||||
}
|
||||
requestBuilder.setHdrMode(bundle.getInt(ConfigurationActivity.HDR_MODE));
|
||||
transformerBuilder.setTransformationRequest(requestBuilder.build());
|
||||
|
||||
float scaleX = bundle.getFloat(ConfigurationActivity.SCALE_X, /* defaultValue= */ 1);
|
||||
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(
|
||||
transformerBuilder.setEncoderFactory(
|
||||
new DefaultEncoderFactory.Builder(this.getApplicationContext())
|
||||
.setEnableFallback(bundle.getBoolean(ConfigurationActivity.ENABLE_FALLBACK))
|
||||
.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)) {
|
||||
transformerBuilder.setDebugViewProvider(new DemoDebugViewProvider());
|
||||
}
|
||||
}
|
||||
|
||||
return transformerBuilder
|
||||
.addListener(
|
||||
new Transformer.Listener() {
|
||||
@Override
|
||||
public void onTransformationCompleted(
|
||||
MediaItem mediaItem, TransformationResult transformationResult) {
|
||||
TransformerActivity.this.onTransformationCompleted(filePath, mediaItem);
|
||||
public void onCompleted(Composition composition, ExportResult exportResult) {
|
||||
TransformerActivity.this.onCompleted(inputUri, filePath);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTransformationError(
|
||||
MediaItem mediaItem, TransformationException exception) {
|
||||
TransformerActivity.this.onTransformationError(exception);
|
||||
public void onError(
|
||||
Composition composition,
|
||||
ExportResult exportResult,
|
||||
ExportException exportException) {
|
||||
TransformerActivity.this.onError(exportException);
|
||||
}
|
||||
})
|
||||
.build();
|
||||
@ -313,26 +349,88 @@ public final class TransformerActivity extends AppCompatActivity {
|
||||
private File createExternalCacheFile(String fileName) throws IOException {
|
||||
File file = new File(getExternalCacheDir(), fileName);
|
||||
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()) {
|
||||
throw new IllegalStateException("Could not create the transformer output file");
|
||||
throw new IllegalStateException("Could not create the export output 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
|
||||
boolean[] selectedEffects =
|
||||
bundle.getBooleanArray(ConfigurationActivity.DEMO_EFFECTS_SELECTIONS);
|
||||
if (selectedEffects == null) {
|
||||
boolean[] selectedAudioEffects =
|
||||
bundle.getBooleanArray(ConfigurationActivity.AUDIO_EFFECTS_SELECTIONS);
|
||||
|
||||
if (selectedAudioEffects == null) {
|
||||
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<>();
|
||||
if (selectedEffects[0]) {
|
||||
if (selectedEffects[ConfigurationActivity.DIZZY_CROP_INDEX]) {
|
||||
effects.add(MatrixTransformationFactory.createDizzyCropEffect());
|
||||
}
|
||||
if (selectedEffects[1]) {
|
||||
if (selectedEffects[ConfigurationActivity.EDGE_DETECTOR_INDEX]) {
|
||||
try {
|
||||
Class<?> clazz = Class.forName("androidx.media3.demo.transformer.MediaPipeProcessor");
|
||||
Constructor<?> constructor =
|
||||
@ -347,7 +445,7 @@ public final class TransformerActivity extends AppCompatActivity {
|
||||
(GlEffect)
|
||||
(Context context, boolean useHdr) -> {
|
||||
try {
|
||||
return (GlTextureProcessor)
|
||||
return (GlShaderProgram)
|
||||
constructor.newInstance(
|
||||
context,
|
||||
useHdr,
|
||||
@ -357,14 +455,14 @@ public final class TransformerActivity extends AppCompatActivity {
|
||||
/* outputStreamName= */ "output_video");
|
||||
} catch (Exception e) {
|
||||
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) {
|
||||
showToast(R.string.no_media_pipe_error);
|
||||
}
|
||||
}
|
||||
if (selectedEffects[2]) {
|
||||
if (selectedEffects[ConfigurationActivity.COLOR_FILTERS_INDEX]) {
|
||||
switch (bundle.getInt(ConfigurationActivity.COLOR_FILTER_SELECTION)) {
|
||||
case ConfigurationActivity.COLOR_FILTER_GRAYSCALE:
|
||||
effects.add(RgbFilter.createGrayscaleFilter());
|
||||
@ -390,7 +488,7 @@ public final class TransformerActivity extends AppCompatActivity {
|
||||
+ bundle.getInt(ConfigurationActivity.COLOR_FILTER_SELECTION));
|
||||
}
|
||||
}
|
||||
if (selectedEffects[3]) {
|
||||
if (selectedEffects[ConfigurationActivity.MAP_WHITE_TO_GREEN_LUT_INDEX]) {
|
||||
int length = 3;
|
||||
int[][][] mapWhiteToGreenLut = new int[length][length][length];
|
||||
int scale = 255 / (length - 1);
|
||||
@ -405,7 +503,7 @@ public final class TransformerActivity extends AppCompatActivity {
|
||||
mapWhiteToGreenLut[length - 1][length - 1][length - 1] = Color.GREEN;
|
||||
effects.add(SingleColorLut.createFromCube(mapWhiteToGreenLut));
|
||||
}
|
||||
if (selectedEffects[4]) {
|
||||
if (selectedEffects[ConfigurationActivity.RGB_ADJUSTMENTS_INDEX]) {
|
||||
effects.add(
|
||||
new RgbAdjustment.Builder()
|
||||
.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))
|
||||
.build());
|
||||
}
|
||||
if (selectedEffects[5]) {
|
||||
if (selectedEffects[ConfigurationActivity.HSL_ADJUSTMENT_INDEX]) {
|
||||
effects.add(
|
||||
new HslAdjustment.Builder()
|
||||
.adjustHue(bundle.getFloat(ConfigurationActivity.HSL_ADJUSTMENTS_HUE))
|
||||
@ -421,14 +519,14 @@ public final class TransformerActivity extends AppCompatActivity {
|
||||
.adjustLightness(bundle.getFloat(ConfigurationActivity.HSL_ADJUSTMENTS_LIGHTNESS))
|
||||
.build());
|
||||
}
|
||||
if (selectedEffects[6]) {
|
||||
if (selectedEffects[ConfigurationActivity.CONTRAST_INDEX]) {
|
||||
effects.add(new Contrast(bundle.getFloat(ConfigurationActivity.CONTRAST_VALUE)));
|
||||
}
|
||||
if (selectedEffects[7]) {
|
||||
if (selectedEffects[ConfigurationActivity.PERIODIC_VIGNETTE_INDEX]) {
|
||||
effects.add(
|
||||
(GlEffect)
|
||||
(Context context, boolean useHdr) ->
|
||||
new PeriodicVignetteProcessor(
|
||||
new PeriodicVignetteShaderProgram(
|
||||
context,
|
||||
useHdr,
|
||||
bundle.getFloat(ConfigurationActivity.PERIODIC_VIGNETTE_CENTER_X),
|
||||
@ -439,61 +537,144 @@ public final class TransformerActivity extends AppCompatActivity {
|
||||
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());
|
||||
}
|
||||
if (selectedEffects[9]) {
|
||||
effects.add((GlEffect) BitmapOverlayProcessor::new);
|
||||
}
|
||||
if (selectedEffects[10]) {
|
||||
if (selectedEffects[ConfigurationActivity.ZOOM_IN_INDEX]) {
|
||||
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();
|
||||
}
|
||||
|
||||
@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({
|
||||
"informationTextView",
|
||||
"progressViewGroup",
|
||||
"debugFrame",
|
||||
"transformationStopwatch",
|
||||
"exportStopwatch",
|
||||
})
|
||||
private void onTransformationError(TransformationException exception) {
|
||||
transformationStopwatch.stop();
|
||||
informationTextView.setText(R.string.transformation_error);
|
||||
private void onError(ExportException exportException) {
|
||||
exportStopwatch.stop();
|
||||
informationTextView.setText(R.string.export_error);
|
||||
progressViewGroup.setVisibility(View.GONE);
|
||||
debugFrame.removeAllViews();
|
||||
Toast.makeText(getApplicationContext(), "Transformation error: " + exception, Toast.LENGTH_LONG)
|
||||
Toast.makeText(getApplicationContext(), "Export error: " + exportException, Toast.LENGTH_LONG)
|
||||
.show();
|
||||
Log.e(TAG, "Transformation error", exception);
|
||||
Log.e(TAG, "Export error", exportException);
|
||||
}
|
||||
|
||||
@RequiresNonNull({
|
||||
"inputCardView",
|
||||
"inputTextView",
|
||||
"inputImageView",
|
||||
"inputPlayerView",
|
||||
"outputPlayerView",
|
||||
"displayInputButton",
|
||||
"outputVideoTextView",
|
||||
"debugTextView",
|
||||
"displayInputButton",
|
||||
"informationTextView",
|
||||
"progressViewGroup",
|
||||
"debugFrame",
|
||||
"transformationStopwatch",
|
||||
"exportStopwatch",
|
||||
})
|
||||
private void onTransformationCompleted(String filePath, MediaItem inputMediaItem) {
|
||||
transformationStopwatch.stop();
|
||||
private void onCompleted(Uri inputUri, String filePath) {
|
||||
exportStopwatch.stop();
|
||||
informationTextView.setText(
|
||||
getString(
|
||||
R.string.transformation_completed, transformationStopwatch.elapsed(TimeUnit.SECONDS)));
|
||||
getString(R.string.export_completed, exportStopwatch.elapsed(TimeUnit.SECONDS), filePath));
|
||||
progressViewGroup.setVisibility(View.GONE);
|
||||
debugFrame.removeAllViews();
|
||||
inputCardView.setVisibility(View.VISIBLE);
|
||||
outputPlayerView.setVisibility(View.VISIBLE);
|
||||
outputVideoTextView.setVisibility(View.VISIBLE);
|
||||
debugTextView.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);
|
||||
}
|
||||
|
||||
@RequiresNonNull({
|
||||
"inputCardView",
|
||||
"inputTextView",
|
||||
"inputImageView",
|
||||
"inputPlayerView",
|
||||
"outputPlayerView",
|
||||
"debugTextView",
|
||||
@ -503,14 +684,7 @@ public final class TransformerActivity extends AppCompatActivity {
|
||||
outputPlayerView.setPlayer(null);
|
||||
releasePlayer();
|
||||
|
||||
ExoPlayer inputPlayer = new ExoPlayer.Builder(/* context= */ this).build();
|
||||
inputPlayerView.setPlayer(inputPlayer);
|
||||
inputPlayerView.setControllerAutoShow(false);
|
||||
inputPlayer.setMediaItem(inputMediaItem);
|
||||
inputPlayer.prepare();
|
||||
this.inputPlayer = inputPlayer;
|
||||
inputPlayer.setVolume(0f);
|
||||
|
||||
Uri uri = checkNotNull(inputMediaItem.localConfiguration).uri;
|
||||
ExoPlayer outputPlayer = new ExoPlayer.Builder(/* context= */ this).build();
|
||||
outputPlayerView.setPlayer(outputPlayer);
|
||||
outputPlayerView.setControllerAutoShow(false);
|
||||
@ -518,13 +692,60 @@ public final class TransformerActivity extends AppCompatActivity {
|
||||
outputPlayer.prepare();
|
||||
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();
|
||||
}
|
||||
outputPlayer.play();
|
||||
|
||||
debugTextViewHelper = new DebugTextViewHelper(outputPlayer, debugTextView);
|
||||
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() {
|
||||
if (debugTextViewHelper != null) {
|
||||
debugTextViewHelper.stop();
|
||||
@ -540,12 +761,11 @@ public final class TransformerActivity extends AppCompatActivity {
|
||||
}
|
||||
}
|
||||
|
||||
private void requestTransformerPermission() {
|
||||
if (Util.SDK_INT < 23) {
|
||||
return;
|
||||
}
|
||||
if (checkSelfPermission(READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
|
||||
requestPermissions(new String[] {READ_EXTERNAL_STORAGE}, /* requestCode= */ 0);
|
||||
private static void requestReadVideoPermission(AppCompatActivity activity) {
|
||||
String permission = SDK_INT >= 33 ? READ_MEDIA_VIDEO : READ_EXTERNAL_STORAGE;
|
||||
if (ActivityCompat.checkSelfPermission(activity, permission)
|
||||
!= PackageManager.PERMISSION_GRANTED) {
|
||||
ActivityCompat.requestPermissions(activity, new String[] {permission}, /* requestCode= */ 0);
|
||||
}
|
||||
}
|
||||
|
||||
@ -562,7 +782,9 @@ public final class TransformerActivity extends AppCompatActivity {
|
||||
inputCardView.setVisibility(View.VISIBLE);
|
||||
displayInputButton.setText(getString(R.string.hide_input_video));
|
||||
} else if (inputCardView.getVisibility() == View.VISIBLE) {
|
||||
checkNotNull(inputPlayer).pause();
|
||||
if (inputPlayer != null) {
|
||||
inputPlayer.pause();
|
||||
}
|
||||
inputCardView.setVisibility(View.GONE);
|
||||
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) {
|
||||
checkState(
|
||||
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) {
|
||||
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.
|
||||
CountDownLatch surfaceCreatedCountDownLatch = new CountDownLatch(1);
|
||||
SurfaceView surfaceView = new SurfaceView(/* context= */ TransformerActivity.this);
|
||||
runOnUiThread(
|
||||
() -> {
|
||||
surfaceView = new SurfaceView(/* context= */ TransformerActivity.this);
|
||||
AspectRatioFrameLayout debugFrame = checkNotNull(TransformerActivity.this.debugFrame);
|
||||
debugFrame.addView(surfaceView);
|
||||
debugFrame.setAspectRatio((float) width / height);
|
||||
@ -628,7 +850,6 @@ public final class TransformerActivity extends AppCompatActivity {
|
||||
Thread.currentThread().interrupt();
|
||||
return null;
|
||||
}
|
||||
this.surfaceView = surfaceView;
|
||||
return surfaceView;
|
||||
}
|
||||
}
|
||||
|
@ -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>
|
@ -75,11 +75,12 @@
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
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
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:stretchColumns="0"
|
||||
android:shrinkColumns="0"
|
||||
android:layout_marginTop="32dp"
|
||||
android:measureWithLargestChild="true"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
@ -112,6 +113,15 @@
|
||||
android:id="@+id/flatten_for_slow_motion_checkbox"
|
||||
android:layout_gravity="end" />
|
||||
</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
|
||||
android:layout_weight="1"
|
||||
android:gravity="center_vertical" >
|
||||
@ -201,53 +211,54 @@
|
||||
android:layout_weight="1">
|
||||
<TextView
|
||||
android:layout_gravity="center_vertical"
|
||||
android:id="@+id/request_sdr_tone_mapping"
|
||||
android:text="@string/request_sdr_tone_mapping" />
|
||||
android:text="@string/abort_slow_export" />
|
||||
<CheckBox
|
||||
android:id="@+id/request_sdr_tone_mapping_checkbox"
|
||||
android:id="@+id/abort_slow_export_checkbox"
|
||||
android:layout_gravity="end"/>
|
||||
</TableRow>
|
||||
<TableRow
|
||||
android:layout_weight="1">
|
||||
android:layout_weight="1"
|
||||
android:gravity="center_vertical" >
|
||||
<TextView
|
||||
android:layout_gravity="center_vertical"
|
||||
android:id="@+id/hdr_editing"
|
||||
android:text="@string/hdr_editing" />
|
||||
<CheckBox
|
||||
android:id="@+id/hdr_editing_checkbox"
|
||||
android:layout_gravity="end" />
|
||||
</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" />
|
||||
android:id="@+id/hdr_mode"
|
||||
android:text="@string/hdr_mode"/>
|
||||
<Spinner
|
||||
android:id="@+id/hdr_mode_spinner"
|
||||
android:layout_gravity="right|center_vertical"
|
||||
android:gravity="right" />
|
||||
</TableRow>
|
||||
</TableLayout>
|
||||
</androidx.core.widget.NestedScrollView>
|
||||
<Button
|
||||
android:id="@+id/select_demo_effects_button"
|
||||
android:id="@+id/select_audio_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_demo_effects"
|
||||
app:layout_constraintBottom_toTopOf="@+id/transform_button"
|
||||
android:text="@string/select_audio_effects"
|
||||
app:layout_constraintBottom_toTopOf="@+id/select_video_effects_button"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent" />
|
||||
<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_height="wrap_content"
|
||||
android:layout_marginBottom="28dp"
|
||||
android:layout_marginStart="32dp"
|
||||
android:layout_marginEnd="32dp"
|
||||
android:text="@string/transform"
|
||||
android:text="@string/export"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent" />
|
||||
|
@ -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>
|
@ -69,16 +69,22 @@
|
||||
android:layout_height="wrap_content" >
|
||||
|
||||
<TextView
|
||||
android:id="@+id/input_text_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:padding="8dp"
|
||||
android:text="@string/input_video" />
|
||||
android:text="@string/input_video_no_sound" />
|
||||
|
||||
<FrameLayout
|
||||
android:layout_width="match_parent"
|
||||
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
|
||||
android:id="@+id/input_player_view"
|
||||
android:layout_width="match_parent"
|
||||
@ -108,10 +114,11 @@
|
||||
android:layout_height="wrap_content" >
|
||||
|
||||
<TextView
|
||||
android:id="@+id/output_video_text_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="8dp"
|
||||
android:text="@string/output_video" />
|
||||
android:text="@string/output_video_playing_sound" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/debug_text_view"
|
||||
|
@ -41,7 +41,7 @@
|
||||
<com.google.android.material.slider.RangeSlider
|
||||
android:id="@+id/trim_bounds_range_slider"
|
||||
android:valueFrom="0.0"
|
||||
android:valueTo="60.0"
|
||||
android:valueTo="10.0"
|
||||
android:layout_gravity="right"/>
|
||||
</TableRow>
|
||||
</TableLayout>
|
||||
|
@ -19,6 +19,7 @@
|
||||
<string name="configuration" translatable="false">Configuration</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="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_video" translatable="false">Remove video</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="enable_fallback" translatable="false">Enable fallback</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="request_sdr_tone_mapping" translatable="false">Request SDR tone-mapping (API 31+)</string>
|
||||
<string name="force_interpret_hdr_video_as_sdr" translatable="false">Interpret HDR as SDR (API 29+)</string>
|
||||
<string name="hdr_editing" translatable="false">HDR editing (API 31+)</string>
|
||||
<string name="select_demo_effects" translatable="false">Add demo effects</string>
|
||||
<string name="hdr_mode" translatable="false">HDR mode</string>
|
||||
<string name="select_audio_effects" translatable="false">Add audio effects</string>
|
||||
<string name="select_video_effects" translatable="false">Add video effects</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="transform" translatable="false">Transform</string>
|
||||
<string name="no_media_pipe_error" translatable="false">Failed to load MediaPipeShaderProgram. Check the README for instructions.</string>
|
||||
<string name="export" translatable="false">Export</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="transformation_started" translatable="false">Transformation started</string>
|
||||
<string name="transformation_timer" translatable="false">Transformation started %d seconds ago.</string>
|
||||
<string name="transformation_completed" translatable="false">Transformation completed in %d seconds.</string>
|
||||
<string name="transformation_error" translatable="false">Transformation error</string>
|
||||
<string name="export_started" translatable="false">Export started</string>
|
||||
<string name="export_timer" translatable="false">Export started %d seconds ago.</string>
|
||||
<string name="export_completed" translatable="false">Export completed in %d seconds.\nOutput: %s</string>
|
||||
<string name="export_error" translatable="false">Export error</string>
|
||||
<string name="trim_range">Bounds in seconds</string>
|
||||
<string-array name="color_filter_options">
|
||||
<item>Grayscale</item>
|
||||
@ -61,9 +62,20 @@
|
||||
<string name="hue_adjustment">Hue adjustment</string>
|
||||
<string name="saturation_adjustment">Saturation adjustment</string>
|
||||
<string name="lightness_adjustment">Lightness adjustment</string>
|
||||
<string name="input_video">Input video:</string>
|
||||
<string name="output_video">Output video:</string>
|
||||
<string name="input_image">Input image:</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="hide_input_video">Hide 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>
|
||||
|
@ -24,11 +24,13 @@ import android.content.Context;
|
||||
import android.opengl.EGL14;
|
||||
import androidx.annotation.Nullable;
|
||||
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.Util;
|
||||
import androidx.media3.effect.GlTextureProcessor;
|
||||
import androidx.media3.effect.TextureInfo;
|
||||
import androidx.media3.effect.GlShaderProgram;
|
||||
import com.google.common.util.concurrent.MoreExecutors;
|
||||
import com.google.mediapipe.components.FrameProcessor;
|
||||
import com.google.mediapipe.framework.AppTextureFrame;
|
||||
import com.google.mediapipe.framework.TextureFrame;
|
||||
@ -37,13 +39,14 @@ import java.util.ArrayDeque;
|
||||
import java.util.Queue;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Future;
|
||||
|
||||
/** 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 RETRY_WAIT_TIME_MS = 1;
|
||||
|
||||
@ -66,7 +69,7 @@ import java.util.concurrent.Future;
|
||||
}
|
||||
|
||||
private final FrameProcessor frameProcessor;
|
||||
private final ConcurrentHashMap<TextureInfo, TextureFrame> outputFrames;
|
||||
private final ConcurrentHashMap<GlTextureInfo, TextureFrame> outputFrames;
|
||||
private final boolean isSingleFrameGraph;
|
||||
@Nullable private final ExecutorService singleThreadExecutorService;
|
||||
private final Queue<Future<?>> futures;
|
||||
@ -74,14 +77,15 @@ import java.util.concurrent.Future;
|
||||
private InputListener inputListener;
|
||||
private OutputListener outputListener;
|
||||
private ErrorListener errorListener;
|
||||
private Executor errorListenerExecutor;
|
||||
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
|
||||
* time by continuously attempting to queue input frames to MediaPipe until they are accepted or
|
||||
* waste memory if MediaPipe accepts and stores many frames internally.
|
||||
* <p>If {@code isSingleFrameGraph} is {@code false}, the {@code MediaPipeShaderProgram} may waste
|
||||
* CPU time by continuously attempting to queue input frames to MediaPipe until they are accepted
|
||||
* or waste memory if MediaPipe accepts and stores many frames internally.
|
||||
*
|
||||
* @param context The {@link Context}.
|
||||
* @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 outputStreamName Name of the input video stream in the graph.
|
||||
*/
|
||||
public MediaPipeProcessor(
|
||||
public MediaPipeShaderProgram(
|
||||
Context context,
|
||||
boolean useHdr,
|
||||
String graphName,
|
||||
@ -100,8 +104,8 @@ import java.util.concurrent.Future;
|
||||
String inputStreamName,
|
||||
String outputStreamName) {
|
||||
checkState(LOADER.isAvailable());
|
||||
// TODO(b/227624622): Confirm whether MediaPipeProcessor could support HDR colors.
|
||||
checkArgument(!useHdr, "MediaPipeProcessor does not support HDR colors.");
|
||||
// TODO(b/227624622): Confirm whether MediaPipeShaderProgram could support HDR colors.
|
||||
checkArgument(!useHdr, "MediaPipeShaderProgram does not support HDR colors.");
|
||||
|
||||
this.isSingleFrameGraph = isSingleFrameGraph;
|
||||
singleThreadExecutorService =
|
||||
@ -109,7 +113,8 @@ import java.util.concurrent.Future;
|
||||
futures = new ArrayDeque<>();
|
||||
inputListener = new InputListener() {};
|
||||
outputListener = new OutputListener() {};
|
||||
errorListener = (frameProcessingException) -> {};
|
||||
errorListener = (videoFrameProcessingException) -> {};
|
||||
errorListenerExecutor = MoreExecutors.directExecutor();
|
||||
EglManager eglManager = new EglManager(EGL14.eglGetCurrentContext());
|
||||
frameProcessor =
|
||||
new FrameProcessor(
|
||||
@ -133,10 +138,11 @@ import java.util.concurrent.Future;
|
||||
this.outputListener = outputListener;
|
||||
frameProcessor.setConsumer(
|
||||
frame -> {
|
||||
TextureInfo texture =
|
||||
new TextureInfo(
|
||||
GlTextureInfo texture =
|
||||
new GlTextureInfo(
|
||||
frame.getTextureName(),
|
||||
/* fboId= */ C.INDEX_UNSET,
|
||||
/* rboId= */ C.INDEX_UNSET,
|
||||
frame.getWidth(),
|
||||
frame.getHeight());
|
||||
outputFrames.put(texture, frame);
|
||||
@ -145,16 +151,23 @@ import java.util.concurrent.Future;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setErrorListener(ErrorListener errorListener) {
|
||||
public void setErrorListener(Executor executor, ErrorListener errorListener) {
|
||||
this.errorListenerExecutor = executor;
|
||||
this.errorListener = errorListener;
|
||||
frameProcessor.setAsynchronousErrorListener(
|
||||
error -> errorListener.onFrameProcessingError(new FrameProcessingException(error)));
|
||||
error ->
|
||||
errorListenerExecutor.execute(
|
||||
() -> errorListener.onError(new VideoFrameProcessingException(error))));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void queueInputFrame(TextureInfo inputTexture, long presentationTimeUs) {
|
||||
public void setGlObjectsProvider(GlObjectsProvider glObjectsProvider) {}
|
||||
|
||||
@Override
|
||||
public void queueInputFrame(GlTextureInfo inputTexture, long presentationTimeUs) {
|
||||
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.
|
||||
// MediaPipe will fail if the timestamps are not monotonically increasing.
|
||||
// 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(
|
||||
AppTextureFrame appTextureFrame, TextureInfo inputTexture) {
|
||||
AppTextureFrame appTextureFrame, GlTextureInfo inputTexture) {
|
||||
acceptedFrame = false;
|
||||
frameProcessor.onNewFrame(appTextureFrame);
|
||||
try {
|
||||
appTextureFrame.waitUntilReleasedWithGpuSync();
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
errorListener.onFrameProcessingError(new FrameProcessingException(e));
|
||||
errorListenerExecutor.execute(
|
||||
() -> errorListener.onError(new VideoFrameProcessingException(e)));
|
||||
}
|
||||
if (acceptedFrame) {
|
||||
inputListener.onInputFrameProcessed(inputTexture);
|
||||
@ -192,7 +206,7 @@ import java.util.concurrent.Future;
|
||||
}
|
||||
|
||||
private void queueInputFrameAsynchronous(
|
||||
AppTextureFrame appTextureFrame, TextureInfo inputTexture) {
|
||||
AppTextureFrame appTextureFrame, GlTextureInfo inputTexture) {
|
||||
removeFinishedFutures();
|
||||
futures.add(
|
||||
checkStateNotNull(singleThreadExecutorService)
|
||||
@ -204,7 +218,8 @@ import java.util.concurrent.Future;
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
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
|
||||
public void releaseOutputFrame(TextureInfo outputTexture) {
|
||||
public void releaseOutputFrame(GlTextureInfo outputTexture) {
|
||||
checkStateNotNull(outputFrames.get(outputTexture)).release();
|
||||
if (isSingleFrameGraph) {
|
||||
inputListener.onReadyToAcceptInputFrame();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void flush() {
|
||||
// TODO(b/238302341) Support seeking in MediaPipeShaderProgram.
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void release() {
|
||||
if (isSingleFrameGraph) {
|
||||
@ -236,11 +257,13 @@ import java.util.concurrent.Future;
|
||||
singleThreadExecutorService.shutdown();
|
||||
try {
|
||||
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) {
|
||||
Thread.currentThread().interrupt();
|
||||
errorListener.onFrameProcessingError(new FrameProcessingException(e));
|
||||
errorListenerExecutor.execute(
|
||||
() -> errorListener.onError(new VideoFrameProcessingException(e)));
|
||||
}
|
||||
|
||||
frameProcessor.close();
|
||||
@ -272,10 +295,12 @@ import java.util.concurrent.Future;
|
||||
try {
|
||||
futures.remove().get();
|
||||
} catch (ExecutionException e) {
|
||||
errorListener.onFrameProcessingError(new FrameProcessingException(e));
|
||||
errorListenerExecutor.execute(
|
||||
() -> errorListener.onError(new VideoFrameProcessingException(e)));
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
errorListener.onFrameProcessingError(new FrameProcessingException(e));
|
||||
errorListenerExecutor.execute(
|
||||
() -> errorListener.onError(new VideoFrameProcessingException(e)));
|
||||
}
|
||||
}
|
||||
}
|
@ -17,11 +17,6 @@ apply from: "${buildscript.sourceFile.parentFile}/javadoc_util.gradle"
|
||||
class CombinedJavadocPlugin implements Plugin<Project> {
|
||||
|
||||
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
|
||||
void apply(Project project) {
|
||||
@ -83,71 +78,6 @@ class CombinedJavadocPlugin implements Plugin<Project> {
|
||||
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
|
||||
|
@ -14,7 +14,7 @@
|
||||
apply from: "$gradle.ext.androidxMediaSettingsDir/common_library_config.gradle"
|
||||
|
||||
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 project(modulePrefix + 'lib-common')
|
||||
compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion
|
||||
|
@ -85,7 +85,7 @@ public final class CastPlayer extends BasePlayer {
|
||||
|
||||
/** The {@link DeviceInfo} returned by {@link #getDeviceInfo() this player}. */
|
||||
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 {
|
||||
MediaLibraryInfo.registerModule("media3.cast");
|
||||
@ -104,11 +104,12 @@ public final class CastPlayer extends BasePlayer {
|
||||
COMMAND_SET_SPEED_AND_PITCH,
|
||||
COMMAND_GET_CURRENT_MEDIA_ITEM,
|
||||
COMMAND_GET_TIMELINE,
|
||||
COMMAND_GET_MEDIA_ITEMS_METADATA,
|
||||
COMMAND_SET_MEDIA_ITEMS_METADATA,
|
||||
COMMAND_GET_METADATA,
|
||||
COMMAND_SET_PLAYLIST_METADATA,
|
||||
COMMAND_SET_MEDIA_ITEM,
|
||||
COMMAND_CHANGE_MEDIA_ITEMS,
|
||||
COMMAND_GET_TRACKS)
|
||||
COMMAND_GET_TRACKS,
|
||||
COMMAND_RELEASE)
|
||||
.build();
|
||||
|
||||
public static final float MIN_SPEED_SUPPORTED = 0.5f;
|
||||
@ -320,6 +321,18 @@ public final class CastPlayer extends BasePlayer {
|
||||
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
|
||||
public void removeMediaItems(int fromIndex, int toIndex) {
|
||||
checkArgument(fromIndex >= 0 && toIndex >= fromIndex);
|
||||
@ -393,8 +406,9 @@ public final class CastPlayer extends BasePlayer {
|
||||
return playWhenReady.value;
|
||||
}
|
||||
|
||||
// We still call Listener#onSeekProcessed() for backwards compatibility with listeners that
|
||||
// don't implement onPositionDiscontinuity().
|
||||
// We still call Listener#onPositionDiscontinuity(@DiscontinuityReason int) for backwards
|
||||
// compatibility with listeners that don't implement
|
||||
// onPositionDiscontinuity(PositionInfo, PositionInfo, @DiscontinuityReason int).
|
||||
@SuppressWarnings("deprecation")
|
||||
@Override
|
||||
@VisibleForTesting(otherwise = PROTECTED)
|
||||
@ -448,8 +462,6 @@ public final class CastPlayer extends BasePlayer {
|
||||
}
|
||||
}
|
||||
updateAvailableCommandsAndNotifyIfChanged();
|
||||
} else if (pendingSeekCount == 0) {
|
||||
listeners.queueEvent(/* eventFlag= */ C.INDEX_UNSET, Listener::onSeekProcessed);
|
||||
}
|
||||
listeners.flushEvents();
|
||||
}
|
||||
@ -476,17 +488,6 @@ public final class CastPlayer extends BasePlayer {
|
||||
|
||||
@Override
|
||||
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;
|
||||
if (remoteMediaClient != null) {
|
||||
// TODO(b/69792021): Support or emulate stop without position reset.
|
||||
@ -765,22 +766,50 @@ public final class CastPlayer extends BasePlayer {
|
||||
return false;
|
||||
}
|
||||
|
||||
/** This method is not supported and does nothing. */
|
||||
/**
|
||||
* @deprecated Use {@link #setDeviceVolume(int, int)} instead.
|
||||
*/
|
||||
@Deprecated
|
||||
@Override
|
||||
public void setDeviceVolume(int volume) {}
|
||||
|
||||
/** This method is not supported and does nothing. */
|
||||
@Override
|
||||
public void setDeviceVolume(int volume, @C.VolumeFlags int flags) {}
|
||||
|
||||
/**
|
||||
* @deprecated Use {@link #increaseDeviceVolume(int)} instead.
|
||||
*/
|
||||
@Deprecated
|
||||
@Override
|
||||
public void increaseDeviceVolume() {}
|
||||
|
||||
/** This method is not supported and does nothing. */
|
||||
@Override
|
||||
public void increaseDeviceVolume(@C.VolumeFlags int flags) {}
|
||||
|
||||
/**
|
||||
* @deprecated Use {@link #decreaseDeviceVolume(int)} instead.
|
||||
*/
|
||||
@Deprecated
|
||||
@Override
|
||||
public void decreaseDeviceVolume() {}
|
||||
|
||||
/** This method is not supported and does nothing. */
|
||||
@Override
|
||||
public void decreaseDeviceVolume(@C.VolumeFlags int flags) {}
|
||||
|
||||
/**
|
||||
* @deprecated Use {@link #setDeviceMuted(boolean, int)} instead.
|
||||
*/
|
||||
@Deprecated
|
||||
@Override
|
||||
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.
|
||||
|
||||
// Call deprecated callbacks.
|
||||
@ -1419,9 +1448,6 @@ public final class CastPlayer extends BasePlayer {
|
||||
|
||||
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
|
||||
public void onResult(MediaChannelResult result) {
|
||||
int statusCode = result.getStatus().getStatusCode();
|
||||
@ -1434,7 +1460,6 @@ public final class CastPlayer extends BasePlayer {
|
||||
currentWindowIndex = pendingSeekWindowIndex;
|
||||
pendingSeekWindowIndex = C.INDEX_UNSET;
|
||||
pendingSeekPositionMs = C.TIME_UNSET;
|
||||
listeners.sendEvent(/* eventFlag= */ C.INDEX_UNSET, Listener::onSeekProcessed);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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_CURRENT_MEDIA_ITEM;
|
||||
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_TIMELINE;
|
||||
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_SET_DEVICE_VOLUME;
|
||||
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_SHUFFLE_MODE;
|
||||
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);
|
||||
}
|
||||
|
||||
@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")
|
||||
@Test
|
||||
public void addMediaItems_fillsTimeline() {
|
||||
@ -1360,8 +1392,8 @@ public class CastPlayerTest {
|
||||
assertThat(castPlayer.isCommandAvailable(COMMAND_SET_REPEAT_MODE)).isTrue();
|
||||
assertThat(castPlayer.isCommandAvailable(COMMAND_GET_CURRENT_MEDIA_ITEM)).isTrue();
|
||||
assertThat(castPlayer.isCommandAvailable(COMMAND_GET_TIMELINE)).isTrue();
|
||||
assertThat(castPlayer.isCommandAvailable(COMMAND_GET_MEDIA_ITEMS_METADATA)).isTrue();
|
||||
assertThat(castPlayer.isCommandAvailable(COMMAND_SET_MEDIA_ITEMS_METADATA)).isTrue();
|
||||
assertThat(castPlayer.isCommandAvailable(COMMAND_GET_METADATA)).isTrue();
|
||||
assertThat(castPlayer.isCommandAvailable(COMMAND_SET_PLAYLIST_METADATA)).isTrue();
|
||||
assertThat(castPlayer.isCommandAvailable(COMMAND_CHANGE_MEDIA_ITEMS)).isTrue();
|
||||
assertThat(castPlayer.isCommandAvailable(COMMAND_SET_MEDIA_ITEM)).isTrue();
|
||||
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_SET_VIDEO_SURFACE)).isFalse();
|
||||
assertThat(castPlayer.isCommandAvailable(COMMAND_GET_TEXT)).isFalse();
|
||||
assertThat(castPlayer.isCommandAvailable(Player.COMMAND_RELEASE)).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -172,6 +172,10 @@ public final class AdPlaybackState implements Bundleable {
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean isLivePostrollPlaceholder() {
|
||||
return isServerSideInserted && timeUs == C.TIME_END_OF_SOURCE && count == C.LENGTH_UNSET;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(@Nullable Object 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.
|
||||
// In practice we expect there to be few ad groups so the search shouldn't be expensive.
|
||||
int index = adGroupCount - 1;
|
||||
index -= isLivePostrollPlaceholder(index) ? 1 : 0;
|
||||
while (index >= 0 && isPositionBeforeAdGroup(positionUs, periodDurationUs, index)) {
|
||||
index--;
|
||||
}
|
||||
@ -976,6 +981,49 @@ public final class AdPlaybackState implements Bundleable {
|
||||
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.
|
||||
*
|
||||
@ -1088,15 +1136,21 @@ public final class AdPlaybackState implements Bundleable {
|
||||
private boolean isPositionBeforeAdGroup(
|
||||
long positionUs, long periodDurationUs, int adGroupIndex) {
|
||||
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;
|
||||
}
|
||||
long adGroupPositionUs = getAdGroup(adGroupIndex).timeUs;
|
||||
AdGroup adGroup = getAdGroup(adGroupIndex);
|
||||
long adGroupPositionUs = adGroup.timeUs;
|
||||
if (adGroupPositionUs == C.TIME_END_OF_SOURCE) {
|
||||
return periodDurationUs == C.TIME_UNSET || positionUs < periodDurationUs;
|
||||
} else {
|
||||
return positionUs < adGroupPositionUs;
|
||||
// Handling postroll: The requested position is considered before a postroll when a)
|
||||
// the period duration is unknown (last period in a live stream), or when b) the postroll is a
|
||||
// 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.
|
||||
|
@ -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
|
||||
public final void removeMediaItem(int index) {
|
||||
removeMediaItems(/* fromIndex= */ index, /* toIndex= */ index + 1);
|
||||
|
@ -60,11 +60,13 @@ public final class C {
|
||||
*/
|
||||
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;
|
||||
|
||||
/** 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. */
|
||||
public static final float RATE_UNSET = -Float.MAX_VALUE;
|
||||
@ -93,36 +95,6 @@ public final class C {
|
||||
/** The number of bytes per float. */
|
||||
@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. */
|
||||
@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})
|
||||
@UnstableApi
|
||||
public @interface CryptoMode {}
|
||||
/**
|
||||
* @see MediaCodec#CRYPTO_MODE_UNENCRYPTED
|
||||
*/
|
||||
/** See {@link MediaCodec#CRYPTO_MODE_UNENCRYPTED}. */
|
||||
@UnstableApi public static final int CRYPTO_MODE_UNENCRYPTED = MediaCodec.CRYPTO_MODE_UNENCRYPTED;
|
||||
/**
|
||||
* @see MediaCodec#CRYPTO_MODE_AES_CTR
|
||||
*/
|
||||
/** See {@link MediaCodec#CRYPTO_MODE_AES_CTR}. */
|
||||
@UnstableApi public static final int CRYPTO_MODE_AES_CTR = MediaCodec.CRYPTO_MODE_AES_CTR;
|
||||
/**
|
||||
* @see MediaCodec#CRYPTO_MODE_AES_CBC
|
||||
*/
|
||||
/** See {@link 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_DOLBY_TRUEHD,
|
||||
ENCODING_OPUS,
|
||||
ENCODING_DTS_UHD_P2,
|
||||
})
|
||||
public @interface Encoding {}
|
||||
|
||||
@ -250,17 +217,11 @@ public final class C {
|
||||
ENCODING_PCM_FLOAT
|
||||
})
|
||||
public @interface PcmEncoding {}
|
||||
/**
|
||||
* @see AudioFormat#ENCODING_INVALID
|
||||
*/
|
||||
/** See {@link AudioFormat#ENCODING_INVALID}. */
|
||||
@UnstableApi public static final int ENCODING_INVALID = AudioFormat.ENCODING_INVALID;
|
||||
/**
|
||||
* @see AudioFormat#ENCODING_PCM_8BIT
|
||||
*/
|
||||
/** See {@link AudioFormat#ENCODING_PCM_8BIT}. */
|
||||
@UnstableApi public static final int ENCODING_PCM_8BIT = AudioFormat.ENCODING_PCM_8BIT;
|
||||
/**
|
||||
* @see AudioFormat#ENCODING_PCM_16BIT
|
||||
*/
|
||||
/** See {@link 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. */
|
||||
@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;
|
||||
/** PCM encoding with 32 bits per sample. */
|
||||
@UnstableApi public static final int ENCODING_PCM_32BIT = 0x30000000;
|
||||
/**
|
||||
* @see AudioFormat#ENCODING_PCM_FLOAT
|
||||
*/
|
||||
/** See {@link AudioFormat#ENCODING_PCM_FLOAT}. */
|
||||
@UnstableApi public static final int ENCODING_PCM_FLOAT = AudioFormat.ENCODING_PCM_FLOAT;
|
||||
/**
|
||||
* @see AudioFormat#ENCODING_MP3
|
||||
*/
|
||||
/** See {@link AudioFormat#ENCODING_MP3}. */
|
||||
@UnstableApi public static final int ENCODING_MP3 = AudioFormat.ENCODING_MP3;
|
||||
/**
|
||||
* @see AudioFormat#ENCODING_AAC_LC
|
||||
*/
|
||||
/** See {@link AudioFormat#ENCODING_AAC_LC}. */
|
||||
@UnstableApi public static final int ENCODING_AAC_LC = AudioFormat.ENCODING_AAC_LC;
|
||||
/**
|
||||
* @see AudioFormat#ENCODING_AAC_HE_V1
|
||||
*/
|
||||
/** See {@link AudioFormat#ENCODING_AAC_HE_V1}. */
|
||||
@UnstableApi public static final int ENCODING_AAC_HE_V1 = AudioFormat.ENCODING_AAC_HE_V1;
|
||||
/**
|
||||
* @see AudioFormat#ENCODING_AAC_HE_V2
|
||||
*/
|
||||
/** See {@link AudioFormat#ENCODING_AAC_HE_V2}. */
|
||||
@UnstableApi public static final int ENCODING_AAC_HE_V2 = AudioFormat.ENCODING_AAC_HE_V2;
|
||||
/**
|
||||
* @see AudioFormat#ENCODING_AAC_XHE
|
||||
*/
|
||||
/** See {@link AudioFormat#ENCODING_AAC_XHE}. */
|
||||
@UnstableApi public static final int ENCODING_AAC_XHE = AudioFormat.ENCODING_AAC_XHE;
|
||||
/**
|
||||
* @see AudioFormat#ENCODING_AAC_ELD
|
||||
*/
|
||||
/** See {@link AudioFormat#ENCODING_AAC_ELD}. */
|
||||
@UnstableApi public static final int ENCODING_AAC_ELD = AudioFormat.ENCODING_AAC_ELD;
|
||||
/** AAC Error Resilient Bit-Sliced Arithmetic Coding. */
|
||||
@UnstableApi public static final int ENCODING_AAC_ER_BSAC = 0x40000000;
|
||||
/**
|
||||
* @see AudioFormat#ENCODING_AC3
|
||||
*/
|
||||
/** See {@link AudioFormat#ENCODING_AC3}. */
|
||||
@UnstableApi public static final int ENCODING_AC3 = AudioFormat.ENCODING_AC3;
|
||||
/**
|
||||
* @see AudioFormat#ENCODING_E_AC3
|
||||
*/
|
||||
/** See {@link AudioFormat#ENCODING_E_AC3}. */
|
||||
@UnstableApi public static final int ENCODING_E_AC3 = AudioFormat.ENCODING_E_AC3;
|
||||
/**
|
||||
* @see AudioFormat#ENCODING_E_AC3_JOC
|
||||
*/
|
||||
/** See {@link AudioFormat#ENCODING_E_AC3_JOC}. */
|
||||
@UnstableApi public static final int ENCODING_E_AC3_JOC = AudioFormat.ENCODING_E_AC3_JOC;
|
||||
/**
|
||||
* @see AudioFormat#ENCODING_AC4
|
||||
*/
|
||||
/** See {@link AudioFormat#ENCODING_AC4}. */
|
||||
@UnstableApi public static final int ENCODING_AC4 = AudioFormat.ENCODING_AC4;
|
||||
/**
|
||||
* @see AudioFormat#ENCODING_DTS
|
||||
*/
|
||||
/** See {@link AudioFormat#ENCODING_DTS}. */
|
||||
@UnstableApi public static final int ENCODING_DTS = AudioFormat.ENCODING_DTS;
|
||||
/**
|
||||
* @see AudioFormat#ENCODING_DTS_HD
|
||||
*/
|
||||
/** See {@link AudioFormat#ENCODING_DTS_HD}. */
|
||||
@UnstableApi public static final int ENCODING_DTS_HD = AudioFormat.ENCODING_DTS_HD;
|
||||
/**
|
||||
* @see AudioFormat#ENCODING_DOLBY_TRUEHD
|
||||
*/
|
||||
// TODO(internal b/283949283): Use AudioFormat.ENCODING_DTS_UHD_P2 when Android 14 is released.
|
||||
@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;
|
||||
/**
|
||||
* @see AudioFormat#ENCODING_OPUS
|
||||
*/
|
||||
/** See {@link 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})
|
||||
public @interface SpatializationBehavior {}
|
||||
|
||||
/**
|
||||
* @see AudioAttributes#SPATIALIZATION_BEHAVIOR_AUTO
|
||||
*/
|
||||
/** See {@link AudioAttributes#SPATIALIZATION_BEHAVIOR_AUTO}. */
|
||||
public static final int SPATIALIZATION_BEHAVIOR_AUTO =
|
||||
AudioAttributes.SPATIALIZATION_BEHAVIOR_AUTO;
|
||||
/**
|
||||
* @see AudioAttributes#SPATIALIZATION_BEHAVIOR_NEVER
|
||||
*/
|
||||
/** See {@link AudioAttributes#SPATIALIZATION_BEHAVIOR_NEVER}. */
|
||||
public static final int SPATIALIZATION_BEHAVIOR_NEVER =
|
||||
AudioAttributes.SPATIALIZATION_BEHAVIOR_NEVER;
|
||||
|
||||
@ -376,37 +305,54 @@ public final class C {
|
||||
STREAM_TYPE_DEFAULT
|
||||
})
|
||||
public @interface StreamType {}
|
||||
/**
|
||||
* @see AudioManager#STREAM_ALARM
|
||||
*/
|
||||
/** See {@link AudioManager#STREAM_ALARM}. */
|
||||
@UnstableApi public static final int STREAM_TYPE_ALARM = AudioManager.STREAM_ALARM;
|
||||
/**
|
||||
* @see AudioManager#STREAM_DTMF
|
||||
*/
|
||||
/** See {@link AudioManager#STREAM_DTMF}. */
|
||||
@UnstableApi public static final int STREAM_TYPE_DTMF = AudioManager.STREAM_DTMF;
|
||||
/**
|
||||
* @see AudioManager#STREAM_MUSIC
|
||||
*/
|
||||
/** See {@link AudioManager#STREAM_MUSIC}. */
|
||||
@UnstableApi public static final int STREAM_TYPE_MUSIC = AudioManager.STREAM_MUSIC;
|
||||
/**
|
||||
* @see AudioManager#STREAM_NOTIFICATION
|
||||
*/
|
||||
/** See {@link AudioManager#STREAM_NOTIFICATION}. */
|
||||
@UnstableApi public static final int STREAM_TYPE_NOTIFICATION = AudioManager.STREAM_NOTIFICATION;
|
||||
/**
|
||||
* @see AudioManager#STREAM_RING
|
||||
*/
|
||||
/** See {@link AudioManager#STREAM_RING}. */
|
||||
@UnstableApi public static final int STREAM_TYPE_RING = AudioManager.STREAM_RING;
|
||||
/**
|
||||
* @see AudioManager#STREAM_SYSTEM
|
||||
*/
|
||||
/** See {@link AudioManager#STREAM_SYSTEM}. */
|
||||
@UnstableApi public static final int STREAM_TYPE_SYSTEM = AudioManager.STREAM_SYSTEM;
|
||||
/**
|
||||
* @see AudioManager#STREAM_VOICE_CALL
|
||||
*/
|
||||
/** See {@link 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}. */
|
||||
@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:
|
||||
*
|
||||
@ -480,9 +426,7 @@ public final class C {
|
||||
flag = true,
|
||||
value = {FLAG_AUDIBILITY_ENFORCED})
|
||||
public @interface AudioFlags {}
|
||||
/**
|
||||
* @see android.media.AudioAttributes#FLAG_AUDIBILITY_ENFORCED
|
||||
*/
|
||||
/** See {@link android.media.AudioAttributes#FLAG_AUDIBILITY_ENFORCED}. */
|
||||
public static final int FLAG_AUDIBILITY_ENFORCED =
|
||||
android.media.AudioAttributes.FLAG_AUDIBILITY_ENFORCED;
|
||||
|
||||
@ -520,78 +464,46 @@ public final class C {
|
||||
USAGE_VOICE_COMMUNICATION_SIGNALLING
|
||||
})
|
||||
public @interface AudioUsage {}
|
||||
/**
|
||||
* @see android.media.AudioAttributes#USAGE_ALARM
|
||||
*/
|
||||
/** See {@link android.media.AudioAttributes#USAGE_ALARM}. */
|
||||
public static final int USAGE_ALARM = android.media.AudioAttributes.USAGE_ALARM;
|
||||
/**
|
||||
* @see android.media.AudioAttributes#USAGE_ASSISTANCE_ACCESSIBILITY
|
||||
*/
|
||||
/** See {@link android.media.AudioAttributes#USAGE_ASSISTANCE_ACCESSIBILITY}. */
|
||||
public static final int USAGE_ASSISTANCE_ACCESSIBILITY =
|
||||
android.media.AudioAttributes.USAGE_ASSISTANCE_ACCESSIBILITY;
|
||||
/**
|
||||
* @see android.media.AudioAttributes#USAGE_ASSISTANCE_NAVIGATION_GUIDANCE
|
||||
*/
|
||||
/** See {@link android.media.AudioAttributes#USAGE_ASSISTANCE_NAVIGATION_GUIDANCE}. */
|
||||
public static final int USAGE_ASSISTANCE_NAVIGATION_GUIDANCE =
|
||||
android.media.AudioAttributes.USAGE_ASSISTANCE_NAVIGATION_GUIDANCE;
|
||||
/**
|
||||
* @see android.media.AudioAttributes#USAGE_ASSISTANCE_SONIFICATION
|
||||
*/
|
||||
/** See {@link android.media.AudioAttributes#USAGE_ASSISTANCE_SONIFICATION}. */
|
||||
public static final int USAGE_ASSISTANCE_SONIFICATION =
|
||||
android.media.AudioAttributes.USAGE_ASSISTANCE_SONIFICATION;
|
||||
/**
|
||||
* @see android.media.AudioAttributes#USAGE_ASSISTANT
|
||||
*/
|
||||
/** See {@link android.media.AudioAttributes#USAGE_ASSISTANT}. */
|
||||
public static final int USAGE_ASSISTANT = android.media.AudioAttributes.USAGE_ASSISTANT;
|
||||
/**
|
||||
* @see android.media.AudioAttributes#USAGE_GAME
|
||||
*/
|
||||
/** See {@link android.media.AudioAttributes#USAGE_GAME}. */
|
||||
public static final int USAGE_GAME = android.media.AudioAttributes.USAGE_GAME;
|
||||
/**
|
||||
* @see android.media.AudioAttributes#USAGE_MEDIA
|
||||
*/
|
||||
/** See {@link android.media.AudioAttributes#USAGE_MEDIA}. */
|
||||
public static final int USAGE_MEDIA = android.media.AudioAttributes.USAGE_MEDIA;
|
||||
/**
|
||||
* @see android.media.AudioAttributes#USAGE_NOTIFICATION
|
||||
*/
|
||||
/** See {@link android.media.AudioAttributes#USAGE_NOTIFICATION}. */
|
||||
public static final int USAGE_NOTIFICATION = android.media.AudioAttributes.USAGE_NOTIFICATION;
|
||||
/**
|
||||
* @see android.media.AudioAttributes#USAGE_NOTIFICATION_COMMUNICATION_DELAYED
|
||||
*/
|
||||
/** See {@link android.media.AudioAttributes#USAGE_NOTIFICATION_COMMUNICATION_DELAYED}. */
|
||||
public static final int USAGE_NOTIFICATION_COMMUNICATION_DELAYED =
|
||||
android.media.AudioAttributes.USAGE_NOTIFICATION_COMMUNICATION_DELAYED;
|
||||
/**
|
||||
* @see android.media.AudioAttributes#USAGE_NOTIFICATION_COMMUNICATION_INSTANT
|
||||
*/
|
||||
/** See {@link android.media.AudioAttributes#USAGE_NOTIFICATION_COMMUNICATION_INSTANT}. */
|
||||
public static final int USAGE_NOTIFICATION_COMMUNICATION_INSTANT =
|
||||
android.media.AudioAttributes.USAGE_NOTIFICATION_COMMUNICATION_INSTANT;
|
||||
/**
|
||||
* @see android.media.AudioAttributes#USAGE_NOTIFICATION_COMMUNICATION_REQUEST
|
||||
*/
|
||||
/** See {@link android.media.AudioAttributes#USAGE_NOTIFICATION_COMMUNICATION_REQUEST}. */
|
||||
public static final int USAGE_NOTIFICATION_COMMUNICATION_REQUEST =
|
||||
android.media.AudioAttributes.USAGE_NOTIFICATION_COMMUNICATION_REQUEST;
|
||||
/**
|
||||
* @see android.media.AudioAttributes#USAGE_NOTIFICATION_EVENT
|
||||
*/
|
||||
/** See {@link android.media.AudioAttributes#USAGE_NOTIFICATION_EVENT}. */
|
||||
public static final int USAGE_NOTIFICATION_EVENT =
|
||||
android.media.AudioAttributes.USAGE_NOTIFICATION_EVENT;
|
||||
/**
|
||||
* @see android.media.AudioAttributes#USAGE_NOTIFICATION_RINGTONE
|
||||
*/
|
||||
/** See {@link android.media.AudioAttributes#USAGE_NOTIFICATION_RINGTONE}. */
|
||||
public static final int USAGE_NOTIFICATION_RINGTONE =
|
||||
android.media.AudioAttributes.USAGE_NOTIFICATION_RINGTONE;
|
||||
/**
|
||||
* @see android.media.AudioAttributes#USAGE_UNKNOWN
|
||||
*/
|
||||
/** See {@link android.media.AudioAttributes#USAGE_UNKNOWN}. */
|
||||
public static final int USAGE_UNKNOWN = android.media.AudioAttributes.USAGE_UNKNOWN;
|
||||
/**
|
||||
* @see android.media.AudioAttributes#USAGE_VOICE_COMMUNICATION
|
||||
*/
|
||||
/** See {@link android.media.AudioAttributes#USAGE_VOICE_COMMUNICATION}. */
|
||||
public static final int USAGE_VOICE_COMMUNICATION =
|
||||
android.media.AudioAttributes.USAGE_VOICE_COMMUNICATION;
|
||||
/**
|
||||
* @see android.media.AudioAttributes#USAGE_VOICE_COMMUNICATION_SIGNALLING
|
||||
*/
|
||||
/** See {@link android.media.AudioAttributes#USAGE_VOICE_COMMUNICATION_SIGNALLING}. */
|
||||
public static final int USAGE_VOICE_COMMUNICATION_SIGNALLING =
|
||||
android.media.AudioAttributes.USAGE_VOICE_COMMUNICATION_SIGNALLING;
|
||||
|
||||
@ -1054,8 +966,8 @@ public final class C {
|
||||
|
||||
// LINT.IfChange(color_space)
|
||||
/**
|
||||
* Video colorspaces. One of {@link Format#NO_VALUE}, {@link #COLOR_SPACE_BT601}, {@link
|
||||
* #COLOR_SPACE_BT709} or {@link #COLOR_SPACE_BT2020}.
|
||||
* Video color spaces, also referred to as color standards. One of {@link Format#NO_VALUE}, {@link
|
||||
* #COLOR_SPACE_BT601}, {@link #COLOR_SPACE_BT709} or {@link #COLOR_SPACE_BT2020}.
|
||||
*/
|
||||
@UnstableApi
|
||||
@Documented
|
||||
@ -1063,41 +975,52 @@ public final class C {
|
||||
@Target(TYPE_USE)
|
||||
@IntDef({Format.NO_VALUE, COLOR_SPACE_BT601, COLOR_SPACE_BT709, COLOR_SPACE_BT2020})
|
||||
public @interface ColorSpace {}
|
||||
/**
|
||||
* @see MediaFormat#COLOR_STANDARD_BT601_PAL
|
||||
*/
|
||||
/** See {@link MediaFormat#COLOR_STANDARD_BT601_PAL}. */
|
||||
@UnstableApi public static final int COLOR_SPACE_BT601 = MediaFormat.COLOR_STANDARD_BT601_PAL;
|
||||
/**
|
||||
* @see MediaFormat#COLOR_STANDARD_BT709
|
||||
*/
|
||||
/** See {@link MediaFormat#COLOR_STANDARD_BT709}. */
|
||||
@UnstableApi public static final int COLOR_SPACE_BT709 = MediaFormat.COLOR_STANDARD_BT709;
|
||||
/**
|
||||
* @see MediaFormat#COLOR_STANDARD_BT2020
|
||||
*/
|
||||
/** See {@link MediaFormat#COLOR_STANDARD_BT2020}. */
|
||||
@UnstableApi public static final int COLOR_SPACE_BT2020 = MediaFormat.COLOR_STANDARD_BT2020;
|
||||
|
||||
// LINT.IfChange(color_transfer)
|
||||
/**
|
||||
* Video color transfer characteristics. One of {@link Format#NO_VALUE}, {@link
|
||||
* #COLOR_TRANSFER_SDR}, {@link #COLOR_TRANSFER_ST2084} or {@link #COLOR_TRANSFER_HLG}.
|
||||
* Video/image color transfer characteristics. One of {@link Format#NO_VALUE}, {@link
|
||||
* #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
|
||||
@Documented
|
||||
@Retention(RetentionPolicy.SOURCE)
|
||||
@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 {}
|
||||
/**
|
||||
* @see MediaFormat#COLOR_TRANSFER_SDR_VIDEO
|
||||
*/
|
||||
/** See {@link MediaFormat#COLOR_TRANSFER_LINEAR}. */
|
||||
@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;
|
||||
/**
|
||||
* @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;
|
||||
|
||||
// LINT.IfChange(color_range)
|
||||
@ -1111,13 +1034,9 @@ public final class C {
|
||||
@Target(TYPE_USE)
|
||||
@IntDef({Format.NO_VALUE, COLOR_RANGE_LIMITED, COLOR_RANGE_FULL})
|
||||
public @interface ColorRange {}
|
||||
/**
|
||||
* @see MediaFormat#COLOR_RANGE_LIMITED
|
||||
*/
|
||||
/** See {@link MediaFormat#COLOR_RANGE_LIMITED}. */
|
||||
@UnstableApi public static final int COLOR_RANGE_LIMITED = MediaFormat.COLOR_RANGE_LIMITED;
|
||||
/**
|
||||
* @see MediaFormat#COLOR_RANGE_FULL
|
||||
*/
|
||||
/** See {@link MediaFormat#COLOR_RANGE_FULL}. */
|
||||
@UnstableApi public static final int COLOR_RANGE_FULL = MediaFormat.COLOR_RANGE_FULL;
|
||||
|
||||
/** Video projection types. */
|
||||
|
@ -19,6 +19,7 @@ import android.os.Bundle;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.media3.common.util.UnstableApi;
|
||||
import androidx.media3.common.util.Util;
|
||||
import com.google.errorprone.annotations.CanIgnoreReturnValue;
|
||||
import java.util.Arrays;
|
||||
import org.checkerframework.dataflow.qual.Pure;
|
||||
|
||||
@ -31,6 +32,96 @@ import org.checkerframework.dataflow.qual.Pure;
|
||||
@UnstableApi
|
||||
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. */
|
||||
public static final ColorInfo SDR_BT709_LIMITED =
|
||||
new ColorInfo(
|
||||
@ -39,6 +130,17 @@ public final class ColorInfo implements Bundleable {
|
||||
C.COLOR_TRANSFER_SDR,
|
||||
/* 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
|
||||
* 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 7: // SMPTE 240M.
|
||||
return C.COLOR_TRANSFER_SDR;
|
||||
case 4:
|
||||
return C.COLOR_TRANSFER_GAMMA_2_2;
|
||||
case 13:
|
||||
return C.COLOR_TRANSFER_SRGB;
|
||||
case 16:
|
||||
return C.COLOR_TRANSFER_ST2084;
|
||||
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) {
|
||||
return colorInfo != null
|
||||
&& colorInfo.colorTransfer != Format.NO_VALUE
|
||||
&& colorInfo.colorTransfer != C.COLOR_TRANSFER_SDR;
|
||||
&& (colorInfo.colorTransfer == C.COLOR_TRANSFER_HLG
|
||||
|| colorInfo.colorTransfer == C.COLOR_TRANSFER_ST2084);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
/** The {@link C.ColorSpace}. */
|
||||
public final @C.ColorSpace int colorSpace;
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
/** The {@link C.ColorRange}. */
|
||||
public final @C.ColorRange int colorRange;
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
/** The {@link C.ColorTransfer}. */
|
||||
public final @C.ColorTransfer int colorTransfer;
|
||||
|
||||
/** 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 colorTransfer The color transfer characteristics of the video.
|
||||
* @param hdrStaticInfo HdrStaticInfo as defined in CTA-861.3, or null if none specified.
|
||||
* @deprecated Use {@link Builder}.
|
||||
*/
|
||||
@Deprecated
|
||||
public ColorInfo(
|
||||
@C.ColorSpace int colorSpace,
|
||||
@C.ColorRange int colorRange,
|
||||
@ -134,6 +237,39 @@ public final class ColorInfo implements Bundleable {
|
||||
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
|
||||
public boolean equals(@Nullable Object obj) {
|
||||
if (this == obj) {
|
||||
@ -152,16 +288,68 @@ public final class ColorInfo implements Bundleable {
|
||||
@Override
|
||||
public String toString() {
|
||||
return "ColorInfo("
|
||||
+ colorSpace
|
||||
+ colorSpaceToString(colorSpace)
|
||||
+ ", "
|
||||
+ colorRange
|
||||
+ colorRangeToString(colorRange)
|
||||
+ ", "
|
||||
+ colorTransfer
|
||||
+ colorTransferToString(colorTransfer)
|
||||
+ ", "
|
||||
+ (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
|
||||
public int hashCode() {
|
||||
if (hashCode == 0) {
|
||||
|
@ -19,7 +19,7 @@ import android.view.SurfaceView;
|
||||
import androidx.annotation.Nullable;
|
||||
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
|
||||
public interface DebugViewProvider {
|
||||
|
||||
|
@ -17,11 +17,15 @@ package androidx.media3.common;
|
||||
|
||||
import static java.lang.annotation.ElementType.TYPE_USE;
|
||||
|
||||
import android.media.MediaRouter2;
|
||||
import android.os.Bundle;
|
||||
import androidx.annotation.IntDef;
|
||||
import androidx.annotation.IntRange;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.media3.common.util.Assertions;
|
||||
import androidx.media3.common.util.UnstableApi;
|
||||
import androidx.media3.common.util.Util;
|
||||
import com.google.errorprone.annotations.CanIgnoreReturnValue;
|
||||
import java.lang.annotation.Documented;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
@ -45,22 +49,116 @@ public final class DeviceInfo implements Bundleable {
|
||||
public static final int PLAYBACK_TYPE_REMOTE = 1;
|
||||
|
||||
/** Unknown DeviceInfo. */
|
||||
public static final DeviceInfo UNKNOWN =
|
||||
new DeviceInfo(PLAYBACK_TYPE_LOCAL, /* minVolume= */ 0, /* maxVolume= */ 0);
|
||||
public static final DeviceInfo UNKNOWN = new Builder(PLAYBACK_TYPE_LOCAL).build();
|
||||
|
||||
/** 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. */
|
||||
public final @PlaybackType int playbackType;
|
||||
/** The minimum volume that the device supports. */
|
||||
@IntRange(from = 0)
|
||||
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;
|
||||
/**
|
||||
* 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
|
||||
public DeviceInfo(@PlaybackType int playbackType, int minVolume, int maxVolume) {
|
||||
this.playbackType = playbackType;
|
||||
this.minVolume = minVolume;
|
||||
this.maxVolume = maxVolume;
|
||||
@Deprecated
|
||||
public DeviceInfo(
|
||||
@PlaybackType int playbackType,
|
||||
@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
|
||||
@ -74,7 +172,8 @@ public final class DeviceInfo implements Bundleable {
|
||||
DeviceInfo other = (DeviceInfo) obj;
|
||||
return playbackType == other.playbackType
|
||||
&& minVolume == other.minVolume
|
||||
&& maxVolume == other.maxVolume;
|
||||
&& maxVolume == other.maxVolume
|
||||
&& Util.areEqual(routingControllerId, other.routingControllerId);
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -83,6 +182,7 @@ public final class DeviceInfo implements Bundleable {
|
||||
result = 31 * result + playbackType;
|
||||
result = 31 * result + minVolume;
|
||||
result = 31 * result + maxVolume;
|
||||
result = 31 * result + (routingControllerId == null ? 0 : routingControllerId.hashCode());
|
||||
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_MIN_VOLUME = Util.intToStringMaxRadix(1);
|
||||
private static final String FIELD_MAX_VOLUME = Util.intToStringMaxRadix(2);
|
||||
private static final String FIELD_ROUTING_CONTROLLER_ID = Util.intToStringMaxRadix(3);
|
||||
|
||||
@UnstableApi
|
||||
@Override
|
||||
public Bundle toBundle() {
|
||||
Bundle bundle = new Bundle();
|
||||
if (playbackType != PLAYBACK_TYPE_LOCAL) {
|
||||
bundle.putInt(FIELD_PLAYBACK_TYPE, playbackType);
|
||||
}
|
||||
if (minVolume != 0) {
|
||||
bundle.putInt(FIELD_MIN_VOLUME, minVolume);
|
||||
}
|
||||
if (maxVolume != 0) {
|
||||
bundle.putInt(FIELD_MAX_VOLUME, maxVolume);
|
||||
}
|
||||
if (routingControllerId != null) {
|
||||
bundle.putString(FIELD_ROUTING_CONTROLLER_ID, routingControllerId);
|
||||
}
|
||||
return bundle;
|
||||
}
|
||||
|
||||
@ -110,6 +220,11 @@ public final class DeviceInfo implements Bundleable {
|
||||
bundle.getInt(FIELD_PLAYBACK_TYPE, /* defaultValue= */ PLAYBACK_TYPE_LOCAL);
|
||||
int minVolume = bundle.getInt(FIELD_MIN_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();
|
||||
};
|
||||
}
|
||||
|
@ -134,7 +134,7 @@ public final class FileTypes {
|
||||
/**
|
||||
* 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) {
|
||||
if (mimeType == null) {
|
||||
|
@ -748,12 +748,12 @@ public final class Format implements Bundleable {
|
||||
|
||||
// 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;
|
||||
|
||||
// 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;
|
||||
/**
|
||||
* 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.
|
||||
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) {
|
||||
id = builder.id;
|
||||
label = builder.label;
|
||||
@ -1080,42 +902,6 @@ public final class Format implements Bundleable {
|
||||
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
|
||||
@SuppressWarnings("ReferenceEquality")
|
||||
public Format withManifestFormatInfo(Format manifestFormat) {
|
||||
@ -1184,63 +970,6 @@ public final class Format implements Bundleable {
|
||||
.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}. */
|
||||
@UnstableApi
|
||||
public Format copyWithCryptoType(@C.CryptoType int cryptoType) {
|
||||
@ -1278,6 +1007,8 @@ public final class Format implements Bundleable {
|
||||
+ height
|
||||
+ ", "
|
||||
+ frameRate
|
||||
+ ", "
|
||||
+ colorInfo
|
||||
+ "]"
|
||||
+ ", ["
|
||||
+ channelCount
|
||||
@ -1444,6 +1175,9 @@ public final class Format implements Bundleable {
|
||||
if (format.width != NO_VALUE && format.height != NO_VALUE) {
|
||||
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) {
|
||||
builder.append(", fps=").append(format.frameRate);
|
||||
}
|
||||
|
@ -147,6 +147,18 @@ public class ForwardingPlayer implements Player {
|
||||
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. */
|
||||
@Override
|
||||
public void removeMediaItem(int index) {
|
||||
@ -478,20 +490,6 @@ public class ForwardingPlayer implements Player {
|
||||
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. */
|
||||
@Override
|
||||
public void release() {
|
||||
@ -860,30 +858,66 @@ public class ForwardingPlayer implements Player {
|
||||
return player.isDeviceMuted();
|
||||
}
|
||||
|
||||
/** Calls {@link Player#setDeviceVolume(int)} on the delegate. */
|
||||
/**
|
||||
* @deprecated Use {@link #setDeviceVolume(int, int)} instead.
|
||||
*/
|
||||
@Deprecated
|
||||
@Override
|
||||
public void setDeviceVolume(int 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
|
||||
public void 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
|
||||
public void 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
|
||||
public void setDeviceMuted(boolean 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. */
|
||||
public Player getWrappedPlayer() {
|
||||
return player;
|
||||
@ -1032,12 +1066,6 @@ public class ForwardingPlayer implements Player {
|
||||
listener.onMaxSeekToPreviousPositionChanged(maxSeekToPreviousPositionMs);
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("deprecation")
|
||||
public void onSeekProcessed() {
|
||||
listener.onSeekProcessed();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onVideoSizeChanged(VideoSize videoSize) {
|
||||
listener.onVideoSizeChanged(videoSize);
|
||||
|
@ -18,10 +18,83 @@ package androidx.media3.common;
|
||||
import static androidx.media3.common.util.Assertions.checkArgument;
|
||||
|
||||
import androidx.media3.common.util.UnstableApi;
|
||||
import com.google.errorprone.annotations.CanIgnoreReturnValue;
|
||||
|
||||
/** Value class specifying information about a decoded video frame. */
|
||||
@UnstableApi
|
||||
public class FrameInfo {
|
||||
|
||||
/** 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. */
|
||||
public final int width;
|
||||
/** The height of the frame, in pixels. */
|
||||
@ -29,32 +102,22 @@ public class FrameInfo {
|
||||
/** The ratio of width over height for each pixel. */
|
||||
public final float pixelWidthHeightRatio;
|
||||
/**
|
||||
* An offset in microseconds that is part of the input timestamps and should be ignored for
|
||||
* processing but added back to the output timestamps.
|
||||
* The offset that must be added to the frame presentation timestamp, in microseconds.
|
||||
*
|
||||
* <p>The offset stays constant within a stream but changes in between streams to ensure that
|
||||
* frame timestamps are always monotonically increasing.
|
||||
* <p>This offset is not part of the input timestamps. It is added to the frame timestamps before
|
||||
* processing, and is retained in the output timestamps.
|
||||
*/
|
||||
public final long streamOffsetUs;
|
||||
public final long offsetToAddUs;
|
||||
|
||||
// TODO(b/227624622): Add color space information for HDR.
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
private FrameInfo(int width, int height, float pixelWidthHeightRatio, long offsetToAddUs) {
|
||||
checkArgument(width > 0, "width must be positive, but is: " + width);
|
||||
checkArgument(height > 0, "height must be positive, but is: " + height);
|
||||
|
||||
this.width = width;
|
||||
this.height = height;
|
||||
this.pixelWidthHeightRatio = pixelWidthHeightRatio;
|
||||
this.streamOffsetUs = streamOffsetUs;
|
||||
this.offsetToAddUs = offsetToAddUs;
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
@ -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;
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -23,6 +23,7 @@ import android.os.Bundle;
|
||||
import androidx.annotation.IntRange;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.media3.common.util.Assertions;
|
||||
import androidx.media3.common.util.BundleableUtil;
|
||||
import androidx.media3.common.util.UnstableApi;
|
||||
import androidx.media3.common.util.Util;
|
||||
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. */
|
||||
@SuppressWarnings("deprecation") // Using PlaybackProperties while it exists.
|
||||
public MediaItem build() {
|
||||
// TODO: remove this check once all the deprecated individual DRM setters are removed.
|
||||
checkState(drmConfiguration.licenseUri == null || drmConfiguration.scheme != null);
|
||||
@Nullable PlaybackProperties localConfiguration = null;
|
||||
@Nullable LocalConfiguration localConfiguration = null;
|
||||
@Nullable Uri uri = this.uri;
|
||||
if (uri != null) {
|
||||
localConfiguration =
|
||||
new PlaybackProperties(
|
||||
new LocalConfiguration(
|
||||
uri,
|
||||
mimeType,
|
||||
drmConfiguration.scheme != null ? drmConfiguration.build() : null,
|
||||
@ -598,7 +598,7 @@ public final class MediaItem implements Bundleable {
|
||||
}
|
||||
|
||||
/** DRM configuration for a media item. */
|
||||
public static final class DrmConfiguration {
|
||||
public static final class DrmConfiguration implements Bundleable {
|
||||
|
||||
/** Builder for {@link DrmConfiguration}. */
|
||||
public static final class Builder {
|
||||
@ -773,7 +773,6 @@ public final class MediaItem implements Bundleable {
|
||||
}
|
||||
|
||||
public DrmConfiguration build() {
|
||||
|
||||
return new DrmConfiguration(this);
|
||||
}
|
||||
}
|
||||
@ -888,10 +887,87 @@ public final class MediaItem implements Bundleable {
|
||||
result = 31 * result + Arrays.hashCode(keySetId);
|
||||
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. */
|
||||
public static final class AdsConfiguration {
|
||||
public static final class AdsConfiguration implements Bundleable {
|
||||
|
||||
/** Builder for {@link AdsConfiguration} instances. */
|
||||
public static final class Builder {
|
||||
@ -975,11 +1051,43 @@ public final class MediaItem implements Bundleable {
|
||||
result = 31 * result + (adsId != null ? adsId.hashCode() : 0);
|
||||
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. */
|
||||
// TODO: Mark this final when PlaybackProperties is deleted.
|
||||
public static class LocalConfiguration {
|
||||
public static final class LocalConfiguration implements Bundleable {
|
||||
|
||||
/** The {@link Uri}. */
|
||||
public final Uri uri;
|
||||
@ -1075,33 +1183,84 @@ public final class MediaItem implements Bundleable {
|
||||
result = 31 * result + (tag == null ? 0 : tag.hashCode());
|
||||
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
|
||||
@Deprecated
|
||||
public static final class PlaybackProperties extends LocalConfiguration {
|
||||
@Override
|
||||
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(
|
||||
Uri uri,
|
||||
@Nullable String mimeType,
|
||||
@Nullable DrmConfiguration drmConfiguration,
|
||||
@Nullable AdsConfiguration adsConfiguration,
|
||||
List<StreamKey> streamKeys,
|
||||
@Nullable String customCacheKey,
|
||||
ImmutableList<SubtitleConfiguration> subtitleConfigurations,
|
||||
@Nullable Object tag) {
|
||||
super(
|
||||
uri,
|
||||
mimeType,
|
||||
/** Object that can restore {@link LocalConfiguration} from a {@link Bundle}. */
|
||||
@UnstableApi
|
||||
public static final Creator<LocalConfiguration> CREATOR = LocalConfiguration::fromBundle;
|
||||
|
||||
@UnstableApi
|
||||
private static LocalConfiguration fromBundle(Bundle bundle) {
|
||||
@Nullable Bundle drmBundle = bundle.getBundle(FIELD_DRM_CONFIGURATION);
|
||||
DrmConfiguration drmConfiguration =
|
||||
drmBundle == null ? null : DrmConfiguration.CREATOR.fromBundle(drmBundle);
|
||||
@Nullable Bundle adsBundle = bundle.getBundle(FIELD_ADS_CONFIGURATION);
|
||||
AdsConfiguration adsConfiguration =
|
||||
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,
|
||||
adsConfiguration,
|
||||
streamKeys,
|
||||
customCacheKey,
|
||||
subtitleConfigurations,
|
||||
tag);
|
||||
bundle.getString(FIELD_CUSTOM_CACHE_KEY),
|
||||
subtitleConfiguration,
|
||||
/* tag= */ null);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1326,7 +1485,7 @@ public final class MediaItem implements Bundleable {
|
||||
return bundle;
|
||||
}
|
||||
|
||||
/** Object that can restore {@link LiveConfiguration} from a {@link Bundle}. */
|
||||
/** An object that can restore {@link LiveConfiguration} from a {@link Bundle}. */
|
||||
@UnstableApi
|
||||
public static final Creator<LiveConfiguration> CREATOR =
|
||||
bundle ->
|
||||
@ -1342,7 +1501,7 @@ public final class MediaItem implements Bundleable {
|
||||
|
||||
/** Properties for a text track. */
|
||||
// TODO: Mark this final when Subtitle is deleted.
|
||||
public static class SubtitleConfiguration {
|
||||
public static class SubtitleConfiguration implements Bundleable {
|
||||
|
||||
/** Builder for {@link SubtitleConfiguration} instances. */
|
||||
public static final class Builder {
|
||||
@ -1513,6 +1672,67 @@ public final class MediaItem implements Bundleable {
|
||||
result = 31 * result + (id == null ? 0 : id.hashCode());
|
||||
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;
|
||||
}
|
||||
|
||||
/** Object that can restore {@link ClippingConfiguration} from a {@link Bundle}. */
|
||||
/** An object that can restore {@link ClippingConfiguration} from a {@link Bundle}. */
|
||||
@UnstableApi
|
||||
public static final Creator<ClippingProperties> CREATOR =
|
||||
bundle ->
|
||||
@ -1917,7 +2137,7 @@ public final class MediaItem implements Bundleable {
|
||||
return bundle;
|
||||
}
|
||||
|
||||
/** Object that can restore {@link RequestMetadata} from a {@link Bundle}. */
|
||||
/** An object that can restore {@link RequestMetadata} from a {@link Bundle}. */
|
||||
@UnstableApi
|
||||
public static final Creator<RequestMetadata> CREATOR =
|
||||
bundle ->
|
||||
@ -1948,7 +2168,7 @@ public final class MediaItem implements Bundleable {
|
||||
/**
|
||||
* @deprecated Use {@link #localConfiguration} instead.
|
||||
*/
|
||||
@UnstableApi @Deprecated @Nullable public final PlaybackProperties playbackProperties;
|
||||
@UnstableApi @Deprecated @Nullable public final LocalConfiguration playbackProperties;
|
||||
|
||||
/** The live playback configuration. */
|
||||
public final LiveConfiguration liveConfiguration;
|
||||
@ -1966,12 +2186,12 @@ public final class MediaItem implements Bundleable {
|
||||
/** The media {@link RequestMetadata}. */
|
||||
public final RequestMetadata requestMetadata;
|
||||
|
||||
// Using PlaybackProperties and ClippingProperties until they're deleted.
|
||||
// Using ClippingProperties until they're deleted.
|
||||
@SuppressWarnings("deprecation")
|
||||
private MediaItem(
|
||||
String mediaId,
|
||||
ClippingProperties clippingConfiguration,
|
||||
@Nullable PlaybackProperties localConfiguration,
|
||||
@Nullable LocalConfiguration localConfiguration,
|
||||
LiveConfiguration liveConfiguration,
|
||||
MediaMetadata mediaMetadata,
|
||||
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_CLIPPING_PROPERTIES = Util.intToStringMaxRadix(3);
|
||||
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
|
||||
@Override
|
||||
public Bundle toBundle() {
|
||||
private Bundle toBundle(boolean includeLocalConfiguration) {
|
||||
Bundle bundle = new Bundle();
|
||||
if (!mediaId.equals(DEFAULT_MEDIA_ID)) {
|
||||
bundle.putString(FIELD_MEDIA_ID, mediaId);
|
||||
@ -2052,11 +2266,35 @@ public final class MediaItem implements Bundleable {
|
||||
if (!requestMetadata.equals(RequestMetadata.EMPTY)) {
|
||||
bundle.putBundle(FIELD_REQUEST_METADATA, requestMetadata.toBundle());
|
||||
}
|
||||
if (includeLocalConfiguration && localConfiguration != null) {
|
||||
bundle.putBundle(FIELD_LOCAL_CONFIGURATION, localConfiguration.toBundle());
|
||||
}
|
||||
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}.
|
||||
*/
|
||||
@ -2093,10 +2331,17 @@ public final class MediaItem implements Bundleable {
|
||||
} else {
|
||||
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(
|
||||
mediaId,
|
||||
clippingConfiguration,
|
||||
/* localConfiguration= */ null,
|
||||
localConfiguration,
|
||||
liveConfiguration,
|
||||
mediaMetadata,
|
||||
requestMetadata);
|
||||
|
@ -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". */
|
||||
// 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}. */
|
||||
// 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.
|
||||
@ -47,7 +47,7 @@ public final class MediaLibraryInfo {
|
||||
* (123-045-006-3-00).
|
||||
*/
|
||||
// Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa.
|
||||
public static final int VERSION_INT = 1_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. */
|
||||
public static final boolean ASSERTIONS_ENABLED = true;
|
||||
|
@ -60,7 +60,11 @@ public final class MediaMetadata implements Bundleable {
|
||||
@Nullable private Uri artworkUri;
|
||||
@Nullable private Integer trackNumber;
|
||||
@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 isPlayable;
|
||||
@Nullable private Integer recordingYear;
|
||||
@ -82,6 +86,7 @@ public final class MediaMetadata implements Bundleable {
|
||||
|
||||
public Builder() {}
|
||||
|
||||
@SuppressWarnings("deprecation") // Assigning from deprecated fields.
|
||||
private Builder(MediaMetadata mediaMetadata) {
|
||||
this.title = mediaMetadata.title;
|
||||
this.artist = mediaMetadata.artist;
|
||||
@ -251,9 +256,11 @@ public final class MediaMetadata implements Bundleable {
|
||||
/**
|
||||
* Sets the {@link FolderType}.
|
||||
*
|
||||
* <p>This method will be deprecated. Use {@link #setIsBrowsable} to indicate if an item is a
|
||||
* browsable folder and use {@link #setMediaType} to indicate the type of the folder.
|
||||
* @deprecated Use {@link #setIsBrowsable} to indicate if an item is a browsable folder and use
|
||||
* {@link #setMediaType} to indicate the type of the folder.
|
||||
*/
|
||||
@SuppressWarnings("deprecation") // Using deprecated type.
|
||||
@Deprecated
|
||||
@CanIgnoreReturnValue
|
||||
public Builder setFolderType(@Nullable @FolderType Integer folderType) {
|
||||
this.folderType = folderType;
|
||||
@ -261,7 +268,6 @@ public final class MediaMetadata implements Bundleable {
|
||||
}
|
||||
|
||||
/** Sets whether the media is a browsable folder. */
|
||||
@UnstableApi
|
||||
@CanIgnoreReturnValue
|
||||
public Builder setIsBrowsable(@Nullable Boolean isBrowsable) {
|
||||
this.isBrowsable = isBrowsable;
|
||||
@ -402,7 +408,6 @@ public final class MediaMetadata implements Bundleable {
|
||||
|
||||
/** Sets the {@link MediaType}. */
|
||||
@CanIgnoreReturnValue
|
||||
@UnstableApi
|
||||
public Builder setMediaType(@Nullable @MediaType Integer mediaType) {
|
||||
this.mediaType = mediaType;
|
||||
return this;
|
||||
@ -458,6 +463,7 @@ public final class MediaMetadata implements Bundleable {
|
||||
}
|
||||
|
||||
/** Populates all the fields from {@code mediaMetadata}, provided they are non-null. */
|
||||
@SuppressWarnings("deprecation") // Populating deprecated fields.
|
||||
@CanIgnoreReturnValue
|
||||
@UnstableApi
|
||||
public Builder populate(@Nullable MediaMetadata mediaMetadata) {
|
||||
@ -595,7 +601,6 @@ public final class MediaMetadata implements Bundleable {
|
||||
@Documented
|
||||
@Retention(RetentionPolicy.SOURCE)
|
||||
@Target(TYPE_USE)
|
||||
@UnstableApi
|
||||
@IntDef({
|
||||
MEDIA_TYPE_MIXED,
|
||||
MEDIA_TYPE_MUSIC,
|
||||
@ -637,111 +642,111 @@ public final class MediaMetadata implements Bundleable {
|
||||
public @interface MediaType {}
|
||||
|
||||
/** 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. */
|
||||
@UnstableApi public static final int MEDIA_TYPE_MUSIC = 1;
|
||||
public static final int MEDIA_TYPE_MUSIC = 1;
|
||||
/** {@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. */
|
||||
@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. */
|
||||
@UnstableApi public static final int MEDIA_TYPE_RADIO_STATION = 4;
|
||||
public static final int MEDIA_TYPE_RADIO_STATION = 4;
|
||||
/** {@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. */
|
||||
@UnstableApi public static final int MEDIA_TYPE_VIDEO = 6;
|
||||
public static final int MEDIA_TYPE_VIDEO = 6;
|
||||
/** {@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. */
|
||||
@UnstableApi public static final int MEDIA_TYPE_MOVIE = 8;
|
||||
public static final int MEDIA_TYPE_MOVIE = 8;
|
||||
/** {@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
|
||||
* 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
|
||||
* 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
|
||||
* 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
|
||||
* 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
|
||||
* 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
|
||||
* 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
|
||||
* 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
|
||||
* typically of type {@link #MEDIA_TYPE_TV_SHOW}, {@link #MEDIA_TYPE_TV_SERIES} or {@link
|
||||
* #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
|
||||
* 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
|
||||
* 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. */
|
||||
@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}. */
|
||||
@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}. */
|
||||
@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}. */
|
||||
@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}. */
|
||||
@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}. */
|
||||
@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}. */
|
||||
@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}. */
|
||||
@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}. */
|
||||
@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}. */
|
||||
@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}. */
|
||||
@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
|
||||
* 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}. */
|
||||
@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}. */
|
||||
@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}. */
|
||||
@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}. */
|
||||
@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.
|
||||
@ -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},
|
||||
* {@link #FOLDER_TYPE_ALBUMS}, {@link #FOLDER_TYPE_ARTISTS}, {@link #FOLDER_TYPE_GENRES}, {@link
|
||||
* #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
|
||||
// with Kotlin usages from before TYPE_USE was added.
|
||||
@Documented
|
||||
@Retention(RetentionPolicy.SOURCE)
|
||||
@Target({FIELD, METHOD, PARAMETER, LOCAL_VARIABLE, TYPE_USE})
|
||||
@Deprecated
|
||||
@SuppressWarnings("deprecation") // Defining deprecated constants.
|
||||
@IntDef({
|
||||
FOLDER_TYPE_NONE,
|
||||
FOLDER_TYPE_MIXED,
|
||||
@ -771,22 +781,60 @@ public final class MediaMetadata implements Bundleable {
|
||||
})
|
||||
public @interface FolderType {}
|
||||
|
||||
/** Type for an item that is not a folder. */
|
||||
public static final int FOLDER_TYPE_NONE = -1;
|
||||
/** Type for a folder containing media of mixed types. */
|
||||
public static final int FOLDER_TYPE_MIXED = 0;
|
||||
/** Type for a folder containing only playable media. */
|
||||
public static final int FOLDER_TYPE_TITLES = 1;
|
||||
/** Type for a folder containing media categorized by album. */
|
||||
public static final int FOLDER_TYPE_ALBUMS = 2;
|
||||
/** Type for a folder containing media categorized by artist. */
|
||||
public static final int FOLDER_TYPE_ARTISTS = 3;
|
||||
/** Type for a folder containing media categorized by genre. */
|
||||
public static final int FOLDER_TYPE_GENRES = 4;
|
||||
/** Type for a folder containing a playlist. */
|
||||
public static final int FOLDER_TYPE_PLAYLISTS = 5;
|
||||
/** Type for a folder containing media categorized by year. */
|
||||
public static final int FOLDER_TYPE_YEARS = 6;
|
||||
/**
|
||||
* Type for an item that is not a folder.
|
||||
*
|
||||
* @deprecated Use {@link #isBrowsable} set to false instead.
|
||||
*/
|
||||
@Deprecated public static final int FOLDER_TYPE_NONE = -1;
|
||||
/**
|
||||
* Type for a folder containing media of mixed types.
|
||||
*
|
||||
* @deprecated Use {@link #isBrowsable} set to true and {@link #mediaType} set to {@link
|
||||
* #MEDIA_TYPE_FOLDER_MIXED} instead.
|
||||
*/
|
||||
@Deprecated public static final int FOLDER_TYPE_MIXED = 0;
|
||||
/**
|
||||
* Type for a folder containing only playable media.
|
||||
*
|
||||
* @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.
|
||||
@ -895,12 +943,15 @@ public final class MediaMetadata implements Bundleable {
|
||||
/**
|
||||
* Optional {@link FolderType}.
|
||||
*
|
||||
* <p>This field will be deprecated. Use {@link #isBrowsable} to indicate if an item is a
|
||||
* browsable folder and use {@link #mediaType} to indicate the type of the folder.
|
||||
* @deprecated Use {@link #isBrowsable} to indicate if an item is a browsable folder and use
|
||||
* {@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. */
|
||||
@UnstableApi @Nullable public final Boolean isBrowsable;
|
||||
@Nullable public final Boolean isBrowsable;
|
||||
/** Optional boolean to indicate that the media is playable. */
|
||||
@Nullable public final Boolean isPlayable;
|
||||
/**
|
||||
@ -953,7 +1004,7 @@ public final class MediaMetadata implements Bundleable {
|
||||
/** Optional name of the station streaming the media. */
|
||||
@Nullable public final CharSequence station;
|
||||
/** Optional {@link MediaType}. */
|
||||
@UnstableApi @Nullable public final @MediaType Integer mediaType;
|
||||
@Nullable public final @MediaType Integer mediaType;
|
||||
|
||||
/**
|
||||
* Optional extras {@link Bundle}.
|
||||
@ -963,6 +1014,7 @@ public final class MediaMetadata implements Bundleable {
|
||||
*/
|
||||
@Nullable public final Bundle extras;
|
||||
|
||||
@SuppressWarnings("deprecation") // Assigning deprecated fields.
|
||||
private MediaMetadata(Builder builder) {
|
||||
// Handle compatibility for deprecated fields.
|
||||
@Nullable Boolean isBrowsable = builder.isBrowsable;
|
||||
@ -1021,6 +1073,7 @@ public final class MediaMetadata implements Bundleable {
|
||||
return new Builder(/* mediaMetadata= */ this);
|
||||
}
|
||||
|
||||
@SuppressWarnings("deprecation") // Comparing deprecated fields.
|
||||
@Override
|
||||
public boolean equals(@Nullable Object obj) {
|
||||
if (this == obj) {
|
||||
@ -1064,6 +1117,7 @@ public final class MediaMetadata implements Bundleable {
|
||||
&& Util.areEqual(mediaType, that.mediaType);
|
||||
}
|
||||
|
||||
@SuppressWarnings("deprecation") // Hashing deprecated fields.
|
||||
@Override
|
||||
public int 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_EXTRAS = Util.intToStringMaxRadix(1000);
|
||||
|
||||
@SuppressWarnings("deprecation") // Bundling deprecated fields.
|
||||
@UnstableApi
|
||||
@Override
|
||||
public Bundle toBundle() {
|
||||
@ -1247,6 +1302,7 @@ public final class MediaMetadata implements Bundleable {
|
||||
/** Object that can restore {@link MediaMetadata} from a {@link Bundle}. */
|
||||
@UnstableApi public static final Creator<MediaMetadata> CREATOR = MediaMetadata::fromBundle;
|
||||
|
||||
@SuppressWarnings("deprecation") // Unbundling deprecated fields.
|
||||
private static MediaMetadata fromBundle(Bundle bundle) {
|
||||
Builder builder = new Builder();
|
||||
builder
|
||||
@ -1329,6 +1385,7 @@ public final class MediaMetadata implements Bundleable {
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
@SuppressWarnings("deprecation") // Converting deprecated field.
|
||||
private static @FolderType int getFolderTypeFromMediaType(@MediaType int mediaType) {
|
||||
switch (mediaType) {
|
||||
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) {
|
||||
switch (folderType) {
|
||||
case FOLDER_TYPE_ALBUMS:
|
||||
|
@ -60,6 +60,7 @@ public final class MimeTypes {
|
||||
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_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";
|
||||
|
||||
// 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_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_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_PGS = BASE_TYPE_APPLICATION + "/pgs";
|
||||
@UnstableApi public static final String APPLICATION_SCTE35 = BASE_TYPE_APPLICATION + "/x-scte35";
|
||||
@ -147,7 +153,11 @@ public final class MimeTypes {
|
||||
|
||||
// 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_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
|
||||
@ -585,6 +595,10 @@ public final class MimeTypes {
|
||||
return C.ENCODING_DTS;
|
||||
case MimeTypes.AUDIO_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:
|
||||
return C.ENCODING_DOLBY_TRUEHD;
|
||||
case MimeTypes.AUDIO_OPUS:
|
||||
|
@ -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;
|
||||
}
|
@ -107,4 +107,15 @@ public class ParserException extends IOException {
|
||||
this.contentIsMalformed = contentIsMalformed;
|
||||
this.dataType = dataType;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public String getMessage() {
|
||||
return super.getMessage()
|
||||
+ "{contentIsMalformed="
|
||||
+ contentIsMalformed
|
||||
+ ", dataType="
|
||||
+ dataType
|
||||
+ "}";
|
||||
}
|
||||
}
|
||||
|
@ -229,6 +229,13 @@ public class PlaybackException extends Exception implements Bundleable {
|
||||
/** Caused by an expired DRM license being loaded into an open DRM session. */
|
||||
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
|
||||
* 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";
|
||||
case 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:
|
||||
if (errorCode >= CUSTOM_ERROR_CODE_BASE) {
|
||||
return "custom error code";
|
||||
|
@ -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.
|
||||
*/
|
||||
public PlaybackParameters(float speed) {
|
||||
public PlaybackParameters(@FloatRange(from = 0, fromInclusive = false) float speed) {
|
||||
this(speed, /* pitch= */ 1f);
|
||||
}
|
||||
|
||||
|
@ -46,26 +46,125 @@ import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* A media player interface defining traditional high-level functionality, such as the ability to
|
||||
* play, pause, seek and query properties of the currently playing media.
|
||||
* A media player interface defining high-level functionality, such as the ability to play, pause,
|
||||
* seek and query properties of the currently playing media.
|
||||
*
|
||||
* <p>All methods must be called from a single {@linkplain #getApplicationLooper() application
|
||||
* 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.
|
||||
* <h2>Player features and usage</h2>
|
||||
*
|
||||
* <p>Some important properties of media players that implement this interface are:
|
||||
*
|
||||
* <ul>
|
||||
* <li>They can provide a {@link Timeline} representing the structure of the media being played,
|
||||
* which can be obtained by calling {@link #getCurrentTimeline()}.
|
||||
* <li>They can provide a {@link Tracks} defining the currently available tracks and which are
|
||||
* selected to be rendered, which can be obtained by calling {@link #getCurrentTracks()}.
|
||||
* <li>All methods must be called from a single {@linkplain #getApplicationLooper() application
|
||||
* thread} unless indicated otherwise. Callbacks in registered listeners are called on the
|
||||
* same thread.
|
||||
* <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>
|
||||
*
|
||||
* <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 {
|
||||
|
||||
@ -371,8 +470,8 @@ public interface Player {
|
||||
COMMAND_SET_REPEAT_MODE,
|
||||
COMMAND_GET_CURRENT_MEDIA_ITEM,
|
||||
COMMAND_GET_TIMELINE,
|
||||
COMMAND_GET_MEDIA_ITEMS_METADATA,
|
||||
COMMAND_SET_MEDIA_ITEMS_METADATA,
|
||||
COMMAND_GET_METADATA,
|
||||
COMMAND_SET_PLAYLIST_METADATA,
|
||||
COMMAND_SET_MEDIA_ITEM,
|
||||
COMMAND_CHANGE_MEDIA_ITEMS,
|
||||
COMMAND_GET_AUDIO_ATTRIBUTES,
|
||||
@ -380,11 +479,14 @@ public interface Player {
|
||||
COMMAND_GET_DEVICE_VOLUME,
|
||||
COMMAND_SET_VOLUME,
|
||||
COMMAND_SET_DEVICE_VOLUME,
|
||||
COMMAND_SET_DEVICE_VOLUME_WITH_FLAGS,
|
||||
COMMAND_ADJUST_DEVICE_VOLUME,
|
||||
COMMAND_ADJUST_DEVICE_VOLUME_WITH_FLAGS,
|
||||
COMMAND_SET_VIDEO_SURFACE,
|
||||
COMMAND_GET_TEXT,
|
||||
COMMAND_SET_TRACK_SELECTION_PARAMETERS,
|
||||
COMMAND_GET_TRACKS,
|
||||
COMMAND_RELEASE
|
||||
};
|
||||
|
||||
private final FlagSet.Builder flagsBuilder;
|
||||
@ -914,15 +1016,6 @@ public interface Player {
|
||||
*/
|
||||
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.
|
||||
*
|
||||
@ -986,7 +1079,7 @@ public interface Player {
|
||||
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
|
||||
* 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_AUDIO_FOCUS_LOSS}, {@link
|
||||
* #PLAY_WHEN_READY_CHANGE_REASON_AUDIO_BECOMING_NOISY}, {@link
|
||||
* #PLAY_WHEN_READY_CHANGE_REASON_REMOTE} or {@link
|
||||
* #PLAY_WHEN_READY_CHANGE_REASON_END_OF_MEDIA_ITEM}.
|
||||
* #PLAY_WHEN_READY_CHANGE_REASON_REMOTE}, {@link
|
||||
* #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
|
||||
// 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_BECOMING_NOISY,
|
||||
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 {}
|
||||
/** 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;
|
||||
/** Playback has been paused at the end of a media item. */
|
||||
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
|
||||
* of {@link #PLAYBACK_SUPPRESSION_REASON_NONE} or {@link
|
||||
* #PLAYBACK_SUPPRESSION_REASON_TRANSIENT_AUDIO_FOCUS_LOSS}.
|
||||
* of {@link #PLAYBACK_SUPPRESSION_REASON_NONE}, {@link
|
||||
* #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
|
||||
// with Kotlin usages from before TYPE_USE was added.
|
||||
@ -1130,13 +1231,19 @@ public interface Player {
|
||||
@Target({FIELD, METHOD, PARAMETER, LOCAL_VARIABLE, TYPE_USE})
|
||||
@IntDef({
|
||||
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 {}
|
||||
/** Playback is not suppressed. */
|
||||
int PLAYBACK_SUPPRESSION_REASON_NONE = 0;
|
||||
/** Playback is suppressed due to transient audio focus loss. */
|
||||
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
|
||||
@ -1413,8 +1520,8 @@ public interface Player {
|
||||
* <li>{@link #COMMAND_SET_REPEAT_MODE}
|
||||
* <li>{@link #COMMAND_GET_CURRENT_MEDIA_ITEM}
|
||||
* <li>{@link #COMMAND_GET_TIMELINE}
|
||||
* <li>{@link #COMMAND_GET_MEDIA_ITEMS_METADATA}
|
||||
* <li>{@link #COMMAND_SET_MEDIA_ITEMS_METADATA}
|
||||
* <li>{@link #COMMAND_GET_METADATA}
|
||||
* <li>{@link #COMMAND_SET_PLAYLIST_METADATA}
|
||||
* <li>{@link #COMMAND_SET_MEDIA_ITEM}
|
||||
* <li>{@link #COMMAND_CHANGE_MEDIA_ITEMS}
|
||||
* <li>{@link #COMMAND_GET_AUDIO_ATTRIBUTES}
|
||||
@ -1422,15 +1529,19 @@ public interface Player {
|
||||
* <li>{@link #COMMAND_GET_DEVICE_VOLUME}
|
||||
* <li>{@link #COMMAND_SET_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_WITH_FLAGS}
|
||||
* <li>{@link #COMMAND_SET_VIDEO_SURFACE}
|
||||
* <li>{@link #COMMAND_GET_TEXT}
|
||||
* <li>{@link #COMMAND_SET_TRACK_SELECTION_PARAMETERS}
|
||||
* <li>{@link #COMMAND_GET_TRACKS}
|
||||
* <li>{@link #COMMAND_RELEASE}
|
||||
* </ul>
|
||||
*/
|
||||
// @Target list includes both 'default' targets and TYPE_USE, to ensure backwards compatibility
|
||||
// with Kotlin usages from before TYPE_USE was added.
|
||||
@SuppressWarnings("deprecation") // Listing deprecated constants.
|
||||
@Documented
|
||||
@Retention(RetentionPolicy.SOURCE)
|
||||
@Target({FIELD, METHOD, PARAMETER, LOCAL_VARIABLE, TYPE_USE})
|
||||
@ -1454,7 +1565,9 @@ public interface Player {
|
||||
COMMAND_GET_CURRENT_MEDIA_ITEM,
|
||||
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_CHANGE_MEDIA_ITEMS,
|
||||
COMMAND_GET_AUDIO_ATTRIBUTES,
|
||||
@ -1462,11 +1575,14 @@ public interface Player {
|
||||
COMMAND_GET_DEVICE_VOLUME,
|
||||
COMMAND_SET_VOLUME,
|
||||
COMMAND_SET_DEVICE_VOLUME,
|
||||
COMMAND_SET_DEVICE_VOLUME_WITH_FLAGS,
|
||||
COMMAND_ADJUST_DEVICE_VOLUME,
|
||||
COMMAND_ADJUST_DEVICE_VOLUME_WITH_FLAGS,
|
||||
COMMAND_SET_VIDEO_SURFACE,
|
||||
COMMAND_GET_TEXT,
|
||||
COMMAND_SET_TRACK_SELECTION_PARAMETERS,
|
||||
COMMAND_GET_TRACKS,
|
||||
COMMAND_RELEASE,
|
||||
})
|
||||
@interface Command {}
|
||||
/**
|
||||
@ -1665,6 +1781,11 @@ public interface Player {
|
||||
*/
|
||||
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}.
|
||||
*
|
||||
@ -1676,8 +1797,12 @@ public interface Player {
|
||||
* <li>{@link #getPlaylistMetadata()}
|
||||
* </ul>
|
||||
*/
|
||||
// TODO(b/263132691): Rename this to COMMAND_GET_METADATA
|
||||
int COMMAND_GET_MEDIA_ITEMS_METADATA = 18;
|
||||
int COMMAND_GET_METADATA = 18;
|
||||
|
||||
/**
|
||||
* @deprecated Use {@link #COMMAND_SET_PLAYLIST_METADATA} instead.
|
||||
*/
|
||||
@Deprecated int COMMAND_SET_MEDIA_ITEMS_METADATA = 19;
|
||||
|
||||
/**
|
||||
* 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
|
||||
* is {@linkplain #isCommandAvailable(int) available}.
|
||||
*/
|
||||
// TODO(b/263132691): Rename this to COMMAND_SET_PLAYLIST_METADATA
|
||||
int COMMAND_SET_MEDIA_ITEMS_METADATA = 19;
|
||||
int COMMAND_SET_PLAYLIST_METADATA = 19;
|
||||
|
||||
/**
|
||||
* Command to set a {@link MediaItem}.
|
||||
@ -1720,6 +1844,8 @@ public interface Player {
|
||||
* <li>{@link #setMediaItems(List)}
|
||||
* <li>{@link #setMediaItems(List, boolean)}
|
||||
* <li>{@link #setMediaItems(List, int, long)}
|
||||
* <li>{@link #replaceMediaItem(int, MediaItem)}
|
||||
* <li>{@link #replaceMediaItems(int, int, List)}
|
||||
* </ul>
|
||||
*/
|
||||
int COMMAND_CHANGE_MEDIA_ITEMS = 20;
|
||||
@ -1760,27 +1886,36 @@ public interface Player {
|
||||
* #isCommandAvailable(int) available}.
|
||||
*/
|
||||
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
|
||||
* #isCommandAvailable(int) available}:
|
||||
*
|
||||
* <ul>
|
||||
* <li>{@link #increaseDeviceVolume()}
|
||||
* <li>{@link #decreaseDeviceVolume()}
|
||||
* <li>{@link #setDeviceMuted(boolean)}
|
||||
* <li>{@link #increaseDeviceVolume(int)}
|
||||
* <li>{@link #decreaseDeviceVolume(int)}
|
||||
* <li>{@link #setDeviceMuted(boolean, int)}
|
||||
* </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.
|
||||
@ -1823,6 +1958,13 @@ public interface Player {
|
||||
* #isCommandAvailable(int) available}.
|
||||
*/
|
||||
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}. */
|
||||
int COMMAND_INVALID = -1;
|
||||
@ -2005,6 +2147,37 @@ public interface Player {
|
||||
*/
|
||||
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.
|
||||
*
|
||||
@ -2500,20 +2673,13 @@ public interface Player {
|
||||
*/
|
||||
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
|
||||
* 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();
|
||||
|
||||
/**
|
||||
@ -2565,7 +2731,7 @@ public interface Player {
|
||||
* 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.
|
||||
*
|
||||
* <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}.
|
||||
*/
|
||||
MediaMetadata getMediaMetadata();
|
||||
@ -2574,7 +2740,7 @@ public interface Player {
|
||||
* Returns the playlist {@link MediaMetadata}, as set by {@link
|
||||
* #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}.
|
||||
*/
|
||||
MediaMetadata getPlaylistMetadata();
|
||||
@ -2582,7 +2748,7 @@ public interface Player {
|
||||
/**
|
||||
* 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}.
|
||||
*/
|
||||
void setPlaylistMetadata(MediaMetadata mediaMetadata);
|
||||
@ -3015,8 +3181,8 @@ public interface Player {
|
||||
/**
|
||||
* 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
|
||||
* determined yet.
|
||||
* <p>The video's width and height are {@code 0} if there is {@linkplain
|
||||
* Tracks#isTypeSupported(int) no supported video track} or its size has not been determined yet.
|
||||
*
|
||||
* @see Listener#onVideoSizeChanged(VideoSize)
|
||||
*/
|
||||
@ -3067,36 +3233,74 @@ public interface Player {
|
||||
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
|
||||
* #getAvailableCommands() available}.
|
||||
* <p>This method must only be called if {@link #COMMAND_SET_DEVICE_VOLUME_WITH_FLAGS} is
|
||||
* {@linkplain #getAvailableCommands() available}.
|
||||
*
|
||||
* @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.
|
||||
*
|
||||
* <p>This method must only be called if {@link #COMMAND_ADJUST_DEVICE_VOLUME} is {@linkplain
|
||||
* #getAvailableCommands() available}.
|
||||
* <p>The {@link #getDeviceVolume()} device volume cannot be increased above {@link
|
||||
* 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.
|
||||
*
|
||||
* <p>This method must only be called if {@link #COMMAND_ADJUST_DEVICE_VOLUME} is {@linkplain
|
||||
* #getAvailableCommands() available}.
|
||||
* <p>The {@link #getDeviceVolume()} device volume cannot be decreased below {@link
|
||||
* 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.
|
||||
*
|
||||
* <p>This method must only be called if {@link #COMMAND_ADJUST_DEVICE_VOLUME} is {@linkplain
|
||||
* #getAvailableCommands() available}.
|
||||
* <p>This method must only be called if {@link #COMMAND_ADJUST_DEVICE_VOLUME_WITH_FLAGS} is
|
||||
* {@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);
|
||||
}
|
||||
|
@ -2101,7 +2101,16 @@ public abstract class SimpleBasePlayer extends BasePlayer {
|
||||
placeholderPlaylist.add(
|
||||
i + correctedIndex, getPlaceholderMediaItemData(mediaItems.get(i)));
|
||||
}
|
||||
if (!state.playlist.isEmpty()) {
|
||||
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
|
||||
public final void removeMediaItems(int fromIndex, int toIndex) {
|
||||
verifyApplicationThreadAndInitState();
|
||||
@ -2325,20 +2373,12 @@ public abstract class SimpleBasePlayer extends BasePlayer {
|
||||
.build());
|
||||
}
|
||||
|
||||
@Override
|
||||
public final void stop(boolean reset) {
|
||||
stop();
|
||||
if (reset) {
|
||||
clearMediaItems();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public final void release() {
|
||||
verifyApplicationThreadAndInitState();
|
||||
// Use a local copy to ensure the lambda below uses the current state value.
|
||||
State state = this.state;
|
||||
if (released) { // TODO(b/261158047): Replace by !shouldHandleCommand(Player.COMMAND_RELEASE)
|
||||
if (!shouldHandleCommand(Player.COMMAND_RELEASE)) {
|
||||
return;
|
||||
}
|
||||
updateStateForPendingOperation(
|
||||
@ -2401,7 +2441,7 @@ public abstract class SimpleBasePlayer extends BasePlayer {
|
||||
verifyApplicationThreadAndInitState();
|
||||
// Use a local copy to ensure the lambda below uses the current state value.
|
||||
State state = this.state;
|
||||
if (!shouldHandleCommand(Player.COMMAND_SET_MEDIA_ITEMS_METADATA)) {
|
||||
if (!shouldHandleCommand(Player.COMMAND_SET_PLAYLIST_METADATA)) {
|
||||
return;
|
||||
}
|
||||
updateStateForPendingOperation(
|
||||
@ -2669,6 +2709,10 @@ public abstract class SimpleBasePlayer extends BasePlayer {
|
||||
return state.isDeviceMuted;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use {@link #setDeviceVolume(int, int)} instead.
|
||||
*/
|
||||
@Deprecated
|
||||
@Override
|
||||
public final void setDeviceVolume(int volume) {
|
||||
verifyApplicationThreadAndInitState();
|
||||
@ -2678,10 +2722,27 @@ public abstract class SimpleBasePlayer extends BasePlayer {
|
||||
return;
|
||||
}
|
||||
updateStateForPendingOperation(
|
||||
/* pendingOperation= */ handleSetDeviceVolume(volume),
|
||||
/* pendingOperation= */ handleSetDeviceVolume(volume, C.VOLUME_FLAG_SHOW_UI),
|
||||
/* 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
|
||||
public final void increaseDeviceVolume() {
|
||||
verifyApplicationThreadAndInitState();
|
||||
@ -2691,11 +2752,29 @@ public abstract class SimpleBasePlayer extends BasePlayer {
|
||||
return;
|
||||
}
|
||||
updateStateForPendingOperation(
|
||||
/* pendingOperation= */ handleIncreaseDeviceVolume(),
|
||||
/* pendingOperation= */ handleIncreaseDeviceVolume(C.VOLUME_FLAG_SHOW_UI),
|
||||
/* placeholderStateSupplier= */ () ->
|
||||
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
|
||||
public final void decreaseDeviceVolume() {
|
||||
verifyApplicationThreadAndInitState();
|
||||
@ -2705,11 +2784,29 @@ public abstract class SimpleBasePlayer extends BasePlayer {
|
||||
return;
|
||||
}
|
||||
updateStateForPendingOperation(
|
||||
/* pendingOperation= */ handleDecreaseDeviceVolume(),
|
||||
/* pendingOperation= */ handleDecreaseDeviceVolume(C.VOLUME_FLAG_SHOW_UI),
|
||||
/* placeholderStateSupplier= */ () ->
|
||||
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
|
||||
public final void setDeviceMuted(boolean muted) {
|
||||
verifyApplicationThreadAndInitState();
|
||||
@ -2719,7 +2816,20 @@ public abstract class SimpleBasePlayer extends BasePlayer {
|
||||
return;
|
||||
}
|
||||
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());
|
||||
}
|
||||
|
||||
@ -2837,10 +2947,11 @@ public abstract class SimpleBasePlayer extends BasePlayer {
|
||||
/**
|
||||
* 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}
|
||||
* changes caused by this call.
|
||||
*/
|
||||
// TODO(b/261158047): Add that this method will only be called if COMMAND_RELEASE is available.
|
||||
@ForOverride
|
||||
protected ListenableFuture<?> handleRelease() {
|
||||
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}.
|
||||
*
|
||||
* <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}.
|
||||
* @return A {@link ListenableFuture} indicating the completion of all immediate {@link State}
|
||||
@ -2916,7 +3027,7 @@ public abstract class SimpleBasePlayer extends BasePlayer {
|
||||
@ForOverride
|
||||
protected ListenableFuture<?> handleSetPlaylistMetadata(MediaMetadata playlistMetadata) {
|
||||
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 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<?> handleSetDeviceVolume(@IntRange(from = 0) int deviceVolume) {
|
||||
throw new IllegalStateException("Missing implementation to handle COMMAND_SET_DEVICE_VOLUME");
|
||||
}
|
||||
|
||||
/**
|
||||
* 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() {
|
||||
protected ListenableFuture<?> handleSetDeviceVolume(
|
||||
@IntRange(from = 0) int deviceVolume, int flags) {
|
||||
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}
|
||||
* changes caused by this call.
|
||||
*/
|
||||
@ForOverride
|
||||
protected ListenableFuture<?> handleDecreaseDeviceVolume() {
|
||||
protected ListenableFuture<?> handleIncreaseDeviceVolume(@C.VolumeFlags int flags) {
|
||||
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 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<?> handleSetDeviceMuted(boolean muted) {
|
||||
protected ListenableFuture<?> handleSetDeviceMuted(boolean muted, @C.VolumeFlags int flags) {
|
||||
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");
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 <=
|
||||
* {@code fromIndex} < {@link #getMediaItemCount()}.
|
||||
* @param toIndex The index of the first item not to be replaced (exclusive). The index is in the
|
||||
* range {@code fromIndex} < {@code toIndex} <= {@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}.
|
||||
*
|
||||
@ -3336,9 +3486,6 @@ public abstract class SimpleBasePlayer extends BasePlayer {
|
||||
listeners.queueEvent(
|
||||
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)) {
|
||||
listeners.queueEvent(
|
||||
Player.EVENT_AVAILABLE_COMMANDS_CHANGED,
|
||||
@ -3717,7 +3864,7 @@ public abstract class SimpleBasePlayer extends BasePlayer {
|
||||
State.Builder stateBuilder = oldState.buildUpon();
|
||||
stateBuilder.setPlaylist(newPlaylist);
|
||||
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);
|
||||
} else {
|
||||
stateBuilder.setPlaybackState(Player.STATE_BUFFERING);
|
||||
|
@ -15,10 +15,12 @@
|
||||
*/
|
||||
package androidx.media3.common;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
import androidx.annotation.Nullable;
|
||||
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").
|
||||
@ -35,7 +37,7 @@ import androidx.media3.common.util.UnstableApi;
|
||||
* particular track selection.
|
||||
*/
|
||||
@UnstableApi
|
||||
public final class StreamKey implements Comparable<StreamKey>, Parcelable {
|
||||
public final class StreamKey implements Comparable<StreamKey>, Parcelable, Bundleable {
|
||||
|
||||
/** The period index. */
|
||||
public final int periodIndex;
|
||||
@ -44,11 +46,6 @@ public final class StreamKey implements Comparable<StreamKey>, Parcelable {
|
||||
/** The stream index. */
|
||||
public final int streamIndex;
|
||||
|
||||
/**
|
||||
* @deprecated Use {@link #streamIndex}.
|
||||
*/
|
||||
@Deprecated public final int trackIndex;
|
||||
|
||||
/**
|
||||
* 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 groupIndex The group index.
|
||||
* @param streamIndex The stream index.
|
||||
*/
|
||||
@SuppressWarnings("deprecation")
|
||||
public StreamKey(int periodIndex, int groupIndex, int streamIndex) {
|
||||
this.periodIndex = periodIndex;
|
||||
this.groupIndex = groupIndex;
|
||||
this.streamIndex = streamIndex;
|
||||
trackIndex = streamIndex;
|
||||
}
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
/* package */ StreamKey(Parcel in) {
|
||||
periodIndex = in.readInt();
|
||||
groupIndex = in.readInt();
|
||||
streamIndex = in.readInt();
|
||||
trackIndex = streamIndex;
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -151,4 +144,36 @@ public final class StreamKey implements Comparable<StreamKey>, Parcelable {
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
@ -37,6 +37,9 @@ import com.google.errorprone.annotations.InlineMe;
|
||||
import java.util.ArrayList;
|
||||
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
|
||||
* 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
|
||||
* 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 {
|
||||
|
||||
/**
|
||||
@ -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
|
||||
* corresponding {@link Period Periods} in the timeline.
|
||||
*
|
||||
* <p style="align:center"><img src="doc-files/timeline-window.svg" alt="Information defined by a
|
||||
* timeline window">
|
||||
* <p style="align:center"><img
|
||||
* 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 {
|
||||
|
||||
@ -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
|
||||
* 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
|
||||
* period">
|
||||
* <p style="align:center"><img
|
||||
* 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 {
|
||||
|
||||
@ -834,6 +837,18 @@ public abstract class Timeline implements Bundleable {
|
||||
: 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
|
||||
* microseconds.
|
||||
|
@ -17,27 +17,31 @@ package androidx.media3.common;
|
||||
|
||||
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
|
||||
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
|
||||
* FrameProcessingException} and returns the exception otherwise.
|
||||
* Wraps the given exception in a {@code VideoFrameProcessingException} if it is not already a
|
||||
* {@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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Wraps the given exception in a {@code FrameProcessingException} with the given timestamp if it
|
||||
* is not already a {@code FrameProcessingException} and returns the exception otherwise.
|
||||
* Wraps the given exception in a {@code VideoFrameProcessingException} with the given timestamp
|
||||
* if it is not already a {@code VideoFrameProcessingException} and returns the exception
|
||||
* otherwise.
|
||||
*/
|
||||
public static FrameProcessingException from(Exception exception, long presentationTimeUs) {
|
||||
if (exception instanceof FrameProcessingException) {
|
||||
return (FrameProcessingException) exception;
|
||||
public static VideoFrameProcessingException from(Exception exception, long presentationTimeUs) {
|
||||
if (exception instanceof VideoFrameProcessingException) {
|
||||
return (VideoFrameProcessingException) exception;
|
||||
} 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.
|
||||
*/
|
||||
public FrameProcessingException(String message) {
|
||||
public VideoFrameProcessingException(String message) {
|
||||
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 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);
|
||||
this.presentationTimeUs = presentationTimeUs;
|
||||
}
|
||||
@ -73,7 +77,7 @@ public final class FrameProcessingException extends Exception {
|
||||
* @param message The detail message for 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);
|
||||
}
|
||||
|
||||
@ -84,7 +88,7 @@ public final class FrameProcessingException extends Exception {
|
||||
* @param cause The cause of this exception.
|
||||
* @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);
|
||||
this.presentationTimeUs = presentationTimeUs;
|
||||
}
|
||||
@ -94,7 +98,7 @@ public final class FrameProcessingException extends Exception {
|
||||
*
|
||||
* @param cause The cause of this exception.
|
||||
*/
|
||||
public FrameProcessingException(Throwable cause) {
|
||||
public VideoFrameProcessingException(Throwable cause) {
|
||||
this(cause, /* presentationTimeUs= */ C.TIME_UNSET);
|
||||
}
|
||||
|
||||
@ -104,7 +108,7 @@ public final class FrameProcessingException extends Exception {
|
||||
* @param cause The cause of this exception.
|
||||
* @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);
|
||||
this.presentationTimeUs = presentationTimeUs;
|
||||
}
|
@ -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();
|
||||
}
|
@ -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 height The video height in pixels.
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
@ -37,6 +37,13 @@ public interface AudioProcessor {
|
||||
|
||||
/** PCM audio format that may be handled by an audio processor. */
|
||||
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 =
|
||||
new AudioFormat(
|
||||
/* 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 {
|
||||
|
||||
public UnhandledAudioFormatException(AudioFormat inputAudioFormat) {
|
||||
super("Unhandled format: " + inputAudioFormat);
|
||||
this("Unhandled input format:", inputAudioFormat);
|
||||
}
|
||||
|
||||
public UnhandledAudioFormatException(String message, AudioFormat audioFormat) {
|
||||
super(message + " " + audioFormat);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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.
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -14,7 +14,7 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package androidx.media3.exoplayer.audio;
|
||||
package androidx.media3.common.audio;
|
||||
|
||||
import static java.lang.Math.min;
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
@ -13,11 +13,11 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package androidx.media3.exoplayer.audio;
|
||||
package androidx.media3.common.audio;
|
||||
|
||||
import androidx.media3.common.C;
|
||||
import androidx.media3.common.Format;
|
||||
import androidx.media3.common.audio.AudioProcessor;
|
||||
import androidx.media3.common.util.UnstableApi;
|
||||
import androidx.media3.common.util.Util;
|
||||
import com.google.errorprone.annotations.CanIgnoreReturnValue;
|
||||
import java.nio.ByteBuffer;
|
||||
@ -35,7 +35,8 @@ import java.nio.ByteBuffer;
|
||||
* <li>{@link C#ENCODING_PCM_FLOAT}
|
||||
* </ul>
|
||||
*/
|
||||
/* package */ final class ResamplingAudioProcessor extends BaseAudioProcessor {
|
||||
@UnstableApi
|
||||
public final class ToInt16PcmAudioProcessor extends BaseAudioProcessor {
|
||||
|
||||
@Override
|
||||
@CanIgnoreReturnValue
|
@ -300,162 +300,6 @@ public final class Cue implements Bundleable {
|
||||
*/
|
||||
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(
|
||||
@Nullable CharSequence text,
|
||||
@Nullable Alignment textAlignment,
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
@ -22,10 +22,14 @@ import android.os.Bundle;
|
||||
import android.util.SparseArray;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.media3.common.Bundleable;
|
||||
import com.google.common.base.Function;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/** Utilities for {@link Bundleable}. */
|
||||
@UnstableApi
|
||||
@ -33,10 +37,21 @@ public final class BundleableUtil {
|
||||
|
||||
/** Converts a list of {@link Bundleable} to a list {@link Bundle}. */
|
||||
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();
|
||||
for (int i = 0; i < bundleableList.size(); i++) {
|
||||
Bundleable bundleable = bundleableList.get(i);
|
||||
builder.add(bundleable.toBundle());
|
||||
T bundleable = bundleableList.get(i);
|
||||
builder.add(customToBundleFunc.apply(bundleable));
|
||||
}
|
||||
return builder.build();
|
||||
}
|
||||
@ -94,6 +109,47 @@ public final class BundleableUtil {
|
||||
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.
|
||||
*
|
||||
|
@ -67,7 +67,7 @@ public final class GlProgram {
|
||||
* @return The content of the file to load.
|
||||
* @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;
|
||||
try {
|
||||
inputStream = context.getAssets().open(assetPath);
|
||||
|
@ -31,6 +31,7 @@ import android.opengl.GLES20;
|
||||
import android.opengl.GLES30;
|
||||
import android.opengl.Matrix;
|
||||
import androidx.annotation.DoNotInline;
|
||||
import androidx.annotation.IntRange;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.RequiresApi;
|
||||
import androidx.media3.common.C;
|
||||
@ -89,7 +90,16 @@ public final class GlUtil {
|
||||
private static final String EXTENSION_SURFACELESS_CONTEXT = "EGL_KHR_surfaceless_context";
|
||||
// https://www.khronos.org/registry/OpenGL/extensions/EXT/EXT_YUV_target.txt
|
||||
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};
|
||||
|
||||
/** Class only contains static methods. */
|
||||
@ -190,7 +200,7 @@ public final class GlUtil {
|
||||
* 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
|
||||
* for HDR.
|
||||
* for HDR input.
|
||||
*/
|
||||
public static boolean isYuvTargetExtensionSupported() {
|
||||
if (Util.SDK_INT < 17) {
|
||||
@ -216,6 +226,13 @@ public final class GlUtil {
|
||||
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}. */
|
||||
@RequiresApi(17)
|
||||
public static EGLDisplay createEglDisplay() throws GlException {
|
||||
@ -232,57 +249,78 @@ public final class GlUtil {
|
||||
*/
|
||||
@RequiresApi(17)
|
||||
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}.
|
||||
*
|
||||
* @param sharedContext The {@link EGLContext} with which to share data.
|
||||
* @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
|
||||
* #EGL_CONFIG_ATTRIBUTES_RGBA_1010102}, which will request OpenGL ES 3.0, or {@link
|
||||
* #EGL_CONFIG_ATTRIBUTES_RGBA_8888}, which will request OpenGL ES 2.0.
|
||||
* #EGL_CONFIG_ATTRIBUTES_RGBA_1010102}, or {@link #EGL_CONFIG_ATTRIBUTES_RGBA_8888}.
|
||||
*/
|
||||
@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 {
|
||||
checkArgument(
|
||||
Arrays.equals(configAttributes, EGL_CONFIG_ATTRIBUTES_RGBA_8888)
|
||||
|| Arrays.equals(configAttributes, EGL_CONFIG_ATTRIBUTES_RGBA_1010102));
|
||||
return Api17.createEglContext(
|
||||
eglDisplay,
|
||||
/* version= */ Arrays.equals(configAttributes, EGL_CONFIG_ATTRIBUTES_RGBA_1010102) ? 3 : 2,
|
||||
configAttributes);
|
||||
checkArgument(openGlVersion == 2 || openGlVersion == 3);
|
||||
return Api17.createEglContext(sharedContext, eglDisplay, openGlVersion, 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
|
||||
* OpenGL ES 2.0.
|
||||
* <p>The {@link EGLSurface} will configure with OpenGL ES 2.0.
|
||||
*
|
||||
* @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. 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)
|
||||
public static EGLSurface getEglSurface(EGLDisplay eglDisplay, Object surface) throws GlException {
|
||||
return Api17.getEglSurface(
|
||||
eglDisplay, surface, EGL_CONFIG_ATTRIBUTES_RGBA_8888, EGL_WINDOW_SURFACE_ATTRIBUTES_NONE);
|
||||
public static EGLSurface createEglSurface(
|
||||
EGLDisplay eglDisplay,
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns 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 configAttributes The attributes to configure EGL with. Accepts {@link
|
||||
* #EGL_CONFIG_ATTRIBUTES_RGBA_1010102} and {@link #EGL_CONFIG_ATTRIBUTES_RGBA_8888}.
|
||||
*/
|
||||
@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);
|
||||
} else if (colorTransfer == C.COLOR_TRANSFER_HLG) {
|
||||
checkArgument(isEncoderInputSurface, "Outputting HLG to the screen is not supported.");
|
||||
configAttributes = EGL_CONFIG_ATTRIBUTES_RGBA_1010102;
|
||||
windowAttributes = EGL_WINDOW_SURFACE_ATTRIBUTES_NONE;
|
||||
} else {
|
||||
throw new IllegalArgumentException("Unsupported color transfer: " + colorTransfer);
|
||||
}
|
||||
return Api17.createEglSurface(eglDisplay, surface, configAttributes, windowAttributes);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -349,6 +387,11 @@ public final class GlUtil {
|
||||
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
|
||||
* GlException} with the combined error message.
|
||||
@ -378,7 +421,7 @@ public final class GlUtil {
|
||||
*/
|
||||
private static void assertValidTextureSize(int width, int height) throws GlException {
|
||||
// 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.
|
||||
// For valid GL sizes, see:
|
||||
// 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). */
|
||||
public static void clearOutputFrame() throws GlException {
|
||||
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();
|
||||
}
|
||||
|
||||
@ -450,26 +494,6 @@ public final class GlUtil {
|
||||
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.
|
||||
*
|
||||
@ -504,8 +528,8 @@ public final class GlUtil {
|
||||
*
|
||||
* @param width The width 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
|
||||
* true}, use 16-bit (half-precision) floating-point.
|
||||
* @param useHighPrecisionColorComponents If {@code false}, uses colors with 8-bit unsigned bytes.
|
||||
* If {@code true}, use 16-bit (half-precision) floating-point.
|
||||
* @throws GlException If the texture allocation fails.
|
||||
* @return The texture identifier for the newly-allocated texture.
|
||||
*/
|
||||
@ -602,6 +626,49 @@ public final class GlUtil {
|
||||
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
|
||||
* false}.
|
||||
@ -639,13 +706,14 @@ public final class GlUtil {
|
||||
|
||||
@DoNotInline
|
||||
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};
|
||||
EGLContext eglContext =
|
||||
EGL14.eglCreateContext(
|
||||
eglDisplay,
|
||||
getEglConfig(eglDisplay, configAttributes),
|
||||
EGL14.EGL_NO_CONTEXT,
|
||||
sharedContext,
|
||||
contextAttributes,
|
||||
/* offset= */ 0);
|
||||
if (eglContext == null) {
|
||||
@ -660,18 +728,15 @@ public final class GlUtil {
|
||||
}
|
||||
|
||||
@DoNotInline
|
||||
public static EGLSurface getEglSurface(
|
||||
EGLDisplay eglDisplay,
|
||||
Object surface,
|
||||
int[] configAttributes,
|
||||
int[] windowSurfaceAttributes)
|
||||
public static EGLSurface createEglSurface(
|
||||
EGLDisplay eglDisplay, Object surface, int[] configAttributes, int[] windowAttributes)
|
||||
throws GlException {
|
||||
EGLSurface eglSurface =
|
||||
EGL14.eglCreateWindowSurface(
|
||||
eglDisplay,
|
||||
getEglConfig(eglDisplay, configAttributes),
|
||||
surface,
|
||||
windowSurfaceAttributes,
|
||||
windowAttributes,
|
||||
/* offset= */ 0);
|
||||
checkEglException("Error creating surface");
|
||||
return eglSurface;
|
||||
@ -739,6 +804,16 @@ public final class GlUtil {
|
||||
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
|
||||
private static EGLConfig getEglConfig(EGLDisplay eglDisplay, int[] attributes)
|
||||
throws GlException {
|
||||
|
@ -99,14 +99,20 @@ public final class ListenerSet<T extends @NonNull Object> {
|
||||
* during one {@link Looper} message queue iteration were handled by the listeners.
|
||||
*/
|
||||
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(
|
||||
CopyOnWriteArraySet<ListenerHolder<T>> listeners,
|
||||
Looper looper,
|
||||
Clock clock,
|
||||
IterationFinishedEvent<T> iterationFinishedEvent) {
|
||||
IterationFinishedEvent<T> iterationFinishedEvent,
|
||||
boolean throwsWhenUsingWrongThread) {
|
||||
this.clock = clock;
|
||||
this.listeners = listeners;
|
||||
this.iterationFinishedEvent = iterationFinishedEvent;
|
||||
@ -117,7 +123,7 @@ public final class ListenerSet<T extends @NonNull Object> {
|
||||
@SuppressWarnings("nullness:methodref.receiver.bound")
|
||||
HandlerWrapper handler = clock.createHandler(looper, this::handleMessage);
|
||||
this.handler = handler;
|
||||
throwsWhenUsingWrongThread = true;
|
||||
this.throwsWhenUsingWrongThread = throwsWhenUsingWrongThread;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -149,7 +155,8 @@ public final class ListenerSet<T extends @NonNull Object> {
|
||||
@CheckResult
|
||||
public ListenerSet<T> copy(
|
||||
Looper looper, Clock clock, IterationFinishedEvent<T> iterationFinishedEvent) {
|
||||
return new ListenerSet<>(listeners, looper, clock, iterationFinishedEvent);
|
||||
return new ListenerSet<>(
|
||||
listeners, looper, clock, iterationFinishedEvent, throwsWhenUsingWrongThread);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -24,6 +24,8 @@ import androidx.annotation.Nullable;
|
||||
import androidx.media3.common.C;
|
||||
import androidx.media3.common.ColorInfo;
|
||||
import androidx.media3.common.Format;
|
||||
import androidx.media3.common.MimeTypes;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.List;
|
||||
|
||||
@ -64,6 +66,70 @@ public final class MediaFormatUtil {
|
||||
|
||||
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}.
|
||||
*
|
||||
@ -196,23 +262,38 @@ public final class MediaFormatUtil {
|
||||
/**
|
||||
* Creates and returns a {@code ColorInfo}, if a valid instance is described in the {@link
|
||||
* 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
|
||||
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;
|
||||
}
|
||||
int colorSpace =
|
||||
mediaFormat.getInteger(MediaFormat.KEY_COLOR_STANDARD, /* defaultValue= */ Format.NO_VALUE);
|
||||
getInteger(
|
||||
mediaFormat, MediaFormat.KEY_COLOR_STANDARD, /* defaultValue= */ Format.NO_VALUE);
|
||||
int colorRange =
|
||||
mediaFormat.getInteger(MediaFormat.KEY_COLOR_RANGE, /* defaultValue= */ Format.NO_VALUE);
|
||||
getInteger(mediaFormat, MediaFormat.KEY_COLOR_RANGE, /* defaultValue= */ Format.NO_VALUE);
|
||||
int colorTransfer =
|
||||
mediaFormat.getInteger(MediaFormat.KEY_COLOR_TRANSFER, /* defaultValue= */ Format.NO_VALUE);
|
||||
getInteger(
|
||||
mediaFormat, MediaFormat.KEY_COLOR_TRANSFER, /* defaultValue= */ Format.NO_VALUE);
|
||||
@Nullable
|
||||
ByteBuffer hdrStaticInfoByteBuffer = mediaFormat.getByteBuffer(MediaFormat.KEY_HDR_STATIC_INFO);
|
||||
@Nullable
|
||||
byte[] hdrStaticInfo =
|
||||
hdrStaticInfoByteBuffer != null ? getArray(hdrStaticInfoByteBuffer) : null;
|
||||
|
||||
if (!allowInvalidValues) {
|
||||
// Some devices may produce invalid values from MediaFormat#getInteger.
|
||||
// See b/239435670 for more information.
|
||||
if (!isValidColorSpace(colorSpace)) {
|
||||
@ -224,22 +305,91 @@ public final class MediaFormatUtil {
|
||||
if (!isValidColorTransfer(colorTransfer)) {
|
||||
colorTransfer = Format.NO_VALUE;
|
||||
}
|
||||
}
|
||||
|
||||
if (colorSpace != Format.NO_VALUE
|
||||
|| colorRange != Format.NO_VALUE
|
||||
|| colorTransfer != Format.NO_VALUE
|
||||
|| hdrStaticInfo != null) {
|
||||
return new ColorInfo(colorSpace, colorRange, colorTransfer, hdrStaticInfo);
|
||||
return new ColorInfo.Builder()
|
||||
.setColorSpace(colorSpace)
|
||||
.setColorRange(colorRange)
|
||||
.setColorTransfer(colorTransfer)
|
||||
.setHdrStaticInfo(hdrStaticInfo)
|
||||
.build();
|
||||
}
|
||||
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) {
|
||||
byte[] array = new byte[byteBuffer.remaining()];
|
||||
byteBuffer.get(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.
|
||||
|
||||
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. */
|
||||
private static boolean isValidColorTransfer(int colorTransfer) {
|
||||
// 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_HLG
|
||||
|| colorTransfer == Format.NO_VALUE;
|
||||
|
@ -15,8 +15,13 @@
|
||||
*/
|
||||
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.media3.common.C;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
|
||||
/**
|
||||
* 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 nextSampleTimestampUs The desired timestamp for the next sample loaded by the calling
|
||||
* 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
|
||||
* 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)
|
||||
throws InterruptedException {
|
||||
Assertions.checkState(firstSampleTimestampUs == MODE_SHARED);
|
||||
if (timestampOffsetUs != C.TIME_UNSET) {
|
||||
// Already initialized.
|
||||
public synchronized void sharedInitializeOrWait(
|
||||
boolean canInitialize, long nextSampleTimestampUs, long timeoutMs)
|
||||
throws InterruptedException, TimeoutException {
|
||||
checkState(firstSampleTimestampUs == MODE_SHARED);
|
||||
if (isInitialized()) {
|
||||
return;
|
||||
} else if (canInitialize) {
|
||||
this.nextSampleTimestampUs.set(nextSampleTimestampUs);
|
||||
} else {
|
||||
// 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();
|
||||
} 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) {
|
||||
return C.TIME_UNSET;
|
||||
}
|
||||
if (timestampOffsetUs == C.TIME_UNSET) {
|
||||
if (!isInitialized()) {
|
||||
long desiredSampleTimestampUs =
|
||||
firstSampleTimestampUs == MODE_SHARED
|
||||
? Assertions.checkNotNull(nextSampleTimestampUs.get())
|
||||
? checkNotNull(nextSampleTimestampUs.get())
|
||||
: firstSampleTimestampUs;
|
||||
timestampOffsetUs = desiredSampleTimestampUs - timeUs;
|
||||
// Notify threads waiting for the timestamp offset to be determined.
|
||||
@ -208,6 +232,11 @@ public final class TimestampAdjuster {
|
||||
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.
|
||||
*
|
||||
|
@ -16,6 +16,8 @@
|
||||
package androidx.media3.common.util;
|
||||
|
||||
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_FORWARD;
|
||||
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_PREVIOUS;
|
||||
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 java.lang.Math.abs;
|
||||
import static java.lang.Math.max;
|
||||
@ -51,6 +54,7 @@ import android.graphics.drawable.Drawable;
|
||||
import android.hardware.display.DisplayManager;
|
||||
import android.media.AudioFormat;
|
||||
import android.media.AudioManager;
|
||||
import android.media.MediaCodec;
|
||||
import android.media.MediaDrm;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
@ -114,6 +118,7 @@ import java.util.concurrent.CancellationException;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
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.nullness.compatqual.NullableType;
|
||||
import org.checkerframework.checker.nullness.qual.EnsuresNonNull;
|
||||
import org.checkerframework.checker.nullness.qual.EnsuresNonNullIf;
|
||||
import org.checkerframework.checker.nullness.qual.PolyNull;
|
||||
|
||||
/** Miscellaneous utility methods. */
|
||||
@ -201,6 +207,55 @@ public final class Util {
|
||||
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
|
||||
* apps. This will be enforced by specifying {@link Context#RECEIVER_NOT_EXPORTED} if {@link
|
||||
@ -440,7 +495,7 @@ public final class Util {
|
||||
@UnstableApi
|
||||
@SuppressWarnings({"nullness:argument", "nullness:return"})
|
||||
public static <T> T[] nullSafeArrayCopy(T[] input, int length) {
|
||||
Assertions.checkArgument(length <= input.length);
|
||||
checkArgument(length <= input.length);
|
||||
return Arrays.copyOf(input, length);
|
||||
}
|
||||
|
||||
@ -455,8 +510,8 @@ public final class Util {
|
||||
@UnstableApi
|
||||
@SuppressWarnings({"nullness:argument", "nullness:return"})
|
||||
public static <T> T[] nullSafeArrayCopyOfRange(T[] input, int from, int to) {
|
||||
Assertions.checkArgument(0 <= from);
|
||||
Assertions.checkArgument(to <= input.length);
|
||||
checkArgument(0 <= from);
|
||||
checkArgument(to <= input.length);
|
||||
return Arrays.copyOfRange(input, from, to);
|
||||
}
|
||||
|
||||
@ -720,6 +775,17 @@ public final class Util {
|
||||
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
|
||||
* java.io.OutputStream} and {@link InputStream} are {@code Closeable}.
|
||||
@ -1575,7 +1641,7 @@ public final class Util {
|
||||
@UnstableApi
|
||||
public static int getIntegerCodeForString(String string) {
|
||||
int length = string.length();
|
||||
Assertions.checkArgument(length <= 4);
|
||||
checkArgument(length <= 4);
|
||||
int result = 0;
|
||||
for (int i = 0; i < length; i++) {
|
||||
result <<= 8;
|
||||
@ -1862,6 +1928,14 @@ public final class Util {
|
||||
return AudioFormat.CHANNEL_OUT_5POINT1 | AudioFormat.CHANNEL_OUT_BACK_CENTER;
|
||||
case 8:
|
||||
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:
|
||||
return AudioFormat.CHANNEL_OUT_7POINT1POINT4;
|
||||
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.
|
||||
*
|
||||
@ -2872,6 +2980,87 @@ public final class Util {
|
||||
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
|
||||
private static String getSystemProperty(String name) {
|
||||
try {
|
||||
|
@ -487,6 +487,65 @@ public class AdPlaybackStateTest {
|
||||
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
|
||||
public void
|
||||
getAdGroupIndexAfterPositionUs_withClientSideInsertedAds_returnsNextAdGroupWithUnplayedAds() {
|
||||
@ -634,4 +693,103 @@ public class AdPlaybackStateTest {
|
||||
/* positionUs= */ C.TIME_END_OF_SOURCE, /* periodDurationUs= */ 5000))
|
||||
.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);
|
||||
}
|
||||
}
|
||||
|
@ -28,7 +28,11 @@ public class DeviceInfoTest {
|
||||
@Test
|
||||
public void roundTripViaBundle_yieldsEqualInstance() {
|
||||
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);
|
||||
}
|
||||
|
@ -247,6 +247,25 @@ public class MediaItemTest {
|
||||
.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
|
||||
public void builderSetCustomCacheKey_setsCustomCacheKey() {
|
||||
MediaItem mediaItem =
|
||||
@ -319,6 +338,42 @@ public class MediaItemTest {
|
||||
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
|
||||
public void builderSetTag_isNullByDefault() {
|
||||
MediaItem mediaItem = new MediaItem.Builder().setUri(URI_STRING).build();
|
||||
@ -538,6 +593,21 @@ public class MediaItemTest {
|
||||
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
|
||||
public void builderSetMediaMetadata_setsMetadata() {
|
||||
MediaMetadata mediaMetadata = new MediaMetadata.Builder().setTitle("title").build();
|
||||
@ -595,6 +665,68 @@ public class MediaItemTest {
|
||||
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
|
||||
public void builderSetLiveConfiguration() {
|
||||
MediaItem mediaItem =
|
||||
@ -727,7 +859,7 @@ public class MediaItemTest {
|
||||
MediaItem copy = mediaItem.buildUpon().build();
|
||||
|
||||
assertThat(copy).isEqualTo(mediaItem);
|
||||
assertThat(copy.localConfiguration).isEqualTo(mediaItem.playbackProperties);
|
||||
assertThat(copy.localConfiguration).isEqualTo(mediaItem.localConfiguration);
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -792,7 +924,7 @@ public class MediaItemTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void roundTripViaBundle_withoutPlaybackProperties_yieldsEqualInstance() {
|
||||
public void roundTripViaBundle_withoutLocalConfiguration_yieldsEqualInstance() {
|
||||
MediaItem mediaItem =
|
||||
new MediaItem.Builder()
|
||||
.setMediaId("mediaId")
|
||||
@ -822,13 +954,25 @@ public class MediaItemTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void roundTripViaBundle_withPlaybackProperties_dropsPlaybackProperties() {
|
||||
public void
|
||||
roundTripViaDefaultBundle_mediaItemContainsLocalConfiguration_dropsLocalConfiguration() {
|
||||
MediaItem mediaItem = new MediaItem.Builder().setUri(URI_STRING).build();
|
||||
|
||||
assertThat(mediaItem.localConfiguration).isNotNull();
|
||||
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
|
||||
public void createDefaultMediaItemInstance_checksDefaultValues() {
|
||||
MediaItem mediaItem = new MediaItem.Builder().build();
|
||||
|
@ -30,6 +30,7 @@ public class MediaMetadataTest {
|
||||
private static final String EXTRAS_KEY = "exampleKey";
|
||||
private static final String EXTRAS_VALUE = "exampleValue";
|
||||
|
||||
@SuppressWarnings("deprecation") // Testing deprecated field.
|
||||
@Test
|
||||
public void builder_minimal_correctDefaults() {
|
||||
MediaMetadata mediaMetadata = new MediaMetadata.Builder().build();
|
||||
@ -134,6 +135,7 @@ public class MediaMetadataTest {
|
||||
assertThat(mediaMetadataFromBundle.extras.getString(EXTRAS_KEY)).isEqualTo(EXTRAS_VALUE);
|
||||
}
|
||||
|
||||
@SuppressWarnings("deprecation") // Testing deprecated setter.
|
||||
@Test
|
||||
public void builderSetFolderType_toNone_setsIsBrowsableToFalse() {
|
||||
MediaMetadata mediaMetadata =
|
||||
@ -142,6 +144,7 @@ public class MediaMetadataTest {
|
||||
assertThat(mediaMetadata.isBrowsable).isFalse();
|
||||
}
|
||||
|
||||
@SuppressWarnings("deprecation") // Testing deprecated setter.
|
||||
@Test
|
||||
public void builderSetFolderType_toNotNone_setsIsBrowsableToTrueAndMatchingMediaType() {
|
||||
MediaMetadata mediaMetadata =
|
||||
@ -151,6 +154,7 @@ public class MediaMetadataTest {
|
||||
assertThat(mediaMetadata.mediaType).isEqualTo(MediaMetadata.MEDIA_TYPE_FOLDER_PLAYLISTS);
|
||||
}
|
||||
|
||||
@SuppressWarnings("deprecation") // Testing deprecated setter.
|
||||
@Test
|
||||
public void
|
||||
builderSetFolderType_toNotNoneWithManualMediaType_setsIsBrowsableToTrueAndDoesNotOverrideMediaType() {
|
||||
@ -164,6 +168,7 @@ public class MediaMetadataTest {
|
||||
assertThat(mediaMetadata.mediaType).isEqualTo(MediaMetadata.MEDIA_TYPE_FOLDER_PODCASTS);
|
||||
}
|
||||
|
||||
@SuppressWarnings("deprecation") // Testing deprecated field.
|
||||
@Test
|
||||
public void builderSetIsBrowsable_toTrueWithoutMediaType_setsFolderTypeToMixed() {
|
||||
MediaMetadata mediaMetadata = new MediaMetadata.Builder().setIsBrowsable(true).build();
|
||||
@ -171,6 +176,7 @@ public class MediaMetadataTest {
|
||||
assertThat(mediaMetadata.folderType).isEqualTo(MediaMetadata.FOLDER_TYPE_MIXED);
|
||||
}
|
||||
|
||||
@SuppressWarnings("deprecation") // Testing deprecated field.
|
||||
@Test
|
||||
public void builderSetIsBrowsable_toTrueWithMediaType_setsFolderTypeToMatchMediaType() {
|
||||
MediaMetadata mediaMetadata =
|
||||
@ -182,6 +188,7 @@ public class MediaMetadataTest {
|
||||
assertThat(mediaMetadata.folderType).isEqualTo(MediaMetadata.FOLDER_TYPE_ARTISTS);
|
||||
}
|
||||
|
||||
@SuppressWarnings("deprecation") // Testing deprecated field.
|
||||
@Test
|
||||
public void builderSetFolderType_toFalse_setsFolderTypeToNone() {
|
||||
MediaMetadata mediaMetadata = new MediaMetadata.Builder().setIsBrowsable(false).build();
|
||||
@ -189,6 +196,7 @@ public class MediaMetadataTest {
|
||||
assertThat(mediaMetadata.folderType).isEqualTo(MediaMetadata.FOLDER_TYPE_NONE);
|
||||
}
|
||||
|
||||
@SuppressWarnings("deprecation") // Setting deprecated fields.
|
||||
private static MediaMetadata getFullyPopulatedMediaMetadata() {
|
||||
Bundle extras = new Bundle();
|
||||
extras.putString(EXTRAS_KEY, EXTRAS_VALUE);
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
@ -17,6 +17,7 @@ package androidx.media3.common;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.os.Parcel;
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
import org.junit.Test;
|
||||
@ -28,7 +29,8 @@ public class StreamKeyTest {
|
||||
|
||||
@Test
|
||||
public void parcelable() {
|
||||
StreamKey streamKeyToParcel = new StreamKey(1, 2, 3);
|
||||
StreamKey streamKeyToParcel =
|
||||
new StreamKey(/* periodIndex= */ 1, /* groupIndex= */ 2, /* streamIndex= */ 3);
|
||||
Parcel parcel = Parcel.obtain();
|
||||
streamKeyToParcel.writeToParcel(parcel, 0);
|
||||
parcel.setDataPosition(0);
|
||||
@ -38,4 +40,36 @@ public class StreamKeyTest {
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
@ -15,12 +15,15 @@
|
||||
*/
|
||||
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 android.os.Bundle;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.media3.common.MediaItem.LiveConfiguration;
|
||||
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.TimelineWindowDefinition;
|
||||
import androidx.media3.test.utils.TimelineAsserts;
|
||||
@ -431,6 +434,34 @@ public class TimelineTest {
|
||||
/* 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.
|
||||
private static Timeline.Window populateWindow(
|
||||
@Nullable MediaItem mediaItem, @Nullable Object tag) {
|
||||
|
@ -0,0 +1,368 @@
|
||||
/*
|
||||
* 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 com.google.common.truth.Truth.assertThat;
|
||||
import static java.lang.Math.min;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.media3.common.C;
|
||||
import androidx.media3.common.audio.AudioProcessor.AudioFormat;
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
// TODO(b/198772621): Add tests for PlaybackParameter changes once Sonic or
|
||||
// DefaultAudioProcessorChain is in common.
|
||||
/** Unit tests for {@link AudioProcessingPipeline}. */
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public final class AudioProcessingPipelineTest {
|
||||
private static final AudioFormat AUDIO_FORMAT =
|
||||
new AudioFormat(/* sampleRate= */ 44100, /* channelCount= */ 2, C.ENCODING_PCM_16BIT);
|
||||
|
||||
@Test
|
||||
public void noAudioProcessors_isNotOperational() throws Exception {
|
||||
AudioProcessingPipeline audioProcessingPipeline =
|
||||
new AudioProcessingPipeline(ImmutableList.of());
|
||||
|
||||
audioProcessingPipeline.configure(AUDIO_FORMAT);
|
||||
audioProcessingPipeline.flush();
|
||||
|
||||
assertThat(audioProcessingPipeline.isOperational()).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void sameProcessors_pipelinesAreOnlyEqualIfSameOrderAndReference() throws Exception {
|
||||
AudioProcessor audioProcessorOne = new FakeAudioProcessor(/* active= */ true);
|
||||
AudioProcessor audioProcessorTwo = new FakeAudioProcessor(/* active= */ false);
|
||||
AudioProcessor audioProcessorThree = new FakeAudioProcessor(/* active= */ true);
|
||||
|
||||
AudioProcessingPipeline pipelineOne =
|
||||
new AudioProcessingPipeline(
|
||||
ImmutableList.of(audioProcessorOne, audioProcessorTwo, audioProcessorThree));
|
||||
// The internal state of the pipeline does not affect equality.
|
||||
pipelineOne.configure(AUDIO_FORMAT);
|
||||
pipelineOne.flush();
|
||||
|
||||
AudioProcessingPipeline pipelineTwo =
|
||||
new AudioProcessingPipeline(
|
||||
ImmutableList.of(audioProcessorOne, audioProcessorTwo, audioProcessorThree));
|
||||
|
||||
assertThat(pipelineOne).isEqualTo(pipelineTwo);
|
||||
|
||||
AudioProcessingPipeline pipelineThree =
|
||||
new AudioProcessingPipeline(
|
||||
ImmutableList.of(audioProcessorThree, audioProcessorTwo, audioProcessorOne));
|
||||
assertThat(pipelineTwo).isNotEqualTo(pipelineThree);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void configuringPipeline_givesFormat() throws Exception {
|
||||
FakeAudioProcessor fakeSampleRateChangingAudioProcessor =
|
||||
new FakeAudioProcessor(/* active= */ true) {
|
||||
@Override
|
||||
public AudioFormat configure(AudioFormat inputAudioFormat)
|
||||
throws UnhandledAudioFormatException {
|
||||
AudioFormat outputFormat =
|
||||
new AudioFormat(
|
||||
inputAudioFormat.sampleRate * 2,
|
||||
inputAudioFormat.channelCount,
|
||||
inputAudioFormat.encoding);
|
||||
return super.configure(outputFormat);
|
||||
}
|
||||
};
|
||||
|
||||
AudioProcessingPipeline audioProcessingPipeline =
|
||||
new AudioProcessingPipeline(ImmutableList.of(fakeSampleRateChangingAudioProcessor));
|
||||
AudioFormat outputFormat = audioProcessingPipeline.configure(AUDIO_FORMAT);
|
||||
|
||||
assertThat(outputFormat.sampleRate).isEqualTo(AUDIO_FORMAT.sampleRate * 2);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void configuringAndFlushingPipeline_isOperational() throws Exception {
|
||||
FakeAudioProcessor fakeSampleRateChangingAudioProcessor =
|
||||
new FakeAudioProcessor(/* active= */ true) {
|
||||
@Override
|
||||
public AudioFormat configure(AudioFormat inputAudioFormat)
|
||||
throws UnhandledAudioFormatException {
|
||||
AudioFormat outputFormat =
|
||||
new AudioFormat(
|
||||
inputAudioFormat.sampleRate * 2,
|
||||
inputAudioFormat.channelCount,
|
||||
inputAudioFormat.encoding);
|
||||
return super.configure(outputFormat);
|
||||
}
|
||||
};
|
||||
|
||||
AudioProcessingPipeline audioProcessingPipeline =
|
||||
new AudioProcessingPipeline(ImmutableList.of(fakeSampleRateChangingAudioProcessor));
|
||||
|
||||
assertThat(audioProcessingPipeline.isOperational()).isFalse();
|
||||
audioProcessingPipeline.configure(AUDIO_FORMAT);
|
||||
// Configuring the pipeline is not enough for it to be operational.
|
||||
assertThat(audioProcessingPipeline.isOperational()).isFalse();
|
||||
audioProcessingPipeline.flush();
|
||||
assertThat(audioProcessingPipeline.isOperational()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void reconfigure_doesNotChangeOperational_untilFlush() throws Exception {
|
||||
FakeAudioProcessor audioProcessor = new FakeAudioProcessor(/* active= */ true);
|
||||
AudioProcessingPipeline audioProcessingPipeline =
|
||||
new AudioProcessingPipeline(ImmutableList.of(audioProcessor));
|
||||
audioProcessingPipeline.configure(AUDIO_FORMAT);
|
||||
audioProcessingPipeline.flush();
|
||||
assertThat(audioProcessingPipeline.isOperational()).isTrue();
|
||||
|
||||
audioProcessor.setActive(false);
|
||||
audioProcessingPipeline.configure(AUDIO_FORMAT);
|
||||
assertThat(audioProcessingPipeline.isOperational()).isTrue();
|
||||
audioProcessingPipeline.flush();
|
||||
assertThat(audioProcessingPipeline.isOperational()).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void inactiveProcessor_isIgnoredInConfiguration() throws Exception {
|
||||
FakeAudioProcessor fakeSampleRateChangingAudioProcessor =
|
||||
new FakeAudioProcessor(/* active= */ false) {
|
||||
@Override
|
||||
public AudioFormat configure(AudioFormat inputAudioFormat)
|
||||
throws UnhandledAudioFormatException {
|
||||
AudioFormat outputFormat =
|
||||
new AudioFormat(
|
||||
inputAudioFormat.sampleRate * 2,
|
||||
inputAudioFormat.channelCount,
|
||||
inputAudioFormat.encoding);
|
||||
return super.configure(outputFormat);
|
||||
}
|
||||
};
|
||||
|
||||
AudioProcessingPipeline audioProcessingPipeline =
|
||||
new AudioProcessingPipeline(ImmutableList.of(fakeSampleRateChangingAudioProcessor));
|
||||
AudioFormat outputFormat = audioProcessingPipeline.configure(AUDIO_FORMAT);
|
||||
audioProcessingPipeline.flush();
|
||||
assertThat(outputFormat).isEqualTo(AUDIO_FORMAT);
|
||||
assertThat(audioProcessingPipeline.isOperational()).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void queueInput_producesOutputBuffer() throws Exception {
|
||||
FakeAudioProcessor audioProcessor = new FakeAudioProcessor(/* active= */ true);
|
||||
AudioProcessingPipeline audioProcessingPipeline =
|
||||
new AudioProcessingPipeline(ImmutableList.of(audioProcessor));
|
||||
audioProcessingPipeline.configure(AUDIO_FORMAT);
|
||||
audioProcessingPipeline.flush();
|
||||
|
||||
ByteBuffer inputBuffer = createOneSecondDefaultSilenceBuffer(AUDIO_FORMAT);
|
||||
long inputBytes = inputBuffer.remaining();
|
||||
audioProcessingPipeline.queueInput(inputBuffer);
|
||||
inputBytes -= inputBuffer.remaining();
|
||||
ByteBuffer outputBuffer = audioProcessingPipeline.getOutput();
|
||||
assertThat(inputBytes).isEqualTo(outputBuffer.remaining());
|
||||
assertThat(inputBuffer).isNotSameInstanceAs(outputBuffer);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void isEnded_needsBufferConsuming() throws Exception {
|
||||
FakeAudioProcessor audioProcessor = new FakeAudioProcessor(/* active= */ true);
|
||||
AudioProcessingPipeline audioProcessingPipeline =
|
||||
new AudioProcessingPipeline(ImmutableList.of(audioProcessor));
|
||||
audioProcessingPipeline.configure(AUDIO_FORMAT);
|
||||
audioProcessingPipeline.flush();
|
||||
|
||||
ByteBuffer inputBuffer = createOneSecondDefaultSilenceBuffer(AUDIO_FORMAT);
|
||||
audioProcessingPipeline.queueInput(inputBuffer);
|
||||
audioProcessingPipeline.queueEndOfStream();
|
||||
assertThat(audioProcessingPipeline.isEnded()).isFalse();
|
||||
ByteBuffer outputBuffer = audioProcessingPipeline.getOutput();
|
||||
assertThat(audioProcessingPipeline.isEnded()).isFalse();
|
||||
|
||||
// "consume" the buffer
|
||||
outputBuffer.position(outputBuffer.limit());
|
||||
assertThat(audioProcessingPipeline.isEnded()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void pipelineWithAdvancedAudioProcessors_drainsAndFeedsCorrectly_duplicatesBytes()
|
||||
throws Exception {
|
||||
AudioProcessingPipeline audioProcessingPipeline =
|
||||
new AudioProcessingPipeline(
|
||||
ImmutableList.of(
|
||||
new FakeAudioProcessor(
|
||||
/* active= */ true, /* maxInputBytesAtOnce= */ 8, /* duplicateBytes= */ true),
|
||||
new FakeAudioProcessor(
|
||||
/* active= */ true, /* maxInputBytesAtOnce= */ 0, /* duplicateBytes= */ false),
|
||||
new FakeAudioProcessor(
|
||||
/* active= */ true, /* maxInputBytesAtOnce= */ 12, /* duplicateBytes= */ true),
|
||||
new FakeAudioProcessor(
|
||||
/* active= */ true,
|
||||
/* maxInputBytesAtOnce= */ 160,
|
||||
/* duplicateBytes= */ false)));
|
||||
audioProcessingPipeline.configure(AUDIO_FORMAT);
|
||||
audioProcessingPipeline.flush();
|
||||
|
||||
ByteBuffer inputBuffer = createOneSecondDefaultSilenceBuffer(AUDIO_FORMAT);
|
||||
inputBuffer.put(0, (byte) 24);
|
||||
inputBuffer.put(1, (byte) 36);
|
||||
inputBuffer.put(2, (byte) 6);
|
||||
int bytesInput = inputBuffer.remaining();
|
||||
List<Byte> bytesOutput = new ArrayList<>();
|
||||
while (!audioProcessingPipeline.isEnded()) {
|
||||
ByteBuffer bufferToConsume;
|
||||
while ((bufferToConsume = audioProcessingPipeline.getOutput()).hasRemaining()) {
|
||||
// "consume" the buffer. Equivalent to writing downstream.
|
||||
bytesOutput.add(bufferToConsume.get());
|
||||
}
|
||||
if (!inputBuffer.hasRemaining()) {
|
||||
audioProcessingPipeline.queueEndOfStream();
|
||||
} else {
|
||||
audioProcessingPipeline.queueInput(inputBuffer);
|
||||
}
|
||||
}
|
||||
assertThat(audioProcessingPipeline.isEnded()).isTrue();
|
||||
assertThat(4 * bytesInput).isEqualTo(bytesOutput.size());
|
||||
|
||||
assertThat(bytesOutput.get(0)).isEqualTo((byte) 24);
|
||||
assertThat(bytesOutput.get(1)).isEqualTo((byte) 24);
|
||||
assertThat(bytesOutput.get(2)).isEqualTo((byte) 24);
|
||||
assertThat(bytesOutput.get(3)).isEqualTo((byte) 24);
|
||||
assertThat(bytesOutput.get(4)).isEqualTo((byte) 36);
|
||||
assertThat(bytesOutput.get(5)).isEqualTo((byte) 36);
|
||||
assertThat(bytesOutput.get(6)).isEqualTo((byte) 36);
|
||||
assertThat(bytesOutput.get(7)).isEqualTo((byte) 36);
|
||||
assertThat(bytesOutput.get(8)).isEqualTo((byte) 6);
|
||||
assertThat(bytesOutput.get(9)).isEqualTo((byte) 6);
|
||||
assertThat(bytesOutput.get(10)).isEqualTo((byte) 6);
|
||||
assertThat(bytesOutput.get(11)).isEqualTo((byte) 6);
|
||||
assertThat(bytesOutput.get(12)).isEqualTo((byte) 0);
|
||||
}
|
||||
|
||||
// TODO(b/198772621): Consider implementing BaseAudioProcessor once that is in common.
|
||||
private static class FakeAudioProcessor implements AudioProcessor {
|
||||
protected ByteBuffer internalBuffer;
|
||||
private boolean inputEnded;
|
||||
private boolean active;
|
||||
private final int maxInputBytesAtOnce;
|
||||
private final boolean duplicateBytes;
|
||||
|
||||
private @MonotonicNonNull AudioFormat pendingOutputFormat;
|
||||
private @MonotonicNonNull AudioFormat outputFormat;
|
||||
|
||||
public FakeAudioProcessor(boolean active) {
|
||||
this(active, /* maxInputBytesAtOnce= */ 0, /* duplicateBytes= */ false);
|
||||
}
|
||||
|
||||
public FakeAudioProcessor(boolean active, int maxInputBytesAtOnce, boolean duplicateBytes) {
|
||||
this.active = active;
|
||||
this.maxInputBytesAtOnce = maxInputBytesAtOnce;
|
||||
this.duplicateBytes = duplicateBytes;
|
||||
internalBuffer = EMPTY_BUFFER;
|
||||
}
|
||||
|
||||
public void setActive(boolean active) {
|
||||
this.active = active;
|
||||
}
|
||||
|
||||
@Override
|
||||
public AudioFormat configure(AudioFormat inputAudioFormat)
|
||||
throws UnhandledAudioFormatException {
|
||||
pendingOutputFormat = inputAudioFormat;
|
||||
return pendingOutputFormat;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isActive() {
|
||||
return active && !pendingOutputFormat.equals(AudioFormat.NOT_SET);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void queueInput(ByteBuffer inputBuffer) {
|
||||
if (outputFormat.equals(AudioFormat.NOT_SET)) {
|
||||
return;
|
||||
}
|
||||
|
||||
int remaining = inputBuffer.remaining();
|
||||
if (remaining == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
internalBuffer =
|
||||
createOrReplaceBuffer(
|
||||
maxInputBytesAtOnce > 0 ? min(remaining, maxInputBytesAtOnce) : remaining,
|
||||
internalBuffer);
|
||||
|
||||
while (internalBuffer.hasRemaining()) {
|
||||
byte b = inputBuffer.get();
|
||||
internalBuffer.put(b);
|
||||
if (duplicateBytes) {
|
||||
internalBuffer.put(b);
|
||||
}
|
||||
}
|
||||
|
||||
internalBuffer.flip();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void queueEndOfStream() {
|
||||
inputEnded = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ByteBuffer getOutput() {
|
||||
return internalBuffer;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isEnded() {
|
||||
return inputEnded && !internalBuffer.hasRemaining();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void flush() {
|
||||
internalBuffer.clear();
|
||||
internalBuffer = EMPTY_BUFFER;
|
||||
inputEnded = false;
|
||||
outputFormat = pendingOutputFormat;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reset() {
|
||||
flush();
|
||||
}
|
||||
}
|
||||
|
||||
private static ByteBuffer createOrReplaceBuffer(int size, @Nullable ByteBuffer buffer) {
|
||||
if (buffer == null || buffer.capacity() < size) {
|
||||
buffer = ByteBuffer.allocateDirect(size).order(ByteOrder.nativeOrder());
|
||||
}
|
||||
buffer.clear();
|
||||
return buffer;
|
||||
}
|
||||
|
||||
/** Creates a one second silence buffer for the given {@link AudioFormat}. */
|
||||
private static ByteBuffer createOneSecondDefaultSilenceBuffer(AudioFormat audioFormat) {
|
||||
return createOrReplaceBuffer(
|
||||
/* size= */ audioFormat.sampleRate * audioFormat.channelCount * audioFormat.bytesPerFrame,
|
||||
/* buffer= */ null);
|
||||
}
|
||||
}
|
@ -0,0 +1,166 @@
|
||||
/*
|
||||
* 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 com.google.common.truth.Truth.assertThat;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
|
||||
import androidx.media3.common.C;
|
||||
import androidx.media3.common.audio.AudioProcessor.AudioFormat;
|
||||
import androidx.media3.common.audio.AudioProcessor.UnhandledAudioFormatException;
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
/** Unit tests for {@link ChannelMixingAudioProcessor}. */
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public final class ChannelMixingAudioProcessorTest {
|
||||
|
||||
private static final AudioFormat AUDIO_FORMAT_48KHZ_STEREO_16BIT =
|
||||
new AudioFormat(/* sampleRate= */ 48000, /* channelCount= */ 2, C.ENCODING_PCM_16BIT);
|
||||
|
||||
private ChannelMixingAudioProcessor audioProcessor;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
audioProcessor = new ChannelMixingAudioProcessor();
|
||||
audioProcessor.putChannelMixingMatrix(
|
||||
ChannelMixingMatrix.create(/* inputChannelCount= */ 2, /* outputChannelCount= */ 1));
|
||||
audioProcessor.putChannelMixingMatrix(
|
||||
ChannelMixingMatrix.create(/* inputChannelCount= */ 1, /* outputChannelCount= */ 2));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void configure_outputAudioFormatMatchesChannelCountOfMatrix() throws Exception {
|
||||
AudioFormat outputAudioFormat = audioProcessor.configure(AUDIO_FORMAT_48KHZ_STEREO_16BIT);
|
||||
|
||||
assertThat(outputAudioFormat.channelCount).isEqualTo(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void configureUnhandledChannelCount_throws() {
|
||||
assertThrows(
|
||||
UnhandledAudioFormatException.class,
|
||||
() ->
|
||||
audioProcessor.configure(
|
||||
new AudioFormat(
|
||||
/* sampleRate= */ 44100, /* channelCount= */ 3, C.ENCODING_PCM_16BIT)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void reconfigureWithDifferentMatrix_outputsCorrectChannelCount() throws Exception {
|
||||
AudioFormat outputAudioFormat = audioProcessor.configure(AUDIO_FORMAT_48KHZ_STEREO_16BIT);
|
||||
assertThat(outputAudioFormat.channelCount).isEqualTo(1);
|
||||
audioProcessor.flush();
|
||||
audioProcessor.putChannelMixingMatrix(
|
||||
new ChannelMixingMatrix(
|
||||
/* inputChannelCount= */ 2,
|
||||
/* outputChannelCount= */ 6,
|
||||
new float[] {
|
||||
/* L channel factors */ 0.5f, 0.1f, 0.1f, 0.1f, 0.1f, 0.1f,
|
||||
/* R channel factors */ 0.1f, 0.5f, 0.1f, 0.1f, 0.1f, 0.1f
|
||||
}));
|
||||
outputAudioFormat = audioProcessor.configure(AUDIO_FORMAT_48KHZ_STEREO_16BIT);
|
||||
|
||||
assertThat(outputAudioFormat.channelCount).isEqualTo(6);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void configureWithCustomMixingMatrix_isActiveReturnsTrue() throws Exception {
|
||||
audioProcessor.putChannelMixingMatrix(
|
||||
new ChannelMixingMatrix(
|
||||
/* inputChannelCount= */ 3,
|
||||
/* outputChannelCount= */ 2,
|
||||
new float[] {
|
||||
/* L channel factors */ 0.5f, 0.5f, 0.0f,
|
||||
/* R channel factors */ 0.0f, 0.5f, 0.5f
|
||||
}));
|
||||
AudioFormat outputAudioFormat =
|
||||
audioProcessor.configure(
|
||||
new AudioFormat(/* sampleRate= */ 48000, /* channelCount= */ 3, C.ENCODING_PCM_16BIT));
|
||||
|
||||
assertThat(audioProcessor.isActive()).isTrue();
|
||||
assertThat(outputAudioFormat.channelCount).isEqualTo(2);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void configureWithIdentityMatrix_isActiveReturnsFalse() throws Exception {
|
||||
audioProcessor.putChannelMixingMatrix(
|
||||
ChannelMixingMatrix.create(/* inputChannelCount= */ 2, /* outputChannelCount= */ 2));
|
||||
|
||||
audioProcessor.configure(AUDIO_FORMAT_48KHZ_STEREO_16BIT);
|
||||
assertThat(audioProcessor.isActive()).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void queueInputGetOutput_frameCountMatches() throws Exception {
|
||||
AudioFormat inputAudioFormat = AUDIO_FORMAT_48KHZ_STEREO_16BIT;
|
||||
AudioFormat outputAudioFormat = audioProcessor.configure(inputAudioFormat);
|
||||
audioProcessor.flush();
|
||||
audioProcessor.queueInput(
|
||||
ByteBuffer.allocateDirect(inputAudioFormat.sampleRate * inputAudioFormat.bytesPerFrame)
|
||||
.order(ByteOrder.nativeOrder()));
|
||||
|
||||
assertThat(audioProcessor.getOutput().remaining() / outputAudioFormat.bytesPerFrame)
|
||||
.isEqualTo(48000);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void stereoToMonoMixingMatrix_queueInput_outputIsMono() throws Exception {
|
||||
audioProcessor.configure(AUDIO_FORMAT_48KHZ_STEREO_16BIT);
|
||||
audioProcessor.flush();
|
||||
audioProcessor.queueInput(getByteBufferFromShortValues(0, 0, 16383, 16383, 32767, 32767));
|
||||
|
||||
assertThat(audioProcessor.getOutput()).isEqualTo(getByteBufferFromShortValues(0, 16383, 32767));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void scaledMixingMatrix_queueInput_outputIsScaled() throws Exception {
|
||||
audioProcessor.putChannelMixingMatrix(
|
||||
ChannelMixingMatrix.create(/* inputChannelCount= */ 2, /* outputChannelCount= */ 2)
|
||||
.scaleBy(0.5f));
|
||||
|
||||
audioProcessor.configure(AUDIO_FORMAT_48KHZ_STEREO_16BIT);
|
||||
audioProcessor.flush();
|
||||
audioProcessor.queueInput(getByteBufferFromShortValues(0, 0, 16383, 16383, 32767, 16383));
|
||||
|
||||
assertThat(audioProcessor.getOutput())
|
||||
.isEqualTo(getByteBufferFromShortValues(0, 0, 8191, 8191, 16383, 8191));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void queueInputMultipleTimes_getOutputAsExpected() throws Exception {
|
||||
audioProcessor.configure(AUDIO_FORMAT_48KHZ_STEREO_16BIT);
|
||||
audioProcessor.flush();
|
||||
audioProcessor.queueInput(getByteBufferFromShortValues(0, 32767, 0, 32767, 0, 0));
|
||||
audioProcessor.getOutput();
|
||||
audioProcessor.queueInput(getByteBufferFromShortValues(32767, 32767, 0, 0, 32767, 0));
|
||||
|
||||
assertThat(audioProcessor.getOutput()).isEqualTo(getByteBufferFromShortValues(32767, 0, 16383));
|
||||
}
|
||||
|
||||
private static ByteBuffer getByteBufferFromShortValues(int... values) {
|
||||
ByteBuffer buffer = ByteBuffer.allocateDirect(values.length * 2).order(ByteOrder.nativeOrder());
|
||||
for (int s : values) {
|
||||
buffer.putShort((short) s);
|
||||
}
|
||||
buffer.rewind();
|
||||
return buffer;
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user