Merge pull request #4 from androidx/main

Merge the code from androidx/media main branch
This commit is contained in:
ybai001 2024-03-18 11:11:27 +08:00 committed by GitHub
commit 563f4d845a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2659 changed files with 425716 additions and 50061 deletions

View File

@ -5,37 +5,28 @@ body:
- type: markdown
attributes:
value: |
We can only process bug reports that are actionable. Unclear bug reports or reports with
insufficient information may not get attention.
We can only process bug reports that are actionable. Unclear bug reports or reports with insufficient information may not get attention.
Before filing a bug:
-------------------------
- Search existing issues, including issues that are closed:
https://github.com/androidx/media/issues?q=is%3Aissue
- For ExoPlayer-related bugs, please also check for existing issues on the ExoPlayer
tracker: https://github.com/google/ExoPlayer/issues?q=is%3Aissue
- Search existing issues, including issues that are closed: https://github.com/androidx/media/issues?q=is%3Aissue
- For ExoPlayer-related bugs, please also check for existing issues on the ExoPlayer 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.0.2
- Media3 1.0.1
- Media3 1.0.0
- Media3 1.0.0-rc02
- Media3 1.0.0-rc01
- Media3 1.0.0-beta03
- Media3 1.0.0-beta02
- Media3 1.0.0-beta01
- Media3 1.0.0-alpha03
- Media3 1.0.0-alpha02
- Media3 1.0.0-alpha01
- Media3 `main` branch
- ExoPlayer 2.18.7
- ExoPlayer 2.18.6
- ExoPlayer 2.18.5
- Media3 main branch
- Media3 pre-release (alpha, beta or RC not in this list)
- Media3 1.3.0
- Media3 1.2.1
- Media3 1.2.0
- Media3 1.1.1 / ExoPlayer 2.19.1
- Media3 1.1.0 / ExoPlayer 2.19.0
- Media3 1.0.2 / ExoPlayer 2.18.7
- Media3 1.0.1 / ExoPlayer 2.18.6
- Media3 1.0.0 / ExoPlayer 2.18.5
- ExoPlayer 2.18.4
- ExoPlayer 2.18.3
- ExoPlayer 2.18.2
@ -50,10 +41,16 @@ body:
- ExoPlayer 2.14.2
- ExoPlayer 2.14.1
- ExoPlayer 2.14.0
- ExoPlayer `dev-v2` branch
- ExoPlayer dev-v2 branch
- 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 +111,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.
@ -124,8 +121,8 @@ body:
attributes:
label: Bug Report
description: |
After filing this issue please run `adb bugreport` shortly after reproducing the problem (ideally in the [demo app](https://github.com/androidx/media/tree/release/demos/main)) to capture a zip file, and email this to dev.exoplayer@gmail.com with subject 'Issue #\<issuenumber\>'.
After filing this issue please run `adb bugreport` shortly after reproducing the problem (ideally in the [demo app](https://github.com/androidx/media/tree/release/demos/main)) to capture a zip file, and email this to android-media-github@google.com with subject 'Issue #\<issuenumber\>'.
**Note:** Logcat output is **not** the same as a full bug report, and is often missing information that's useful for diagnosing issues. Please ensure you're sending a full bug report zip file.
options:
- label: You will email the zip file produced by `adb bugreport` to dev.exoplayer@gmail.com after filing this issue.
- label: You will email the zip file produced by `adb bugreport` to android-media-github@google.com after filing this issue.

View File

@ -39,6 +39,6 @@ Don't forget to check ExoPlayer's supported formats and devices, if applicable
(https://developer.android.com/guide/topics/media/exoplayer/supported-formats).
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
View File

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

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

@ -38,6 +38,24 @@ you made on top of `main` using
$ git diff -U0 main... | google-java-format-diff.py -p1 -i
```
### 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

View File

@ -1,19 +1,21 @@
# AndroidX Media
AndroidX Media is a collection of libraries for implementing media use cases on
Android, including local playback (via ExoPlayer) and media sessions.
Android, including local playback (via ExoPlayer), video editing (via Transformer) and media sessions.
## Documentation
* The [developer guide][] provides a wealth of information.
* The [class reference][] documents the classes and methods.
* The [release notes][] document the major changes in each release.
* The [media dev center][] provides samples and guidelines.
* Follow our [developer blog][] to keep up to date with the latest
developments!
[developer guide]: https://developer.android.com/guide/topics/media/media3
[class reference]: https://developer.android.com/reference/androidx/media3/common/package-summary
[release notes]: RELEASENOTES.md
[media dev center]: https://developer.android.com/media
[developer blog]: https://medium.com/google-exoplayer
## Migration for existing ExoPlayer and MediaSession projects
@ -45,13 +47,21 @@ also possible to clone this GitHub repository and depend on the modules locally.
#### 1. Add module dependencies
The easiest way to get started using AndroidX Media is to add gradle
dependencies on the libraries you need in the `build.gradle` file of your app
module.
dependencies on the libraries you need in the `build.gradle.kts` file of your
app module.
For example, to depend on ExoPlayer with DASH playback support and UI components
you can add dependencies on the modules like this:
```gradle
```kotlin
implementation("androidx.media3:media3-exoplayer:1.X.X")
implementation("androidx.media3:media3-exoplayer-dash:1.X.X")
implementation("androidx.media3:media3-ui:1.X.X")
```
Or in Gradle Groovy DSL `build.gradle`:
```groovy
implementation 'androidx.media3:media3-exoplayer:1.X.X'
implementation 'androidx.media3:media3-exoplayer-dash:1.X.X'
implementation 'androidx.media3:media3-ui:1.X.X'
@ -73,10 +83,18 @@ details.
#### 2. Turn on Java 8 support
If not enabled already, you also need to turn on Java 8 support in all
`build.gradle` files depending on AndroidX Media, by adding the following to the
`android` section:
`build.gradle.kts` files depending on AndroidX Media, by adding the following to
the `android` section:
```gradle
```kotlin
compileOptions {
targetCompatibility = JavaVersion.VERSION_1_8
}
```
Or in Gradle Groovy DSL `build.gradle`:
```groovy
compileOptions {
targetCompatibility JavaVersion.VERSION_1_8
}
@ -101,23 +119,61 @@ git clone https://github.com/androidx/media.git
cd media
```
Next, add the following to your project's `settings.gradle` file, replacing
Next, add the following to your project's `settings.gradle.kts` file, replacing
`path/to/media` with the path to your local copy:
```gradle
```kotlin
gradle.extra.apply {
set("androidxMediaModulePrefix", "media-")
}
apply(from = file("path/to/media/core_settings.gradle"))
```
Or in Gradle Groovy DSL `settings.gradle`:
```groovy
gradle.ext.androidxMediaModulePrefix = 'media-'
apply from: file("path/to/media/core_settings.gradle")
```
You should now see the AndroidX Media modules appear as part of your project.
You can depend on them as you would on any other local module, for example:
You can depend on them from `build.gradle.kts` as you would on any other local
module, for example:
```gradle
```kotlin
implementation(project(":media-lib-exoplayer"))
implementation(project(":media-lib-exoplayer-dash"))
implementation(project(":media-lib-ui"))
```
Or in Gradle Groovy DSL `build.gradle`:
```groovy
implementation project(':media-lib-exoplayer')
implementation project(':media-lib-exoplayer-dash')
implementation project(':media-lib-ui')
```
#### MIDI module
By default the [MIDI module](libraries/decoder_midi) is disabled as a local
dependency, because it requires additional Maven repository config. If you want
to use it as a local dependency, please configure the JitPack repository as
[described in the module README](libraries/decoder_midi/README.md#getting-the-module),
and then enable building the module in your `settings.gradle.kts` file:
```kotlin
gradle.extra.apply {
set("androidxMediaEnableMidiModule", true)
}
```
Or in Gradle Groovy DSL `settings.gradle`:
```groovy
gradle.ext.androidxMediaEnableMidiModule = true
```
## Developing AndroidX Media
#### Project branches

File diff suppressed because it is too large Load Diff

437
api.txt
View File

@ -1,4 +1,4 @@
// Signature format: 3.0
// Signature format: 2.0
package androidx.media3.common {
public final class AdOverlayInfo {
@ -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 {
@ -209,8 +226,8 @@ package androidx.media3.common {
public final class MediaItem {
method public androidx.media3.common.MediaItem.Builder buildUpon();
method public static androidx.media3.common.MediaItem fromUri(String);
method public static androidx.media3.common.MediaItem fromUri(android.net.Uri);
method public static androidx.media3.common.MediaItem fromUri(String);
field public static final String DEFAULT_MEDIA_ID = "";
field public static final androidx.media3.common.MediaItem EMPTY;
field public final androidx.media3.common.MediaItem.ClippingConfiguration clippingConfiguration;
@ -247,8 +264,8 @@ package androidx.media3.common {
method public androidx.media3.common.MediaItem.Builder setRequestMetadata(androidx.media3.common.MediaItem.RequestMetadata);
method public androidx.media3.common.MediaItem.Builder setSubtitleConfigurations(java.util.List<androidx.media3.common.MediaItem.SubtitleConfiguration>);
method public androidx.media3.common.MediaItem.Builder setTag(@Nullable Object);
method public androidx.media3.common.MediaItem.Builder setUri(@Nullable String);
method public androidx.media3.common.MediaItem.Builder setUri(@Nullable android.net.Uri);
method public androidx.media3.common.MediaItem.Builder setUri(@Nullable String);
}
public static class MediaItem.ClippingConfiguration {
@ -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";
@ -557,8 +617,8 @@ package androidx.media3.common {
public class PlaybackException extends java.lang.Exception {
method @CallSuper public boolean errorInfoEquals(@Nullable androidx.media3.common.PlaybackException);
method public static String getErrorCodeName(@androidx.media3.common.PlaybackException.ErrorCode int);
method public final String getErrorCodeName();
method public static String getErrorCodeName(@androidx.media3.common.PlaybackException.ErrorCode int);
field public static final int CUSTOM_ERROR_CODE_BASE = 1000000; // 0xf4240
field public static final int ERROR_CODE_AUDIO_TRACK_INIT_FAILED = 5001; // 0x1389
field public static final int ERROR_CODE_AUDIO_TRACK_WRITE_FAILED = 5002; // 0x138a
@ -602,7 +662,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;
@ -614,8 +674,8 @@ package androidx.media3.common {
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 void addMediaItems(java.util.List<androidx.media3.common.MediaItem>);
method public boolean canAdvertiseSession();
method public void clearMediaItems();
method public void clearVideoSurface();
@ -623,7 +683,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 +728,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,21 +747,26 @@ 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);
method public void seekTo(int, long);
method public void seekTo(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 void setDeviceMuted(boolean);
method public void setDeviceVolume(@IntRange(from=0) int);
method public void setAudioAttributes(androidx.media3.common.AudioAttributes, boolean);
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, @androidx.media3.common.C.VolumeFlags 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 setMediaItem(androidx.media3.common.MediaItem, long);
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);
@ -716,12 +783,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 +798,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 +808,12 @@ 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 public static final int COMMAND_SET_AUDIO_ATTRIBUTES = 35; // 0x23
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
@ -753,6 +826,7 @@ package androidx.media3.common {
field public static final int DISCONTINUITY_REASON_REMOVE = 4; // 0x4
field public static final int DISCONTINUITY_REASON_SEEK = 1; // 0x1
field public static final int DISCONTINUITY_REASON_SEEK_ADJUSTMENT = 2; // 0x2
field public static final int DISCONTINUITY_REASON_SILENCE_SKIP = 6; // 0x6
field public static final int DISCONTINUITY_REASON_SKIP = 3; // 0x3
field public static final int EVENT_AUDIO_ATTRIBUTES_CHANGED = 20; // 0x14
field public static final int EVENT_AUDIO_SESSION_ID = 21; // 0x15
@ -791,10 +865,13 @@ 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_OUTPUT = 3; // 0x3
field @Deprecated 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_AUDIO_ATTRIBUTES, 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 {
@ -818,7 +895,7 @@ package androidx.media3.common {
field public static final androidx.media3.common.Player.Commands EMPTY;
}
@IntDef({androidx.media3.common.Player.DISCONTINUITY_REASON_AUTO_TRANSITION, androidx.media3.common.Player.DISCONTINUITY_REASON_SEEK, androidx.media3.common.Player.DISCONTINUITY_REASON_SEEK_ADJUSTMENT, androidx.media3.common.Player.DISCONTINUITY_REASON_SKIP, androidx.media3.common.Player.DISCONTINUITY_REASON_REMOVE, androidx.media3.common.Player.DISCONTINUITY_REASON_INTERNAL}) @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.DiscontinuityReason {
@IntDef({androidx.media3.common.Player.DISCONTINUITY_REASON_AUTO_TRANSITION, androidx.media3.common.Player.DISCONTINUITY_REASON_SEEK, androidx.media3.common.Player.DISCONTINUITY_REASON_SEEK_ADJUSTMENT, androidx.media3.common.Player.DISCONTINUITY_REASON_SKIP, androidx.media3.common.Player.DISCONTINUITY_REASON_REMOVE, androidx.media3.common.Player.DISCONTINUITY_REASON_INTERNAL, androidx.media3.common.Player.DISCONTINUITY_REASON_SILENCE_SKIP}) @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.DiscontinuityReason {
}
@IntDef({androidx.media3.common.Player.EVENT_TIMELINE_CHANGED, androidx.media3.common.Player.EVENT_MEDIA_ITEM_TRANSITION, androidx.media3.common.Player.EVENT_TRACKS_CHANGED, androidx.media3.common.Player.EVENT_IS_LOADING_CHANGED, androidx.media3.common.Player.EVENT_PLAYBACK_STATE_CHANGED, androidx.media3.common.Player.EVENT_PLAY_WHEN_READY_CHANGED, androidx.media3.common.Player.EVENT_PLAYBACK_SUPPRESSION_REASON_CHANGED, androidx.media3.common.Player.EVENT_IS_PLAYING_CHANGED, androidx.media3.common.Player.EVENT_REPEAT_MODE_CHANGED, androidx.media3.common.Player.EVENT_SHUFFLE_MODE_ENABLED_CHANGED, androidx.media3.common.Player.EVENT_PLAYER_ERROR, androidx.media3.common.Player.EVENT_POSITION_DISCONTINUITY, androidx.media3.common.Player.EVENT_PLAYBACK_PARAMETERS_CHANGED, androidx.media3.common.Player.EVENT_AVAILABLE_COMMANDS_CHANGED, androidx.media3.common.Player.EVENT_MEDIA_METADATA_CHANGED, androidx.media3.common.Player.EVENT_PLAYLIST_METADATA_CHANGED, androidx.media3.common.Player.EVENT_SEEK_BACK_INCREMENT_CHANGED, androidx.media3.common.Player.EVENT_SEEK_FORWARD_INCREMENT_CHANGED, androidx.media3.common.Player.EVENT_MAX_SEEK_TO_PREVIOUS_POSITION_CHANGED, androidx.media3.common.Player.EVENT_TRACK_SELECTION_PARAMETERS_CHANGED, androidx.media3.common.Player.EVENT_AUDIO_ATTRIBUTES_CHANGED, androidx.media3.common.Player.EVENT_AUDIO_SESSION_ID, androidx.media3.common.Player.EVENT_VOLUME_CHANGED, androidx.media3.common.Player.EVENT_SKIP_SILENCE_ENABLED_CHANGED, androidx.media3.common.Player.EVENT_SURFACE_SIZE_CHANGED, androidx.media3.common.Player.EVENT_VIDEO_SIZE_CHANGED, androidx.media3.common.Player.EVENT_RENDERED_FIRST_FRAME, androidx.media3.common.Player.EVENT_CUES, androidx.media3.common.Player.EVENT_METADATA, androidx.media3.common.Player.EVENT_DEVICE_INFO_CHANGED, androidx.media3.common.Player.EVENT_DEVICE_VOLUME_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.Event {
@ -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, androidx.media3.common.Player.PLAYBACK_SUPPRESSION_REASON_UNSUITABLE_AUDIO_OUTPUT}) @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 {
@ -1110,7 +1187,7 @@ package androidx.media3.common.text {
field public static final int ANCHOR_TYPE_MIDDLE = 1; // 0x1
field public static final int ANCHOR_TYPE_START = 0; // 0x0
field public static final float DIMEN_UNSET = -3.4028235E38f;
field public static final androidx.media3.common.text.Cue EMPTY;
field @Deprecated public static final androidx.media3.common.text.Cue EMPTY;
field public static final int LINE_TYPE_FRACTION = 0; // 0x0
field public static final int LINE_TYPE_NUMBER = 1; // 0x1
field public static final int TEXT_SIZE_TYPE_ABSOLUTE = 2; // 0x2
@ -1162,11 +1239,16 @@ 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 @Deprecated public static boolean maybeRequestReadExternalStoragePermission(android.app.Activity, android.net.Uri...);
method @Deprecated public static boolean maybeRequestReadExternalStoragePermission(android.app.Activity, androidx.media3.common.MediaItem...);
method public static boolean maybeRequestReadStoragePermission(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);
}
}
@ -1264,7 +1346,6 @@ package androidx.media3.exoplayer {
method public void addAnalyticsListener(androidx.media3.exoplayer.analytics.AnalyticsListener);
method @Nullable public androidx.media3.exoplayer.ExoPlaybackException getPlayerError();
method public void removeAnalyticsListener(androidx.media3.exoplayer.analytics.AnalyticsListener);
method public void setAudioAttributes(androidx.media3.common.AudioAttributes, boolean);
method public void setHandleAudioBecomingNoisy(boolean);
method public void setWakeMode(@androidx.media3.common.C.WakeMode int);
}
@ -1289,7 +1370,7 @@ package androidx.media3.exoplayer.analytics {
package androidx.media3.exoplayer.drm {
@RequiresApi(18) public final class FrameworkMediaDrm {
public final class FrameworkMediaDrm {
method public static boolean isCryptoSchemeSupported(java.util.UUID);
}
@ -1438,122 +1519,129 @@ 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(int, java.util.List<androidx.media3.common.MediaItem>);
method public final void addMediaItems(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);
method public final boolean isSessionCommandAvailable(@androidx.media3.session.SessionCommand.CommandCode int);
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(int, long);
method public final void seekTo(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 public final void setAudioAttributes(androidx.media3.common.AudioAttributes, boolean);
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, boolean);
method public final void setMediaItem(androidx.media3.common.MediaItem, long);
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(androidx.media3.common.Rating);
method public final com.google.common.util.concurrent.ListenableFuture<androidx.media3.session.SessionResult> setRating(String, 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 +1710,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,10 +1742,10 @@ 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);
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);
}
public static final class MediaSession.ConnectionResult {
@ -1682,7 +1771,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";
}
@ -1719,6 +1809,7 @@ package androidx.media3.session {
ctor public SessionCommands.Builder();
method public androidx.media3.session.SessionCommands.Builder add(androidx.media3.session.SessionCommand);
method public androidx.media3.session.SessionCommands.Builder add(@androidx.media3.session.SessionCommand.CommandCode int);
method public androidx.media3.session.SessionCommands.Builder addSessionCommands(java.util.Collection<androidx.media3.session.SessionCommand>);
method public androidx.media3.session.SessionCommands build();
method public androidx.media3.session.SessionCommands.Builder remove(androidx.media3.session.SessionCommand);
method public androidx.media3.session.SessionCommands.Builder remove(@androidx.media3.session.SessionCommand.CommandCode int);

View File

@ -17,15 +17,21 @@ 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:8.0.1'
classpath 'com.google.android.gms:strict-version-matcher-plugin:1.2.4'
classpath 'org.jetbrains.kotlin:kotlin-gradle-plugin:1.8.20'
}
}
allprojects {
repositories {
google()
mavenCentral()
maven {
url 'https://jitpack.io'
content {
includeGroup "com.github.philburk"
}
}
}
if (it.hasProperty('externalBuildDir')) {
if (!new File(externalBuildDir).isAbsolute()) {
@ -35,5 +41,3 @@ allprojects {
}
group = 'androidx.media3'
}
apply from: 'javadoc_combined.gradle'

View File

@ -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 {
androidTestImplementation 'androidx.multidex:multidex:' + androidxMultidexVersion
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5'
}

View File

@ -12,50 +12,54 @@
// 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
minSdkVersion = 16
appTargetSdkVersion = 33
// API version before restricting local file access.
// https://developer.android.com/training/data-storage/app-specific
mainDemoAppTargetSdkVersion = 29
releaseVersion = '1.3.0'
releaseVersionCode = 1_003_000_3_00
minSdkVersion = 19
// See https://developer.android.com/training/cars/media/automotive-os#automotive-module
automotiveMinSdkVersion = 28
appTargetSdkVersion = 34
// Upgrading this requires [Internal ref: b/193254928] to be fixed, or some
// additional robolectric config.
targetSdkVersion = 30
compileSdkVersion = 33
compileSdkVersion = 34
dexmakerVersion = '2.28.3'
// Use the same JUnit version as the Android repo:
// https://cs.android.com/android/platform/superproject/main/+/main:external/junit/METADATA
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'
// https://cs.android.com/android/platform/superproject/main/+/main:external/guava/METADATA
guavaVersion = '32.1.3-android'
mockitoVersion = '3.12.4'
robolectricVersion = '4.8.1'
robolectricVersion = '4.11'
// 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'
androidxAnnotationExperimentalVersion = '1.2.0'
androidxAppCompatVersion = '1.3.1'
androidxCollectionVersion = '1.1.0'
androidxConstraintLayoutVersion = '2.0.4'
androidxCoreVersion = '1.7.0'
androidxAnnotationExperimentalVersion = '1.3.1'
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'
androidxExifInterfaceVersion = '1.3.6'
androidxFuturesVersion = '1.1.0'
androidxMediaVersion = '1.6.0'
androidxMedia2Version = '1.2.0'
androidxMediaVersion = '1.7.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'
androidxTestEspressoVersion = '3.5.1'
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.12.0'
modulePrefix = ':'
if (gradle.ext.has('androidxMediaModulePrefix')) {
modulePrefix += gradle.ext.androidxMediaModulePrefix

View File

@ -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')
@ -56,6 +57,8 @@ include modulePrefix + 'lib-datasource'
project(modulePrefix + 'lib-datasource').projectDir = new File(rootDir, 'libraries/datasource')
include modulePrefix + 'lib-datasource-cronet'
project(modulePrefix + 'lib-datasource-cronet').projectDir = new File(rootDir, 'libraries/datasource_cronet')
include modulePrefix + 'lib-datasource-httpengine'
project(modulePrefix + 'lib-datasource-httpengine').projectDir = new File(rootDir, 'libraries/datasource_httpengine')
include modulePrefix + 'lib-datasource-rtmp'
project(modulePrefix + 'lib-datasource-rtmp').projectDir = new File(rootDir, 'libraries/datasource_rtmp')
include modulePrefix + 'lib-datasource-okhttp'
@ -69,6 +72,10 @@ include modulePrefix + 'lib-decoder-ffmpeg'
project(modulePrefix + 'lib-decoder-ffmpeg').projectDir = new File(rootDir, 'libraries/decoder_ffmpeg')
include modulePrefix + 'lib-decoder-flac'
project(modulePrefix + 'lib-decoder-flac').projectDir = new File(rootDir, 'libraries/decoder_flac')
if (gradle.ext.has('androidxMediaEnableMidiModule') && gradle.ext.androidxMediaEnableMidiModule) {
include modulePrefix + 'lib-decoder-midi'
project(modulePrefix + 'lib-decoder-midi').projectDir = new File(rootDir, 'libraries/decoder_midi')
}
include modulePrefix + 'lib-decoder-opus'
project(modulePrefix + 'lib-decoder-opus').projectDir = new File(rootDir, 'libraries/decoder_opus')
include modulePrefix + 'lib-decoder-vp9'
@ -83,6 +90,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')
@ -92,7 +102,3 @@ include modulePrefix + 'test-data'
project(modulePrefix + 'test-data').projectDir = new File(rootDir, 'libraries/test_data')
include modulePrefix + 'test-utils'
project(modulePrefix + 'test-utils').projectDir = new File(rootDir, 'libraries/test_utils')
include modulePrefix + 'test-session-common'
project(modulePrefix + 'test-session-common').projectDir = new File(rootDir, 'libraries/test_session_common')
include modulePrefix + 'test-session-current'
project(modulePrefix + 'test-session-current').projectDir = new File(rootDir, 'libraries/test_session_current')

View File

@ -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.

View File

@ -15,6 +15,8 @@ apply from: '../../constants.gradle'
apply plugin: 'com.android.application'
android {
namespace 'androidx.media3.demo.cast'
compileSdkVersion project.ext.compileSdkVersion
compileOptions {

View File

@ -37,27 +37,90 @@ import java.util.List;
static {
ArrayList<MediaItem> samples = new ArrayList<>();
// Clear content.
// HLS streams.
samples.add(
new MediaItem.Builder()
.setUri("https://storage.googleapis.com/wvmedia/clear/h264/tears/tears.mpd")
.setMediaMetadata(new MediaMetadata.Builder().setTitle("Clear DASH: Tears").build())
.setMimeType(MIME_TYPE_DASH)
.build());
samples.add(
new MediaItem.Builder()
.setUri("https://storage.googleapis.com/shaka-demo-assets/angel-one-hls/hls.m3u8")
.setMediaMetadata(new MediaMetadata.Builder().setTitle("Clear HLS: Angel one").build())
.setUri(
"https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_4x3/bipbop_4x3_variant.m3u8")
.setMediaMetadata(
new MediaMetadata.Builder()
.setTitle("HLS (adaptive): Apple 4x3 basic stream (TS/h264/aac)")
.build())
.setMimeType(MIME_TYPE_HLS)
.build());
samples.add(
new MediaItem.Builder()
.setUri(
"https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_16x9/bipbop_16x9_variant.m3u8")
.setMediaMetadata(
new MediaMetadata.Builder()
.setTitle("HLS (adaptive): Apple 16x9 basic stream (TS/h264/aac)")
.build())
.setMimeType(MIME_TYPE_HLS)
.build());
samples.add(
new MediaItem.Builder()
.setUri(
"https://commondatastorage.googleapis.com/gtv-videos-bucket/CastVideos/hls/DesigningForGoogleCast.m3u8")
.setMediaMetadata(
new MediaMetadata.Builder()
.setTitle("HLS (1280x720): Designing For Google Cast (TS/h264/aac)")
.build())
.setMimeType(MIME_TYPE_HLS)
.build());
// DASH streams
samples.add(
new MediaItem.Builder()
.setUri("https://storage.googleapis.com/wvmedia/clear/h264/tears/tears.mpd")
.setMediaMetadata(
new MediaMetadata.Builder()
.setTitle("DASH (adaptive): Tears of steal (HD, MP4, H264/aac)")
.build())
.setMimeType(MIME_TYPE_DASH)
.build());
samples.add(
new MediaItem.Builder()
.setUri("https://storage.googleapis.com/wvmedia/clear/h264/tears/tears_uhd.mpd")
.setMediaMetadata(
new MediaMetadata.Builder()
.setTitle("DASH (3840x1714): Tears of steal (MP4, H264/aac)")
.build())
.setMimeType(MIME_TYPE_DASH)
.build());
// Progressive video streams
samples.add(
new MediaItem.Builder()
.setUri("https://html5demos.com/assets/dizzy.mp4")
.setMediaMetadata(new MediaMetadata.Builder().setTitle("Clear MP4: Dizzy").build())
.setMediaMetadata(
new MediaMetadata.Builder().setTitle("MP4 (480x360): Dizzy (H264/aac)").build())
.setMimeType(MIME_TYPE_VIDEO_MP4)
.build());
samples.add(
new MediaItem.Builder()
.setUri(
"https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv")
.setMediaMetadata(
new MediaMetadata.Builder().setTitle("MKV (1280x720): Screens (h264/aac)").build())
.setMimeType(MIME_TYPE_VIDEO_MP4)
.build());
// Progressive audio streams with artwork
samples.add(
new MediaItem.Builder()
.setUri("https://storage.googleapis.com/automotive-media/Keys_To_The_Kingdom.mp3")
.setMediaMetadata(
new MediaMetadata.Builder()
.setTitle("MP3: Keys To The Kingdom (44100/stereo/320kb/s)")
.setArtist("The 126ers")
.setAlbumTitle("Youtube Audio Library Rock 2")
.setGenre("Rock")
.setTrackNumber(1)
.setTotalTrackCount(4)
.setArtworkUri(
Uri.parse(
"https://storage.googleapis.com/automotive-media/album_art_3.jpg"))
.build())
.setMimeType(MimeTypes.AUDIO_MPEG)
.build());
// DRM content.
samples.add(
new MediaItem.Builder()

View File

@ -44,6 +44,7 @@ import androidx.recyclerview.widget.RecyclerView.ViewHolder;
import com.google.android.gms.cast.framework.CastButtonFactory;
import com.google.android.gms.cast.framework.CastContext;
import com.google.android.gms.dynamite.DynamiteModule;
import com.google.common.util.concurrent.MoreExecutors;
/**
* An activity that plays video using {@link ExoPlayer} and supports casting using ExoPlayer's Cast
@ -65,7 +66,7 @@ public class MainActivity extends AppCompatActivity
super.onCreate(savedInstanceState);
// Getting the cast context later than onStart can cause device discovery not to take place.
try {
castContext = CastContext.getSharedInstance(this);
castContext = CastContext.getSharedInstance(this, MoreExecutors.directExecutor()).getResult();
} catch (RuntimeException e) {
Throwable cause = e.getCause();
while (cause != null) {

View File

@ -14,6 +14,9 @@
* limitations under the License.
*/
@NonNullApi
@OptIn(markerClass = UnstableApi.class)
package androidx.media3.demo.cast;
import androidx.annotation.OptIn;
import androidx.media3.common.util.NonNullApi;
import androidx.media3.common.util.UnstableApi;

View File

@ -13,8 +13,11 @@
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<!-- The NewApi lint can be safely ignored because vector support is available on pre-21 devices
through Android Gradle Plugin 1.4.0+. -->
<vector android:height="400dp" android:tint="#FFFFFF"
android:viewportHeight="24" android:viewportWidth="24"
android:width="400dp" xmlns:android="http://schemas.android.com/apk/res/android">
android:width="400dp" xmlns:android="http://schemas.android.com/apk/res/android"
tools:ignore="NewApi" xmlns:tools="http://schemas.android.com/tools">
<path android:fillColor="@android:color/white" android:pathData="M1,18v3h3c0,-1.66 -1.34,-3 -3,-3zM1,14v2c2.76,0 5,2.24 5,5h2c0,-3.87 -3.13,-7 -7,-7zM19,7L5,7v1.63c3.96,1.28 7.09,4.41 8.37,8.37L19,17L19,7zM1,10v2c4.97,0 9,4.03 9,9h2c0,-6.08 -4.93,-11 -11,-11zM21,3L3,3c-1.1,0 -2,0.9 -2,2v3h2L3,5h18v14h-7v2h7c1.1,0 2,-0.9 2,-2L23,5c0,-1.1 -0.9,-2 -2,-2z"/>
</vector>

View File

@ -13,11 +13,15 @@
See the License for the specific language governing permissions and
limitations under the License.
-->
<!-- The NewApi lint can be safely ignored because vector support is available on pre-21 devices
through Android Gradle Plugin 1.4.0+. -->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:height="24.0dp"
android:viewportHeight="24.0"
android:viewportWidth="24.0"
android:width="24.0dp" >
android:width="24.0dp"
tools:ignore="NewApi" >
<path
android:fillColor="#FFFFFFFF"
android:pathData="M18,13h-5v5c0,0.55 -0.45,1 -1,1h0c-0.55,0 -1,-0.45 -1,-1v-5H6c-0.55,0 -1,-0.45 -1,-1v0c0,-0.55 0.45,-1 1,-1h5V6c0,-0.55 0.45,-1 1,-1h0c0.55,0 1,0.45 1,1v5h5c0.55,0 1,0.45 1,1v0C19,12.55 18.55,13 18,13z"/>

View File

@ -15,6 +15,8 @@ apply from: '../../constants.gradle'
apply plugin: 'com.android.application'
android {
namespace 'androidx.media3.demo.gl'
compileSdkVersion project.ext.compileSdkVersion
compileOptions {
@ -55,5 +57,4 @@ dependencies {
implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion
implementation 'androidx.multidex:multidex:' + androidxMultidexVersion
compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion
compileOnly 'org.checkerframework:checker-compat-qual:' + checkerframeworkCompatVersion
}

View File

@ -63,7 +63,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
paint = new Paint();
paint.setTextSize(64);
paint.setAntiAlias(true);
paint.setARGB(0xFF, 0xFF, 0xFF, 0xFF);
paint.setColor(Color.WHITE);
textures = new int[1];
overlayBitmap = Bitmap.createBitmap(OVERLAY_WIDTH, OVERLAY_HEIGHT, Bitmap.Config.ARGB_8888);
overlayCanvas = new Canvas(overlayBitmap);

View File

@ -143,7 +143,7 @@ public final class MainActivity extends Activity {
? Assertions.checkNotNull(intent.getData())
: Uri.parse(DEFAULT_MEDIA_URI);
DrmSessionManager drmSessionManager;
if (Util.SDK_INT >= 18 && intent.hasExtra(DRM_SCHEME_EXTRA)) {
if (intent.hasExtra(DRM_SCHEME_EXTRA)) {
String drmScheme = Assertions.checkNotNull(intent.getStringExtra(DRM_SCHEME_EXTRA));
String drmLicenseUrl = Assertions.checkNotNull(intent.getStringExtra(DRM_LICENSE_URL_EXTRA));
UUID drmSchemeUuid = Assertions.checkNotNull(Util.getDrmUuid(drmScheme));

View File

@ -14,6 +14,9 @@
* limitations under the License.
*/
@NonNullApi
@OptIn(markerClass = UnstableApi.class)
package androidx.media3.demo.gl;
import androidx.annotation.OptIn;
import androidx.media3.common.util.NonNullApi;
import androidx.media3.common.util.UnstableApi;

View File

@ -14,8 +14,11 @@
apply from: '../../constants.gradle'
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
android {
namespace 'androidx.media3.demo.main'
compileSdkVersion project.ext.compileSdkVersion
compileOptions {
@ -27,9 +30,7 @@ android {
versionName project.ext.releaseVersion
versionCode project.ext.releaseVersionCode
minSdkVersion project.ext.minSdkVersion
// Not using appTargetSDKVersion to allow local file access on API 29
// and higher [Internal ref: b/191644662]
targetSdkVersion project.ext.mainDemoAppTargetSdkVersion
targetSdkVersion project.ext.appTargetSdkVersion
multiDexEnabled true
}
@ -87,6 +88,7 @@ dependencies {
withDecoderExtensionsImplementation project(modulePrefix + 'lib-decoder-flac')
withDecoderExtensionsImplementation project(modulePrefix + 'lib-decoder-opus')
withDecoderExtensionsImplementation project(modulePrefix + 'lib-decoder-vp9')
withDecoderExtensionsImplementation project(modulePrefix + 'lib-decoder-midi')
withDecoderExtensionsImplementation project(modulePrefix + 'lib-datasource-rtmp')
}

View File

@ -21,8 +21,12 @@
<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_AUDIO"/>
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES"/>
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO"/>
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC"/>
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
<uses-feature android:name="android.software.leanback" android:required="false"/>
@ -35,7 +39,6 @@
android:banner="@drawable/ic_banner"
android:largeHeap="true"
android:allowBackup="false"
android:requestLegacyExternalStorage="true"
android:supportsRtl="true"
android:name="androidx.multidex.MultiDexApplication"
tools:targetApi="29">
@ -52,6 +55,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"/>
@ -93,7 +97,8 @@
</activity>
<service android:name=".DemoDownloadService"
android:exported="false">
android:exported="false"
android:foregroundServiceType="dataSync">
<intent-filter>
<action android:name="androidx.media3.exoplayer.downloadService.action.RESTART"/>
<category android:name="android.intent.category.DEFAULT"/>

View File

@ -143,6 +143,12 @@
"drm_scheme": "widevine",
"drm_license_uri": "https://proxy.uat.widevine.com/proxy?video_id=GTS_HW_SECURE_ALL&provider=widevine_test"
},
{
"name": "20s license with renewal",
"uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd",
"drm_scheme": "widevine",
"drm_license_uri": "https://proxy.uat.widevine.com/proxy?video_id=GTS_CAN_RENEW&provider=widevine_test"
},
{
"name": "30s license (fails at ~30s)",
"uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd",
@ -406,6 +412,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 +452,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": [
@ -476,6 +480,34 @@
"uri": "https://html5demos.com/assets/dizzy.mp4"
}
]
},
{
"name": "Playlist: No ads - DASH live: Tears of Steel (mid) - No ads",
"playlist": [
{
"uri": "https://html5demos.com/assets/dizzy.mp4"
},
{
"uri": "ssai://dai.google.com/?assetKey=PSzZMzAkSXCmlJOWDmRj8Q&format=0&adsId=1"
},
{
"uri": "https://html5demos.com/assets/dizzy.mp4"
}
]
},
{
"name": "Playlist: No ads - HLS live: Big Buck Bunny - No ads",
"playlist": [
{
"uri": "https://html5demos.com/assets/dizzy.mp4"
},
{
"uri": "ssai://dai.google.com/?assetKey=sN_IYUG8STe1ZzhIIE_ksA&format=2&adsId=3"
},
{
"uri": "https://html5demos.com/assets/dizzy.mp4"
}
]
}
]
},
@ -494,7 +526,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 +534,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"
}
@ -628,6 +672,14 @@
{
"name": "MPEG-4 Timed Text",
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/mp4/dizzy-with-tx3g.mp4"
},
{
"name": "SubRip muxed into MKV",
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-with-subrip.mkv"
},
{
"name": "Overlapping SSA muxed into MKV",
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-with-overlapping-ssa.mkv"
}
]
},

View File

@ -16,16 +16,18 @@
package androidx.media3.demo.main;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;
import android.content.Context;
import android.content.DialogInterface;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Handler;
import android.os.Looper;
import android.widget.Toast;
import androidx.annotation.Nullable;
import androidx.annotation.OptIn;
import androidx.annotation.RequiresApi;
import androidx.fragment.app.FragmentManager;
import androidx.media3.common.C;
import androidx.media3.common.DrmInitData;
import androidx.media3.common.Format;
import androidx.media3.common.MediaItem;
@ -51,7 +53,11 @@ import androidx.media3.exoplayer.source.TrackGroupArray;
import androidx.media3.exoplayer.trackselection.MappingTrackSelector.MappedTrackInfo;
import java.io.IOException;
import java.util.HashMap;
import java.util.UUID;
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
/** Tracks media that has been downloaded. */
@OptIn(markerClass = androidx.media3.common.util.UnstableApi.class)
@ -180,7 +186,7 @@ public class DownloadTracker {
trackSelectionDialog.dismiss();
}
if (widevineOfflineLicenseFetchTask != null) {
widevineOfflineLicenseFetchTask.cancel(false);
widevineOfflineLicenseFetchTask.cancel();
}
}
@ -195,14 +201,9 @@ public class DownloadTracker {
}
// The content is DRM protected. We need to acquire an offline license.
if (Util.SDK_INT < 18) {
Toast.makeText(context, R.string.error_drm_unsupported_before_api_18, Toast.LENGTH_LONG)
.show();
Log.e(TAG, "Downloading DRM protected content is not supported on API versions below 18");
return;
}
// TODO(internal b/163107948): Support cases where DrmInitData are not in the manifest.
if (!hasSchemaData(format.drmInitData)) {
if (!hasNonNullWidevineSchemaData(format.drmInitData)) {
Toast.makeText(context, R.string.download_start_error_offline_license, Toast.LENGTH_LONG)
.show();
Log.e(
@ -323,12 +324,14 @@ public class DownloadTracker {
}
/**
* Returns whether any the {@link DrmInitData.SchemeData} contained in {@code drmInitData} has
* non-null {@link DrmInitData.SchemeData#data}.
* Returns whether any {@link DrmInitData.SchemeData} that {@linkplain
* DrmInitData.SchemeData#matches(UUID) matches} {@link C#WIDEVINE_UUID} has non-null {@link
* DrmInitData.SchemeData#data}.
*/
private boolean hasSchemaData(DrmInitData drmInitData) {
private boolean hasNonNullWidevineSchemaData(DrmInitData drmInitData) {
for (int i = 0; i < drmInitData.schemeDataCount; i++) {
if (drmInitData.get(i).hasData()) {
DrmInitData.SchemeData schemeData = drmInitData.get(i);
if (schemeData.matches(C.WIDEVINE_UUID) && schemeData.hasData()) {
return true;
}
}
@ -353,15 +356,16 @@ public class DownloadTracker {
}
/** Downloads a Widevine offline license in a background thread. */
@RequiresApi(18)
private static final class WidevineOfflineLicenseFetchTask extends AsyncTask<Void, Void, Void> {
private static final class WidevineOfflineLicenseFetchTask {
private final Format format;
private final MediaItem.DrmConfiguration drmConfiguration;
private final DataSource.Factory dataSourceFactory;
private final StartDownloadDialogHelper dialogHelper;
private final DownloadHelper downloadHelper;
private final ExecutorService executorService;
@Nullable Future<?> future;
@Nullable private byte[] keySetId;
@Nullable private DrmSession.DrmSessionException drmSessionException;
@ -371,6 +375,8 @@ public class DownloadTracker {
DataSource.Factory dataSourceFactory,
StartDownloadDialogHelper dialogHelper,
DownloadHelper downloadHelper) {
checkState(drmConfiguration.scheme.equals(C.WIDEVINE_UUID));
this.executorService = Executors.newSingleThreadExecutor();
this.format = format;
this.drmConfiguration = drmConfiguration;
this.dataSourceFactory = dataSourceFactory;
@ -378,32 +384,41 @@ public class DownloadTracker {
this.downloadHelper = downloadHelper;
}
@Override
protected Void doInBackground(Void... voids) {
OfflineLicenseHelper offlineLicenseHelper =
OfflineLicenseHelper.newWidevineInstance(
drmConfiguration.licenseUri.toString(),
drmConfiguration.forceDefaultLicenseUri,
dataSourceFactory,
drmConfiguration.licenseRequestHeaders,
new DrmSessionEventListener.EventDispatcher());
try {
keySetId = offlineLicenseHelper.downloadLicense(format);
} catch (DrmSession.DrmSessionException e) {
drmSessionException = e;
} finally {
offlineLicenseHelper.release();
public void cancel() {
if (future != null) {
future.cancel(/* mayInterruptIfRunning= */ false);
}
return null;
}
@Override
protected void onPostExecute(Void aVoid) {
if (drmSessionException != null) {
dialogHelper.onOfflineLicenseFetchedError(drmSessionException);
} else {
dialogHelper.onOfflineLicenseFetched(downloadHelper, checkNotNull(keySetId));
}
public void execute() {
future =
executorService.submit(
() -> {
OfflineLicenseHelper offlineLicenseHelper =
OfflineLicenseHelper.newWidevineInstance(
drmConfiguration.licenseUri.toString(),
drmConfiguration.forceDefaultLicenseUri,
dataSourceFactory,
drmConfiguration.licenseRequestHeaders,
new DrmSessionEventListener.EventDispatcher());
try {
keySetId = offlineLicenseHelper.downloadLicense(format);
} catch (DrmSession.DrmSessionException e) {
drmSessionException = e;
} finally {
offlineLicenseHelper.release();
new Handler(Looper.getMainLooper())
.post(
() -> {
if (drmSessionException != null) {
dialogHelper.onOfflineLicenseFetchedError(drmSessionException);
} else {
dialogHelper.onOfflineLicenseFetched(
downloadHelper, checkNotNull(keySetId));
}
});
}
});
}
}
}

View File

@ -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)

View File

@ -93,11 +93,11 @@ public class PlayerActivity extends AppCompatActivity
@Nullable private AdsLoader clientSideAdsLoader;
// TODO: Annotate this and serverSideAdsLoaderState below with @OptIn when it can be applied to
// fields (needs http://r.android.com/2004032 to be released into a version of
// androidx.annotation:annotation-experimental).
@Nullable private ImaServerSideAdInsertionMediaSource.AdsLoader serverSideAdsLoader;
@OptIn(markerClass = UnstableApi.class)
@Nullable
private ImaServerSideAdInsertionMediaSource.AdsLoader serverSideAdsLoader;
@OptIn(markerClass = UnstableApi.class)
private ImaServerSideAdInsertionMediaSource.AdsLoader.@MonotonicNonNull State
serverSideAdsLoaderState;
@ -354,18 +354,14 @@ public class PlayerActivity extends AppCompatActivity
finish();
return Collections.emptyList();
}
if (Util.maybeRequestReadExternalStoragePermission(/* activity= */ this, mediaItem)) {
if (Util.maybeRequestReadStoragePermission(/* activity= */ this, mediaItem)) {
// The player will be reinitialized if the permission is granted.
return Collections.emptyList();
}
MediaItem.DrmConfiguration drmConfiguration = mediaItem.localConfiguration.drmConfiguration;
if (drmConfiguration != null) {
if (Build.VERSION.SDK_INT < 18) {
showToast(R.string.error_drm_unsupported_before_api_18);
finish();
return Collections.emptyList();
} else if (!FrameworkMediaDrm.isCryptoSchemeSupported(drmConfiguration.scheme)) {
if (!FrameworkMediaDrm.isCryptoSchemeSupported(drmConfiguration.scheme)) {
showToast(R.string.error_drm_unsupported_scheme);
finish();
return Collections.emptyList();
@ -429,8 +425,7 @@ public class PlayerActivity extends AppCompatActivity
Bundle adsLoaderStateBundle = savedInstanceState.getBundle(KEY_SERVER_SIDE_ADS_LOADER_STATE);
if (adsLoaderStateBundle != null) {
serverSideAdsLoaderState =
ImaServerSideAdInsertionMediaSource.AdsLoader.State.CREATOR.fromBundle(
adsLoaderStateBundle);
ImaServerSideAdInsertionMediaSource.AdsLoader.State.fromBundle(adsLoaderStateBundle);
}
}

View File

@ -26,9 +26,10 @@ import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.content.res.AssetManager;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.text.TextUtils;
import android.util.JsonReader;
import android.view.Menu;
@ -48,6 +49,7 @@ import androidx.annotation.Nullable;
import androidx.annotation.OptIn;
import androidx.annotation.RequiresApi;
import androidx.appcompat.app.AppCompatActivity;
import androidx.media3.common.C;
import androidx.media3.common.MediaItem;
import androidx.media3.common.MediaItem.ClippingConfiguration;
import androidx.media3.common.MediaMetadata;
@ -72,6 +74,8 @@ import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/** An activity for selecting from a list of media samples. */
public class SampleChooserActivity extends AppCompatActivity
@ -80,7 +84,6 @@ public class SampleChooserActivity extends AppCompatActivity
private static final String TAG = "SampleChooserActivity";
private static final String GROUP_POSITION_PREFERENCE_KEY = "sample_chooser_group_position";
private static final String CHILD_POSITION_PREFERENCE_KEY = "sample_chooser_child_position";
private static final int POST_NOTIFICATION_PERMISSION_REQUEST_CODE = 100;
private String[] uris;
private boolean useExtensionRenderers;
@ -179,14 +182,6 @@ public class SampleChooserActivity extends AppCompatActivity
public void onRequestPermissionsResult(
int requestCode, String[] permissions, int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
if (requestCode == POST_NOTIFICATION_PERMISSION_REQUEST_CODE) {
handlePostNotificationPermissionGrantResults(grantResults);
} else {
handleExternalStoragePermissionGrantResults(grantResults);
}
}
private void handlePostNotificationPermissionGrantResults(int[] grantResults) {
if (!notificationPermissionToastShown
&& (grantResults.length == 0 || grantResults[0] != PackageManager.PERMISSION_GRANTED)) {
Toast.makeText(
@ -201,30 +196,8 @@ public class SampleChooserActivity extends AppCompatActivity
}
}
private void handleExternalStoragePermissionGrantResults(int[] grantResults) {
if (grantResults.length == 0) {
// Empty results are triggered if a permission is requested while another request was already
// pending and can be safely ignored in this case.
return;
} else if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
loadSample();
} else {
Toast.makeText(getApplicationContext(), R.string.sample_list_load_error, Toast.LENGTH_LONG)
.show();
finish();
}
}
private void loadSample() {
checkNotNull(uris);
for (int i = 0; i < uris.length; i++) {
Uri uri = Uri.parse(uris[i]);
if (Util.maybeRequestReadExternalStoragePermission(this, uri)) {
return;
}
}
SampleListLoader loaderTask = new SampleListLoader();
loaderTask.execute(uris);
}
@ -279,8 +252,7 @@ public class SampleChooserActivity extends AppCompatActivity
!= PackageManager.PERMISSION_GRANTED) {
downloadMediaItemWaitingForNotificationPermission = playlistHolder.mediaItems.get(0);
requestPermissions(
new String[] {Api33.getPostNotificationPermissionString()},
/* requestCode= */ POST_NOTIFICATION_PERMISSION_REQUEST_CODE);
new String[] {Api33.getPostNotificationPermissionString()}, /* requestCode= */ 0);
} else {
toggleDownload(playlistHolder.mediaItems.get(0));
}
@ -302,6 +274,10 @@ public class SampleChooserActivity extends AppCompatActivity
if (localConfiguration.adsConfiguration != null) {
return R.string.download_ads_unsupported;
}
@Nullable MediaItem.DrmConfiguration drmConfiguration = localConfiguration.drmConfiguration;
if (drmConfiguration != null && !drmConfiguration.scheme.equals(C.WIDEVINE_UUID)) {
return R.string.download_only_widevine_drm_supported;
}
String scheme = localConfiguration.uri.getScheme();
if (!("http".equals(scheme) || "https".equals(scheme))) {
return R.string.download_scheme_unsupported;
@ -314,34 +290,42 @@ public class SampleChooserActivity extends AppCompatActivity
return menuItem != null && menuItem.isChecked();
}
private final class SampleListLoader extends AsyncTask<String, Void, List<PlaylistGroup>> {
private final class SampleListLoader {
private final ExecutorService executorService;
private boolean sawError;
@OptIn(markerClass = androidx.media3.common.util.UnstableApi.class)
@Override
protected List<PlaylistGroup> doInBackground(String... uris) {
List<PlaylistGroup> result = new ArrayList<>();
Context context = getApplicationContext();
DataSource dataSource = DemoUtil.getDataSourceFactory(context).createDataSource();
for (String uri : uris) {
DataSpec dataSpec = new DataSpec(Uri.parse(uri));
InputStream inputStream = new DataSourceInputStream(dataSource, dataSpec);
try {
readPlaylistGroups(new JsonReader(new InputStreamReader(inputStream, "UTF-8")), result);
} catch (Exception e) {
Log.e(TAG, "Error loading sample list: " + uri, e);
sawError = true;
} finally {
DataSourceUtil.closeQuietly(dataSource);
}
}
return result;
public SampleListLoader() {
executorService = Executors.newSingleThreadExecutor();
}
@Override
protected void onPostExecute(List<PlaylistGroup> result) {
onPlaylistGroups(result, sawError);
@OptIn(markerClass = androidx.media3.common.util.UnstableApi.class)
public void execute(String... uris) {
executorService.execute(
() -> {
List<PlaylistGroup> result = new ArrayList<>();
Context context = getApplicationContext();
DataSource dataSource = DemoUtil.getDataSourceFactory(context).createDataSource();
for (String uri : uris) {
DataSpec dataSpec = new DataSpec(Uri.parse(uri));
InputStream inputStream = new DataSourceInputStream(dataSource, dataSpec);
try {
readPlaylistGroups(
new JsonReader(new InputStreamReader(inputStream, "UTF-8")), result);
} catch (Exception e) {
Log.e(TAG, "Error loading sample list: " + uri, e);
sawError = true;
} finally {
DataSourceUtil.closeQuietly(dataSource);
}
}
new Handler(Looper.getMainLooper())
.post(
() -> {
onPlaylistGroups(result, sawError);
});
});
}
private void readPlaylistGroups(JsonReader reader, List<PlaylistGroup> groups)

View File

@ -25,8 +25,6 @@
<string name="error_generic">Playback failed</string>
<string name="error_drm_unsupported_before_api_18">DRM content not supported on API levels below 18</string>
<string name="error_drm_unsupported_scheme">This device does not support the required DRM scheme</string>
<string name="error_no_decoder">This device does not provide a decoder for <xliff:g id="mime_type">%1$s</xliff:g></string>
@ -59,6 +57,8 @@
<string name="download_ads_unsupported">IMA does not support offline ads</string>
<string name="download_only_widevine_drm_supported">This demo app only supports downloading unencrypted or Widevine DRM content</string>
<string name="prefer_extension_decoders">Prefer extension decoders</string>
</resources>

View File

@ -16,6 +16,8 @@ apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
android {
namespace 'androidx.media3.demo.session'
compileSdkVersion project.ext.compileSdkVersion
compileOptions {
@ -57,13 +59,13 @@ 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
implementation 'com.google.android.material:material:' + androidxMaterialVersion
implementation project(modulePrefix + 'lib-exoplayer')
implementation project(modulePrefix + 'lib-exoplayer-dash')
implementation project(modulePrefix + 'lib-exoplayer-hls')
implementation project(modulePrefix + 'lib-ui')
implementation project(modulePrefix + 'lib-session')
implementation project(modulePrefix + 'demo-session-service')
}

View File

@ -20,6 +20,7 @@
<uses-sdk/>
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" />
<application
android:name="androidx.multidex.MultiDexApplication"
@ -29,6 +30,11 @@
android:theme="@style/Theme.Media3Demo"
tools:replace="android:name">
<!-- Declare that this session demo supports Android Auto. -->
<meta-data
android:name="com.google.android.gms.car.application"
android:resource="@xml/auto_app_desc" />
<activity
android:name=".MainActivity"
android:exported="true">
@ -40,7 +46,9 @@
<activity
android:name=".PlayerActivity"
android:exported="true"/>
android:exported="true"
android:launchMode="singleTop"
android:theme="@style/Theme.AppCompat.NoActionBar"/>
<activity
android:name=".PlayableFolderActivity"
@ -51,8 +59,9 @@
android:foregroundServiceType="mediaPlayback"
android:exported="true">
<intent-filter>
<action android:name="androidx.media3.session.MediaSessionService"/>
<action android:name="androidx.media3.session.MediaLibraryService"/>
<action android:name="android.media.browse.MediaBrowserService"/>
<action android:name="android.media.action.MEDIA_PLAY_FROM_SEARCH"/>
</intent-filter>
</service>

View File

@ -15,9 +15,11 @@
*/
package androidx.media3.demo.session
import android.Manifest
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.os.Build
import android.os.Bundle
import android.view.LayoutInflater
import android.view.MenuItem
@ -26,6 +28,7 @@ import android.view.ViewGroup
import android.widget.ArrayAdapter
import android.widget.ListView
import android.widget.TextView
import android.widget.Toast
import androidx.activity.OnBackPressedCallback
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
@ -70,18 +73,26 @@ 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()
}
}
)
if (
Build.VERSION.SDK_INT >= 33 &&
checkSelfPermission(Manifest.permission.POST_NOTIFICATIONS) !=
PackageManager.PERMISSION_GRANTED
) {
requestPermissions(arrayOf(Manifest.permission.POST_NOTIFICATIONS), /* requestCode= */ 0)
}
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
@ -102,6 +113,23 @@ class MainActivity : AppCompatActivity() {
super.onStop()
}
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<out String>,
grantResults: IntArray
) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
if (grantResults.isEmpty()) {
// Empty results are triggered if a permission is requested while another request was already
// pending and can be safely ignored in this case.
return
}
if (grantResults[0] != PackageManager.PERMISSION_GRANTED) {
Toast.makeText(applicationContext, R.string.notification_permission_denied, Toast.LENGTH_LONG)
.show()
}
}
private fun initializeBrowser() {
browserFuture =
MediaBrowser.Builder(

View File

@ -43,7 +43,7 @@ import com.google.common.util.concurrent.ListenableFuture
class PlayableFolderActivity : AppCompatActivity() {
private lateinit var browserFuture: ListenableFuture<MediaBrowser>
private val browser: MediaBrowser?
get() = if (browserFuture.isDone) browserFuture.get() else null
get() = if (browserFuture.isDone && !browserFuture.isCancelled) browserFuture.get() else null
private lateinit var mediaList: ListView
private lateinit var mediaListAdapter: PlayableMediaItemArrayAdapter
@ -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()
}
}

View File

@ -1,319 +1,32 @@
/*
* Copyright 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.media3.demo.session
import android.annotation.SuppressLint
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent.*
import android.app.TaskStackBuilder
import android.app.PendingIntent
import android.app.PendingIntent.FLAG_UPDATE_CURRENT
import android.app.PendingIntent.getActivity
import android.content.Intent
import android.os.Build
import android.os.Bundle
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import androidx.media3.common.AudioAttributes
import androidx.media3.common.MediaItem
import androidx.media3.common.util.Util
import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.session.*
import androidx.media3.session.LibraryResult.RESULT_ERROR_NOT_SUPPORTED
import androidx.media3.session.MediaSession.ControllerInfo
import com.google.common.collect.ImmutableList
import com.google.common.util.concurrent.Futures
import com.google.common.util.concurrent.ListenableFuture
import androidx.core.app.TaskStackBuilder
class PlaybackService : MediaLibraryService() {
private val librarySessionCallback = CustomMediaLibrarySessionCallback()
private lateinit var player: ExoPlayer
private lateinit var mediaLibrarySession: MediaLibrarySession
private lateinit var customCommands: List<CommandButton>
private var customLayout = ImmutableList.of<CommandButton>()
class PlaybackService : DemoPlaybackService() {
companion object {
private const val SEARCH_QUERY_PREFIX_COMPAT = "androidx://media3-session/playFromSearch"
private const val SEARCH_QUERY_PREFIX = "androidx://media3-session/setMediaUri"
private const val CUSTOM_COMMAND_TOGGLE_SHUFFLE_MODE_ON =
"android.media3.session.demo.SHUFFLE_ON"
private const val CUSTOM_COMMAND_TOGGLE_SHUFFLE_MODE_OFF =
"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) PendingIntent.FLAG_IMMUTABLE else 0
}
override fun onCreate() {
super.onCreate()
customCommands =
listOf(
getShuffleCommandButton(
SessionCommand(CUSTOM_COMMAND_TOGGLE_SHUFFLE_MODE_ON, Bundle.EMPTY)
),
getShuffleCommandButton(
SessionCommand(CUSTOM_COMMAND_TOGGLE_SHUFFLE_MODE_OFF, Bundle.EMPTY)
)
)
customLayout = ImmutableList.of(customCommands[0])
initializeSessionAndPlayer()
setListener(MediaSessionServiceListener())
override fun getSingleTopActivity(): PendingIntent? {
return getActivity(
this,
0,
Intent(this, PlayerActivity::class.java),
immutableFlag or FLAG_UPDATE_CURRENT
)
}
override fun onGetSession(controllerInfo: ControllerInfo): MediaLibrarySession {
return mediaLibrarySession
}
override fun onTaskRemoved(rootIntent: Intent?) {
if (!player.playWhenReady) {
stopSelf()
override 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)
}
}
override fun onDestroy() {
player.release()
mediaLibrarySession.release()
clearListener()
super.onDestroy()
}
private inner class CustomMediaLibrarySessionCallback : MediaLibrarySession.Callback {
override fun onConnect(
session: MediaSession,
controller: ControllerInfo
): MediaSession.ConnectionResult {
val connectionResult = super.onConnect(session, controller)
val availableSessionCommands = connectionResult.availableSessionCommands.buildUpon()
for (commandButton in customCommands) {
// Add custom command to available session commands.
commandButton.sessionCommand?.let { availableSessionCommands.add(it) }
}
return MediaSession.ConnectionResult.accept(
availableSessionCommands.build(),
connectionResult.availablePlayerCommands
)
}
override fun onPostConnect(session: MediaSession, controller: ControllerInfo) {
if (!customLayout.isEmpty() && controller.controllerVersion != 0) {
// Let Media3 controller (for instance the MediaNotificationProvider) know about the custom
// layout right after it connected.
ignoreFuture(mediaLibrarySession.setCustomLayout(controller, customLayout))
}
}
override fun onCustomCommand(
session: MediaSession,
controller: ControllerInfo,
customCommand: SessionCommand,
args: Bundle
): ListenableFuture<SessionResult> {
if (CUSTOM_COMMAND_TOGGLE_SHUFFLE_MODE_ON == customCommand.customAction) {
// Enable shuffling.
player.shuffleModeEnabled = true
// Change the custom layout to contain the `Disable shuffling` command.
customLayout = ImmutableList.of(customCommands[1])
// Send the updated custom layout to controllers.
session.setCustomLayout(customLayout)
} else if (CUSTOM_COMMAND_TOGGLE_SHUFFLE_MODE_OFF == customCommand.customAction) {
// Disable shuffling.
player.shuffleModeEnabled = false
// Change the custom layout to contain the `Enable shuffling` command.
customLayout = ImmutableList.of(customCommands[0])
// Send the updated custom layout to controllers.
session.setCustomLayout(customLayout)
}
return Futures.immediateFuture(SessionResult(SessionResult.RESULT_SUCCESS))
}
override fun onGetLibraryRoot(
session: MediaLibrarySession,
browser: ControllerInfo,
params: LibraryParams?
): ListenableFuture<LibraryResult<MediaItem>> {
if (params != null && params.isRecent) {
// The service currently does not support playback resumption. Tell System UI by returning
// an error of type 'RESULT_ERROR_NOT_SUPPORTED' for a `params.isRecent` request. See
// https://github.com/androidx/media/issues/355
return Futures.immediateFuture(LibraryResult.ofError(RESULT_ERROR_NOT_SUPPORTED))
}
return Futures.immediateFuture(LibraryResult.ofItem(MediaItemTree.getRootItem(), params))
}
override fun onGetItem(
session: MediaLibrarySession,
browser: ControllerInfo,
mediaId: String
): ListenableFuture<LibraryResult<MediaItem>> {
val item =
MediaItemTree.getItem(mediaId)
?: return Futures.immediateFuture(
LibraryResult.ofError(LibraryResult.RESULT_ERROR_BAD_VALUE)
)
return Futures.immediateFuture(LibraryResult.ofItem(item, /* params= */ null))
}
override fun onSubscribe(
session: MediaLibrarySession,
browser: ControllerInfo,
parentId: String,
params: LibraryParams?
): ListenableFuture<LibraryResult<Void>> {
val children =
MediaItemTree.getChildren(parentId)
?: return Futures.immediateFuture(
LibraryResult.ofError(LibraryResult.RESULT_ERROR_BAD_VALUE)
)
session.notifyChildrenChanged(browser, parentId, children.size, params)
return Futures.immediateFuture(LibraryResult.ofVoid())
}
override fun onGetChildren(
session: MediaLibrarySession,
browser: ControllerInfo,
parentId: String,
page: Int,
pageSize: Int,
params: LibraryParams?
): ListenableFuture<LibraryResult<ImmutableList<MediaItem>>> {
val children =
MediaItemTree.getChildren(parentId)
?: return Futures.immediateFuture(
LibraryResult.ofError(LibraryResult.RESULT_ERROR_BAD_VALUE)
)
return Futures.immediateFuture(LibraryResult.ofItemList(children, params))
}
override fun onAddMediaItems(
mediaSession: MediaSession,
controller: MediaSession.ControllerInfo,
mediaItems: List<MediaItem>
): ListenableFuture<List<MediaItem>> {
val updatedMediaItems: List<MediaItem> =
mediaItems.map { mediaItem ->
if (mediaItem.requestMetadata.searchQuery != null)
getMediaItemFromSearchQuery(mediaItem.requestMetadata.searchQuery!!)
else MediaItemTree.getItem(mediaItem.mediaId) ?: mediaItem
}
return Futures.immediateFuture(updatedMediaItems)
}
private fun getMediaItemFromSearchQuery(query: String): MediaItem {
// Only accept query with pattern "play [Title]" or "[Title]"
// Where [Title]: must be exactly matched
// If no media with exact name found, play a random media instead
val mediaTitle =
if (query.startsWith("play ", ignoreCase = true)) {
query.drop(5)
} else {
query
}
return MediaItemTree.getItemFromTitle(mediaTitle) ?: MediaItemTree.getRandomItem()
}
}
private fun initializeSessionAndPlayer() {
player =
ExoPlayer.Builder(this)
.setAudioAttributes(AudioAttributes.DEFAULT, /* handleAudioFocus= */ true)
.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)
.build()
if (!customLayout.isEmpty()) {
// Send custom layout to legacy session.
mediaLibrarySession.setCustomLayout(customLayout)
}
}
private fun getShuffleCommandButton(sessionCommand: SessionCommand): CommandButton {
val isOn = sessionCommand.customAction == CUSTOM_COMMAND_TOGGLE_SHUFFLE_MODE_ON
return CommandButton.Builder()
.setDisplayName(
getString(
if (isOn) R.string.exo_controls_shuffle_on_description
else R.string.exo_controls_shuffle_off_description
)
)
.setSessionCommand(sessionCommand)
.setIconResId(if (isOn) R.drawable.exo_icon_shuffle_off else R.drawable.exo_icon_shuffle_on)
.build()
}
private fun ignoreFuture(customLayout: ListenableFuture<SessionResult>) {
/* Do nothing. */
}
private inner class MediaSessionServiceListener : Listener {
/**
* This method is only required to be implemented on Android 12 or above when an attempt is made
* by a media controller to resume playback when the {@link MediaSessionService} is in the
* background.
*/
@SuppressLint("MissingPermission") // TODO: b/280766358 - Request this permission at runtime.
override fun onForegroundServiceStartNotAllowedException() {
val notificationManagerCompat = NotificationManagerCompat.from(this@PlaybackService)
ensureNotificationChannel(notificationManagerCompat)
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 =
NotificationCompat.Builder(this@PlaybackService, CHANNEL_ID)
.setContentIntent(pendingIntent)
.setSmallIcon(R.drawable.media3_notification_small_icon)
.setContentTitle(getString(R.string.notification_content_title))
.setStyle(
NotificationCompat.BigTextStyle().bigText(getString(R.string.notification_content_text))
)
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
.setAutoCancel(true)
notificationManagerCompat.notify(NOTIFICATION_ID, builder.build())
}
}
private fun ensureNotificationChannel(notificationManagerCompat: NotificationManagerCompat) {
if (Util.SDK_INT < 26 || notificationManagerCompat.getNotificationChannel(CHANNEL_ID) != null) {
return
}
val channel =
NotificationChannel(
CHANNEL_ID,
getString(R.string.notification_channel_name),
NotificationManager.IMPORTANCE_DEFAULT
)
notificationManagerCompat.createNotificationChannel(channel)
}
}

View File

@ -19,21 +19,23 @@ 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
import android.widget.Button
import android.widget.ImageView
import android.widget.ListView
import android.widget.TextView
import androidx.annotation.OptIn
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
import androidx.media3.common.C.TRACK_TYPE_TEXT
import androidx.media3.common.MediaItem
import androidx.media3.common.MediaMetadata
import androidx.media3.common.Player
import androidx.media3.common.Tracks
import androidx.media3.common.Player.EVENT_MEDIA_ITEM_TRANSITION
import androidx.media3.common.Player.EVENT_MEDIA_METADATA_CHANGED
import androidx.media3.common.Player.EVENT_TIMELINE_CHANGED
import androidx.media3.common.Player.EVENT_TRACKS_CHANGED
import androidx.media3.common.util.UnstableApi
import androidx.media3.session.MediaController
import androidx.media3.session.SessionToken
import androidx.media3.ui.PlayerView
@ -43,44 +45,38 @@ import com.google.common.util.concurrent.MoreExecutors
class PlayerActivity : AppCompatActivity() {
private lateinit var controllerFuture: ListenableFuture<MediaController>
private val controller: MediaController?
get() = if (controllerFuture.isDone) controllerFuture.get() else null
get() =
if (controllerFuture.isDone && !controllerFuture.isCancelled) 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
@OptIn(UnstableApi::class) // PlayerView.hideController
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,21 +90,14 @@ 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(
this,
SessionToken(this, ComponentName(this, PlaybackService::class.java))
SessionToken(this, ComponentName(this, PlaybackService::class.java)),
)
.buildAsync()
updateMediaMetadataUI()
controllerFuture.addListener({ setController() }, MoreExecutors.directExecutor())
}
@ -116,83 +105,65 @@ class PlayerActivity : AppCompatActivity() {
MediaController.releaseFuture(controllerFuture)
}
@OptIn(UnstableApi::class) // PlayerView.setShowSubtitleButton
private fun setController() {
val controller = this.controller ?: return
playerView.player = controller
updateCurrentPlaylistUI()
updateMediaMetadataUI(controller.mediaMetadata)
updateShuffleSwitchUI(controller.shuffleModeEnabled)
updateRepeatSwitchUI(controller.repeatMode)
updateMediaMetadataUI()
playerView.setShowSubtitleButton(controller.currentTracks.isTypeSupported(TRACK_TYPE_TEXT))
controller.addListener(
object : Player.Listener {
override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) {
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))
override fun onEvents(player: Player, events: Player.Events) {
if (events.contains(EVENT_TRACKS_CHANGED)) {
playerView.setShowSubtitleButton(player.currentTracks.isTypeSupported(TRACK_TYPE_TEXT))
}
if (events.contains(EVENT_TIMELINE_CHANGED)) {
updateCurrentPlaylistUI()
}
if (events.contains(EVENT_MEDIA_METADATA_CHANGED)) {
updateMediaMetadataUI()
}
if (events.contains(EVENT_MEDIA_ITEM_TRANSITION)) {
// Trigger adapter update to change highlight of current item.
mediaItemListAdapter.notifyDataSetChanged()
}
}
}
)
}
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 updateMediaMetadataUI() {
val controller = this.controller
if (controller == null || controller.mediaItemCount == 0) {
findViewById<TextView>(R.id.media_title).text = getString(R.string.waiting_for_metadata)
findViewById<TextView>(R.id.media_artist).text = ""
return
}
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))
}
val mediaMetadata = controller.mediaMetadata
val title: CharSequence = mediaMetadata.title ?: ""
private fun updateMediaMetadataUI(mediaMetadata: MediaMetadata) {
val title: CharSequence = mediaMetadata.title ?: getString(R.string.no_item_prompt)
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
// Trick to update playlist UI
mediaListAdapter.notifyDataSetChanged()
findViewById<TextView>(R.id.media_title).text = title
findViewById<TextView>(R.id.media_artist).text = mediaMetadata.artist
}
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>
mediaItemList: List<MediaItem>,
) : ArrayAdapter<MediaItem>(context, viewID, mediaItemList) {
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
val mediaItem = getItem(position)!!
@ -201,22 +172,30 @@ 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 {
val controller = this@PlayerActivity.controller ?: return@setOnClickListener
controller.removeMediaItem(position)
updateCurrentPlaylistUI()
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

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

View File

@ -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_height="wrap_content"/>
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</LinearLayout>

View File

@ -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"
/>

View File

@ -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>

View File

@ -19,14 +19,8 @@
<string name="open_player_content_description">Click to view your play list</string>
<string name="added_media_item_format">Added %1$s to playlist</string>
<string name="shuffle">Shuffle</string>
<string name="repeat">Repeat</string>
<string name="play_button">Play</string>
<string name="no_item_prompt">
"! No media in the play list !\nPlease try to add more from browser"
</string>
<string name="notification_content_title">Playback cannot be resumed</string>
<string name="notification_content_text">Press on the play button on the media notification if it
is still present, otherwise please open the app to start the playback and re-connect the session
to the controller</string>
<string name="notification_channel_name">Playback cannot be resumed</string>
<string name="waiting_for_metadata">Waiting for playlist to load…</string>
<string name="notification_permission_denied">
"Without notification access the app can't warn about failed background operations"</string>
</resources>

View File

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 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.
-->
<automotiveApp>
<uses name="media" />
</automotiveApp>

View File

@ -0,0 +1,7 @@
# Media3 Automotive session demo
This app demonstrates use of the `MediaLibraryService` for
[Android Automotive](https://developer.android.com/training/cars/media/automotive-os).
See the [demos README](../README.md) for instructions on how to build and run
this demo.

View File

@ -0,0 +1,67 @@
// 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.
apply from: '../../constants.gradle'
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
android {
namespace 'androidx.media3.demo.session.automotive'
compileSdkVersion project.ext.compileSdkVersion
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = '1.8'
}
defaultConfig {
versionName project.ext.releaseVersion
versionCode project.ext.releaseVersionCode
minSdkVersion project.ext.automotiveMinSdkVersion
targetSdkVersion project.ext.appTargetSdkVersion
multiDexEnabled true
}
buildTypes {
release {
shrinkResources true
minifyEnabled true
proguardFiles = [
getDefaultProguardFile('proguard-android-optimize.txt')
]
signingConfig signingConfigs.debug
}
debug {
jniDebuggable = true
}
}
lintOptions {
// The demo app isn't indexed, and doesn't have translations.
disable 'GoogleAppIndexingWarning','MissingTranslation'
}
}
dependencies {
implementation 'androidx.core:core-ktx:' + androidxCoreVersion
implementation 'androidx.appcompat:appcompat:' + androidxAppCompatVersion
implementation 'androidx.multidex:multidex:' + androidxMultidexVersion
implementation 'com.google.android.material:material:' + androidxMaterialVersion
implementation project(modulePrefix + 'lib-session')
implementation project(modulePrefix + 'demo-session-service')
}

View File

@ -0,0 +1,72 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 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.
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="androidx.media3.demo.session.automotive">
<uses-sdk/>
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" />
<uses-feature
android:name="android.hardware.type.automotive"
android:required="true" />
<uses-feature
android:name="android.hardware.wifi"
android:required="false" />
<uses-feature
android:name="android.hardware.screen.portrait"
android:required="false" />
<uses-feature
android:name="android.hardware.screen.landscape"
android:required="false" />
<meta-data android:name="com.android.automotive"
android:resource="@xml/automotive_app_desc"/>
<application
android:name="androidx.multidex.MultiDexApplication"
android:allowBackup="false"
android:taskAffinity=""
android:appCategory="audio"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
tools:replace="android:name">
<meta-data
android:name="androidx.car.app.TintableAttributionIcon"
android:resource="@mipmap/ic_launcher" />
<service
android:name=".AutomotiveService"
android:foregroundServiceType="mediaPlayback"
android:exported="true">
<intent-filter>
<action android:name="androidx.media3.session.MediaLibraryService"/>
<action android:name="android.media.browse.MediaBrowserService"/>
</intent-filter>
</service>
<!-- Artwork provider for content:// URIs -->
<provider
android:name="BitmapContentProvider"
android:authorities="androidx.media3"
android:exported="true" />
</application>
</manifest>

Binary file not shown.

After

Width:  |  Height:  |  Size: 353 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 215 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 278 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 240 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 425 KiB

View File

@ -0,0 +1,472 @@
{
"media": [
{
"id": "wake_up_01",
"title": "Intro - The Way Of Waking Up (feat. Alan Watts)",
"album": "Wake Up",
"artist": "The Kyoto Connection",
"genre": "Electronic",
"source": "https://storage.googleapis.com/uamp/The_Kyoto_Connection_-_Wake_Up/01_-_Intro_-_The_Way_Of_Waking_Up_feat_Alan_Watts.mp3",
"image": "content://androidx.media3/artwork/album_kyoto_connection.png",
"trackNumber": 1,
"totalTrackCount": 13,
"duration": 90,
"site": "http://freemusicarchive.org/music/The_Kyoto_Connection/Wake_Up_1957/"
},
{
"id": "wake_up_02",
"title": "Geisha",
"album": "Wake Up",
"artist": "The Kyoto Connection",
"genre": "Electronic",
"source": "https://storage.googleapis.com/uamp/The_Kyoto_Connection_-_Wake_Up/02_-_Geisha.mp3",
"image": "content://androidx.media3/artwork/album_kyoto_connection.png",
"trackNumber": 2,
"totalTrackCount": 13,
"duration": 267,
"site": "http://freemusicarchive.org/music/The_Kyoto_Connection/Wake_Up_1957/"
},
{
"id": "wake_up_03",
"title": "Voyage I - Waterfall",
"album": "Wake Up",
"artist": "The Kyoto Connection",
"genre": "Electronic",
"source": "https://storage.googleapis.com/uamp/The_Kyoto_Connection_-_Wake_Up/03_-_Voyage_I_-_Waterfall.mp3",
"image": "content://androidx.media3/artwork/album_kyoto_connection.png",
"trackNumber": 3,
"totalTrackCount": 13,
"duration": 264,
"site": "http://freemusicarchive.org/music/The_Kyoto_Connection/Wake_Up_1957/"
},
{
"id": "wake_up_04",
"title": "The Music In You",
"album": "Wake Up",
"artist": "The Kyoto Connection",
"genre": "Electronic",
"source": "https://storage.googleapis.com/uamp/The_Kyoto_Connection_-_Wake_Up/04_-_The_Music_In_You.mp3",
"image": "content://androidx.media3/artwork/album_kyoto_connection.png",
"trackNumber": 4,
"totalTrackCount": 13,
"duration": 223,
"site": "http://freemusicarchive.org/music/The_Kyoto_Connection/Wake_Up_1957/"
},
{
"id": "wake_up_05",
"title": "The Calm Before The Storm",
"album": "Wake Up",
"artist": "The Kyoto Connection",
"genre": "Electronic",
"source": "https://storage.googleapis.com/uamp/The_Kyoto_Connection_-_Wake_Up/05_-_The_Calm_Before_The_Storm.mp3",
"image": "content://androidx.media3/artwork/album_kyoto_connection.png",
"trackNumber": 5,
"totalTrackCount": 13,
"duration": 229,
"site": "http://freemusicarchive.org/music/The_Kyoto_Connection/Wake_Up_1957/"
},
{
"id": "wake_up_06",
"title": "No Pain, No Gain",
"album": "Wake Up",
"artist": "The Kyoto Connection",
"genre": "Electronic",
"source": "https://storage.googleapis.com/uamp/The_Kyoto_Connection_-_Wake_Up/06_-_No_Pain_No_Gain.mp3",
"image": "content://androidx.media3/artwork/album_kyoto_connection.png",
"trackNumber": 6,
"totalTrackCount": 13,
"duration": 304,
"site": "http://freemusicarchive.org/music/The_Kyoto_Connection/Wake_Up_1957/"
},
{
"id": "wake_up_07",
"title": "Voyage II - Satori",
"album": "Wake Up",
"artist": "The Kyoto Connection",
"genre": "Electronic",
"source": "https://storage.googleapis.com/uamp/The_Kyoto_Connection_-_Wake_Up/07_-_Voyage_II_-_Satori.mp3",
"image": "content://androidx.media3/artwork/album_kyoto_connection.png",
"trackNumber": 7,
"totalTrackCount": 13,
"duration": 256,
"site": "http://freemusicarchive.org/music/The_Kyoto_Connection/Wake_Up_1957/"
},
{
"id": "wake_up_08",
"title": "Reveal the Magic",
"album": "Wake Up",
"artist": "The Kyoto Connection",
"genre": "Electronic",
"source": "https://storage.googleapis.com/uamp/The_Kyoto_Connection_-_Wake_Up/08_-_Reveal_the_Magic.mp3",
"image": "content://androidx.media3/artwork/album_kyoto_connection.png",
"trackNumber": 8,
"totalTrackCount": 13,
"duration": 293,
"site": "http://freemusicarchive.org/music/The_Kyoto_Connection/Wake_Up_1957/"
},
{
"id": "wake_up_09",
"title": "Hachiko (The Faithtful Dog)",
"album": "Wake Up",
"artist": "The Kyoto Connection",
"genre": "Electronic",
"source": "https://storage.googleapis.com/uamp/The_Kyoto_Connection_-_Wake_Up/09_-_Hachiko_The_Faithtful_Dog.mp3",
"image": "content://androidx.media3/artwork/album_kyoto_connection.png",
"trackNumber": 9,
"totalTrackCount": 13,
"duration": 185,
"site": "http://freemusicarchive.org/music/The_Kyoto_Connection/Wake_Up_1957/"
},
{
"id": "wake_up_10",
"title": "Wake Up",
"album": "Wake Up",
"artist": "The Kyoto Connection",
"genre": "Electronic",
"source": "https://storage.googleapis.com/uamp/The_Kyoto_Connection_-_Wake_Up/10_-_Wake_Up.mp3",
"image": "content://androidx.media3/artwork/album_kyoto_connection.png",
"trackNumber": 10,
"totalTrackCount": 13,
"duration": 251,
"site": "http://freemusicarchive.org/music/The_Kyoto_Connection/Wake_Up_1957/"
},
{
"id": "wake_up_11",
"title": "Voyage III - The Space Between Us",
"album": "Wake Up",
"artist": "The Kyoto Connection",
"genre": "Electronic",
"source": "https://storage.googleapis.com/uamp/The_Kyoto_Connection_-_Wake_Up/11_-_Voyage_III_-_The_Space_Between_Us.mp3",
"image": "content://androidx.media3/artwork/album_kyoto_connection.png",
"trackNumber": 11,
"totalTrackCount": 13,
"duration": 290,
"site": "http://freemusicarchive.org/music/The_Kyoto_Connection/Wake_Up_1957/"
},
{
"id": "wake_up_12",
"title": "Ume No Kaori (feat. Sunawai)",
"album": "Wake Up",
"artist": "The Kyoto Connection",
"genre": "Electronic",
"source": "https://storage.googleapis.com/uamp/The_Kyoto_Connection_-_Wake_Up/12_-_Ume_No_Kaori_feat_Sunawai.mp3",
"image": "content://androidx.media3/artwork/album_kyoto_connection.png",
"trackNumber": 12,
"totalTrackCount": 13,
"duration": 334,
"site": "http://freemusicarchive.org/music/The_Kyoto_Connection/Wake_Up_1957/"
},
{
"id": "wake_up_13",
"title": "Outro - Totally Here and Now (feat. Alan Watts)",
"album": "Wake Up",
"artist": "The Kyoto Connection",
"genre": "Electronic",
"source": "https://storage.googleapis.com/uamp/The_Kyoto_Connection_-_Wake_Up/13_-_Outro_-_Totally_Here_and_Now_feat_Alan_Watts.mp3",
"image": "content://androidx.media3/artwork/album_kyoto_connection.png",
"trackNumber": 13,
"totalTrackCount": 13,
"duration": 242,
"site": "http://freemusicarchive.org/music/The_Kyoto_Connection/Wake_Up_1957/"
},
{
"id": "irsens_tale_01",
"title": "Intro (.udonthear)",
"album": "Irsen's Tale",
"artist": "Kai Engel",
"genre": "Ambient",
"source": "https://storage.googleapis.com/uamp/Kai_Engel_-_Irsens_Tale/01_-_Intro_udonthear.mp3",
"image": "content://androidx.media3/artwork/album_kai_engel.png",
"trackNumber": 1,
"totalTrackCount": 9,
"duration": 63,
"site": "http://freemusicarchive.org/music/Kai_Engel/Irsens_Tale/"
},
{
"id": "irsens_tale_02",
"title": "Leaving",
"album": "Irsen's Tale",
"artist": "Kai Engel",
"genre": "Ambient",
"source": "https://storage.googleapis.com/uamp/Kai_Engel_-_Irsens_Tale/02_-_Leaving.mp3",
"image": "content://androidx.media3/artwork/album_kai_engel.png",
"trackNumber": 2,
"totalTrackCount": 9,
"duration": 170,
"site": "http://freemusicarchive.org/music/Kai_Engel/Irsens_Tale/"
},
{
"id": "irsens_tale_03",
"title": "Irsen's Tale",
"album": "Irsen's Tale",
"artist": "Kai Engel",
"genre": "Ambient",
"source": "https://storage.googleapis.com/uamp/Kai_Engel_-_Irsens_Tale/03_-_Irsens_Tale.mp3",
"image": "content://androidx.media3/artwork/album_kai_engel.png",
"trackNumber": 3,
"totalTrackCount": 9,
"duration": 164,
"site": "http://freemusicarchive.org/music/Kai_Engel/Irsens_Tale/"
},
{
"id": "irsens_tale_04",
"title": "Moonlight Reprise",
"album": "Irsen's Tale",
"artist": "Kai Engel",
"genre": "Ambient",
"source": "https://storage.googleapis.com/uamp/Kai_Engel_-_Irsens_Tale/04_-_Moonlight_Reprise.mp3",
"image": "content://androidx.media3/artwork/album_kai_engel.png",
"trackNumber": 4,
"totalTrackCount": 9,
"duration": 181,
"site": "http://freemusicarchive.org/music/Kai_Engel/Irsens_Tale/"
},
{
"id": "irsens_tale_05",
"title": "Nothing Lasts Forever",
"album": "Irsen's Tale",
"artist": "Kai Engel",
"genre": "Ambient",
"source": "https://storage.googleapis.com/uamp/Kai_Engel_-_Irsens_Tale/05_-_Nothing_Lasts_Forever.mp3",
"image": "content://androidx.media3/artwork/album_kai_engel.png",
"trackNumber": 5,
"totalTrackCount": 9,
"duration": 132,
"site": "http://freemusicarchive.org/music/Kai_Engel/Irsens_Tale/"
},
{
"id": "irsens_tale_06",
"title": "The Moments of Our Mornings",
"album": "Irsen's Tale",
"artist": "Kai Engel",
"genre": "Ambient",
"source": "https://storage.googleapis.com/uamp/Kai_Engel_-_Irsens_Tale/06_-_The_Moments_of_Our_Mornings.mp3",
"image": "content://androidx.media3/artwork/album_kai_engel.png",
"trackNumber": 6,
"totalTrackCount": 9,
"duration": 104,
"site": "http://freemusicarchive.org/music/Kai_Engel/Irsens_Tale/"
},
{
"id": "irsens_tale_07",
"title": "Laceration",
"album": "Irsen's Tale",
"artist": "Kai Engel",
"genre": "Ambient",
"source": "https://storage.googleapis.com/uamp/Kai_Engel_-_Irsens_Tale/07_-_Laceration.mp3",
"image": "content://androidx.media3/artwork/album_kai_engel.png",
"trackNumber": 7,
"totalTrackCount": 9,
"duration": 173,
"site": "http://freemusicarchive.org/music/Kai_Engel/Irsens_Tale/"
},
{
"id": "irsens_tale_08",
"title": "Memories",
"album": "Irsen's Tale",
"artist": "Kai Engel",
"genre": "Ambient",
"source": "https://storage.googleapis.com/uamp/Kai_Engel_-_Irsens_Tale/08_-_Memories.mp3",
"image": "content://androidx.media3/artwork/album_kai_engel.png",
"trackNumber": 8,
"totalTrackCount": 9,
"duration": 213,
"site": "http://freemusicarchive.org/music/Kai_Engel/Irsens_Tale/"
},
{
"id": "irsens_tale_09",
"title": "Outro",
"album": "Irsen's Tale",
"artist": "Kai Engel",
"genre": "Ambient",
"source": "https://storage.googleapis.com/uamp/Kai_Engel_-_Irsens_Tale/09_-_Outro.mp3",
"image": "content://androidx.media3/artwork/album_kai_engel.png",
"trackNumber": 9,
"totalTrackCount": 9,
"duration": 65,
"site": "http://freemusicarchive.org/music/Kai_Engel/Irsens_Tale/"
},
{
"id": "jazz_in_paris",
"title": "Jazz in Paris",
"album": "Jazz & Blues",
"artist": "Media Right Productions",
"genre": "Jazz & Blues",
"source": "https://storage.googleapis.com/automotive-media/Jazz_In_Paris.mp3",
"image": "content://androidx.media3/artwork/album_sea.png",
"trackNumber": 1,
"totalTrackCount": 6,
"duration": 103,
"site": "https://www.youtube.com/audiolibrary/music"
},
{
"id": "the_messenger",
"title": "The Messenger",
"album": "Jazz & Blues",
"artist": "Silent Partner",
"genre": "Jazz & Blues",
"source": "https://storage.googleapis.com/automotive-media/The_Messenger.mp3",
"image": "content://androidx.media3/artwork/album_sea.png",
"trackNumber": 2,
"totalTrackCount": 6,
"duration": 132,
"site": "https://www.youtube.com/audiolibrary/music"
},
{
"id": "talkies",
"title": "Talkies",
"album": "Jazz & Blues",
"artist": "Huma-Huma",
"genre": "Jazz & Blues",
"source": "https://storage.googleapis.com/automotive-media/Talkies.mp3",
"image": "content://androidx.media3/artwork/album_sea.png",
"trackNumber": 3,
"totalTrackCount": 6,
"duration": 162,
"site": "https://www.youtube.com/audiolibrary/music"
},
{
"id": "on_the_bach",
"title": "On the Bach",
"album": "Cinematic",
"artist": "Jingle Punks",
"genre": "Cinematic",
"source": "https://storage.googleapis.com/automotive-media/On_the_Bach.mp3",
"image": "content://androidx.media3/artwork/album_drinks.png",
"trackNumber": 4,
"totalTrackCount": 6,
"duration": 66,
"site": "https://www.youtube.com/audiolibrary/music"
},
{
"id": "the_story_unfolds",
"title": "The Story Unfolds",
"album": "Cinematic",
"artist": "Jingle Punks",
"genre": "Cinematic",
"source": "https://storage.googleapis.com/automotive-media/The_Story_Unfolds.mp3",
"image": "content://androidx.media3/artwork/album_drinks.png",
"trackNumber": 5,
"totalTrackCount": 6,
"duration": 91,
"site": "https://www.youtube.com/audiolibrary/music"
},
{
"id": "drop_and_roll",
"title": "Drop and Roll",
"album": "Youtube Audio Library Rock",
"artist": "Silent Partner",
"genre": "Rock",
"source": "https://storage.googleapis.com/automotive-media/Drop_and_Roll.mp3",
"image": "content://androidx.media3/artwork/album_road.png",
"trackNumber": 1,
"totalTrackCount": 7,
"duration": 121,
"site": "https://www.youtube.com/audiolibrary/music"
},
{
"id": "motocross",
"title": "Motocross",
"album": "Youtube Audio Library Rock",
"artist": "Topher Mohr and Alex Elena",
"genre": "Rock",
"source": "https://storage.googleapis.com/automotive-media/Motocross.mp3",
"image": "content://androidx.media3/artwork/album_road.png",
"trackNumber": 2,
"totalTrackCount": 7,
"duration": 182,
"site": "https://www.youtube.com/audiolibrary/music"
},
{
"id": "wish_youd_come_true",
"title": "Wish You'd Come True",
"album": "Youtube Audio Library Rock",
"artist": "The 126ers",
"genre": "Rock",
"source": "https://storage.googleapis.com/automotive-media/Wish_You_d_Come_True.mp3",
"image": "content://androidx.media3/artwork/album_road.png",
"trackNumber": 3,
"totalTrackCount": 7,
"duration": 169,
"site": "https://www.youtube.com/audiolibrary/music"
},
{
"id": "awakening",
"title": "Awakening",
"album": "Youtube Audio Library Rock",
"artist": "Silent Partner",
"genre": "Rock",
"source": "https://storage.googleapis.com/automotive-media/Awakening.mp3",
"image": "content://androidx.media3/artwork/album_road.png",
"trackNumber": 4,
"totalTrackCount": 7,
"duration": 220,
"site": "https://www.youtube.com/audiolibrary/music"
},
{
"id": "home",
"title": "Home",
"album": "Youtube Audio Library Rock",
"artist": "Letter Box",
"genre": "Rock",
"source": "https://storage.googleapis.com/automotive-media/Home.mp3",
"image": "content://androidx.media3/artwork/album_road.png",
"trackNumber": 5,
"totalTrackCount": 7,
"duration": 213,
"site": "https://www.youtube.com/audiolibrary/music"
},
{
"id": "tell_the_angels",
"title": "Tell The Angels",
"album": "Youtube Audio Library Rock 2",
"artist": "Letter Box",
"genre": "Rock",
"source": "https://storage.googleapis.com/automotive-media/Tell_The_Angels.mp3",
"image": "content://androidx.media3/artwork/album_skyline.png",
"trackNumber": 6,
"totalTrackCount": 7,
"duration": 208,
"site": "https://www.youtube.com/audiolibrary/music"
},
{
"id": "hey_sailor",
"title": "Hey Sailor",
"album": "Youtube Audio Library Rock 2",
"artist": "Letter Box",
"genre": "Rock",
"source": "https://storage.googleapis.com/automotive-media/Hey_Sailor.mp3",
"image": "content://androidx.media3/artwork/album_skyline.png",
"trackNumber": 7,
"totalTrackCount": 7,
"duration": 193,
"site": "https://www.youtube.com/audiolibrary/music"
},
{
"id": "keys_to_the_kingdom",
"title": "Keys To The Kingdom",
"album": "Youtube Audio Library Rock 2",
"artist": "The 126ers",
"genre": "Rock",
"source": "https://storage.googleapis.com/automotive-media/Keys_To_The_Kingdom.mp3",
"image": "content://androidx.media3/artwork/album_skyline.png",
"trackNumber": 1,
"totalTrackCount": 2,
"duration": 221,
"site": "https://www.youtube.com/audiolibrary/music"
},
{
"id": "the_coldest_shoulder",
"title": "The Coldest Shoulder",
"album": "Youtube Audio Library Rock 2",
"artist": "The 126ers",
"genre": "Rock",
"source": "https://storage.googleapis.com/automotive-media/The_Coldest_Shoulder.mp3",
"image": "content://androidx.media3/artwork/album_skyline.png",
"trackNumber": 2,
"totalTrackCount": 2,
"duration": 160,
"site": "https://www.youtube.com/audiolibrary/music"
}
]
}

View File

@ -0,0 +1,59 @@
/*
* 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.demo.session.automotive
import androidx.annotation.OptIn
import androidx.media3.common.MediaItem
import androidx.media3.common.util.UnstableApi
import androidx.media3.demo.session.DemoMediaLibrarySessionCallback
import androidx.media3.demo.session.DemoPlaybackService
import androidx.media3.session.LibraryResult
import androidx.media3.session.MediaConstants
import androidx.media3.session.MediaSession.ControllerInfo
import com.google.common.util.concurrent.ListenableFuture
class AutomotiveService : DemoPlaybackService() {
override fun createLibrarySessionCallback(): MediaLibrarySession.Callback {
return object : DemoMediaLibrarySessionCallback(this@AutomotiveService) {
@OptIn(UnstableApi::class)
override fun onGetLibraryRoot(
session: MediaLibrarySession,
browser: ControllerInfo,
params: LibraryParams?
): ListenableFuture<LibraryResult<MediaItem>> {
var responseParams = params
if (session.isAutomotiveController(browser)) {
// See https://developer.android.com/training/cars/media#apply_content_style
val rootHintParams = params ?: LibraryParams.Builder().build()
rootHintParams.extras.putInt(
MediaConstants.EXTRAS_KEY_CONTENT_STYLE_BROWSABLE,
MediaConstants.EXTRAS_VALUE_CONTENT_STYLE_GRID_ITEM
)
rootHintParams.extras.putInt(
MediaConstants.EXTRAS_KEY_CONTENT_STYLE_PLAYABLE,
MediaConstants.EXTRAS_VALUE_CONTENT_STYLE_LIST_ITEM
)
// Tweaked params are propagated to Automotive browsers as root hints.
responseParams = rootHintParams
}
// Use super to return the common library root with the tweaked params sent to the browser.
return super.onGetLibraryRoot(session, browser, responseParams)
}
}
}
}

View File

@ -0,0 +1,89 @@
/*
* 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.demo.session.automotive
import android.content.ContentProvider
import android.content.ContentValues
import android.content.Context
import android.database.Cursor
import android.net.Uri
import android.os.ParcelFileDescriptor
import java.io.File
/**
* Provides artwork for content URIs.
*
* <p>A bitmap file in the asset folder with path 'artwork/album1.png' can be referenced as artwork
* URI with 'content://androidx.media3/artwork/album1.png'. 'androidx.media3' is the authority
* declared for the content provider in 'AndroidManifest.xml'.
*
* <p>For demo use only.
*/
class BitmapContentProvider : ContentProvider() {
override fun onCreate() = true
override fun openFile(uri: Uri, mode: String): ParcelFileDescriptor? {
context?.let { ctx ->
getAssetPath(uri)?.let {
return ParcelFileDescriptor.open(
copyAssetFileToCacheDirectory(ctx, it),
ParcelFileDescriptor.MODE_READ_ONLY
)
}
}
return super.openFile(uri, mode)
}
private fun getAssetPath(contentUri: Uri): String? {
contentUri.path?.let {
return it.substring(1)
}
return null
}
private fun copyAssetFileToCacheDirectory(context: Context, assetPath: String): File {
val publicFile = File(context.cacheDir, assetPath.replace("/", "_"))
if (!publicFile.exists()) {
context.assets.open(assetPath).copyTo(publicFile.outputStream())
}
return publicFile
}
// No-op implementations of abstract ContentProvider methods.
override fun insert(uri: Uri, values: ContentValues?): Uri? = null
override fun query(
uri: Uri,
projection: Array<String>?,
selection: String?,
selectionArgs: Array<String>?,
sortOrder: String?
): Cursor? = null
override fun update(
uri: Uri,
values: ContentValues?,
selection: String?,
selectionArgs: Array<String>?
) = 0
override fun delete(uri: Uri, selection: String?, selectionArgs: Array<String>?) = 0
override fun getType(uri: Uri): String? = null
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

View File

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 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.
-->
<resources>
<string name="app_name">Media3 Automotive Demo</string>
</resources>

View File

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 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.
-->
<automotiveApp>
<uses name="media"/>
</automotiveApp>

View File

@ -0,0 +1,8 @@
# Demo `MediaLibraryService` implementation
A library module with a demo implementation of `MediaLibraryService` and
`MediaLibrarySession.Callback`.
See the `PlaybackService` of the [session demo](../session/README.md) how to use
it. Override `assets/cataglog.json` by creating such a file in the same format
in your application module that the service will use.

View File

@ -0,0 +1,63 @@
// 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.
apply from: "$gradle.ext.androidxMediaSettingsDir/common_library_config.gradle"
apply plugin: 'kotlin-android'
android {
namespace 'androidx.media3.demo.session.service'
compileSdkVersion project.ext.compileSdkVersion
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = '1.8'
}
defaultConfig {
versionName project.ext.releaseVersion
versionCode project.ext.releaseVersionCode
minSdkVersion project.ext.minSdkVersion
targetSdkVersion project.ext.appTargetSdkVersion
multiDexEnabled true
}
buildTypes {
release {
signingConfig signingConfigs.debug
}
debug {
jniDebuggable = true
}
}
lintOptions {
// The demo service module isn't indexed, and doesn't have translations.
disable 'GoogleAppIndexingWarning','MissingTranslation'
}
}
dependencies {
implementation 'androidx.core:core-ktx:' + androidxCoreVersion
implementation 'androidx.appcompat:appcompat:' + androidxAppCompatVersion
implementation 'androidx.multidex:multidex:' + androidxMultidexVersion
implementation project(modulePrefix + 'lib-exoplayer')
implementation project(modulePrefix + 'lib-exoplayer-dash')
implementation project(modulePrefix + 'lib-exoplayer-hls')
implementation project(modulePrefix + 'lib-ui')
implementation project(modulePrefix + 'lib-session')
}

View File

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 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.
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="androidx.media3.demo.session.service">
<uses-sdk />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
</manifest>

View File

@ -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"
}
]
}

View File

@ -0,0 +1,248 @@
/*
* 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.demo.session
import android.content.Context
import android.os.Bundle
import androidx.annotation.OptIn
import androidx.media3.common.MediaItem
import androidx.media3.common.util.UnstableApi
import androidx.media3.demo.session.service.R
import androidx.media3.session.CommandButton
import androidx.media3.session.LibraryResult
import androidx.media3.session.MediaLibraryService
import androidx.media3.session.MediaSession
import androidx.media3.session.MediaSession.MediaItemsWithStartPosition
import androidx.media3.session.SessionCommand
import androidx.media3.session.SessionResult
import com.google.common.collect.ImmutableList
import com.google.common.util.concurrent.Futures
import com.google.common.util.concurrent.ListenableFuture
/** A [MediaLibraryService.MediaLibrarySession.Callback] implementation. */
open class DemoMediaLibrarySessionCallback(context: Context) :
MediaLibraryService.MediaLibrarySession.Callback {
init {
MediaItemTree.initialize(context.assets)
}
@OptIn(UnstableApi::class) // TODO: b/328238954 - Remove once new CommandButton icons are stable.
private val customLayoutCommandButtons: List<CommandButton> =
listOf(
CommandButton.Builder(CommandButton.ICON_SHUFFLE_OFF)
.setDisplayName(context.getString(R.string.exo_controls_shuffle_on_description))
.setSessionCommand(SessionCommand(CUSTOM_COMMAND_TOGGLE_SHUFFLE_MODE_ON, Bundle.EMPTY))
.build(),
CommandButton.Builder(CommandButton.ICON_SHUFFLE_ON)
.setDisplayName(context.getString(R.string.exo_controls_shuffle_off_description))
.setSessionCommand(SessionCommand(CUSTOM_COMMAND_TOGGLE_SHUFFLE_MODE_OFF, Bundle.EMPTY))
.build(),
)
@OptIn(UnstableApi::class) // MediaSession.ConnectionResult.DEFAULT_SESSION_AND_LIBRARY_COMMANDS
val mediaNotificationSessionCommands =
MediaSession.ConnectionResult.DEFAULT_SESSION_AND_LIBRARY_COMMANDS.buildUpon()
.also { builder ->
// Put all custom session commands in the list that may be used by the notification.
customLayoutCommandButtons.forEach { commandButton ->
commandButton.sessionCommand?.let { builder.add(it) }
}
}
.build()
// ConnectionResult.DEFAULT_SESSION_AND_LIBRARY_COMMANDS
// ConnectionResult.AcceptedResultBuilder
@OptIn(UnstableApi::class)
override fun onConnect(
session: MediaSession,
controller: MediaSession.ControllerInfo,
): MediaSession.ConnectionResult {
if (
session.isMediaNotificationController(controller) ||
session.isAutomotiveController(controller) ||
session.isAutoCompanionController(controller)
) {
// Select the button to display.
val customLayout = customLayoutCommandButtons[if (session.player.shuffleModeEnabled) 1 else 0]
return MediaSession.ConnectionResult.AcceptedResultBuilder(session)
.setAvailableSessionCommands(mediaNotificationSessionCommands)
.setCustomLayout(ImmutableList.of(customLayout))
.build()
}
// Default commands without custom layout for common controllers.
return MediaSession.ConnectionResult.AcceptedResultBuilder(session).build()
}
@OptIn(UnstableApi::class) // MediaSession.isMediaNotificationController
override fun onCustomCommand(
session: MediaSession,
controller: MediaSession.ControllerInfo,
customCommand: SessionCommand,
args: Bundle,
): ListenableFuture<SessionResult> {
if (CUSTOM_COMMAND_TOGGLE_SHUFFLE_MODE_ON == customCommand.customAction) {
// Enable shuffling.
session.player.shuffleModeEnabled = true
// Change the custom layout to contain the `Disable shuffling` command.
session.setCustomLayout(
session.mediaNotificationControllerInfo!!,
ImmutableList.of(customLayoutCommandButtons[1]),
)
return Futures.immediateFuture(SessionResult(SessionResult.RESULT_SUCCESS))
} else if (CUSTOM_COMMAND_TOGGLE_SHUFFLE_MODE_OFF == customCommand.customAction) {
// Disable shuffling.
session.player.shuffleModeEnabled = false
// Change the custom layout to contain the `Enable shuffling` command.
session.setCustomLayout(
session.mediaNotificationControllerInfo!!,
ImmutableList.of(customLayoutCommandButtons[0]),
)
return Futures.immediateFuture(SessionResult(SessionResult.RESULT_SUCCESS))
}
return Futures.immediateFuture(SessionResult(SessionResult.RESULT_ERROR_NOT_SUPPORTED))
}
override fun onGetLibraryRoot(
session: MediaLibraryService.MediaLibrarySession,
browser: MediaSession.ControllerInfo,
params: MediaLibraryService.LibraryParams?,
): ListenableFuture<LibraryResult<MediaItem>> {
return Futures.immediateFuture(LibraryResult.ofItem(MediaItemTree.getRootItem(), params))
}
override fun onGetItem(
session: MediaLibraryService.MediaLibrarySession,
browser: MediaSession.ControllerInfo,
mediaId: String,
): ListenableFuture<LibraryResult<MediaItem>> {
MediaItemTree.getItem(mediaId)?.let {
return Futures.immediateFuture(LibraryResult.ofItem(it, /* params= */ null))
}
return Futures.immediateFuture(LibraryResult.ofError(LibraryResult.RESULT_ERROR_BAD_VALUE))
}
override fun onGetChildren(
session: MediaLibraryService.MediaLibrarySession,
browser: MediaSession.ControllerInfo,
parentId: String,
page: Int,
pageSize: Int,
params: MediaLibraryService.LibraryParams?,
): ListenableFuture<LibraryResult<ImmutableList<MediaItem>>> {
val children = MediaItemTree.getChildren(parentId)
if (children.isNotEmpty()) {
return Futures.immediateFuture(LibraryResult.ofItemList(children, params))
}
return Futures.immediateFuture(LibraryResult.ofError(LibraryResult.RESULT_ERROR_BAD_VALUE))
}
override fun onAddMediaItems(
mediaSession: MediaSession,
controller: MediaSession.ControllerInfo,
mediaItems: List<MediaItem>,
): ListenableFuture<List<MediaItem>> {
return Futures.immediateFuture(resolveMediaItems(mediaItems))
}
@OptIn(UnstableApi::class) // MediaSession.MediaItemsWithStartPosition
override fun onSetMediaItems(
mediaSession: MediaSession,
browser: MediaSession.ControllerInfo,
mediaItems: List<MediaItem>,
startIndex: Int,
startPositionMs: Long,
): ListenableFuture<MediaItemsWithStartPosition> {
if (mediaItems.size == 1) {
// Try to expand a single item to a playlist.
maybeExpandSingleItemToPlaylist(mediaItems.first(), startIndex, startPositionMs)?.also {
return Futures.immediateFuture(it)
}
}
return Futures.immediateFuture(
MediaItemsWithStartPosition(resolveMediaItems(mediaItems), startIndex, startPositionMs)
)
}
private fun resolveMediaItems(mediaItems: List<MediaItem>): List<MediaItem> {
val playlist = mutableListOf<MediaItem>()
mediaItems.forEach { mediaItem ->
if (mediaItem.mediaId.isNotEmpty()) {
MediaItemTree.expandItem(mediaItem)?.let { playlist.add(it) }
} else if (mediaItem.requestMetadata.searchQuery != null) {
playlist.addAll(MediaItemTree.search(mediaItem.requestMetadata.searchQuery!!))
}
}
return playlist
}
@OptIn(UnstableApi::class) // MediaSession.MediaItemsWithStartPosition
private fun maybeExpandSingleItemToPlaylist(
mediaItem: MediaItem,
startIndex: Int,
startPositionMs: Long,
): MediaItemsWithStartPosition? {
var playlist = listOf<MediaItem>()
var indexInPlaylist = startIndex
MediaItemTree.getItem(mediaItem.mediaId)?.apply {
if (mediaMetadata.isBrowsable == true) {
// Get children browsable item.
playlist = MediaItemTree.getChildren(mediaId)
} else if (requestMetadata.searchQuery == null) {
// Try to get the parent and its children.
MediaItemTree.getParentId(mediaId)?.let {
playlist =
MediaItemTree.getChildren(it).map { mediaItem ->
if (mediaItem.mediaId == mediaId) MediaItemTree.expandItem(mediaItem)!! else mediaItem
}
indexInPlaylist = MediaItemTree.getIndexInMediaItems(mediaId, playlist)
}
}
}
if (playlist.isNotEmpty()) {
return MediaItemsWithStartPosition(playlist, indexInPlaylist, startPositionMs)
}
return null
}
override fun onSearch(
session: MediaLibraryService.MediaLibrarySession,
browser: MediaSession.ControllerInfo,
query: String,
params: MediaLibraryService.LibraryParams?,
): ListenableFuture<LibraryResult<Void>> {
session.notifySearchResultChanged(browser, query, MediaItemTree.search(query).size, params)
return Futures.immediateFuture(LibraryResult.ofVoid())
}
override fun onGetSearchResult(
session: MediaLibraryService.MediaLibrarySession,
browser: MediaSession.ControllerInfo,
query: String,
page: Int,
pageSize: Int,
params: MediaLibraryService.LibraryParams?,
): ListenableFuture<LibraryResult<ImmutableList<MediaItem>>> {
return Futures.immediateFuture(LibraryResult.ofItemList(MediaItemTree.search(query), params))
}
companion object {
private const val CUSTOM_COMMAND_TOGGLE_SHUFFLE_MODE_ON =
"android.media3.session.demo.SHUFFLE_ON"
private const val CUSTOM_COMMAND_TOGGLE_SHUFFLE_MODE_OFF =
"android.media3.session.demo.SHUFFLE_OFF"
}
}

View File

@ -0,0 +1,173 @@
/*
* Copyright 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.media3.demo.session
import android.Manifest
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.content.Intent
import android.content.pm.PackageManager
import android.os.Build
import androidx.annotation.OptIn
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import androidx.media3.common.AudioAttributes
import androidx.media3.common.util.UnstableApi
import androidx.media3.demo.session.service.R
import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.exoplayer.util.EventLogger
import androidx.media3.session.MediaLibraryService
import androidx.media3.session.MediaSession
import androidx.media3.session.MediaSession.ControllerInfo
open class DemoPlaybackService : MediaLibraryService() {
private lateinit var mediaLibrarySession: MediaLibrarySession
companion object {
private const val NOTIFICATION_ID = 123
private const val CHANNEL_ID = "demo_session_notification_channel_id"
}
/**
* Returns the single top session activity. It is used by the notification when the app task is
* active and an activity is in the fore or background.
*
* Tapping the notification then typically should trigger a single top activity. This way, the
* user navigates to the previous activity when pressing back.
*
* If null is returned, [MediaSession.setSessionActivity] is not set by the demo service.
*/
open fun getSingleTopActivity(): PendingIntent? = null
/**
* Returns a back stacked session activity that is used by the notification when the service is
* running standalone as a foreground service. This is typically the case after the app has been
* dismissed from the recent tasks, or after automatic playback resumption.
*
* Typically, a playback activity should be started with a stack of activities underneath. This
* way, when pressing back, the user doesn't land on the home screen of the device, but on an
* activity defined in the back stack.
*
* See [androidx.core.app.TaskStackBuilder] to construct a back stack.
*
* If null is returned, [MediaSession.setSessionActivity] is not set by the demo service.
*/
open fun getBackStackedActivity(): PendingIntent? = null
/**
* Creates the library session callback to implement the domain logic. Can be overridden to return
* an alternative callback, for example a subclass of [DemoMediaLibrarySessionCallback].
*
* This method is called when the session is built by the [DemoPlaybackService].
*/
protected open fun createLibrarySessionCallback(): MediaLibrarySession.Callback {
return DemoMediaLibrarySessionCallback(this)
}
@OptIn(UnstableApi::class) // MediaSessionService.setListener
override fun onCreate() {
super.onCreate()
initializeSessionAndPlayer()
setListener(MediaSessionServiceListener())
}
override fun onGetSession(controllerInfo: ControllerInfo): MediaLibrarySession {
return mediaLibrarySession
}
override fun onTaskRemoved(rootIntent: Intent?) {
val player = mediaLibrarySession.player
if (!player.playWhenReady || player.mediaItemCount == 0) {
stopSelf()
}
}
// MediaSession.setSessionActivity
// MediaSessionService.clearListener
@OptIn(UnstableApi::class)
override fun onDestroy() {
getBackStackedActivity()?.let { mediaLibrarySession.setSessionActivity(it) }
mediaLibrarySession.release()
mediaLibrarySession.player.release()
clearListener()
super.onDestroy()
}
private fun initializeSessionAndPlayer() {
val player =
ExoPlayer.Builder(this)
.setAudioAttributes(AudioAttributes.DEFAULT, /* handleAudioFocus= */ true)
.build()
player.addAnalyticsListener(EventLogger())
mediaLibrarySession =
MediaLibrarySession.Builder(this, player, createLibrarySessionCallback())
.also { builder -> getSingleTopActivity()?.let { builder.setSessionActivity(it) } }
.build()
}
@OptIn(UnstableApi::class) // MediaSessionService.Listener
private inner class MediaSessionServiceListener : Listener {
/**
* This method is only required to be implemented on Android 12 or above when an attempt is made
* by a media controller to resume playback when the {@link MediaSessionService} is in the
* background.
*/
override fun onForegroundServiceStartNotAllowedException() {
if (
Build.VERSION.SDK_INT >= 33 &&
checkSelfPermission(Manifest.permission.POST_NOTIFICATIONS) !=
PackageManager.PERMISSION_GRANTED
) {
// Notification permission is required but not granted
return
}
val notificationManagerCompat = NotificationManagerCompat.from(this@DemoPlaybackService)
ensureNotificationChannel(notificationManagerCompat)
val builder =
NotificationCompat.Builder(this@DemoPlaybackService, CHANNEL_ID)
.setSmallIcon(R.drawable.media3_notification_small_icon)
.setContentTitle(getString(R.string.notification_content_title))
.setStyle(
NotificationCompat.BigTextStyle().bigText(getString(R.string.notification_content_text))
)
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
.setAutoCancel(true)
.also { builder -> getBackStackedActivity()?.let { builder.setContentIntent(it) } }
notificationManagerCompat.notify(NOTIFICATION_ID, builder.build())
}
}
private fun ensureNotificationChannel(notificationManagerCompat: NotificationManagerCompat) {
if (
Build.VERSION.SDK_INT < 26 ||
notificationManagerCompat.getNotificationChannel(CHANNEL_ID) != null
) {
return
}
val channel =
NotificationChannel(
CHANNEL_ID,
getString(R.string.notification_channel_name),
NotificationManager.IMPORTANCE_DEFAULT
)
notificationManagerCompat.createNotificationChannel(channel)
}
}

View File

@ -17,11 +17,14 @@ package androidx.media3.demo.session
import android.content.res.AssetManager
import android.net.Uri
import androidx.annotation.OptIn
import androidx.media3.common.MediaItem
import androidx.media3.common.MediaItem.SubtitleConfiguration
import androidx.media3.common.MediaMetadata
import androidx.media3.common.util.Util
import androidx.media3.common.util.UnstableApi
import com.google.common.collect.ImmutableList
import java.io.BufferedReader
import java.lang.StringBuilder
import org.json.JSONObject
/**
@ -47,6 +50,20 @@ object MediaItemTree {
private const val ITEM_PREFIX = "[item]"
private class MediaItemNode(val item: MediaItem) {
val searchTitle = normalizeSearchText(item.mediaMetadata.title)
val searchText =
StringBuilder()
.append(searchTitle)
.append(" ")
.append(normalizeSearchText(item.mediaMetadata.subtitle))
.append(" ")
.append(normalizeSearchText(item.mediaMetadata.artist))
.append(" ")
.append(normalizeSearchText(item.mediaMetadata.albumArtist))
.append(" ")
.append(normalizeSearchText(item.mediaMetadata.albumTitle))
.toString()
private val children: MutableList<MediaItem> = ArrayList()
fun addChild(childID: String) {
@ -91,10 +108,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
@ -211,7 +226,12 @@ object MediaItemTree {
isPlayable = true,
isBrowsable = true,
mediaType = MediaMetadata.MEDIA_TYPE_ALBUM,
subtitleConfigurations
subtitleConfigurations,
album = null,
artist = null,
genre = null,
sourceUri = null,
imageUri
)
)
treeNodes[ALBUM_ID]!!.addChild(albumFolderIdInTree)
@ -257,24 +277,89 @@ object MediaItemTree {
return treeNodes[id]?.item
}
fun expandItem(item: MediaItem): MediaItem? {
val treeItem = getItem(item.mediaId) ?: return null
@OptIn(UnstableApi::class) // MediaMetadata.populate
val metadata = treeItem.mediaMetadata.buildUpon().populate(item.mediaMetadata).build()
return item
.buildUpon()
.setMediaMetadata(metadata)
.setSubtitleConfigurations(treeItem.localConfiguration?.subtitleConfigurations ?: listOf())
.setUri(treeItem.localConfiguration?.uri)
.build()
}
/**
* Returns the media ID of the parent of the given media ID, or null if the media ID wasn't found.
*
* @param mediaId The media ID of which to search the parent.
* @Param parentId The media ID of the media item to start the search from, or undefined to search
* from the top most node.
*/
fun getParentId(mediaId: String, parentId: String = ROOT_ID): String? {
for (child in treeNodes[parentId]!!.getChildren()) {
if (child.mediaId == mediaId) {
return parentId
} else if (child.mediaMetadata.isBrowsable == true) {
val nextParentId = getParentId(mediaId, child.mediaId)
if (nextParentId != null) {
return nextParentId
}
}
}
return null
}
/**
* Returns the index of the [MediaItem] with the give media ID in the given list of items. If the
* media ID wasn't found, 0 (zero) is returned.
*/
fun getIndexInMediaItems(mediaId: String, mediaItems: List<MediaItem>): Int {
for ((index, child) in mediaItems.withIndex()) {
if (child.mediaId == mediaId) {
return index
}
}
return 0
}
/**
* Tokenizes the query into a list of words with at least two letters and searches in the search
* text of the [MediaItemNode].
*/
fun search(query: String): List<MediaItem> {
val matches: MutableList<MediaItem> = mutableListOf()
val titleMatches: MutableList<MediaItem> = mutableListOf()
val words = query.split(" ").map { it.trim().lowercase() }.filter { it.length > 1 }
titleMap.keys.forEach { title ->
val mediaItemNode = titleMap[title]!!
for (word in words) {
if (mediaItemNode.searchText.contains(word)) {
if (mediaItemNode.searchTitle.contains(query.lowercase())) {
titleMatches.add(mediaItemNode.item)
} else {
matches.add(mediaItemNode.item)
}
break
}
}
}
titleMatches.addAll(matches)
return titleMatches
}
fun getRootItem(): MediaItem {
return treeNodes[ROOT_ID]!!.item
}
fun getChildren(id: String): List<MediaItem>? {
return treeNodes[id]?.getChildren()
fun getChildren(id: String): List<MediaItem> {
return treeNodes[id]?.getChildren() ?: listOf()
}
fun getRandomItem(): MediaItem {
var curRoot = getRootItem()
while (curRoot.mediaMetadata.isBrowsable == true) {
val children = getChildren(curRoot.mediaId)!!
curRoot = children.random()
private fun normalizeSearchText(text: CharSequence?): String {
if (text.isNullOrEmpty() || text.trim().length == 1) {
return ""
}
return curRoot
}
fun getItemFromTitle(title: String): MediaItem? {
return titleMap[title]?.item
return "$text".trim().lowercase()
}
}

View File

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 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.
-->
<resources>
<string name="notification_content_title">Playback cannot be resumed</string>
<string name="notification_content_text">Press on the play button on the media notification if it
is still present, otherwise please open the app to start the playback and re-connect the session
to the controller</string>
<string name="notification_channel_name">Playback cannot be resumed</string>
</resources>

View File

@ -0,0 +1,6 @@
# Short form content demo
This app demonstrates usage of ExoPlayer in common short form content UI setups.
See the [demos README](../README.md) for instructions on how to build and run
this demo.

View File

@ -0,0 +1,96 @@
// 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.
apply from: '../../constants.gradle'
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
android {
namespace 'androidx.media3.demo.shortform'
compileSdkVersion project.ext.compileSdkVersion
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = '1.8'
}
defaultConfig {
versionName project.ext.releaseVersion
versionCode project.ext.releaseVersionCode
minSdkVersion project.ext.minSdkVersion
targetSdkVersion project.ext.appTargetSdkVersion
multiDexEnabled true
}
buildTypes {
release {
shrinkResources true
minifyEnabled true
proguardFiles = [
'proguard-rules.txt',
getDefaultProguardFile('proguard-android-optimize.txt')
]
signingConfig signingConfigs.debug
}
debug {
jniDebuggable = true
}
}
lintOptions {
// The demo app isn't indexed, and doesn't have translations.
disable 'GoogleAppIndexingWarning','MissingTranslation'
}
buildFeatures {
viewBinding true
}
sourceSets {
main {
java {
srcDirs 'src/main/java'
}
}
test {
java {
srcDirs 'src/test/java'
}
}
}
}
dependencies {
implementation 'androidx.appcompat:appcompat:1.6.1'
implementation 'com.google.android.material:material:1.9.0'
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
implementation 'androidx.core:core-ktx:' + androidxCoreVersion
implementation 'androidx.appcompat:appcompat:' + androidxAppCompatVersion
implementation 'androidx.multidex:multidex:' + androidxMultidexVersion
implementation 'com.google.android.material:material:' + androidxMaterialVersion
implementation project(modulePrefix + 'lib-exoplayer')
implementation project(modulePrefix + 'lib-exoplayer-dash')
implementation project(modulePrefix + 'lib-exoplayer-hls')
implementation project(modulePrefix + 'lib-ui')
testImplementation 'androidx.test:core:' + androidxTestCoreVersion
testImplementation 'androidx.test.ext:junit:' + androidxTestJUnitVersion
testImplementation 'junit:junit:' + junitVersion
testImplementation 'com.google.truth:truth:' + truthVersion
testImplementation 'org.robolectric:robolectric:' + robolectricVersion
testImplementation 'org.robolectric:robolectric:' + robolectricVersion
}

View File

@ -0,0 +1,2 @@
# Proguard rules specific to the media3 short form content demo app.

View File

@ -0,0 +1,46 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 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.
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="androidx.media3.demo.shortform">
<application
android:allowBackup="false"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:name="androidx.multidex.MultiDexApplication"
android:theme="@style/Theme.MaterialComponents.DayNight.NoActionBar"
android:taskAffinity=""
tools:replace="android:name">
<activity
android:exported="true"
android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity
android:exported="false"
android:label="@string/title_activity_view_pager"
android:name=".viewpager.ViewPagerActivity"/>
</application>
<uses-permission android:name="android.permission.INTERNET" />
<uses-sdk />
</manifest>

View File

@ -0,0 +1,103 @@
/*
* 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.demo.shortform
import android.content.Intent
import android.os.Bundle
import android.text.Editable
import android.text.TextWatcher
import android.view.View
import android.widget.EditText
import androidx.appcompat.app.AppCompatActivity
import androidx.media3.common.util.UnstableApi
import androidx.media3.demo.shortform.viewpager.ViewPagerActivity
import java.lang.Integer.max
import java.lang.Integer.min
class MainActivity : AppCompatActivity() {
@androidx.annotation.OptIn(UnstableApi::class)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
var numberOfPlayers = 3
val numPlayersFieldView = findViewById<EditText>(R.id.num_players_field)
numPlayersFieldView.addTextChangedListener(
object : TextWatcher {
override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) = Unit
override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) = Unit
override fun afterTextChanged(s: Editable) {
val newText = numPlayersFieldView.text.toString()
if (newText != "") {
numberOfPlayers = max(1, min(newText.toInt(), 5))
}
}
}
)
var mediaItemsBackwardCacheSize = 2
val mediaItemsBCacheSizeView = findViewById<EditText>(R.id.media_items_b_cache_size)
mediaItemsBCacheSizeView.addTextChangedListener(
object : TextWatcher {
override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) = Unit
override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) = Unit
override fun afterTextChanged(s: Editable) {
val newText = mediaItemsBCacheSizeView.text.toString()
if (newText != "") {
mediaItemsBackwardCacheSize = max(1, min(newText.toInt(), 20))
}
}
}
)
var mediaItemsForwardCacheSize = 3
val mediaItemsFCacheSizeView = findViewById<EditText>(R.id.media_items_f_cache_size)
mediaItemsFCacheSizeView.addTextChangedListener(
object : TextWatcher {
override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) = Unit
override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) = Unit
override fun afterTextChanged(s: Editable) {
val newText = mediaItemsFCacheSizeView.text.toString()
if (newText != "") {
mediaItemsForwardCacheSize = max(1, min(newText.toInt(), 20))
}
}
}
)
findViewById<View>(R.id.view_pager_button).setOnClickListener {
startActivity(
Intent(this, ViewPagerActivity::class.java)
.putExtra(NUM_PLAYERS_EXTRA, numberOfPlayers)
.putExtra(MEDIA_ITEMS_BACKWARD_CACHE_SIZE, mediaItemsBackwardCacheSize)
.putExtra(MEDIA_ITEMS_FORWARD_CACHE_SIZE, mediaItemsForwardCacheSize)
)
}
}
companion object {
const val MEDIA_ITEMS_BACKWARD_CACHE_SIZE = "media_items_backward_cache_size"
const val MEDIA_ITEMS_FORWARD_CACHE_SIZE = "media_items_forward_cache_size"
const val NUM_PLAYERS_EXTRA = "number_of_players"
}
}

View File

@ -0,0 +1,66 @@
/*
* 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.demo.shortform
import androidx.media3.common.MediaItem
import androidx.media3.common.util.Log
import androidx.media3.common.util.UnstableApi
@UnstableApi
class MediaItemDatabase() {
var lCacheSize: Int = 2
var rCacheSize: Int = 7
private val mediaItems =
mutableListOf(
MediaItem.fromUri("https://storage.googleapis.com/exoplayer-test-media-0/shortform_1.mp4"),
MediaItem.fromUri("https://storage.googleapis.com/exoplayer-test-media-0/shortform_2.mp4"),
MediaItem.fromUri("https://storage.googleapis.com/exoplayer-test-media-0/shortform_3.mp4"),
MediaItem.fromUri("https://storage.googleapis.com/exoplayer-test-media-0/shortform_4.mp4"),
MediaItem.fromUri("https://storage.googleapis.com/exoplayer-test-media-0/shortform_6.mp4")
)
// Effective sliding window of size = lCacheSize + 1 + rCacheSize
private val slidingWindowCache = HashMap<Int, MediaItem>()
private fun getRaw(index: Int): MediaItem {
return mediaItems[index.mod(mediaItems.size)]
}
private fun getCached(index: Int): MediaItem {
var mediaItem = slidingWindowCache[index]
if (mediaItem == null) {
mediaItem = getRaw(index)
slidingWindowCache[index] = mediaItem
Log.d("viewpager", "Put URL ${mediaItem.localConfiguration?.uri} into sliding cache")
slidingWindowCache.remove(index - lCacheSize - 1)
slidingWindowCache.remove(index + rCacheSize + 1)
}
return mediaItem
}
fun get(index: Int): MediaItem {
return getCached(index)
}
fun get(fromIndex: Int, toIndex: Int): List<MediaItem> {
val result: MutableList<MediaItem> = mutableListOf()
for (i in fromIndex..toIndex) {
result.add(get(i))
}
return result
}
}

View File

@ -0,0 +1,139 @@
/*
* 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.demo.shortform
import android.os.Handler
import android.os.HandlerThread
import android.os.Looper
import android.os.Process
import androidx.media3.common.MediaItem
import androidx.media3.common.Metadata
import androidx.media3.common.text.CueGroup
import androidx.media3.common.util.UnstableApi
import androidx.media3.common.util.Util
import androidx.media3.exoplayer.RendererCapabilities
import androidx.media3.exoplayer.RenderersFactory
import androidx.media3.exoplayer.analytics.PlayerId
import androidx.media3.exoplayer.audio.AudioRendererEventListener
import androidx.media3.exoplayer.source.MediaSource
import androidx.media3.exoplayer.source.preload.PreloadMediaSource
import androidx.media3.exoplayer.trackselection.TrackSelector
import androidx.media3.exoplayer.upstream.Allocator
import androidx.media3.exoplayer.upstream.BandwidthMeter
import androidx.media3.exoplayer.video.VideoRendererEventListener
@UnstableApi
class MediaSourceManager(
mediaSourceFactory: MediaSource.Factory,
preloadLooper: Looper,
allocator: Allocator,
renderersFactory: RenderersFactory,
trackSelector: TrackSelector,
bandwidthMeter: BandwidthMeter,
) {
private val mediaSourcesThread = HandlerThread("playback-thread", Process.THREAD_PRIORITY_AUDIO)
private var handler: Handler
private var sourceMap: MutableMap<MediaItem, PreloadMediaSource> = HashMap()
private var preloadMediaSourceFactory: PreloadMediaSource.Factory
init {
mediaSourcesThread.start()
handler = Handler(mediaSourcesThread.looper)
trackSelector.init({}, bandwidthMeter)
preloadMediaSourceFactory =
PreloadMediaSource.Factory(
mediaSourceFactory,
PreloadControlImpl(targetPreloadPositionUs = 5_000_000L),
trackSelector,
bandwidthMeter,
getRendererCapabilities(renderersFactory = renderersFactory),
allocator,
preloadLooper
)
}
fun add(mediaItem: MediaItem) {
if (!sourceMap.containsKey(mediaItem)) {
val preloadMediaSource = preloadMediaSourceFactory.createMediaSource(mediaItem)
sourceMap[mediaItem] = preloadMediaSource
handler.post { preloadMediaSource.preload(/* startPositionUs= */ 0L) }
}
}
fun addAll(mediaItems: List<MediaItem>) {
mediaItems.forEach {
if (!sourceMap.containsKey(it)) {
add(it)
}
}
}
operator fun get(mediaItem: MediaItem): PreloadMediaSource {
if (!sourceMap.containsKey(mediaItem)) {
add(mediaItem)
}
return sourceMap[mediaItem]!!
}
/** Releases the instance. The instance can't be used after being released. */
fun release() {
sourceMap.keys.forEach { sourceMap[it]!!.releasePreloadMediaSource() }
handler.removeCallbacksAndMessages(null)
mediaSourcesThread.quit()
}
@UnstableApi
private fun getRendererCapabilities(
renderersFactory: RenderersFactory
): Array<RendererCapabilities> {
val renderers =
renderersFactory.createRenderers(
Util.createHandlerForCurrentOrMainLooper(),
object : VideoRendererEventListener {},
object : AudioRendererEventListener {},
{ _: CueGroup? -> }
) { _: Metadata ->
}
val capabilities = ArrayList<RendererCapabilities>()
for (i in renderers.indices) {
capabilities.add(renderers[i].capabilities)
}
return capabilities.toTypedArray()
}
companion object {
private const val TAG = "MSManager"
}
private class PreloadControlImpl(private val targetPreloadPositionUs: Long) :
PreloadMediaSource.PreloadControl {
override fun onTimelineRefreshed(mediaSource: PreloadMediaSource): Boolean {
return true
}
override fun onPrepared(mediaSource: PreloadMediaSource): Boolean {
return true
}
override fun onContinueLoadingRequested(
mediaSource: PreloadMediaSource,
bufferedPositionUs: Long
): Boolean {
return bufferedPositionUs < targetPreloadPositionUs
}
}
}

View File

@ -0,0 +1,151 @@
/*
* 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.demo.shortform
import android.content.Context
import android.os.Handler
import android.os.Looper
import androidx.annotation.OptIn
import androidx.media3.common.Player
import androidx.media3.common.util.UnstableApi
import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.exoplayer.LoadControl
import androidx.media3.exoplayer.RenderersFactory
import androidx.media3.exoplayer.upstream.BandwidthMeter
import androidx.media3.exoplayer.util.EventLogger
import com.google.common.collect.BiMap
import com.google.common.collect.HashBiMap
import com.google.common.collect.Maps
import java.util.Collections
import java.util.LinkedList
import java.util.Queue
class PlayerPool(
private val numberOfPlayers: Int,
context: Context,
playbackLooper: Looper,
loadControl: LoadControl,
renderersFactory: RenderersFactory,
bandwidthMeter: BandwidthMeter
) {
/** Creates a player instance to be used by the pool. */
interface PlayerFactory {
/** Creates an [ExoPlayer] instance. */
fun createPlayer(): ExoPlayer
}
private val availablePlayerQueue: Queue<Int> = LinkedList()
private val playerMap: BiMap<Int, ExoPlayer> = Maps.synchronizedBiMap(HashBiMap.create())
private val playerRequestTokenSet: MutableSet<Int> = Collections.synchronizedSet(HashSet<Int>())
private val playerFactory: PlayerFactory =
DefaultPlayerFactory(context, playbackLooper, loadControl, renderersFactory, bandwidthMeter)
fun acquirePlayer(token: Int, callback: (ExoPlayer) -> Unit) {
synchronized(playerMap) {
if (playerMap.size < numberOfPlayers) {
val player = playerFactory.createPlayer()
playerMap[playerMap.size] = player
callback.invoke(player)
return
}
// Add token to set of views requesting players
playerRequestTokenSet.add(token)
acquirePlayerInternal(token, callback)
}
}
private fun acquirePlayerInternal(token: Int, callback: (ExoPlayer) -> Unit) {
synchronized(playerMap) {
if (!availablePlayerQueue.isEmpty()) {
val playerNumber = availablePlayerQueue.remove()
playerMap[playerNumber]?.let { callback.invoke(it) }
playerRequestTokenSet.remove(token)
return
} else if (playerRequestTokenSet.contains(token)) {
Handler(Looper.getMainLooper()).postDelayed({ acquirePlayerInternal(token, callback) }, 500)
}
}
}
/** Calls [Player.play()] for the given player and pauses all other players. */
fun play(player: Player) {
pauseAllPlayers(player)
player.play()
}
/**
* Pauses all players.
*
* @param keepOngoingPlayer The optional player that should keep playing if not paused.
*/
fun pauseAllPlayers(keepOngoingPlayer: Player? = null) {
playerMap.values.forEach {
if (it != keepOngoingPlayer) {
it.pause()
}
}
}
fun releasePlayer(token: Int, player: ExoPlayer?) {
synchronized(playerMap) {
// Remove token from set of views requesting players & remove potential callbacks
// trying to grab the player
playerRequestTokenSet.remove(token)
// Stop the player and release into the pool for reusing, do not player.release()
player?.stop()
player?.clearMediaItems()
if (player != null) {
val playerNumber = playerMap.inverse()[player]
availablePlayerQueue.add(playerNumber)
}
}
}
fun destroyPlayers() {
synchronized(playerMap) {
for (i in 0 until playerMap.size) {
playerMap[i]?.release()
playerMap.remove(i)
}
}
}
@OptIn(UnstableApi::class)
private class DefaultPlayerFactory(
private val context: Context,
private val playbackLooper: Looper,
private val loadControl: LoadControl,
private val renderersFactory: RenderersFactory,
private val bandwidthMeter: BandwidthMeter
) : PlayerFactory {
private var playerCounter = 0
override fun createPlayer(): ExoPlayer {
val player =
ExoPlayer.Builder(context)
.setPlaybackLooper(playbackLooper)
.setLoadControl(loadControl)
.setRenderersFactory(renderersFactory)
.setBandwidthMeter(bandwidthMeter)
.build()
player.addAnalyticsListener(EventLogger("player-$playerCounter"))
playerCounter++
player.repeatMode = ExoPlayer.REPEAT_MODE_ONE
return player
}
}
}

View File

@ -0,0 +1,66 @@
/*
* 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.demo.shortform.viewpager
import android.os.Bundle
import android.util.Log
import androidx.appcompat.app.AppCompatActivity
import androidx.media3.common.util.UnstableApi
import androidx.media3.demo.shortform.MainActivity
import androidx.media3.demo.shortform.MediaItemDatabase
import androidx.media3.demo.shortform.R
import androidx.viewpager2.widget.ViewPager2
@UnstableApi
class ViewPagerActivity : AppCompatActivity() {
private lateinit var viewPagerView: ViewPager2
private lateinit var adapter: ViewPagerMediaAdapter
private var numberOfPlayers = 3
private var mediaItemDatabase = MediaItemDatabase()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_view_pager)
numberOfPlayers = intent.getIntExtra(MainActivity.NUM_PLAYERS_EXTRA, numberOfPlayers)
mediaItemDatabase.lCacheSize =
intent.getIntExtra(MainActivity.MEDIA_ITEMS_BACKWARD_CACHE_SIZE, mediaItemDatabase.lCacheSize)
mediaItemDatabase.rCacheSize =
intent.getIntExtra(MainActivity.MEDIA_ITEMS_FORWARD_CACHE_SIZE, mediaItemDatabase.rCacheSize)
Log.d("viewpager", "Using a pool of $numberOfPlayers players")
Log.d("viewpager", "Backward cache is of size: ${mediaItemDatabase.lCacheSize}")
Log.d("viewpager", "Forward cache is of size: ${mediaItemDatabase.rCacheSize}")
viewPagerView = findViewById(R.id.viewPager)
viewPagerView.offscreenPageLimit = 1
viewPagerView.registerOnPageChangeCallback(
object : ViewPager2.OnPageChangeCallback() {
override fun onPageSelected(position: Int) {
adapter.play(position)
}
}
)
}
override fun onStart() {
super.onStart()
adapter = ViewPagerMediaAdapter(mediaItemDatabase, numberOfPlayers, this)
viewPagerView.adapter = adapter
}
override fun onStop() {
adapter.onDestroy()
super.onStop()
}
}

View File

@ -0,0 +1,151 @@
/*
* 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.demo.shortform.viewpager
import android.content.Context
import android.os.HandlerThread
import android.os.Process
import android.util.Log
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.media3.common.util.UnstableApi
import androidx.media3.datasource.DefaultDataSource
import androidx.media3.demo.shortform.MediaItemDatabase
import androidx.media3.demo.shortform.MediaSourceManager
import androidx.media3.demo.shortform.PlayerPool
import androidx.media3.demo.shortform.R
import androidx.media3.exoplayer.DefaultLoadControl
import androidx.media3.exoplayer.DefaultRenderersFactory
import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.exoplayer.source.DefaultMediaSourceFactory
import androidx.media3.exoplayer.trackselection.DefaultTrackSelector
import androidx.media3.exoplayer.upstream.DefaultBandwidthMeter
import androidx.media3.exoplayer.util.EventLogger
import androidx.recyclerview.widget.RecyclerView
@UnstableApi
class ViewPagerMediaAdapter(
private val mediaItemDatabase: MediaItemDatabase,
numberOfPlayers: Int,
private val context: Context
) : RecyclerView.Adapter<ViewPagerMediaHolder>() {
private val playbackThread: HandlerThread =
HandlerThread("playback-thread", Process.THREAD_PRIORITY_AUDIO)
private val mediaSourceManager: MediaSourceManager
private var viewCounter = 0
private var playerPool: PlayerPool
private val holderMap: MutableMap<Int, ViewPagerMediaHolder>
init {
playbackThread.start()
val loadControl = DefaultLoadControl()
val renderersFactory = DefaultRenderersFactory(context)
playerPool =
PlayerPool(
numberOfPlayers,
context,
playbackThread.looper,
loadControl,
renderersFactory,
DefaultBandwidthMeter.getSingletonInstance(context)
)
holderMap = mutableMapOf()
mediaSourceManager =
MediaSourceManager(
DefaultMediaSourceFactory(DefaultDataSource.Factory(context)),
playbackThread.looper,
loadControl.allocator,
renderersFactory,
DefaultTrackSelector(context),
DefaultBandwidthMeter.getSingletonInstance(context)
)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewPagerMediaHolder {
Log.d("viewpager", "onCreateViewHolder: $viewCounter")
val view =
LayoutInflater.from(parent.context).inflate(R.layout.media_item_view_pager, parent, false)
val holder = ViewPagerMediaHolder(view, viewCounter++, playerPool)
view.addOnAttachStateChangeListener(holder)
return holder
}
override fun onBindViewHolder(holder: ViewPagerMediaHolder, position: Int) {
// TODO could give more information to the database about which item to supply
// e.g. based on how long the previous item was in view (i.e. "popularity" of content)
// need to measure how long it's been since the last onBindViewHolder call
val mediaItem = mediaItemDatabase.get(position)
Log.d("viewpager", "onBindViewHolder: Getting item at position $position")
holder.bindData(position, mediaSourceManager[mediaItem])
// We are moving to <position>, so should prepare the next couple of items
// Potentially most of those are already cached on the database side because of the sliding
// window and we would only require one more item at index=mediaItemHorizon
val mediaItemHorizon = position + mediaItemDatabase.rCacheSize
val reachableMediaItems =
mediaItemDatabase.get(fromIndex = position + 1, toIndex = mediaItemHorizon)
// Same as with the data retrieval, most items will have been converted to MediaSources and
// prepared already, but not on the first swipe
mediaSourceManager.addAll(reachableMediaItems)
}
override fun onViewAttachedToWindow(holder: ViewPagerMediaHolder) {
holderMap[holder.currentToken] = holder
}
override fun onViewDetachedFromWindow(holder: ViewPagerMediaHolder) {
holderMap.remove(holder.currentToken)
}
override fun getItemCount(): Int {
// Effectively infinite scroll
return Int.MAX_VALUE
}
override fun onViewRecycled(holder: ViewPagerMediaHolder) {
super.onViewRecycled(holder)
}
fun onDestroy() {
playbackThread.quit()
playerPool.destroyPlayers()
mediaSourceManager.release()
}
fun play(position: Int) {
holderMap[position]?.let { holder -> holder.player?.let { playerPool.play(it) } }
}
inner class Factory : PlayerPool.PlayerFactory {
private var playerCounter = 0
override fun createPlayer(): ExoPlayer {
val loadControl =
DefaultLoadControl.Builder()
.setBufferDurationsMs(
/* minBufferMs= */ 15_000,
/* maxBufferMs= */ 15_000,
/* bufferForPlaybackMs= */ 500,
/* bufferForPlaybackAfterRebufferMs= */ 1_000
)
.build()
val player = ExoPlayer.Builder(context).setLoadControl(loadControl).build()
player.addAnalyticsListener(EventLogger("player-$playerCounter"))
playerCounter++
player.repeatMode = ExoPlayer.REPEAT_MODE_ONE
return player
}
}
}

View File

@ -0,0 +1,109 @@
/*
* 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.demo.shortform.viewpager
import android.util.Log
import android.view.View
import androidx.annotation.OptIn
import androidx.media3.common.Player
import androidx.media3.common.util.UnstableApi
import androidx.media3.demo.shortform.PlayerPool
import androidx.media3.demo.shortform.R
import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.exoplayer.source.preload.PreloadMediaSource
import androidx.media3.ui.PlayerView
import androidx.recyclerview.widget.RecyclerView
@OptIn(UnstableApi::class) // Using PreloadMediaSource.
class ViewPagerMediaHolder(
itemView: View,
private val viewCounter: Int,
private val playerPool: PlayerPool
) : RecyclerView.ViewHolder(itemView), View.OnAttachStateChangeListener {
private val playerView: PlayerView = itemView.findViewById(R.id.player_view)
private var exoPlayer: ExoPlayer? = null
private var isInView: Boolean = false
private var token: Int = -1
private lateinit var mediaSource: PreloadMediaSource
init {
// Define click listener for the ViewHolder's View
playerView.findViewById<PlayerView>(R.id.player_view).setOnClickListener {
if (it is PlayerView) {
it.player?.run { playWhenReady = !playWhenReady }
}
}
}
val currentToken: Int
get() {
return token
}
val player: Player?
get() {
return exoPlayer
}
override fun onViewAttachedToWindow(view: View) {
Log.d("viewpager", "onViewAttachedToWindow: $viewCounter")
isInView = true
if (player == null) {
playerPool.acquirePlayer(token, ::setupPlayer)
}
}
override fun onViewDetachedFromWindow(view: View) {
Log.d("viewpager", "onViewDetachedFromWindow: $viewCounter")
isInView = false
releasePlayer(exoPlayer)
// This is a hacky way of keep preloading sources that are removed from players. This does only
// work because the demo app cycles endlessly through the same 5 URIs. Preloading is still
// uncoordinated meaning it just preloading as soon as this method is called.
mediaSource.preload(0)
}
fun bindData(token: Int, mediaSource: PreloadMediaSource) {
this.mediaSource = mediaSource
this.token = token
}
fun releasePlayer(player: ExoPlayer?) {
playerPool.releasePlayer(token, player ?: exoPlayer)
this.exoPlayer = null
playerView.player = null
}
fun setupPlayer(player: ExoPlayer) {
if (!isInView) {
releasePlayer(player)
} else {
if (player != exoPlayer) {
releasePlayer(exoPlayer)
}
player.run {
repeatMode = ExoPlayer.REPEAT_MODE_ONE
setMediaSource(mediaSource)
seekTo(currentPosition)
this@ViewPagerMediaHolder.exoPlayer = player
player.prepare()
playerView.player = player
}
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

View File

@ -0,0 +1,78 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 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.
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".MainActivity">
<Button android:id="@+id/view_pager_button"
android:text="@string/open_view_pager_activity"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="12dp"
android:layout_marginStart="12dp"
android:layout_marginEnd="12dp"
android:layout_marginRight="12dp"/>
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/num_players_field"
android:layout_width="match_parent"
android:layout_height="40dp"
android:layout_marginTop="150dp"
android:layout_marginStart="12dp"
android:layout_marginEnd="12dp"
android:layout_marginLeft="12dp"
android:layout_marginRight="12dp"
android:layout_gravity="center_horizontal"
android:background="@color/purple_700"
android:gravity="center"
android:hint="@string/num_of_players"
android:inputType="numberDecimal"
android:textColorHint="@color/grey" />
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/media_items_b_cache_size"
android:layout_width="match_parent"
android:layout_height="40dp"
android:layout_marginTop="20dp"
android:layout_marginStart="12dp"
android:layout_marginEnd="12dp"
android:layout_marginLeft="12dp"
android:layout_marginRight="12dp"
android:layout_gravity="center_horizontal"
android:background="@color/purple_700"
android:gravity="center"
android:hint="@string/how_many_previous_videos_cached"
android:inputType="numberDecimal"
android:textColorHint="@color/grey" />
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/media_items_f_cache_size"
android:layout_width="match_parent"
android:layout_height="40dp"
android:layout_marginTop="20dp"
android:layout_marginStart="12dp"
android:layout_marginEnd="12dp"
android:layout_marginLeft="12dp"
android:layout_marginRight="12dp"
android:layout_gravity="center_horizontal"
android:background="@color/purple_700"
android:gravity="center"
android:hint="@string/how_many_future_videos_cached"
android:inputType="numberDecimal"
android:textColorHint="@color/grey" />
</LinearLayout>

View File

@ -0,0 +1,29 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 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.
-->
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".viewpager.ViewPagerActivity">
<androidx.viewpager2.widget.ViewPager2
android:id="@+id/viewPager"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</FrameLayout>

View File

@ -0,0 +1,32 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 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.
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".viewpager.ViewPagerActivity">
<androidx.media3.ui.PlayerView android:id="@+id/player_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:use_controller="false"
app:resize_mode="fill"
app:show_shuffle_button="true"
app:show_subtitle_button="true"/>
</LinearLayout>

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

View File

@ -0,0 +1,30 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright 2021 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<resources>
<color name="purple_200">#FFBB86FC</color>
<color name="purple_500">#FF6200EE</color>
<color name="purple_700">#FF3700B3</color>
<color name="teal_200">#FF03DAC5</color>
<color name="teal_700">#FF018786</color>
<color name="black">#FF000000</color>
<color name="white">#FFFFFFFF</color>
<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>

View File

@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?><!-- Copyright 2021 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<resources>
<string name="app_name">Media3 short-form content Demo</string>
<string name="open_view_pager_activity">Open view pager activity</string>
<string name="add_view_pager">Add view pager, please!</string>
<string name="title_activity_view_pager">ViewPager activity</string>
<string name="num_of_players">How Many Players?</string>
<string name="how_many_previous_videos_cached">How Many Previous Videos Cached</string>
<string name="how_many_future_videos_cached">How Many Future Videos Cached</string>
</resources>

View File

@ -0,0 +1,33 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright 2021 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- Base application theme. -->
<style name="Theme.Media3Demo" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
<!-- Primary brand color. -->
<item name="colorPrimary">@color/purple_500</item>
<item name="colorPrimaryVariant">@color/purple_700</item>
<item name="colorOnPrimary">@color/white</item>
<!-- Secondary brand color. -->
<item name="colorSecondary">@color/teal_200</item>
<item name="colorSecondaryVariant">@color/teal_700</item>
<item name="colorOnSecondary">@color/black</item>
<!-- Status bar color. -->
<item name="android:statusBarColor" tools:targetApi="l">
?attr/colorPrimaryVariant
</item>
<!-- Customize your theme here. -->
</style>
</resources>

View File

@ -15,6 +15,8 @@ apply from: '../../constants.gradle'
apply plugin: 'com.android.application'
android {
namespace 'androidx.media3.demo.surface'
compileSdkVersion project.ext.compileSdkVersion
compileOptions {

View File

@ -14,6 +14,9 @@
* limitations under the License.
*/
@NonNullApi
@OptIn(markerClass = UnstableApi.class)
package androidx.media3.demo.surface;
import androidx.annotation.OptIn;
import androidx.media3.common.util.NonNullApi;
import androidx.media3.common.util.UnstableApi;

View File

@ -61,6 +61,6 @@ manual steps.
(this will only appear if the AAR is present), then build and run the demo
app and select a MediaPipe-based effect.
[Transformer]: https://developer.android.com/guide/topics/media/transforming-media
[Transformer]: https://developer.android.com/media/media3/transformer
[MediaPipe]: https://google.github.io/mediapipe/
[build an AAR]: https://google.github.io/mediapipe/getting_started/android_archive_library.html

View File

@ -17,6 +17,8 @@ apply from: '../../constants.gradle'
apply plugin: 'com.android.application'
android {
namespace 'androidx.media3.demo.transformer'
compileSdkVersion project.ext.compileSdkVersion
compileOptions {
@ -75,6 +77,7 @@ dependencies {
implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion
implementation 'androidx.appcompat:appcompat:' + androidxAppCompatVersion
implementation 'androidx.constraintlayout:constraintlayout:' + androidxConstraintLayoutVersion
implementation 'androidx.recyclerview:recyclerview:' + androidxRecyclerViewVersion
implementation 'androidx.multidex:multidex:' + androidxMultidexVersion
implementation 'com.google.android.material:material:' + androidxMaterialVersion
implementation project(modulePrefix + 'lib-effect')

View File

@ -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"

View File

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

View File

@ -15,20 +15,28 @@
*/
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 static androidx.media3.transformer.Composition.HDR_MODE_EXPERIMENTAL_FORCE_INTERPRET_HDR_AS_SDR;
import static androidx.media3.transformer.Composition.HDR_MODE_KEEP_HDR;
import static androidx.media3.transformer.Composition.HDR_MODE_TONE_MAP_HDR_TO_SDR_USING_MEDIACODEC;
import static androidx.media3.transformer.Composition.HDR_MODE_TONE_MAP_HDR_TO_SDR_USING_OPEN_GL;
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 +49,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.Composition;
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 +77,11 @@ 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 PRODUCE_FRAGMENTED_MP4 = "produce_fragmented_mp4";
public static final String HDR_MODE = "hdr_mode";
public static final String AUDIO_EFFECTS_SELECTIONS = "audio_effects_selections";
public static final String VIDEO_EFFECTS_SELECTIONS = "video_effects_selections";
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 +94,39 @@ 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;
public static final int CHANNEL_MIXING_INDEX = 3;
public static final int VOLUME_SCALING_INDEX = 4;
// Color filter options.
public static final int COLOR_FILTER_GRAYSCALE = 0;
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,26 +139,41 @@ 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",
"1080p H265 video and AAC audio",
"720p H264 video and AAC audio (B-frames)",
"1080p H265 video and AAC audio (B-frames)",
"360p H264 video and AAC audio",
"360p VP8 video and Vorbis audio",
"4K H264 video and AAC audio (portrait, no B-frames)",
"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 (B-frames)",
};
private static final String[] DEMO_EFFECTS = {
private static final String[] AUDIO_EFFECTS = {
"High pitched",
"Sample rate of 48000Hz",
"Skip silence",
"Mix channels into mono",
"Scale volume to 50%"
};
private static final String[] VIDEO_EFFECTS = {
"Dizzy crop",
"Edge detector (Media Pipe)",
"Color filters",
@ -127,24 +183,45 @@ 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, @Composition.HdrMode Integer> HDR_MODE_DESCRIPTIONS =
new ImmutableMap.Builder<String, @Composition.HdrMode Integer>()
.put("Keep HDR", HDR_MODE_KEEP_HDR)
.put("MediaCodec tone-map HDR to SDR", HDR_MODE_TONE_MAP_HDR_TO_SDR_USING_MEDIACODEC)
.put("OpenGL tone-map HDR to SDR", HDR_MODE_TONE_MAP_HDR_TO_SDR_USING_OPEN_GL)
.put("Force Interpret HDR as SDR", 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 +230,13 @@ 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 CheckBox produceFragmentedMp4CheckBox;
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 +253,37 @@ 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(
checkNotNull(videoLocalFilePickerLauncher),
/* mimeTypes= */ new String[] {"image/*", "video/*", "audio/*"}));
selectedFileTextView = findViewById(R.id.selected_file_text_view);
selectedFileTextView.setText(PRESET_FILE_URI_DESCRIPTIONS[inputUriPosition]);
@ -193,11 +294,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,9 +313,12 @@ 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);
}
if (SDK_INT >= 34) {
videoMimeAdapter.add(MimeTypes.VIDEO_AV1);
}
ArrayAdapter<String> resolutionHeightAdapter =
new ArrayAdapter<>(/* context= */ this, R.layout.spinner_item);
@ -247,21 +349,24 @@ 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);
produceFragmentedMp4CheckBox = findViewById(R.id.produce_fragmented_mp4_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 +377,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 +406,7 @@ public final class ConfigurationActivity extends AppCompatActivity {
"removeAudioCheckbox",
"removeVideoCheckbox",
"flattenForSlowMotionCheckbox",
"forceAudioTrackCheckbox",
"audioMimeSpinner",
"videoMimeSpinner",
"resolutionHeightSpinner",
@ -309,17 +415,19 @@ public final class ConfigurationActivity extends AppCompatActivity {
"trimCheckBox",
"enableFallbackCheckBox",
"enableDebugPreviewCheckBox",
"enableRequestSdrToneMappingCheckBox",
"forceInterpretHdrVideoAsSdrCheckBox",
"enableHdrEditingCheckBox",
"demoEffectsSelections"
"abortSlowExportCheckBox",
"produceFragmentedMp4CheckBox",
"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 +457,12 @@ 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());
bundle.putBoolean(PRODUCE_FRAGMENTED_MP4, produceFragmentedMp4CheckBox.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 +475,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 +512,70 @@ 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(
ActivityResultLauncher<Intent> localFilePickerLauncher, String[] mimeTypes) {
String permission = SDK_INT >= 33 ? READ_MEDIA_VIDEO : READ_EXTERNAL_STORAGE;
if (ActivityCompat.checkSelfPermission(/* context= */ this, permission)
!= PackageManager.PERMISSION_GRANTED) {
onPermissionsGranted = () -> launchLocalFilePicker(localFilePickerLauncher, mimeTypes);
ActivityCompat.requestPermissions(
ConfigurationActivity.this, neededPermissions, FILE_PERMISSION_REQUEST_CODE);
/* activity= */ this, new String[] {permission}, FILE_PERMISSION_REQUEST_CODE);
} else {
launchLocalFilePicker();
launchLocalFilePicker(localFilePickerLauncher, mimeTypes);
}
}
private void launchLocalFilePicker() {
private void launchLocalFilePicker(
ActivityResultLauncher<Intent> localFilePickerLauncher, String[] mimeTypes) {
Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
intent.setType("video/*");
intent.setType("*/*");
intent.putExtra(Intent.EXTRA_MIME_TYPES, mimeTypes);
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 +588,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 +602,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 +630,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 +738,67 @@ 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(
checkNotNull(overlayLocalFilePickerLauncher),
/* mimeTypes= */ new String[] {"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 +811,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 +832,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);
}
}

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