Merge pull request #4 from androidx/main
Merge the code from androidx/media main branch
51
.github/ISSUE_TEMPLATE/bug.yml
vendored
@ -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.
|
||||
|
6
.github/ISSUE_TEMPLATE/question.md
vendored
@ -39,6 +39,6 @@ Don't forget to check ExoPlayer's supported formats and devices, if applicable
|
||||
(https://developer.android.com/guide/topics/media/exoplayer/supported-formats).
|
||||
|
||||
If there's something you don't want to post publicly, please submit the issue,
|
||||
then email the link/bug report to dev.exoplayer@gmail.com using a subject in the
|
||||
format "Issue #1234", where #1234 is your issue number (we don't reply to
|
||||
emails).
|
||||
then email the link/bug report to android-media-github@google.com using a
|
||||
subject in the format "Issue #1234", where #1234 is your issue number (we don't
|
||||
reply to emails).
|
||||
|
10
.idea/icon.svg
generated
Normal file
@ -0,0 +1,10 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" viewBox="60 60 130 130">
|
||||
<g>
|
||||
<path transform="matrix(1,0,0,-1,91.2359,110.7836)" d="M0 0C-1.459 1.259-2.67 2.872-3.493 4.807-5.848 10.342-4.026 16.923 .843 20.454 7.261 25.107 16.099 23.076 19.927 16.386 20.683 15.065 21.18 13.667 21.437 12.251 21.753 10.51 23.603 9.59 25.201 10.181L42.333 20.072-3.502 46.535C-9.567 50.037-17.147 45.66-17.147 38.657V-14.113L-.508-4.594C1.175-3.631 1.468-1.267 0 0" fill="#fcb64e"/>
|
||||
<path transform="matrix(1,0,0,-1,74.2803,124.942)" d="M0 0C-.064 0-.128-.002-.192-.004V-.005L-.101-.058Z" fill="#fcb64e"/>
|
||||
<path transform="matrix(1,0,0,-1,112.6543,151.6317)" d="M0 0C-.354-1.895-1.137-3.753-2.395-5.438-5.992-10.259-12.595-11.998-18.097-9.568-25.348-6.366-28.043 2.293-24.19 8.968-23.429 10.287-22.471 11.42-21.377 12.355-19.908 13.611-20.189 15.969-21.862 16.935L-38.566 26.579V-26.242C-38.566-33.245-30.985-37.621-24.92-34.12L20.823-7.71 4.225 1.874C2.545 2.843 .355 1.906 0 0" fill="#56a0d7"/>
|
||||
<path transform="matrix(1,0,0,-1,74.0884,124.9471)" d="M0 0V-.106L.091-.053Z" fill="#56a0d7"/>
|
||||
<path transform="matrix(1,0,0,-1,129.8726,137.40271)" d="M0 0C-1.352-.476-2.805-.736-4.321-.736-12.028-.736-18.18 5.928-17.328 13.809-16.665 19.936-11.683 24.817-5.545 25.38-3.585 25.559-1.707 25.302 .007 24.697 1.712 24.096 3.467 25.291 3.696 27.027V46.485C3.711 46.51 3.728 46.535 3.742 46.56V46.561L3.696 46.535V46.691L-13.436 36.8C-15.034 36.209-16.884 37.129-17.2 38.87-17.457 40.286-17.954 41.684-18.71 43.005-22.538 49.696-31.376 51.726-37.793 47.073-42.663 43.542-44.484 36.961-42.13 31.426-41.307 29.491-40.096 27.878-38.637 26.619-37.169 25.352-37.461 22.988-39.145 22.026L-55.784 12.506-55.873 12.456C-55.843 12.456-55.814 12.457-55.784 12.457-55.721 12.459-55.657 12.46-55.592 12.461L-55.693 12.403-55.784 12.35-39.081 2.706C-37.407 1.74-37.126-.618-38.595-1.874-39.689-2.809-40.647-3.942-41.408-5.261-45.262-11.936-42.566-20.595-35.315-23.797-29.813-26.227-23.21-24.488-19.613-19.667-18.355-17.982-17.572-16.124-17.218-14.229-16.863-12.323-14.673-11.386-12.994-12.355L3.605-21.939 3.696-21.991V-2.331C3.467-.592 1.709 .602 0 0" fill="#ae1e59"/>
|
||||
<path transform="matrix(1,0,0,-1,179.4517,117.17461)" d="M0 0-45.835 26.463V26.461C-45.835 26.461-45.836 26.462-45.837 26.463V26.333 26.332 7.172C-45.837 7.043-45.866 6.923-45.883 6.799-46.112 5.062-47.867 3.868-49.572 4.469-51.286 5.074-53.164 5.331-55.124 5.151-61.262 4.589-66.244-.292-66.907-6.42-67.759-14.3-61.607-20.964-53.9-20.964-52.384-20.964-50.931-20.704-49.579-20.228-47.87-19.626-46.112-20.82-45.883-22.559-45.866-22.683-45.837-22.803-45.837-22.932V-42.219C-45.836-42.219-45.836-42.218-45.836-42.218L-45.835-42.22 0-15.756C6.064-12.255 6.064-3.501 0 0" fill="#ef5451"/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 2.7 KiB |
@ -38,6 +38,24 @@ you made on top of `main` using
|
||||
$ git diff -U0 main... | google-java-format-diff.py -p1 -i
|
||||
```
|
||||
|
||||
### Push access to PR branches
|
||||
|
||||
Please ensure maintainers of this repository have push access to your PR branch
|
||||
by ticking the `Allow edits from maintainers` checkbox when creating the PR (or
|
||||
after it's created). See the
|
||||
[GitHub docs](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/creating-a-pull-request-from-a-fork)
|
||||
for more info. This allows us to make changes and fixes to the PR while it goes
|
||||
through internal review, and ensures we don't create an
|
||||
['evil' merge](https://git-scm.com/docs/gitglossary#Documentation/gitglossary.txt-aiddefevilmergeaevilmerge)
|
||||
when it gets merged.
|
||||
|
||||
This checkbox only appears on PRs from individual-owned forks
|
||||
(https://github.com/orgs/community/discussions/5634). If you open a PR from an
|
||||
organization-owned fork we will ask you to open a new one from an
|
||||
individual-owned fork. If this isn't possible we can still merge the PR, but it
|
||||
will result in an 'evil' merge because the changes and fixes we make during
|
||||
internal review will be part of the merge commit.
|
||||
|
||||
## Contributor license agreement
|
||||
|
||||
Contributions to any Google project must be accompanied by a Contributor
|
||||
|
78
README.md
@ -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
|
||||
|
1031
RELEASENOTES.md
437
api.txt
@ -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);
|
||||
|
14
build.gradle
@ -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'
|
||||
|
@ -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'
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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')
|
||||
|
@ -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.
|
||||
|
@ -15,6 +15,8 @@ apply from: '../../constants.gradle'
|
||||
apply plugin: 'com.android.application'
|
||||
|
||||
android {
|
||||
namespace 'androidx.media3.demo.cast'
|
||||
|
||||
compileSdkVersion project.ext.compileSdkVersion
|
||||
|
||||
compileOptions {
|
||||
|
@ -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()
|
||||
|
@ -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) {
|
||||
|
@ -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;
|
||||
|
@ -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>
|
||||
|
@ -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"/>
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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));
|
||||
|
@ -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;
|
||||
|
@ -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')
|
||||
}
|
||||
|
||||
|
@ -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"/>
|
||||
|
@ -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"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
@ -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));
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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>
|
||||
|
@ -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')
|
||||
}
|
||||
|
@ -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>
|
||||
|
||||
|
@ -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(
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
BIN
demos/session/src/main/res/drawable/artwork_placeholder.png
Normal file
After Width: | Height: | Size: 40 KiB |
@ -19,7 +19,7 @@
|
||||
android:orientation="vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@color/black"
|
||||
android:background="@color/player_background"
|
||||
tools:context=".PlayerActivity">
|
||||
|
||||
<androidx.media3.ui.AspectRatioFrameLayout
|
||||
@ -28,77 +28,48 @@
|
||||
>
|
||||
<androidx.media3.ui.PlayerView
|
||||
android:id="@+id/player_view"
|
||||
android:background="@color/player_background"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
app:use_artwork="true" />
|
||||
app:artwork_display_mode="fill"
|
||||
app:default_artwork="@drawable/artwork_placeholder"
|
||||
app:repeat_toggle_modes="one|all"
|
||||
app:show_shuffle_button="true"
|
||||
app:shutter_background_color="@color/player_background" />
|
||||
</androidx.media3.ui.AspectRatioFrameLayout>
|
||||
|
||||
|
||||
<TextView
|
||||
android:id="@+id/video_title"
|
||||
android:id="@+id/media_artist"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingLeft="10dp"
|
||||
android:paddingStart="10dp"
|
||||
android:paddingTop="10dp"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="14sp"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/media_title"
|
||||
android:ellipsize="end"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:maxLines="1"
|
||||
android:paddingLeft="10dp"
|
||||
android:paddingBottom="10dp"
|
||||
android:paddingStart="10dp"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="20sp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/video_album"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingLeft="10dp"
|
||||
android:paddingBottom="10dp"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="20sp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/video_artist"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:textColor="@color/white"
|
||||
android:paddingLeft="10dp"
|
||||
android:paddingBottom="10dp"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/video_genre"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:textColor="@color/white"
|
||||
android:paddingLeft="10dp"
|
||||
android:paddingBottom="10dp"
|
||||
android:textSize="12sp" />
|
||||
|
||||
<LinearLayout
|
||||
android:gravity="center_vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/shuffle_switch"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:contentDescription="@string/shuffle"
|
||||
android:src="@drawable/exo_styled_controls_shuffle_off"
|
||||
android:textColor="@color/white" />
|
||||
|
||||
|
||||
<ImageView
|
||||
android:layout_margin="@dimen/exo_icon_horizontal_margin"
|
||||
android:id="@+id/repeat_switch"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:src="@drawable/exo_styled_controls_repeat_off"
|
||||
android:textColor="@color/white"
|
||||
android:contentDescription="@string/repeat"
|
||||
/>
|
||||
</LinearLayout>
|
||||
<View
|
||||
android:background="@color/divider"
|
||||
android:layout_height="1dp"
|
||||
android:layout_width="match_parent" />
|
||||
|
||||
<ListView
|
||||
android:id="@+id/current_playing_list"
|
||||
android:layout_width="match_parent"
|
||||
android:divider="@drawable/divider"
|
||||
android:dividerHeight="1px"
|
||||
android:layout_height="wrap_content"/>
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
</LinearLayout>
|
||||
|
@ -25,6 +25,8 @@
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center_vertical"
|
||||
android:paddingStart="10dp"
|
||||
android:maxLines="1"
|
||||
android:ellipsize="end"
|
||||
android:textColor="@color/white"
|
||||
android:paddingEnd="10dp"
|
||||
android:minHeight="50dp" />
|
||||
@ -35,6 +37,7 @@
|
||||
android:layout_width="50dp"
|
||||
android:layout_height="match_parent"
|
||||
android:id="@+id/delete_button"
|
||||
android:backgroundTint="@color/playlist_item_foreground"
|
||||
android:background="@drawable/baseline_playlist_remove_white_48"
|
||||
/>
|
||||
|
||||
|
@ -22,4 +22,9 @@
|
||||
<color name="black">#FF000000</color>
|
||||
<color name="white">#FFFFFFFF</color>
|
||||
<color name="grey">#FF999999</color>
|
||||
<color name="background">#292929</color>
|
||||
<color name="player_background">#1c1c1c</color>
|
||||
<color name="playlist_item_background">#363434</color>
|
||||
<color name="playlist_item_foreground">#635E5E</color>
|
||||
<color name="divider">#646464</color>
|
||||
</resources>
|
||||
|
@ -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>
|
||||
|
18
demos/session/src/main/res/xml/auto_app_desc.xml
Normal 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>
|
7
demos/session_automotive/README.md
Normal 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.
|
67
demos/session_automotive/build.gradle
Normal 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')
|
||||
}
|
72
demos/session_automotive/src/main/AndroidManifest.xml
Normal 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>
|
After Width: | Height: | Size: 353 KiB |
After Width: | Height: | Size: 215 KiB |
After Width: | Height: | Size: 41 KiB |
BIN
demos/session_automotive/src/main/assets/artwork/album_road.png
Normal file
After Width: | Height: | Size: 278 KiB |
BIN
demos/session_automotive/src/main/assets/artwork/album_sea.png
Normal file
After Width: | Height: | Size: 240 KiB |
After Width: | Height: | Size: 425 KiB |
472
demos/session_automotive/src/main/assets/catalog.json
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
After Width: | Height: | Size: 3.3 KiB |
After Width: | Height: | Size: 2.1 KiB |
After Width: | Height: | Size: 4.8 KiB |
After Width: | Height: | Size: 7.3 KiB |
After Width: | Height: | Size: 10 KiB |
18
demos/session_automotive/src/main/res/values/strings.xml
Normal 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>
|
@ -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>
|
8
demos/session_service/README.md
Normal 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.
|
63
demos/session_service/build.gradle
Normal 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')
|
||||
}
|
20
demos/session_service/src/main/AndroidManifest.xml
Normal 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>
|
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
@ -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"
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
@ -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()
|
||||
}
|
||||
}
|
22
demos/session_service/src/main/res/values/strings.xml
Normal 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>
|
6
demos/shortform/README.md
Normal 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.
|
96
demos/shortform/build.gradle
Normal 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
|
||||
}
|
2
demos/shortform/proguard-rules.txt
Normal file
@ -0,0 +1,2 @@
|
||||
# Proguard rules specific to the media3 short form content demo app.
|
||||
|
46
demos/shortform/src/main/AndroidManifest.xml
Normal 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>
|
@ -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"
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
@ -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()
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
BIN
demos/shortform/src/main/res/drawable/placeholder.png
Normal file
After Width: | Height: | Size: 36 KiB |
78
demos/shortform/src/main/res/layout/activity_main.xml
Normal 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>
|
29
demos/shortform/src/main/res/layout/activity_view_pager.xml
Normal 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>
|
@ -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>
|
BIN
demos/shortform/src/main/res/mipmap-hdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 3.3 KiB |
BIN
demos/shortform/src/main/res/mipmap-mdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 2.1 KiB |
BIN
demos/shortform/src/main/res/mipmap-xhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 4.8 KiB |
BIN
demos/shortform/src/main/res/mipmap-xxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 7.3 KiB |
BIN
demos/shortform/src/main/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 10 KiB |
30
demos/shortform/src/main/res/values/colors.xml
Normal 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>
|
23
demos/shortform/src/main/res/values/strings.xml
Normal 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>
|
33
demos/shortform/src/main/res/values/themes.xml
Normal 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>
|
@ -15,6 +15,8 @@ apply from: '../../constants.gradle'
|
||||
apply plugin: 'com.android.application'
|
||||
|
||||
android {
|
||||
namespace 'androidx.media3.demo.surface'
|
||||
|
||||
compileSdkVersion project.ext.compileSdkVersion
|
||||
|
||||
compileOptions {
|
||||
|
@ -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;
|
||||
|
@ -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
|
||||
|
@ -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')
|
||||
|
@ -22,6 +22,7 @@
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
|
||||
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO"/>
|
||||
|
||||
<application
|
||||
android:allowBackup="false"
|
||||
|
@ -1,181 +0,0 @@
|
||||
/*
|
||||
* Copyright 2022 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package androidx.media3.demo.transformer;
|
||||
|
||||
import static androidx.media3.common.util.Assertions.checkArgument;
|
||||
import static androidx.media3.common.util.Assertions.checkStateNotNull;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.Matrix;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.drawable.BitmapDrawable;
|
||||
import android.opengl.GLES20;
|
||||
import android.opengl.GLUtils;
|
||||
import android.util.Pair;
|
||||
import androidx.media3.common.C;
|
||||
import androidx.media3.common.FrameProcessingException;
|
||||
import androidx.media3.common.util.GlProgram;
|
||||
import androidx.media3.common.util.GlUtil;
|
||||
import androidx.media3.effect.SingleFrameGlTextureProcessor;
|
||||
import java.io.IOException;
|
||||
import java.util.Locale;
|
||||
|
||||
/**
|
||||
* A {@link SingleFrameGlTextureProcessor} that overlays a bitmap with a logo and timer on each
|
||||
* frame.
|
||||
*
|
||||
* <p>The bitmap is drawn using an Android {@link Canvas}.
|
||||
*/
|
||||
// TODO(b/227625365): Delete this class and use a texture processor from the Transformer library,
|
||||
// once overlaying a bitmap and text is supported in Transformer.
|
||||
/* package */ final class BitmapOverlayProcessor extends SingleFrameGlTextureProcessor {
|
||||
|
||||
private static final String VERTEX_SHADER_PATH = "vertex_shader_copy_es2.glsl";
|
||||
private static final String FRAGMENT_SHADER_PATH = "fragment_shader_bitmap_overlay_es2.glsl";
|
||||
|
||||
private static final int BITMAP_WIDTH_HEIGHT = 512;
|
||||
|
||||
private final Paint paint;
|
||||
private final Bitmap overlayBitmap;
|
||||
private final Bitmap logoBitmap;
|
||||
private final Canvas overlayCanvas;
|
||||
private final GlProgram glProgram;
|
||||
|
||||
private float bitmapScaleX;
|
||||
private float bitmapScaleY;
|
||||
private int bitmapTexId;
|
||||
|
||||
/**
|
||||
* Creates a new instance.
|
||||
*
|
||||
* @param context The {@link Context}.
|
||||
* @param useHdr Whether input textures come from an HDR source. If {@code true}, colors will be
|
||||
* in linear RGB BT.2020. If {@code false}, colors will be in linear RGB BT.709.
|
||||
* @throws FrameProcessingException If a problem occurs while reading shader files.
|
||||
*/
|
||||
public BitmapOverlayProcessor(Context context, boolean useHdr) throws FrameProcessingException {
|
||||
super(useHdr);
|
||||
checkArgument(!useHdr, "BitmapOverlayProcessor does not support HDR colors.");
|
||||
paint = new Paint();
|
||||
paint.setTextSize(64);
|
||||
paint.setAntiAlias(true);
|
||||
paint.setARGB(0xFF, 0xFF, 0xFF, 0xFF);
|
||||
paint.setColor(Color.GRAY);
|
||||
overlayBitmap =
|
||||
Bitmap.createBitmap(BITMAP_WIDTH_HEIGHT, BITMAP_WIDTH_HEIGHT, Bitmap.Config.ARGB_8888);
|
||||
overlayCanvas = new Canvas(overlayBitmap);
|
||||
|
||||
try {
|
||||
logoBitmap =
|
||||
((BitmapDrawable)
|
||||
context.getPackageManager().getApplicationIcon(context.getPackageName()))
|
||||
.getBitmap();
|
||||
} catch (PackageManager.NameNotFoundException e) {
|
||||
throw new IllegalStateException(e);
|
||||
}
|
||||
try {
|
||||
bitmapTexId =
|
||||
GlUtil.createTexture(
|
||||
BITMAP_WIDTH_HEIGHT,
|
||||
BITMAP_WIDTH_HEIGHT,
|
||||
/* useHighPrecisionColorComponents= */ false);
|
||||
GLUtils.texImage2D(GLES20.GL_TEXTURE_2D, /* level= */ 0, overlayBitmap, /* border= */ 0);
|
||||
|
||||
glProgram = new GlProgram(context, VERTEX_SHADER_PATH, FRAGMENT_SHADER_PATH);
|
||||
} catch (GlUtil.GlException | IOException e) {
|
||||
throw new FrameProcessingException(e);
|
||||
}
|
||||
// Draw the frame on the entire normalized device coordinate space, from -1 to 1, for x and y.
|
||||
glProgram.setBufferAttribute(
|
||||
"aFramePosition",
|
||||
GlUtil.getNormalizedCoordinateBounds(),
|
||||
GlUtil.HOMOGENEOUS_COORDINATE_VECTOR_SIZE);
|
||||
glProgram.setSamplerTexIdUniform("uTexSampler1", bitmapTexId, /* texUnitIndex= */ 1);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Pair<Integer, Integer> configure(int inputWidth, int inputHeight) {
|
||||
if (inputWidth > inputHeight) {
|
||||
bitmapScaleX = inputWidth / (float) inputHeight;
|
||||
bitmapScaleY = 1f;
|
||||
} else {
|
||||
bitmapScaleX = 1f;
|
||||
bitmapScaleY = inputHeight / (float) inputWidth;
|
||||
}
|
||||
|
||||
glProgram.setFloatUniform("uScaleX", bitmapScaleX);
|
||||
glProgram.setFloatUniform("uScaleY", bitmapScaleY);
|
||||
|
||||
return Pair.create(inputWidth, inputHeight);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void drawFrame(int inputTexId, long presentationTimeUs) throws FrameProcessingException {
|
||||
try {
|
||||
glProgram.use();
|
||||
|
||||
// Draw to the canvas and store it in a texture.
|
||||
String text =
|
||||
String.format(Locale.US, "%.02f", presentationTimeUs / (float) C.MICROS_PER_SECOND);
|
||||
overlayBitmap.eraseColor(Color.TRANSPARENT);
|
||||
overlayCanvas.drawBitmap(checkStateNotNull(logoBitmap), /* left= */ 3, /* top= */ 378, paint);
|
||||
overlayCanvas.drawText(text, /* x= */ 160, /* y= */ 466, paint);
|
||||
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, bitmapTexId);
|
||||
GLUtils.texSubImage2D(
|
||||
GLES20.GL_TEXTURE_2D,
|
||||
/* level= */ 0,
|
||||
/* xoffset= */ 0,
|
||||
/* yoffset= */ 0,
|
||||
flipBitmapVertically(overlayBitmap));
|
||||
GlUtil.checkGlError();
|
||||
|
||||
glProgram.setSamplerTexIdUniform("uTexSampler0", inputTexId, /* texUnitIndex= */ 0);
|
||||
glProgram.bindAttributesAndUniforms();
|
||||
// The four-vertex triangle strip forms a quad.
|
||||
GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, /* first= */ 0, /* count= */ 4);
|
||||
GlUtil.checkGlError();
|
||||
} catch (GlUtil.GlException e) {
|
||||
throw new FrameProcessingException(e, presentationTimeUs);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void release() throws FrameProcessingException {
|
||||
super.release();
|
||||
try {
|
||||
glProgram.delete();
|
||||
} catch (GlUtil.GlException e) {
|
||||
throw new FrameProcessingException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private static Bitmap flipBitmapVertically(Bitmap bitmap) {
|
||||
Matrix flip = new Matrix();
|
||||
flip.postScale(1f, -1f);
|
||||
return Bitmap.createBitmap(
|
||||
bitmap,
|
||||
/* x= */ 0,
|
||||
/* y= */ 0,
|
||||
bitmap.getWidth(),
|
||||
bitmap.getHeight(),
|
||||
flip,
|
||||
/* filter= */ true);
|
||||
}
|
||||
}
|
@ -15,20 +15,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);
|
||||
}
|
||||
}
|
||||
|