Compare commits

..

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

3213 changed files with 39005 additions and 468321 deletions

View file

@ -17,16 +17,10 @@ body:
label: Version label: Version
description: What version of Media3 (or ExoPlayer) are you using? description: What version of Media3 (or ExoPlayer) are you using?
options: options:
- Media3 main branch
- Media3 pre-release (alpha, beta or RC not in this list)
- Media3 1.5.1
- Media3 1.5.0
- Media3 1.4.1
- Media3 1.4.0
- Media3 1.3.1
- Media3 1.3.0
- Media3 1.2.1 - Media3 1.2.1
- Media3 1.2.0 - Media3 1.2.0
- Media3 main branch
- Media3 pre-release (alpha, beta or RC not in this list)
- Media3 1.1.1 / ExoPlayer 2.19.1 - Media3 1.1.1 / ExoPlayer 2.19.1
- Media3 1.1.0 / ExoPlayer 2.19.0 - Media3 1.1.0 / ExoPlayer 2.19.0
- Media3 1.0.2 / ExoPlayer 2.18.7 - Media3 1.0.2 / ExoPlayer 2.18.7

41
.gitignore vendored
View file

@ -52,31 +52,30 @@ tmp
# External native builds # External native builds
.externalNativeBuild .externalNativeBuild
.cxx
# VP9 decoder extension # VP9 extension
libraries/decoder_vp9/src/main/jni/libvpx extensions/vp9/src/main/jni/libvpx
libraries/decoder_vp9/src/main/jni/libvpx_android_configs extensions/vp9/src/main/jni/libvpx_android_configs
libraries/decoder_vp9/src/main/jni/libyuv extensions/vp9/src/main/jni/libyuv
# AV1 decoder extension # AV1 extension
libraries/decoder_av1/src/main/jni/cpu_features extensions/av1/src/main/jni/cpu_features
libraries/decoder_av1/src/main/jni/libgav1 extensions/av1/src/main/jni/libgav1
# Opus decoder extension # Opus extension
libraries/decoder_opus/src/main/jni/libopus extensions/opus/src/main/jni/libopus
# FLAC decoder extension # FLAC extension
libraries/decoder_flac/src/main/jni/flac extensions/flac/src/main/jni/flac
# FFmpeg decoder extension # FFmpeg extension
libraries/decoder_ffmpeg/src/main/jni/ffmpeg extensions/ffmpeg/src/main/jni/ffmpeg
# Cronet datasource extension # Cronet extension
libraries/datasource_cronet/jniLibs/* extensions/cronet/jniLibs/*
!libraries/datasource_cronet/jniLibs/README.md !extensions/cronet/jniLibs/README.md
libraries/datasource_cronet/libs/* extensions/cronet/libs/*
!libraries/datasource_cronet/libs/README.md !extensions/cronet/libs/README.md
# MIDI decoder extension # MIDI extension
libraries/decoder_midi/lib extensions/midi/lib

View file

@ -100,6 +100,12 @@ compileOptions {
} }
``` ```
#### 3. Enable multidex
If your Gradle `minSdkVersion` is 20 or lower, you should
[enable multidex](https://developer.android.com/studio/build/multidex) in order
to prevent build errors.
### Locally ### Locally
Cloning the repository and depending on the modules locally is required when Cloning the repository and depending on the modules locally is required when
@ -110,20 +116,23 @@ First, clone the repository into a local directory:
```sh ```sh
git clone https://github.com/androidx/media.git git clone https://github.com/androidx/media.git
cd media
``` ```
Next, add the following to your project's `settings.gradle.kts` 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: `path/to/media` with the path to your local copy:
```kotlin ```kotlin
(gradle as ExtensionAware).extra["androidxMediaModulePrefix"] = "media3-" gradle.extra.apply {
set("androidxMediaModulePrefix", "media-")
}
apply(from = file("path/to/media/core_settings.gradle")) apply(from = file("path/to/media/core_settings.gradle"))
``` ```
Or in Gradle Groovy DSL `settings.gradle`: Or in Gradle Groovy DSL `settings.gradle`:
```groovy ```groovy
gradle.ext.androidxMediaModulePrefix = 'media3-' gradle.ext.androidxMediaModulePrefix = 'media-'
apply from: file("path/to/media/core_settings.gradle") apply from: file("path/to/media/core_settings.gradle")
``` ```
@ -132,37 +141,17 @@ You can depend on them from `build.gradle.kts` as you would on any other local
module, for example: module, for example:
```kotlin ```kotlin
implementation(project(":media3-lib-exoplayer")) implementation(project(":media-lib-exoplayer"))
implementation(project(":media3-lib-exoplayer-dash")) implementation(project(":media-lib-exoplayer-dash"))
implementation(project(":media3-lib-ui")) implementation(project(":media-lib-ui"))
``` ```
Or in Gradle Groovy DSL `build.gradle`: Or in Gradle Groovy DSL `build.gradle`:
```groovy ```groovy
implementation project(':media3-lib-exoplayer') implementation project(':media-lib-exoplayer')
implementation project(':media3-lib-exoplayer-dash') implementation project(':media-lib-exoplayer-dash')
implementation project(':media3-lib-ui') 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 ## Developing AndroidX Media

File diff suppressed because it is too large Load diff

67
api.txt
View file

@ -26,7 +26,7 @@ package androidx.media3.common {
} }
public final class AudioAttributes { public final class AudioAttributes {
method public androidx.media3.common.AudioAttributes.AudioAttributesV21 getAudioAttributesV21(); method @RequiresApi(21) public androidx.media3.common.AudioAttributes.AudioAttributesV21 getAudioAttributesV21();
field public static final androidx.media3.common.AudioAttributes DEFAULT; field public static final androidx.media3.common.AudioAttributes DEFAULT;
field @androidx.media3.common.C.AudioAllowedCapturePolicy public final int allowedCapturePolicy; field @androidx.media3.common.C.AudioAllowedCapturePolicy public final int allowedCapturePolicy;
field @androidx.media3.common.C.AudioContentType public final int contentType; field @androidx.media3.common.C.AudioContentType public final int contentType;
@ -35,7 +35,7 @@ package androidx.media3.common {
field @androidx.media3.common.C.AudioUsage public final int usage; field @androidx.media3.common.C.AudioUsage public final int usage;
} }
public static final class AudioAttributes.AudioAttributesV21 { @RequiresApi(21) public static final class AudioAttributes.AudioAttributesV21 {
field public final android.media.AudioAttributes audioAttributes; field public final android.media.AudioAttributes audioAttributes;
} }
@ -79,7 +79,6 @@ package androidx.media3.common {
field public static final java.util.UUID PLAYREADY_UUID; field public static final java.util.UUID PLAYREADY_UUID;
field public static final float RATE_UNSET = -3.4028235E38f; field public static final float RATE_UNSET = -3.4028235E38f;
field public static final int ROLE_FLAG_ALTERNATE = 2; // 0x2 field public static final int ROLE_FLAG_ALTERNATE = 2; // 0x2
field public static final int ROLE_FLAG_AUXILIARY = 32768; // 0x8000
field public static final int ROLE_FLAG_CAPTION = 64; // 0x40 field public static final int ROLE_FLAG_CAPTION = 64; // 0x40
field public static final int ROLE_FLAG_COMMENTARY = 8; // 0x8 field public static final int ROLE_FLAG_COMMENTARY = 8; // 0x8
field public static final int ROLE_FLAG_DESCRIBES_MUSIC_AND_SOUND = 1024; // 0x400 field public static final int ROLE_FLAG_DESCRIBES_MUSIC_AND_SOUND = 1024; // 0x400
@ -157,7 +156,7 @@ package androidx.media3.common {
@IntDef(open=true, value={androidx.media3.common.C.CRYPTO_TYPE_UNSUPPORTED, androidx.media3.common.C.CRYPTO_TYPE_NONE, androidx.media3.common.C.CRYPTO_TYPE_FRAMEWORK}) @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.CryptoType { @IntDef(open=true, value={androidx.media3.common.C.CRYPTO_TYPE_UNSUPPORTED, androidx.media3.common.C.CRYPTO_TYPE_NONE, androidx.media3.common.C.CRYPTO_TYPE_FRAMEWORK}) @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.CryptoType {
} }
@IntDef(flag=true, value={androidx.media3.common.C.ROLE_FLAG_MAIN, androidx.media3.common.C.ROLE_FLAG_ALTERNATE, androidx.media3.common.C.ROLE_FLAG_SUPPLEMENTARY, androidx.media3.common.C.ROLE_FLAG_COMMENTARY, androidx.media3.common.C.ROLE_FLAG_DUB, androidx.media3.common.C.ROLE_FLAG_EMERGENCY, androidx.media3.common.C.ROLE_FLAG_CAPTION, androidx.media3.common.C.ROLE_FLAG_SUBTITLE, androidx.media3.common.C.ROLE_FLAG_SIGN, androidx.media3.common.C.ROLE_FLAG_DESCRIBES_VIDEO, androidx.media3.common.C.ROLE_FLAG_DESCRIBES_MUSIC_AND_SOUND, androidx.media3.common.C.ROLE_FLAG_ENHANCED_DIALOG_INTELLIGIBILITY, androidx.media3.common.C.ROLE_FLAG_TRANSCRIBES_DIALOG, androidx.media3.common.C.ROLE_FLAG_EASY_TO_READ, androidx.media3.common.C.ROLE_FLAG_TRICK_PLAY, androidx.media3.common.C.ROLE_FLAG_AUXILIARY}) @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.RoleFlags { @IntDef(flag=true, value={androidx.media3.common.C.ROLE_FLAG_MAIN, androidx.media3.common.C.ROLE_FLAG_ALTERNATE, androidx.media3.common.C.ROLE_FLAG_SUPPLEMENTARY, androidx.media3.common.C.ROLE_FLAG_COMMENTARY, androidx.media3.common.C.ROLE_FLAG_DUB, androidx.media3.common.C.ROLE_FLAG_EMERGENCY, androidx.media3.common.C.ROLE_FLAG_CAPTION, androidx.media3.common.C.ROLE_FLAG_SUBTITLE, androidx.media3.common.C.ROLE_FLAG_SIGN, androidx.media3.common.C.ROLE_FLAG_DESCRIBES_VIDEO, androidx.media3.common.C.ROLE_FLAG_DESCRIBES_MUSIC_AND_SOUND, androidx.media3.common.C.ROLE_FLAG_ENHANCED_DIALOG_INTELLIGIBILITY, androidx.media3.common.C.ROLE_FLAG_TRANSCRIBES_DIALOG, androidx.media3.common.C.ROLE_FLAG_EASY_TO_READ, androidx.media3.common.C.ROLE_FLAG_TRICK_PLAY}) @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.RoleFlags {
} }
@IntDef(flag=true, value={androidx.media3.common.C.SELECTION_FLAG_DEFAULT, androidx.media3.common.C.SELECTION_FLAG_FORCED, androidx.media3.common.C.SELECTION_FLAG_AUTOSELECT}) @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.SelectionFlags { @IntDef(flag=true, value={androidx.media3.common.C.SELECTION_FLAG_DEFAULT, androidx.media3.common.C.SELECTION_FLAG_FORCED, androidx.media3.common.C.SELECTION_FLAG_AUTOSELECT}) @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.SelectionFlags {
@ -549,7 +548,6 @@ package androidx.media3.common {
field public static final String APPLICATION_PGS = "application/pgs"; field public static final String APPLICATION_PGS = "application/pgs";
field @Deprecated public static final String APPLICATION_RAWCC = "application/x-rawcc"; field @Deprecated public static final String APPLICATION_RAWCC = "application/x-rawcc";
field public static final String APPLICATION_RTSP = "application/x-rtsp"; field public static final String APPLICATION_RTSP = "application/x-rtsp";
field public static final String APPLICATION_SDP = "application/sdp";
field public static final String APPLICATION_SS = "application/vnd.ms-sstr+xml"; field public static final String APPLICATION_SS = "application/vnd.ms-sstr+xml";
field public static final String APPLICATION_SUBRIP = "application/x-subrip"; field public static final String APPLICATION_SUBRIP = "application/x-subrip";
field public static final String APPLICATION_TTML = "application/ttml+xml"; field public static final String APPLICATION_TTML = "application/ttml+xml";
@ -623,20 +621,13 @@ package androidx.media3.common {
method public static String getErrorCodeName(@androidx.media3.common.PlaybackException.ErrorCode int); 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 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_INIT_FAILED = 5001; // 0x1389
field public static final int ERROR_CODE_AUDIO_TRACK_OFFLOAD_INIT_FAILED = 5004; // 0x138c
field public static final int ERROR_CODE_AUDIO_TRACK_OFFLOAD_WRITE_FAILED = 5003; // 0x138b
field public static final int ERROR_CODE_AUDIO_TRACK_WRITE_FAILED = 5002; // 0x138a field public static final int ERROR_CODE_AUDIO_TRACK_WRITE_FAILED = 5002; // 0x138a
field public static final int ERROR_CODE_AUTHENTICATION_EXPIRED = -102; // 0xffffff9a
field public static final int ERROR_CODE_BAD_VALUE = -3; // 0xfffffffd
field public static final int ERROR_CODE_BEHIND_LIVE_WINDOW = 1002; // 0x3ea field public static final int ERROR_CODE_BEHIND_LIVE_WINDOW = 1002; // 0x3ea
field public static final int ERROR_CODE_CONCURRENT_STREAM_LIMIT = -104; // 0xffffff98
field public static final int ERROR_CODE_CONTENT_ALREADY_PLAYING = -110; // 0xffffff92
field public static final int ERROR_CODE_DECODER_INIT_FAILED = 4001; // 0xfa1 field public static final int ERROR_CODE_DECODER_INIT_FAILED = 4001; // 0xfa1
field public static final int ERROR_CODE_DECODER_QUERY_FAILED = 4002; // 0xfa2 field public static final int ERROR_CODE_DECODER_QUERY_FAILED = 4002; // 0xfa2
field public static final int ERROR_CODE_DECODING_FAILED = 4003; // 0xfa3 field public static final int ERROR_CODE_DECODING_FAILED = 4003; // 0xfa3
field public static final int ERROR_CODE_DECODING_FORMAT_EXCEEDS_CAPABILITIES = 4004; // 0xfa4 field public static final int ERROR_CODE_DECODING_FORMAT_EXCEEDS_CAPABILITIES = 4004; // 0xfa4
field public static final int ERROR_CODE_DECODING_FORMAT_UNSUPPORTED = 4005; // 0xfa5 field public static final int ERROR_CODE_DECODING_FORMAT_UNSUPPORTED = 4005; // 0xfa5
field public static final int ERROR_CODE_DISCONNECTED = -100; // 0xffffff9c
field public static final int ERROR_CODE_DRM_CONTENT_ERROR = 6003; // 0x1773 field public static final int ERROR_CODE_DRM_CONTENT_ERROR = 6003; // 0x1773
field public static final int ERROR_CODE_DRM_DEVICE_REVOKED = 6007; // 0x1777 field public static final int ERROR_CODE_DRM_DEVICE_REVOKED = 6007; // 0x1777
field public static final int ERROR_CODE_DRM_DISALLOWED_OPERATION = 6005; // 0x1775 field public static final int ERROR_CODE_DRM_DISALLOWED_OPERATION = 6005; // 0x1775
@ -646,9 +637,7 @@ package androidx.media3.common {
field public static final int ERROR_CODE_DRM_SCHEME_UNSUPPORTED = 6001; // 0x1771 field public static final int ERROR_CODE_DRM_SCHEME_UNSUPPORTED = 6001; // 0x1771
field public static final int ERROR_CODE_DRM_SYSTEM_ERROR = 6006; // 0x1776 field public static final int ERROR_CODE_DRM_SYSTEM_ERROR = 6006; // 0x1776
field public static final int ERROR_CODE_DRM_UNSPECIFIED = 6000; // 0x1770 field public static final int ERROR_CODE_DRM_UNSPECIFIED = 6000; // 0x1770
field public static final int ERROR_CODE_END_OF_PLAYLIST = -109; // 0xffffff93
field public static final int ERROR_CODE_FAILED_RUNTIME_CHECK = 1004; // 0x3ec field public static final int ERROR_CODE_FAILED_RUNTIME_CHECK = 1004; // 0x3ec
field public static final int ERROR_CODE_INVALID_STATE = -2; // 0xfffffffe
field public static final int ERROR_CODE_IO_BAD_HTTP_STATUS = 2004; // 0x7d4 field public static final int ERROR_CODE_IO_BAD_HTTP_STATUS = 2004; // 0x7d4
field public static final int ERROR_CODE_IO_CLEARTEXT_NOT_PERMITTED = 2007; // 0x7d7 field public static final int ERROR_CODE_IO_CLEARTEXT_NOT_PERMITTED = 2007; // 0x7d7
field public static final int ERROR_CODE_IO_FILE_NOT_FOUND = 2005; // 0x7d5 field public static final int ERROR_CODE_IO_FILE_NOT_FOUND = 2005; // 0x7d5
@ -658,25 +647,18 @@ package androidx.media3.common {
field public static final int ERROR_CODE_IO_NO_PERMISSION = 2006; // 0x7d6 field public static final int ERROR_CODE_IO_NO_PERMISSION = 2006; // 0x7d6
field public static final int ERROR_CODE_IO_READ_POSITION_OUT_OF_RANGE = 2008; // 0x7d8 field public static final int ERROR_CODE_IO_READ_POSITION_OUT_OF_RANGE = 2008; // 0x7d8
field public static final int ERROR_CODE_IO_UNSPECIFIED = 2000; // 0x7d0 field public static final int ERROR_CODE_IO_UNSPECIFIED = 2000; // 0x7d0
field public static final int ERROR_CODE_NOT_AVAILABLE_IN_REGION = -106; // 0xffffff96
field public static final int ERROR_CODE_NOT_SUPPORTED = -6; // 0xfffffffa
field public static final int ERROR_CODE_PARENTAL_CONTROL_RESTRICTED = -105; // 0xffffff97
field public static final int ERROR_CODE_PARSING_CONTAINER_MALFORMED = 3001; // 0xbb9 field public static final int ERROR_CODE_PARSING_CONTAINER_MALFORMED = 3001; // 0xbb9
field public static final int ERROR_CODE_PARSING_CONTAINER_UNSUPPORTED = 3003; // 0xbbb field public static final int ERROR_CODE_PARSING_CONTAINER_UNSUPPORTED = 3003; // 0xbbb
field public static final int ERROR_CODE_PARSING_MANIFEST_MALFORMED = 3002; // 0xbba field public static final int ERROR_CODE_PARSING_MANIFEST_MALFORMED = 3002; // 0xbba
field public static final int ERROR_CODE_PARSING_MANIFEST_UNSUPPORTED = 3004; // 0xbbc field public static final int ERROR_CODE_PARSING_MANIFEST_UNSUPPORTED = 3004; // 0xbbc
field public static final int ERROR_CODE_PERMISSION_DENIED = -4; // 0xfffffffc
field public static final int ERROR_CODE_PREMIUM_ACCOUNT_REQUIRED = -103; // 0xffffff99
field public static final int ERROR_CODE_REMOTE_ERROR = 1001; // 0x3e9 field public static final int ERROR_CODE_REMOTE_ERROR = 1001; // 0x3e9
field public static final int ERROR_CODE_SETUP_REQUIRED = -108; // 0xffffff94
field public static final int ERROR_CODE_SKIP_LIMIT_REACHED = -107; // 0xffffff95
field public static final int ERROR_CODE_TIMEOUT = 1003; // 0x3eb field public static final int ERROR_CODE_TIMEOUT = 1003; // 0x3eb
field public static final int ERROR_CODE_UNSPECIFIED = 1000; // 0x3e8 field public static final int ERROR_CODE_UNSPECIFIED = 1000; // 0x3e8
field @androidx.media3.common.PlaybackException.ErrorCode public final int errorCode; field @androidx.media3.common.PlaybackException.ErrorCode public final int errorCode;
field public final long timestampMs; field public final long timestampMs;
} }
@IntDef(open=true, value={androidx.media3.common.PlaybackException.ERROR_CODE_INVALID_STATE, androidx.media3.common.PlaybackException.ERROR_CODE_BAD_VALUE, androidx.media3.common.PlaybackException.ERROR_CODE_PERMISSION_DENIED, androidx.media3.common.PlaybackException.ERROR_CODE_NOT_SUPPORTED, androidx.media3.common.PlaybackException.ERROR_CODE_DISCONNECTED, androidx.media3.common.PlaybackException.ERROR_CODE_AUTHENTICATION_EXPIRED, androidx.media3.common.PlaybackException.ERROR_CODE_PREMIUM_ACCOUNT_REQUIRED, androidx.media3.common.PlaybackException.ERROR_CODE_CONCURRENT_STREAM_LIMIT, androidx.media3.common.PlaybackException.ERROR_CODE_PARENTAL_CONTROL_RESTRICTED, androidx.media3.common.PlaybackException.ERROR_CODE_NOT_AVAILABLE_IN_REGION, androidx.media3.common.PlaybackException.ERROR_CODE_SKIP_LIMIT_REACHED, androidx.media3.common.PlaybackException.ERROR_CODE_SETUP_REQUIRED, androidx.media3.common.PlaybackException.ERROR_CODE_END_OF_PLAYLIST, androidx.media3.common.PlaybackException.ERROR_CODE_CONTENT_ALREADY_PLAYING, androidx.media3.common.PlaybackException.ERROR_CODE_UNSPECIFIED, androidx.media3.common.PlaybackException.ERROR_CODE_REMOTE_ERROR, androidx.media3.common.PlaybackException.ERROR_CODE_BEHIND_LIVE_WINDOW, androidx.media3.common.PlaybackException.ERROR_CODE_TIMEOUT, androidx.media3.common.PlaybackException.ERROR_CODE_FAILED_RUNTIME_CHECK, androidx.media3.common.PlaybackException.ERROR_CODE_IO_UNSPECIFIED, androidx.media3.common.PlaybackException.ERROR_CODE_IO_NETWORK_CONNECTION_FAILED, androidx.media3.common.PlaybackException.ERROR_CODE_IO_NETWORK_CONNECTION_TIMEOUT, androidx.media3.common.PlaybackException.ERROR_CODE_IO_INVALID_HTTP_CONTENT_TYPE, androidx.media3.common.PlaybackException.ERROR_CODE_IO_BAD_HTTP_STATUS, androidx.media3.common.PlaybackException.ERROR_CODE_IO_FILE_NOT_FOUND, androidx.media3.common.PlaybackException.ERROR_CODE_IO_NO_PERMISSION, androidx.media3.common.PlaybackException.ERROR_CODE_IO_CLEARTEXT_NOT_PERMITTED, androidx.media3.common.PlaybackException.ERROR_CODE_IO_READ_POSITION_OUT_OF_RANGE, androidx.media3.common.PlaybackException.ERROR_CODE_PARSING_CONTAINER_MALFORMED, androidx.media3.common.PlaybackException.ERROR_CODE_PARSING_MANIFEST_MALFORMED, androidx.media3.common.PlaybackException.ERROR_CODE_PARSING_CONTAINER_UNSUPPORTED, androidx.media3.common.PlaybackException.ERROR_CODE_PARSING_MANIFEST_UNSUPPORTED, androidx.media3.common.PlaybackException.ERROR_CODE_DECODER_INIT_FAILED, androidx.media3.common.PlaybackException.ERROR_CODE_DECODER_QUERY_FAILED, androidx.media3.common.PlaybackException.ERROR_CODE_DECODING_FAILED, androidx.media3.common.PlaybackException.ERROR_CODE_DECODING_FORMAT_EXCEEDS_CAPABILITIES, androidx.media3.common.PlaybackException.ERROR_CODE_DECODING_FORMAT_UNSUPPORTED, androidx.media3.common.PlaybackException.ERROR_CODE_AUDIO_TRACK_INIT_FAILED, androidx.media3.common.PlaybackException.ERROR_CODE_AUDIO_TRACK_WRITE_FAILED, androidx.media3.common.PlaybackException.ERROR_CODE_AUDIO_TRACK_OFFLOAD_INIT_FAILED, androidx.media3.common.PlaybackException.ERROR_CODE_AUDIO_TRACK_OFFLOAD_WRITE_FAILED, androidx.media3.common.PlaybackException.ERROR_CODE_DRM_UNSPECIFIED, androidx.media3.common.PlaybackException.ERROR_CODE_DRM_SCHEME_UNSUPPORTED, androidx.media3.common.PlaybackException.ERROR_CODE_DRM_PROVISIONING_FAILED, androidx.media3.common.PlaybackException.ERROR_CODE_DRM_CONTENT_ERROR, androidx.media3.common.PlaybackException.ERROR_CODE_DRM_LICENSE_ACQUISITION_FAILED, androidx.media3.common.PlaybackException.ERROR_CODE_DRM_DISALLOWED_OPERATION, androidx.media3.common.PlaybackException.ERROR_CODE_DRM_SYSTEM_ERROR, androidx.media3.common.PlaybackException.ERROR_CODE_DRM_DEVICE_REVOKED, androidx.media3.common.PlaybackException.ERROR_CODE_DRM_LICENSE_EXPIRED}) @java.lang.annotation.Documented @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.SOURCE) @java.lang.annotation.Target({java.lang.annotation.ElementType.FIELD, java.lang.annotation.ElementType.METHOD, java.lang.annotation.ElementType.PARAMETER, java.lang.annotation.ElementType.LOCAL_VARIABLE, java.lang.annotation.ElementType.TYPE_USE}) public static @interface PlaybackException.ErrorCode { @IntDef(open=true, value={androidx.media3.common.PlaybackException.ERROR_CODE_UNSPECIFIED, androidx.media3.common.PlaybackException.ERROR_CODE_REMOTE_ERROR, androidx.media3.common.PlaybackException.ERROR_CODE_BEHIND_LIVE_WINDOW, androidx.media3.common.PlaybackException.ERROR_CODE_TIMEOUT, androidx.media3.common.PlaybackException.ERROR_CODE_FAILED_RUNTIME_CHECK, androidx.media3.common.PlaybackException.ERROR_CODE_IO_UNSPECIFIED, androidx.media3.common.PlaybackException.ERROR_CODE_IO_NETWORK_CONNECTION_FAILED, androidx.media3.common.PlaybackException.ERROR_CODE_IO_NETWORK_CONNECTION_TIMEOUT, androidx.media3.common.PlaybackException.ERROR_CODE_IO_INVALID_HTTP_CONTENT_TYPE, androidx.media3.common.PlaybackException.ERROR_CODE_IO_BAD_HTTP_STATUS, androidx.media3.common.PlaybackException.ERROR_CODE_IO_FILE_NOT_FOUND, androidx.media3.common.PlaybackException.ERROR_CODE_IO_NO_PERMISSION, androidx.media3.common.PlaybackException.ERROR_CODE_IO_CLEARTEXT_NOT_PERMITTED, androidx.media3.common.PlaybackException.ERROR_CODE_IO_READ_POSITION_OUT_OF_RANGE, androidx.media3.common.PlaybackException.ERROR_CODE_PARSING_CONTAINER_MALFORMED, androidx.media3.common.PlaybackException.ERROR_CODE_PARSING_MANIFEST_MALFORMED, androidx.media3.common.PlaybackException.ERROR_CODE_PARSING_CONTAINER_UNSUPPORTED, androidx.media3.common.PlaybackException.ERROR_CODE_PARSING_MANIFEST_UNSUPPORTED, androidx.media3.common.PlaybackException.ERROR_CODE_DECODER_INIT_FAILED, androidx.media3.common.PlaybackException.ERROR_CODE_DECODER_QUERY_FAILED, androidx.media3.common.PlaybackException.ERROR_CODE_DECODING_FAILED, androidx.media3.common.PlaybackException.ERROR_CODE_DECODING_FORMAT_EXCEEDS_CAPABILITIES, androidx.media3.common.PlaybackException.ERROR_CODE_DECODING_FORMAT_UNSUPPORTED, androidx.media3.common.PlaybackException.ERROR_CODE_AUDIO_TRACK_INIT_FAILED, androidx.media3.common.PlaybackException.ERROR_CODE_AUDIO_TRACK_WRITE_FAILED, androidx.media3.common.PlaybackException.ERROR_CODE_DRM_UNSPECIFIED, androidx.media3.common.PlaybackException.ERROR_CODE_DRM_SCHEME_UNSUPPORTED, androidx.media3.common.PlaybackException.ERROR_CODE_DRM_PROVISIONING_FAILED, androidx.media3.common.PlaybackException.ERROR_CODE_DRM_CONTENT_ERROR, androidx.media3.common.PlaybackException.ERROR_CODE_DRM_LICENSE_ACQUISITION_FAILED, androidx.media3.common.PlaybackException.ERROR_CODE_DRM_DISALLOWED_OPERATION, androidx.media3.common.PlaybackException.ERROR_CODE_DRM_SYSTEM_ERROR, androidx.media3.common.PlaybackException.ERROR_CODE_DRM_DEVICE_REVOKED, androidx.media3.common.PlaybackException.ERROR_CODE_DRM_LICENSE_EXPIRED}) @java.lang.annotation.Documented @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.SOURCE) @java.lang.annotation.Target({java.lang.annotation.ElementType.FIELD, java.lang.annotation.ElementType.METHOD, java.lang.annotation.ElementType.PARAMETER, java.lang.annotation.ElementType.LOCAL_VARIABLE, java.lang.annotation.ElementType.TYPE_USE}) public static @interface PlaybackException.ErrorCode {
} }
public final class PlaybackParameters { public final class PlaybackParameters {
@ -781,7 +763,7 @@ package androidx.media3.common {
method @Deprecated public void setDeviceMuted(boolean); method @Deprecated public void setDeviceMuted(boolean);
method public void setDeviceMuted(boolean, @androidx.media3.common.C.VolumeFlags int); method public void setDeviceMuted(boolean, @androidx.media3.common.C.VolumeFlags int);
method @Deprecated public void setDeviceVolume(@IntRange(from=0) 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 setDeviceVolume(@IntRange(from=0) int, int);
method public void setMediaItem(androidx.media3.common.MediaItem); method public void setMediaItem(androidx.media3.common.MediaItem);
method public void setMediaItem(androidx.media3.common.MediaItem, boolean); method public void setMediaItem(androidx.media3.common.MediaItem, boolean);
method public void setMediaItem(androidx.media3.common.MediaItem, long); method public void setMediaItem(androidx.media3.common.MediaItem, long);
@ -844,7 +826,6 @@ package androidx.media3.common {
field public static final int DISCONTINUITY_REASON_REMOVE = 4; // 0x4 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 = 1; // 0x1
field public static final int DISCONTINUITY_REASON_SEEK_ADJUSTMENT = 2; // 0x2 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 DISCONTINUITY_REASON_SKIP = 3; // 0x3
field public static final int EVENT_AUDIO_ATTRIBUTES_CHANGED = 20; // 0x14 field public static final int EVENT_AUDIO_ATTRIBUTES_CHANGED = 20; // 0x14
field public static final int EVENT_AUDIO_SESSION_ID = 21; // 0x15 field public static final int EVENT_AUDIO_SESSION_ID = 21; // 0x15
@ -913,7 +894,7 @@ package androidx.media3.common {
field public static final androidx.media3.common.Player.Commands EMPTY; 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, 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.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.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 { @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 {
@ -1094,7 +1075,7 @@ package androidx.media3.common {
method public androidx.media3.common.TrackSelectionParameters.Builder buildUpon(); method public androidx.media3.common.TrackSelectionParameters.Builder buildUpon();
method public static androidx.media3.common.TrackSelectionParameters fromBundle(android.os.Bundle); method public static androidx.media3.common.TrackSelectionParameters fromBundle(android.os.Bundle);
method public static androidx.media3.common.TrackSelectionParameters getDefaults(android.content.Context); method public static androidx.media3.common.TrackSelectionParameters getDefaults(android.content.Context);
method @CallSuper public android.os.Bundle toBundle(); method public android.os.Bundle toBundle();
field public final com.google.common.collect.ImmutableSet<java.lang.Integer> disabledTrackTypes; field public final com.google.common.collect.ImmutableSet<java.lang.Integer> disabledTrackTypes;
field public final boolean forceHighestSupportedBitrate; field public final boolean forceHighestSupportedBitrate;
field public final boolean forceLowestBitrate; field public final boolean forceLowestBitrate;
@ -1192,7 +1173,7 @@ package androidx.media3.common {
field public static final androidx.media3.common.VideoSize UNKNOWN; field public static final androidx.media3.common.VideoSize UNKNOWN;
field @IntRange(from=0) public final int height; field @IntRange(from=0) public final int height;
field @FloatRange(from=0, fromInclusive=false) public final float pixelWidthHeightRatio; field @FloatRange(from=0, fromInclusive=false) public final float pixelWidthHeightRatio;
field @Deprecated @IntRange(from=0, to=359) public final int unappliedRotationDegrees; field @IntRange(from=0, to=359) public final int unappliedRotationDegrees;
field @IntRange(from=0) public final int width; field @IntRange(from=0) public final int width;
} }
@ -1388,7 +1369,7 @@ package androidx.media3.exoplayer.analytics {
package androidx.media3.exoplayer.drm { package androidx.media3.exoplayer.drm {
public final class FrameworkMediaDrm { @RequiresApi(18) public final class FrameworkMediaDrm {
method public static boolean isCryptoSchemeSupported(java.util.UUID); method public static boolean isCryptoSchemeSupported(java.util.UUID);
} }
@ -1406,29 +1387,6 @@ package androidx.media3.exoplayer.ima {
method public androidx.media3.exoplayer.ima.ImaAdsLoader build(); method public androidx.media3.exoplayer.ima.ImaAdsLoader build();
} }
public final class ImaServerSideAdInsertionMediaSource implements androidx.media3.exoplayer.source.MediaSource {
}
public static final class ImaServerSideAdInsertionMediaSource.AdsLoader {
method public androidx.media3.exoplayer.ima.ImaServerSideAdInsertionMediaSource.AdsLoader.State release();
method public void setPlayer(androidx.media3.common.Player);
}
public static final class ImaServerSideAdInsertionMediaSource.AdsLoader.Builder {
ctor public ImaServerSideAdInsertionMediaSource.AdsLoader.Builder(android.content.Context, androidx.media3.common.AdViewProvider);
method public androidx.media3.exoplayer.ima.ImaServerSideAdInsertionMediaSource.AdsLoader build();
method public androidx.media3.exoplayer.ima.ImaServerSideAdInsertionMediaSource.AdsLoader.Builder setAdsLoaderState(androidx.media3.exoplayer.ima.ImaServerSideAdInsertionMediaSource.AdsLoader.State);
}
public static class ImaServerSideAdInsertionMediaSource.AdsLoader.State {
method public static androidx.media3.exoplayer.ima.ImaServerSideAdInsertionMediaSource.AdsLoader.State fromBundle(android.os.Bundle);
method public android.os.Bundle toBundle();
}
public static final class ImaServerSideAdInsertionMediaSource.Factory implements androidx.media3.exoplayer.source.MediaSource.Factory {
ctor public ImaServerSideAdInsertionMediaSource.Factory(androidx.media3.exoplayer.ima.ImaServerSideAdInsertionMediaSource.AdsLoader, androidx.media3.exoplayer.source.MediaSource.Factory);
}
} }
package androidx.media3.exoplayer.source { package androidx.media3.exoplayer.source {
@ -1438,7 +1396,6 @@ package androidx.media3.exoplayer.source {
method public androidx.media3.exoplayer.source.DefaultMediaSourceFactory clearLocalAdInsertionComponents(); method public androidx.media3.exoplayer.source.DefaultMediaSourceFactory clearLocalAdInsertionComponents();
method public androidx.media3.exoplayer.source.DefaultMediaSourceFactory setDataSourceFactory(androidx.media3.datasource.DataSource.Factory); method public androidx.media3.exoplayer.source.DefaultMediaSourceFactory setDataSourceFactory(androidx.media3.datasource.DataSource.Factory);
method public androidx.media3.exoplayer.source.DefaultMediaSourceFactory setLocalAdInsertionComponents(androidx.media3.exoplayer.source.ads.AdsLoader.Provider, androidx.media3.common.AdViewProvider); method public androidx.media3.exoplayer.source.DefaultMediaSourceFactory setLocalAdInsertionComponents(androidx.media3.exoplayer.source.ads.AdsLoader.Provider, androidx.media3.common.AdViewProvider);
method public androidx.media3.exoplayer.source.DefaultMediaSourceFactory setServerSideAdInsertionMediaSourceFactory(@Nullable androidx.media3.exoplayer.source.MediaSource.Factory);
} }
public interface MediaSource { public interface MediaSource {
@ -1527,7 +1484,7 @@ package androidx.media3.session {
field @Nullable public final V value; field @Nullable public final V value;
} }
@IntDef({androidx.media3.session.LibraryResult.RESULT_SUCCESS, androidx.media3.session.SessionError.INFO_CANCELLED, androidx.media3.session.SessionError.ERROR_UNKNOWN, androidx.media3.session.SessionError.ERROR_INVALID_STATE, androidx.media3.session.SessionError.ERROR_BAD_VALUE, androidx.media3.session.SessionError.ERROR_PERMISSION_DENIED, androidx.media3.session.SessionError.ERROR_IO, androidx.media3.session.SessionError.ERROR_SESSION_DISCONNECTED, androidx.media3.session.SessionError.ERROR_NOT_SUPPORTED, androidx.media3.session.SessionError.ERROR_SESSION_AUTHENTICATION_EXPIRED, androidx.media3.session.SessionError.ERROR_SESSION_PREMIUM_ACCOUNT_REQUIRED, androidx.media3.session.SessionError.ERROR_SESSION_CONCURRENT_STREAM_LIMIT, androidx.media3.session.SessionError.ERROR_SESSION_PARENTAL_CONTROL_RESTRICTED, androidx.media3.session.SessionError.ERROR_SESSION_NOT_AVAILABLE_IN_REGION, androidx.media3.session.SessionError.ERROR_SESSION_SKIP_LIMIT_REACHED, androidx.media3.session.SessionError.ERROR_SESSION_SETUP_REQUIRED}) @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 LibraryResult.Code { @IntDef({androidx.media3.session.LibraryResult.RESULT_SUCCESS, androidx.media3.session.LibraryResult.RESULT_ERROR_UNKNOWN, androidx.media3.session.LibraryResult.RESULT_ERROR_INVALID_STATE, androidx.media3.session.LibraryResult.RESULT_ERROR_BAD_VALUE, androidx.media3.session.LibraryResult.RESULT_ERROR_PERMISSION_DENIED, androidx.media3.session.LibraryResult.RESULT_ERROR_IO, androidx.media3.session.LibraryResult.RESULT_INFO_SKIPPED, androidx.media3.session.LibraryResult.RESULT_ERROR_SESSION_DISCONNECTED, androidx.media3.session.LibraryResult.RESULT_ERROR_NOT_SUPPORTED, androidx.media3.session.LibraryResult.RESULT_ERROR_SESSION_AUTHENTICATION_EXPIRED, androidx.media3.session.LibraryResult.RESULT_ERROR_SESSION_PREMIUM_ACCOUNT_REQUIRED, androidx.media3.session.LibraryResult.RESULT_ERROR_SESSION_CONCURRENT_STREAM_LIMIT, androidx.media3.session.LibraryResult.RESULT_ERROR_SESSION_PARENTAL_CONTROL_RESTRICTED, androidx.media3.session.LibraryResult.RESULT_ERROR_SESSION_NOT_AVAILABLE_IN_REGION, androidx.media3.session.LibraryResult.RESULT_ERROR_SESSION_SKIP_LIMIT_REACHED, androidx.media3.session.LibraryResult.RESULT_ERROR_SESSION_SETUP_REQUIRED}) @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 LibraryResult.Code {
} }
public final class MediaBrowser extends androidx.media3.session.MediaController { public final class MediaBrowser extends androidx.media3.session.MediaController {
@ -1803,7 +1760,6 @@ package androidx.media3.session {
method public int getControllerVersion(); method public int getControllerVersion();
method public String getPackageName(); method public String getPackageName();
method public int getUid(); method public int getUid();
field public static final String LEGACY_CONTROLLER_PACKAGE_NAME = "android.media.session.MediaController";
field public static final int LEGACY_CONTROLLER_VERSION = 0; // 0x0 field public static final int LEGACY_CONTROLLER_VERSION = 0; // 0x0
} }
@ -1852,7 +1808,6 @@ package androidx.media3.session {
ctor public SessionCommands.Builder(); 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);
method public androidx.media3.session.SessionCommands.Builder add(@androidx.media3.session.SessionCommand.CommandCode int); 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 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);
method public androidx.media3.session.SessionCommands.Builder remove(@androidx.media3.session.SessionCommand.CommandCode int); method public androidx.media3.session.SessionCommands.Builder remove(@androidx.media3.session.SessionCommand.CommandCode int);
@ -1882,7 +1837,7 @@ package androidx.media3.session {
field @androidx.media3.session.SessionResult.Code public final int resultCode; field @androidx.media3.session.SessionResult.Code public final int resultCode;
} }
@IntDef({androidx.media3.session.SessionResult.RESULT_SUCCESS, androidx.media3.session.SessionError.INFO_CANCELLED, androidx.media3.session.SessionError.ERROR_UNKNOWN, androidx.media3.session.SessionError.ERROR_INVALID_STATE, androidx.media3.session.SessionError.ERROR_BAD_VALUE, androidx.media3.session.SessionError.ERROR_PERMISSION_DENIED, androidx.media3.session.SessionError.ERROR_IO, androidx.media3.session.SessionError.ERROR_SESSION_DISCONNECTED, androidx.media3.session.SessionError.ERROR_NOT_SUPPORTED, androidx.media3.session.SessionError.ERROR_SESSION_AUTHENTICATION_EXPIRED, androidx.media3.session.SessionError.ERROR_SESSION_PREMIUM_ACCOUNT_REQUIRED, androidx.media3.session.SessionError.ERROR_SESSION_CONCURRENT_STREAM_LIMIT, androidx.media3.session.SessionError.ERROR_SESSION_PARENTAL_CONTROL_RESTRICTED, androidx.media3.session.SessionError.ERROR_SESSION_NOT_AVAILABLE_IN_REGION, androidx.media3.session.SessionError.ERROR_SESSION_SKIP_LIMIT_REACHED, androidx.media3.session.SessionError.ERROR_SESSION_SETUP_REQUIRED}) @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 SessionResult.Code { @IntDef({androidx.media3.session.SessionResult.RESULT_SUCCESS, androidx.media3.session.SessionResult.RESULT_ERROR_UNKNOWN, androidx.media3.session.SessionResult.RESULT_ERROR_INVALID_STATE, androidx.media3.session.SessionResult.RESULT_ERROR_BAD_VALUE, androidx.media3.session.SessionResult.RESULT_ERROR_PERMISSION_DENIED, androidx.media3.session.SessionResult.RESULT_ERROR_IO, androidx.media3.session.SessionResult.RESULT_INFO_SKIPPED, androidx.media3.session.SessionResult.RESULT_ERROR_SESSION_DISCONNECTED, androidx.media3.session.SessionResult.RESULT_ERROR_NOT_SUPPORTED, androidx.media3.session.SessionResult.RESULT_ERROR_SESSION_AUTHENTICATION_EXPIRED, androidx.media3.session.SessionResult.RESULT_ERROR_SESSION_PREMIUM_ACCOUNT_REQUIRED, androidx.media3.session.SessionResult.RESULT_ERROR_SESSION_CONCURRENT_STREAM_LIMIT, androidx.media3.session.SessionResult.RESULT_ERROR_SESSION_PARENTAL_CONTROL_RESTRICTED, androidx.media3.session.SessionResult.RESULT_ERROR_SESSION_NOT_AVAILABLE_IN_REGION, androidx.media3.session.SessionResult.RESULT_ERROR_SESSION_SKIP_LIMIT_REACHED, androidx.media3.session.SessionResult.RESULT_ERROR_SESSION_SETUP_REQUIRED}) @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 SessionResult.Code {
} }
public final class SessionToken { public final class SessionToken {

View file

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

View file

@ -14,8 +14,6 @@
apply from: "$gradle.ext.androidxMediaSettingsDir/constants.gradle" apply from: "$gradle.ext.androidxMediaSettingsDir/constants.gradle"
apply plugin: 'com.android.library' apply plugin: 'com.android.library'
group = 'androidx.media3'
android { android {
compileSdkVersion project.ext.compileSdkVersion compileSdkVersion project.ext.compileSdkVersion
@ -27,9 +25,11 @@ android {
aarMetadata { aarMetadata {
minCompileSdk = project.ext.compileSdkVersion minCompileSdk = project.ext.compileSdkVersion
} }
multiDexEnabled true
} }
compileOptions { compileOptions {
coreLibraryDesugaringEnabled true
sourceCompatibility JavaVersion.VERSION_1_8 sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8
} }
@ -41,3 +41,8 @@ android {
unitTests.includeAndroidResources true unitTests.includeAndroidResources true
} }
} }
dependencies {
androidTestImplementation 'androidx.multidex:multidex:' + androidxMultidexVersion
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5'
}

View file

@ -12,42 +12,41 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
project.ext { project.ext {
releaseVersion = '1.5.1' releaseVersion = '1.2.1'
releaseVersionCode = 1_005_001_3_00 releaseVersionCode = 1_002_001_3_00
minSdkVersion = 21 minSdkVersion = 16
// See https://developer.android.com/training/cars/media/automotive-os#automotive-module // See https://developer.android.com/training/cars/media/automotive-os#automotive-module
automotiveMinSdkVersion = 28 automotiveMinSdkVersion = 28
appTargetSdkVersion = 34 appTargetSdkVersion = 34
// Upgrading this requires [Internal ref: b/193254928] to be fixed, or some // Upgrading this requires [Internal ref: b/193254928] to be fixed, or some
// additional robolectric config. // additional robolectric config.
targetSdkVersion = 30 targetSdkVersion = 30
compileSdkVersion = 35 compileSdkVersion = 34
dexmakerVersion = '2.28.3' 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' junitVersion = '4.13.2'
// Use the same Guava version as the Android repo: // Use the same Guava version as the Android repo:
// https://cs.android.com/android/platform/superproject/main/+/main:external/guava/METADATA // https://cs.android.com/android/platform/superproject/main/+/main:external/guava/METADATA
guavaVersion = '33.3.1-android' guavaVersion = '31.1-android'
glideVersion = '4.14.2'
kotlinxCoroutinesVersion = '1.8.1'
leakCanaryVersion = '2.10'
mockitoVersion = '3.12.4' mockitoVersion = '3.12.4'
robolectricVersion = '4.11' robolectricVersion = '4.10.3'
// Keep this in sync with Google's internal Checker Framework version. // Keep this in sync with Google's internal Checker Framework version.
checkerframeworkVersion = '3.13.0' checkerframeworkVersion = '3.13.0'
errorProneVersion = '2.18.0' errorProneVersion = '2.18.0'
jsr305Version = '3.0.2' jsr305Version = '3.0.2'
kotlinAnnotationsVersion = '1.9.0' kotlinAnnotationsVersion = '1.8.20'
androidxAnnotationVersion = '1.6.0' // Updating this to 1.4.0+ will import Kotlin stdlib [internal ref: b/277891049].
androidxAnnotationVersion = '1.3.0'
androidxAnnotationExperimentalVersion = '1.3.1' androidxAnnotationExperimentalVersion = '1.3.1'
androidxAppCompatVersion = '1.6.1' androidxAppCompatVersion = '1.6.1'
androidxCollectionVersion = '1.2.0' androidxCollectionVersion = '1.2.0'
androidxConstraintLayoutVersion = '2.1.4' androidxConstraintLayoutVersion = '2.1.4'
// Updating this to 1.9.0+ will import Kotlin stdlib [internal ref: b/277891049].
androidxCoreVersion = '1.8.0' androidxCoreVersion = '1.8.0'
androidxExifInterfaceVersion = '1.3.6' androidxExifInterfaceVersion = '1.3.6'
androidxLifecycleVersion = '2.6.0' androidxFuturesVersion = '1.1.0'
androidxMediaVersion = '1.7.0' androidxMediaVersion = '1.6.0'
androidxMedia2Version = '1.2.1'
androidxMultidexVersion = '2.0.1'
androidxRecyclerViewVersion = '1.3.0' androidxRecyclerViewVersion = '1.3.0'
androidxMaterialVersion = '1.8.0' androidxMaterialVersion = '1.8.0'
androidxTestCoreVersion = '1.5.0' androidxTestCoreVersion = '1.5.0'
@ -55,8 +54,9 @@ project.ext {
androidxTestJUnitVersion = '1.1.5' androidxTestJUnitVersion = '1.1.5'
androidxTestRunnerVersion = '1.5.2' androidxTestRunnerVersion = '1.5.2'
androidxTestRulesVersion = '1.5.0' androidxTestRulesVersion = '1.5.0'
androidxTestServicesStorageVersion = '1.4.2'
androidxTestTruthVersion = '1.5.0' androidxTestTruthVersion = '1.5.0'
truthVersion = '1.4.0' truthVersion = '1.1.3'
okhttpVersion = '4.12.0' okhttpVersion = '4.12.0'
modulePrefix = ':' modulePrefix = ':'
if (gradle.ext.has('androidxMediaModulePrefix')) { if (gradle.ext.has('androidxMediaModulePrefix')) {

View file

@ -24,9 +24,6 @@ if (gradle.ext.has('androidxMediaModulePrefix')) {
include modulePrefix + 'lib-common' include modulePrefix + 'lib-common'
project(modulePrefix + 'lib-common').projectDir = new File(rootDir, 'libraries/common') project(modulePrefix + 'lib-common').projectDir = new File(rootDir, 'libraries/common')
include modulePrefix + 'lib-common-ktx'
project(modulePrefix + 'lib-common-ktx').projectDir = new File(rootDir, 'libraries/common_ktx')
include modulePrefix + 'lib-container' include modulePrefix + 'lib-container'
project(modulePrefix + 'lib-container').projectDir = new File(rootDir, 'libraries/container') project(modulePrefix + 'lib-container').projectDir = new File(rootDir, 'libraries/container')
@ -60,8 +57,6 @@ include modulePrefix + 'lib-datasource'
project(modulePrefix + 'lib-datasource').projectDir = new File(rootDir, 'libraries/datasource') project(modulePrefix + 'lib-datasource').projectDir = new File(rootDir, 'libraries/datasource')
include modulePrefix + 'lib-datasource-cronet' include modulePrefix + 'lib-datasource-cronet'
project(modulePrefix + 'lib-datasource-cronet').projectDir = new File(rootDir, 'libraries/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' include modulePrefix + 'lib-datasource-rtmp'
project(modulePrefix + 'lib-datasource-rtmp').projectDir = new File(rootDir, 'libraries/datasource_rtmp') project(modulePrefix + 'lib-datasource-rtmp').projectDir = new File(rootDir, 'libraries/datasource_rtmp')
include modulePrefix + 'lib-datasource-okhttp' include modulePrefix + 'lib-datasource-okhttp'
@ -75,12 +70,8 @@ include modulePrefix + 'lib-decoder-ffmpeg'
project(modulePrefix + 'lib-decoder-ffmpeg').projectDir = new File(rootDir, 'libraries/decoder_ffmpeg') project(modulePrefix + 'lib-decoder-ffmpeg').projectDir = new File(rootDir, 'libraries/decoder_ffmpeg')
include modulePrefix + 'lib-decoder-flac' include modulePrefix + 'lib-decoder-flac'
project(modulePrefix + 'lib-decoder-flac').projectDir = new File(rootDir, 'libraries/decoder_flac') project(modulePrefix + 'lib-decoder-flac').projectDir = new File(rootDir, 'libraries/decoder_flac')
include modulePrefix + 'lib-decoder-iamf' include modulePrefix + 'lib-decoder-midi'
project(modulePrefix + 'lib-decoder-iamf').projectDir = new File(rootDir, 'libraries/decoder_iamf') project(modulePrefix + 'lib-decoder-midi').projectDir = new File(rootDir, 'libraries/decoder_midi')
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' include modulePrefix + 'lib-decoder-opus'
project(modulePrefix + 'lib-decoder-opus').projectDir = new File(rootDir, 'libraries/decoder_opus') project(modulePrefix + 'lib-decoder-opus').projectDir = new File(rootDir, 'libraries/decoder_opus')
include modulePrefix + 'lib-decoder-vp9' include modulePrefix + 'lib-decoder-vp9'
@ -107,3 +98,7 @@ include modulePrefix + 'test-data'
project(modulePrefix + 'test-data').projectDir = new File(rootDir, 'libraries/test_data') project(modulePrefix + 'test-data').projectDir = new File(rootDir, 'libraries/test_data')
include modulePrefix + 'test-utils' include modulePrefix + 'test-utils'
project(modulePrefix + 'test-utils').projectDir = new File(rootDir, 'libraries/test_utils') project(modulePrefix + 'test-utils').projectDir = new File(rootDir, 'libraries/test_utils')
include modulePrefix + 'test-session-common'
project(modulePrefix + 'test-session-common').projectDir = new File(rootDir, 'libraries/test_session_common')
include modulePrefix + 'test-session-current'
project(modulePrefix + 'test-session-current').projectDir = new File(rootDir, 'libraries/test_session_current')

View file

@ -17,7 +17,7 @@ apply plugin: 'com.android.application'
android { android {
namespace 'androidx.media3.demo.cast' namespace 'androidx.media3.demo.cast'
compileSdk project.ext.compileSdkVersion compileSdkVersion project.ext.compileSdkVersion
compileOptions { compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8 sourceCompatibility JavaVersion.VERSION_1_8
@ -29,6 +29,7 @@ android {
versionCode project.ext.releaseVersionCode versionCode project.ext.releaseVersionCode
minSdkVersion project.ext.minSdkVersion minSdkVersion project.ext.minSdkVersion
targetSdkVersion project.ext.appTargetSdkVersion targetSdkVersion project.ext.appTargetSdkVersion
multiDexEnabled true
} }
buildTypes { buildTypes {
@ -61,6 +62,7 @@ dependencies {
implementation project(modulePrefix + 'lib-ui') implementation project(modulePrefix + 'lib-ui')
implementation project(modulePrefix + 'lib-cast') implementation project(modulePrefix + 'lib-cast')
implementation 'androidx.appcompat:appcompat:' + androidxAppCompatVersion implementation 'androidx.appcompat:appcompat:' + androidxAppCompatVersion
implementation 'androidx.multidex:multidex:' + androidxMultidexVersion
implementation 'androidx.recyclerview:recyclerview:' + androidxRecyclerViewVersion implementation 'androidx.recyclerview:recyclerview:' + androidxRecyclerViewVersion
implementation 'com.google.android.material:material:' + androidxMaterialVersion implementation 'com.google.android.material:material:' + androidxMaterialVersion
} }

View file

@ -23,6 +23,7 @@
<uses-sdk/> <uses-sdk/>
<application <application
android:name="androidx.multidex.MultiDexApplication"
android:label="@string/application_name" android:label="@string/application_name"
android:icon="@mipmap/ic_launcher" android:icon="@mipmap/ic_launcher"
android:largeHeap="true" android:largeHeap="true"

View file

@ -1,5 +1,5 @@
/* /*
* Copyright (C) 2024 The Android Open Source Project * Copyright 2020 The Android Open Source Project
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -13,10 +13,11 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
@NonNullApi package androidx.media3.demo.cast;
@OptIn(markerClass = UnstableApi.class)
package androidx.media3.demo.composition;
import androidx.annotation.OptIn; import androidx.multidex.MultiDexApplication;
import androidx.media3.common.util.NonNullApi;
import androidx.media3.common.util.UnstableApi; // Note: Multidex is enabled in code not AndroidManifest.xml because the internal build system
// doesn't dejetify MultiDexApplication in AndroidManifest.xml.
/** Application for multidex support. */
public final class DemoApplication extends MultiDexApplication {}

View file

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

View file

@ -1,14 +0,0 @@
# ExoPlayer demo with Compose integration
This is an experimental ExoPlayer demo app that is built fully using Compose
features. This should be taken as Work-In-Progress, rather than experimental API
for testing out application development with the media3 and Jetpack Compose
libraries. Please await further announcement via Release Notes for when the
implementation is fully integrated into the library.
For an intermediate solution, use Jetpack Compose Interop with AndroidView and
PlayerView. However, note that it provides limited functionality and some
features may not be supported.
See the [demos README](../README.md) for instructions on how to build and run
this demo.

View file

@ -1,87 +0,0 @@
// Copyright 2024 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
apply from: '../../constants.gradle'
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
android {
namespace 'androidx.media3.demo.compose'
compileSdk project.ext.compileSdkVersion
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = '1.8'
}
defaultConfig {
versionName project.ext.releaseVersion
versionCode project.ext.releaseVersionCode
minSdkVersion project.ext.minSdkVersion
targetSdkVersion project.ext.appTargetSdkVersion
}
buildTypes {
release {
shrinkResources true
minifyEnabled true
signingConfig signingConfigs.debug
}
debug {
jniDebuggable = true
}
}
lintOptions {
// The demo app isn't indexed, and doesn't have translations.
disable 'GoogleAppIndexingWarning','MissingTranslation'
}
buildFeatures {
viewBinding true
compose true
}
composeOptions {
kotlinCompilerExtensionVersion = "1.5.3"
}
testOptions {
unitTests {
includeAndroidResources = true
}
}
}
dependencies {
def composeBom = platform('androidx.compose:compose-bom:2024.05.00')
implementation composeBom
implementation 'androidx.activity:activity-compose:1.9.0'
implementation 'androidx.compose.foundation:foundation-android:1.6.7'
implementation 'androidx.compose.material3:material3-android:1.2.1'
implementation 'com.google.android.material:material:' + androidxMaterialVersion
implementation project(modulePrefix + 'lib-exoplayer')
// For detecting and debugging leaks only. LeakCanary is not needed for demo app to work.
debugImplementation 'com.squareup.leakcanary:leakcanary-android:' + leakCanaryVersion
testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:' + kotlinxCoroutinesVersion
testImplementation 'org.robolectric:robolectric:' + robolectricVersion
testImplementation project(modulePrefix + 'test-utils')
}

View file

@ -1,38 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright 2024 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
https://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="androidx.media3.demo.compose">
<uses-sdk/>
<uses-permission android:name="android.permission.INTERNET" />
<application
android:allowBackup="false"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:theme="@style/Theme.Media3ComposeDemo">
<activity
android:name="androidx.media3.demo.compose.MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>

View file

@ -1,63 +0,0 @@
/*
* Copyright 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.media3.demo.compose
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.material3.Surface
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.media3.common.MediaItem
import androidx.media3.common.Player
import androidx.media3.demo.compose.data.videos
import androidx.media3.exoplayer.ExoPlayer
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
Surface {
Column(
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally,
) {
val context = LocalContext.current
val exoPlayer = remember {
ExoPlayer.Builder(context).build().apply {
setMediaItem(MediaItem.fromUri(videos[0]))
prepare()
playWhenReady = true
repeatMode = Player.REPEAT_MODE_ONE
}
}
PlayerSurface(
player = exoPlayer,
surfaceType = SURFACE_TYPE_SURFACE_VIEW,
modifier = Modifier.align(Alignment.CenterHorizontally),
)
}
}
}
}
}

View file

@ -1,76 +0,0 @@
/*
* Copyright 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.media3.demo.compose
import android.view.Surface
import android.view.SurfaceView
import android.view.TextureView
import androidx.annotation.IntDef
import androidx.compose.foundation.AndroidEmbeddedExternalSurface
import androidx.compose.foundation.AndroidExternalSurface
import androidx.compose.foundation.AndroidExternalSurfaceScope
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.media3.common.Player
/**
* Provides a dedicated drawing [Surface] for media playbacks using a [Player].
*
* The player's video output is displayed with either a [SurfaceView]/[AndroidExternalSurface] or a
* [TextureView]/[AndroidEmbeddedExternalSurface].
*
* [Player] takes care of attaching the rendered output to the [Surface] and clearing it, when it is
* destroyed.
*
* See
* [Choosing a surface type](https://developer.android.com/media/media3/ui/playerview#surfacetype)
* for more information.
*/
@Composable
fun PlayerSurface(player: Player, surfaceType: @SurfaceType Int, modifier: Modifier = Modifier) {
val onSurfaceCreated: (Surface) -> Unit = { surface -> player.setVideoSurface(surface) }
val onSurfaceDestroyed: () -> Unit = { player.setVideoSurface(null) }
val onSurfaceInitialized: AndroidExternalSurfaceScope.() -> Unit = {
onSurface { surface, _, _ ->
onSurfaceCreated(surface)
surface.onDestroyed { onSurfaceDestroyed() }
}
}
when (surfaceType) {
SURFACE_TYPE_SURFACE_VIEW ->
AndroidExternalSurface(modifier = modifier, onInit = onSurfaceInitialized)
SURFACE_TYPE_TEXTURE_VIEW ->
AndroidEmbeddedExternalSurface(modifier = modifier, onInit = onSurfaceInitialized)
else -> throw IllegalArgumentException("Unrecognized surface type: $surfaceType")
}
}
/**
* The type of surface view used for media playbacks. One of [SURFACE_TYPE_SURFACE_VIEW] or
* [SURFACE_TYPE_TEXTURE_VIEW].
*/
@MustBeDocumented
@Retention(AnnotationRetention.SOURCE)
@Target(AnnotationTarget.CLASS, AnnotationTarget.TYPE, AnnotationTarget.TYPE_PARAMETER)
@IntDef(SURFACE_TYPE_SURFACE_VIEW, SURFACE_TYPE_TEXTURE_VIEW)
annotation class SurfaceType
/** Surface type equivalent to [SurfaceView] . */
const val SURFACE_TYPE_SURFACE_VIEW = 1
/** Surface type equivalent to [TextureView]. */
const val SURFACE_TYPE_TEXTURE_VIEW = 2

View file

@ -1,23 +0,0 @@
/*
* Copyright 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.media3.demo.compose.data
val videos =
listOf(
"https://html5demos.com/assets/dizzy.mp4",
"https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_4x3/gear0/fileSequence0.aac",
"https://storage.googleapis.com/exoplayer-test-media-1/gen-3/screens/dash-vod-single-segment/video-vp9-360.webm",
)

View file

@ -1,20 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright 2024 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
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.
-->
<shape
xmlns:android="http://schemas.android.com/apk/res/android">
<gradient android:startColor="@color/grey" android:endColor="@color/grey"/>
</shape>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

View file

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

View file

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

View file

@ -1,26 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright 2024 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
https://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<resources>
<string name="app_name">Media3 Compose Demo</string>
<string name="current_playlist_name">Current playlist</string>
<string name="open_player_content_description">Click to view your play list</string>
<string name="added_media_item_format">Added %1$s to playlist</string>
<string name="shuffle">Shuffle</string>
<string name="play_button">Play</string>
<string name="waiting_for_metadata">Waiting for playlist to load…</string>
<string name="notification_permission_denied">
"Without notification access the app can't warn about failed background operations"</string>
</resources>

View file

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

View file

@ -1,8 +0,0 @@
# Composition demo
This app is an **EXPERIMENTAL** demo app created to explore the potential of `Composition` and `CompositionPlayer` APIs. It may exhibit limited features, occasional bugs, or unexpected behaviors.
**Attention**: `CompositionPlayer` APIs should be taken as work in progress, rather than experimental API. Please await further announcement via [Release Notes](https://github.com/androidx/media/releases) for when the APIs are fully integrated.
See the [demos README](../README.md) for instructions on how to build and run
this demo.

View file

@ -1,62 +0,0 @@
/*
* Copyright 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
apply from: '../../constants.gradle'
apply plugin: 'com.android.application'
android {
namespace 'androidx.media3.demo.composition'
compileSdk project.ext.compileSdkVersion
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
defaultConfig {
versionName project.ext.releaseVersion
versionCode project.ext.releaseVersionCode
minSdkVersion project.ext.minSdkVersion
targetSdkVersion project.ext.appTargetSdkVersion
}
buildTypes {
release {
shrinkResources true
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.txt'
signingConfig signingConfigs.debug
}
}
lintOptions {
// This demo app isn't indexed and doesn't have translations.
disable 'GoogleAppIndexingWarning','MissingTranslation'
}
}
dependencies {
implementation 'androidx.appcompat:appcompat:1.6.1'
implementation 'com.google.android.material:material:' + androidxMaterialVersion
implementation project(modulePrefix + 'lib-effect')
implementation project(modulePrefix + 'lib-exoplayer')
implementation project(modulePrefix + 'lib-exoplayer-dash')
implementation project(modulePrefix + 'lib-muxer')
implementation project(modulePrefix + 'lib-transformer')
implementation project(modulePrefix + 'lib-ui')
implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion
compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion
}

View file

@ -1 +0,0 @@
# Proguard rules specific to the composition demo app.

View file

@ -1,51 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright 2024 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
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.composition">
<uses-sdk />
<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"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:requestLegacyExternalStorage="true"
tools:targetApi="29"
android:taskAffinity=""
android:theme="@style/Theme.AppCompat" >
<activity android:name=".CompositionPreviewActivity"
android:configChanges="keyboard|keyboardHidden|orientation|screenSize|screenLayout|smallestScreenSize|uiMode"
android:launchMode="singleTop"
android:label="@string/app_name"
android:exported="true"
android:theme="@style/Theme.MaterialComponents.DayNight.NoActionBar">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
</application>
</manifest>

View file

@ -1,69 +0,0 @@
/*
* Copyright 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.media3.demo.composition;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import androidx.recyclerview.widget.RecyclerView;
import java.util.List;
/** A {@link RecyclerView.Adapter} that displays assets in a sequence in a {@link RecyclerView}. */
public final class AssetItemAdapter extends RecyclerView.Adapter<AssetItemAdapter.ViewHolder> {
private static final String TAG = "AssetItemAdapter";
private final List<String> data;
/**
* Creates a new instance
*
* @param data A list of items to populate RecyclerView with.
*/
public AssetItemAdapter(List<String> data) {
this.data = data;
}
@Override
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.preset_item, parent, false);
return new ViewHolder(v);
}
@Override
public void onBindViewHolder(ViewHolder holder, int position) {
holder.getTextView().setText(data.get(position));
}
@Override
public int getItemCount() {
return data.size();
}
/** A {@link RecyclerView.ViewHolder} used to build {@link AssetItemAdapter}. */
public static final class ViewHolder extends RecyclerView.ViewHolder {
private final TextView textView;
private ViewHolder(View view) {
super(view);
textView = view.findViewById(R.id.preset_name_text);
}
private TextView getTextView() {
return textView;
}
}
}

View file

@ -1,505 +0,0 @@
/*
* Copyright 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.media3.demo.composition;
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.app.Activity;
import android.content.DialogInterface;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.ArrayAdapter;
import android.widget.CheckBox;
import android.widget.Spinner;
import android.widget.Toast;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.AppCompatButton;
import androidx.appcompat.widget.AppCompatCheckBox;
import androidx.appcompat.widget.AppCompatTextView;
import androidx.media3.common.Effect;
import androidx.media3.common.MediaItem;
import androidx.media3.common.MimeTypes;
import androidx.media3.common.PlaybackException;
import androidx.media3.common.Player;
import androidx.media3.common.audio.SonicAudioProcessor;
import androidx.media3.common.util.Clock;
import androidx.media3.common.util.Log;
import androidx.media3.common.util.Util;
import androidx.media3.effect.DebugTraceUtil;
import androidx.media3.effect.LanczosResample;
import androidx.media3.effect.Presentation;
import androidx.media3.effect.RgbFilter;
import androidx.media3.transformer.Composition;
import androidx.media3.transformer.CompositionPlayer;
import androidx.media3.transformer.EditedMediaItem;
import androidx.media3.transformer.EditedMediaItemSequence;
import androidx.media3.transformer.Effects;
import androidx.media3.transformer.ExportException;
import androidx.media3.transformer.ExportResult;
import androidx.media3.transformer.InAppMuxer;
import androidx.media3.transformer.JsonUtil;
import androidx.media3.transformer.Transformer;
import androidx.media3.ui.PlayerView;
import androidx.recyclerview.widget.DividerItemDecoration;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import com.google.common.base.Stopwatch;
import com.google.common.base.Ticker;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
import org.json.JSONException;
import org.json.JSONObject;
/**
* An {@link Activity} that previews compositions, using {@link
* androidx.media3.transformer.CompositionPlayer}.
*/
public final class CompositionPreviewActivity extends AppCompatActivity {
private static final String TAG = "CompPreviewActivity";
private static final String AUDIO_URI =
"https://storage.googleapis.com/exoplayer-test-media-0/play.mp3";
private static final String SAME_AS_INPUT_OPTION = "same as input";
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 ImmutableList<String> RESOLUTION_HEIGHTS =
ImmutableList.of(
SAME_AS_INPUT_OPTION, "144", "240", "360", "480", "720", "1080", "1440", "2160");
private ArrayList<String> sequenceAssetTitles;
private boolean[] selectedMediaItems;
private String[] presetDescriptions;
private AssetItemAdapter assetItemAdapter;
@Nullable private CompositionPlayer compositionPlayer;
@Nullable private Transformer transformer;
@Nullable private File outputFile;
private PlayerView playerView;
private AppCompatButton exportButton;
private AppCompatTextView exportInformationTextView;
private Stopwatch exportStopwatch;
private boolean includeBackgroundAudioTrack;
private boolean appliesVideoEffects;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.composition_preview_activity);
playerView = findViewById(R.id.composition_player_view);
findViewById(R.id.preview_button).setOnClickListener(view -> previewComposition());
findViewById(R.id.edit_sequence_button).setOnClickListener(view -> selectPreset());
RecyclerView presetList = findViewById(R.id.composition_preset_list);
presetList.addItemDecoration(new DividerItemDecoration(this, DividerItemDecoration.VERTICAL));
LinearLayoutManager layoutManager =
new LinearLayoutManager(this, LinearLayoutManager.VERTICAL, /* reverseLayout= */ false);
presetList.setLayoutManager(layoutManager);
exportInformationTextView = findViewById(R.id.export_information_text);
exportButton = findViewById(R.id.composition_export_button);
exportButton.setOnClickListener(view -> showExportSettings());
AppCompatCheckBox backgroundAudioCheckBox = findViewById(R.id.background_audio_checkbox);
backgroundAudioCheckBox.setOnCheckedChangeListener(
(compoundButton, checked) -> includeBackgroundAudioTrack = checked);
ArrayAdapter<String> resolutionHeightAdapter =
new ArrayAdapter<>(/* context= */ this, R.layout.spinner_item);
resolutionHeightAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
Spinner resolutionHeightSpinner = findViewById(R.id.resolution_height_spinner);
resolutionHeightSpinner.setAdapter(resolutionHeightAdapter);
resolutionHeightAdapter.addAll(RESOLUTION_HEIGHTS);
ArrayAdapter<String> hdrModeAdapter = new ArrayAdapter<>(this, R.layout.spinner_item);
hdrModeAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
Spinner hdrModeSpinner = findViewById(R.id.hdr_mode_spinner);
hdrModeSpinner.setAdapter(hdrModeAdapter);
hdrModeAdapter.addAll(HDR_MODE_DESCRIPTIONS.keySet());
AppCompatCheckBox applyVideoEffectsCheckBox = findViewById(R.id.apply_video_effects_checkbox);
applyVideoEffectsCheckBox.setOnCheckedChangeListener(
((compoundButton, checked) -> appliesVideoEffects = checked));
presetDescriptions = getResources().getStringArray(R.array.preset_descriptions);
// Select two media items by default.
selectedMediaItems = new boolean[presetDescriptions.length];
selectedMediaItems[0] = true;
selectedMediaItems[2] = true;
sequenceAssetTitles = new ArrayList<>();
for (int i = 0; i < selectedMediaItems.length; i++) {
if (selectedMediaItems[i]) {
sequenceAssetTitles.add(presetDescriptions[i]);
}
}
assetItemAdapter = new AssetItemAdapter(sequenceAssetTitles);
presetList.setAdapter(assetItemAdapter);
exportStopwatch =
Stopwatch.createUnstarted(
new Ticker() {
@Override
public long read() {
return android.os.SystemClock.elapsedRealtimeNanos();
}
});
}
@Override
protected void onStart() {
super.onStart();
playerView.onResume();
}
@Override
protected void onStop() {
super.onStop();
playerView.onPause();
releasePlayer();
cancelExport();
exportStopwatch.reset();
}
private Composition prepareComposition() {
String[] presetUris = getResources().getStringArray(/* id= */ R.array.preset_uris);
int[] presetDurationsUs = getResources().getIntArray(/* id= */ R.array.preset_durations);
List<EditedMediaItem> mediaItems = new ArrayList<>();
ImmutableList.Builder<Effect> videoEffectsBuilder = new ImmutableList.Builder<>();
if (appliesVideoEffects) {
videoEffectsBuilder.add(MatrixTransformationFactory.createDizzyCropEffect());
videoEffectsBuilder.add(RgbFilter.createGrayscaleFilter());
}
Spinner resolutionHeightSpinner = findViewById(R.id.resolution_height_spinner);
String selectedResolutionHeight = String.valueOf(resolutionHeightSpinner.getSelectedItem());
if (!SAME_AS_INPUT_OPTION.equals(selectedResolutionHeight)) {
int resolutionHeight = Integer.parseInt(selectedResolutionHeight);
videoEffectsBuilder.add(LanczosResample.scaleToFit(10000, resolutionHeight));
videoEffectsBuilder.add(Presentation.createForHeight(resolutionHeight));
}
ImmutableList<Effect> videoEffects = videoEffectsBuilder.build();
// Preview requires all sequences to be the same duration, so calculate main sequence duration
// and limit background sequence duration to match.
long videoSequenceDurationUs = 0;
for (int i = 0; i < selectedMediaItems.length; i++) {
if (selectedMediaItems[i]) {
SonicAudioProcessor pitchChanger = new SonicAudioProcessor();
pitchChanger.setPitch(mediaItems.size() % 2 == 0 ? 2f : 0.2f);
MediaItem mediaItem =
new MediaItem.Builder()
.setUri(presetUris[i])
.setImageDurationMs(Util.usToMs(presetDurationsUs[i])) // Ignored for audio/video
.build();
EditedMediaItem.Builder itemBuilder =
new EditedMediaItem.Builder(mediaItem)
.setEffects(
new Effects(
/* audioProcessors= */ ImmutableList.of(pitchChanger),
/* videoEffects= */ videoEffects))
.setDurationUs(presetDurationsUs[i]);
videoSequenceDurationUs += presetDurationsUs[i];
mediaItems.add(itemBuilder.build());
}
}
EditedMediaItemSequence videoSequence = new EditedMediaItemSequence.Builder(mediaItems).build();
List<EditedMediaItemSequence> compositionSequences = new ArrayList<>();
compositionSequences.add(videoSequence);
if (includeBackgroundAudioTrack) {
compositionSequences.add(getAudioBackgroundSequence(Util.usToMs(videoSequenceDurationUs)));
}
SonicAudioProcessor sampleRateChanger = new SonicAudioProcessor();
sampleRateChanger.setOutputSampleRateHz(8_000);
Spinner hdrModeSpinner = findViewById(R.id.hdr_mode_spinner);
int selectedHdrMode =
HDR_MODE_DESCRIPTIONS.get(String.valueOf(hdrModeSpinner.getSelectedItem()));
return new Composition.Builder(compositionSequences)
.setEffects(
new Effects(
/* audioProcessors= */ ImmutableList.of(sampleRateChanger),
/* videoEffects= */ ImmutableList.of()))
.setHdrMode(selectedHdrMode)
.build();
}
private EditedMediaItemSequence getAudioBackgroundSequence(long durationMs) {
MediaItem audioMediaItem =
new MediaItem.Builder()
.setUri(AUDIO_URI)
.setClippingConfiguration(
new MediaItem.ClippingConfiguration.Builder()
.setStartPositionMs(0)
.setEndPositionMs(durationMs)
.build())
.build();
EditedMediaItem audioItem =
new EditedMediaItem.Builder(audioMediaItem).setDurationUs(59_000_000).build();
return new EditedMediaItemSequence.Builder(audioItem).build();
}
private void previewComposition() {
releasePlayer();
Composition composition = prepareComposition();
playerView.setPlayer(null);
CompositionPlayer player = new CompositionPlayer.Builder(getApplicationContext()).build();
this.compositionPlayer = player;
playerView.setPlayer(compositionPlayer);
playerView.setControllerAutoShow(false);
player.addListener(
new Player.Listener() {
@Override
public void onPlayerError(PlaybackException error) {
Toast.makeText(getApplicationContext(), "Preview error: " + error, Toast.LENGTH_LONG)
.show();
Log.e(TAG, "Preview error", error);
}
});
player.setRepeatMode(Player.REPEAT_MODE_ALL);
player.setComposition(composition);
player.prepare();
player.play();
}
private void selectPreset() {
new AlertDialog.Builder(/* context= */ this)
.setTitle(R.string.select_preset_title)
.setMultiChoiceItems(presetDescriptions, selectedMediaItems, this::selectPresetInDialog)
.setPositiveButton(R.string.ok, /* listener= */ null)
.setCancelable(false)
.create()
.show();
}
private void selectPresetInDialog(DialogInterface dialog, int which, boolean isChecked) {
selectedMediaItems[which] = isChecked;
// The items will be added to a the sequence in the order they were selected.
if (isChecked) {
sequenceAssetTitles.add(presetDescriptions[which]);
assetItemAdapter.notifyItemInserted(sequenceAssetTitles.size() - 1);
} else {
int index = sequenceAssetTitles.indexOf(presetDescriptions[which]);
sequenceAssetTitles.remove(presetDescriptions[which]);
assetItemAdapter.notifyItemRemoved(index);
}
}
private void showExportSettings() {
AlertDialog.Builder alertDialogBuilder = new AlertDialog.Builder(this);
LayoutInflater inflater = this.getLayoutInflater();
View exportSettingsDialogView = inflater.inflate(R.layout.export_settings, null);
alertDialogBuilder
.setView(exportSettingsDialogView)
.setTitle(R.string.export_settings)
.setPositiveButton(
R.string.export, (dialog, id) -> exportComposition(exportSettingsDialogView))
.setNegativeButton(R.string.cancel, (dialog, id) -> dialog.dismiss());
ArrayAdapter<String> audioMimeAdapter =
new ArrayAdapter<>(/* context= */ this, R.layout.spinner_item);
audioMimeAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
Spinner audioMimeSpinner = exportSettingsDialogView.findViewById(R.id.audio_mime_spinner);
audioMimeSpinner.setAdapter(audioMimeAdapter);
audioMimeAdapter.addAll(
SAME_AS_INPUT_OPTION, MimeTypes.AUDIO_AAC, MimeTypes.AUDIO_AMR_NB, MimeTypes.AUDIO_AMR_WB);
ArrayAdapter<String> videoMimeAdapter =
new ArrayAdapter<>(/* context= */ this, R.layout.spinner_item);
videoMimeAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
Spinner videoMimeSpinner = exportSettingsDialogView.findViewById(R.id.video_mime_spinner);
videoMimeSpinner.setAdapter(videoMimeAdapter);
videoMimeAdapter.addAll(
SAME_AS_INPUT_OPTION,
MimeTypes.VIDEO_H263,
MimeTypes.VIDEO_H264,
MimeTypes.VIDEO_H265,
MimeTypes.VIDEO_MP4V,
MimeTypes.VIDEO_AV1);
CheckBox enableDebugTracingCheckBox =
exportSettingsDialogView.findViewById(R.id.enable_debug_tracing_checkbox);
enableDebugTracingCheckBox.setOnCheckedChangeListener(
(buttonView, isChecked) -> DebugTraceUtil.enableTracing = isChecked);
// Connect producing fragmented MP4 to using Media3 Muxer
CheckBox useMedia3MuxerCheckBox =
exportSettingsDialogView.findViewById(R.id.use_media3_muxer_checkbox);
CheckBox produceFragmentedMp4CheckBox =
exportSettingsDialogView.findViewById(R.id.produce_fragmented_mp4_checkbox);
useMedia3MuxerCheckBox.setOnCheckedChangeListener(
(buttonView, isChecked) -> {
if (!isChecked) {
produceFragmentedMp4CheckBox.setChecked(false);
}
});
produceFragmentedMp4CheckBox.setOnCheckedChangeListener(
(buttonView, isChecked) -> {
if (isChecked) {
useMedia3MuxerCheckBox.setChecked(true);
}
});
AlertDialog dialog = alertDialogBuilder.create();
dialog.show();
}
private void exportComposition(View exportSettingsDialogView) {
// Cancel and clean up files from any ongoing export.
cancelExport();
Composition composition = prepareComposition();
try {
outputFile =
createExternalCacheFile(
"composition-preview-" + Clock.DEFAULT.elapsedRealtime() + ".mp4");
} catch (IOException e) {
Toast.makeText(
getApplicationContext(),
"Aborting export! Unable to create output file: " + e,
Toast.LENGTH_LONG)
.show();
Log.e(TAG, "Aborting export! Unable to create output file: ", e);
return;
}
String filePath = outputFile.getAbsolutePath();
Transformer.Builder transformerBuilder = new Transformer.Builder(/* context= */ this);
Spinner audioMimeTypeSpinner = exportSettingsDialogView.findViewById(R.id.audio_mime_spinner);
String selectedAudioMimeType = String.valueOf(audioMimeTypeSpinner.getSelectedItem());
if (!SAME_AS_INPUT_OPTION.equals(selectedAudioMimeType)) {
transformerBuilder.setAudioMimeType(selectedAudioMimeType);
}
Spinner videoMimeTypeSpinner = exportSettingsDialogView.findViewById(R.id.video_mime_spinner);
String selectedVideoMimeType = String.valueOf(videoMimeTypeSpinner.getSelectedItem());
if (!SAME_AS_INPUT_OPTION.equals(selectedVideoMimeType)) {
transformerBuilder.setVideoMimeType(selectedVideoMimeType);
}
CheckBox useMedia3MuxerCheckBox =
exportSettingsDialogView.findViewById(R.id.use_media3_muxer_checkbox);
CheckBox produceFragmentedMp4CheckBox =
exportSettingsDialogView.findViewById(R.id.produce_fragmented_mp4_checkbox);
if (useMedia3MuxerCheckBox.isChecked()) {
transformerBuilder.setMuxerFactory(
new InAppMuxer.Factory.Builder()
.setOutputFragmentedMp4(produceFragmentedMp4CheckBox.isChecked())
.build());
}
transformer =
transformerBuilder
.addListener(
new Transformer.Listener() {
@Override
public void onCompleted(Composition composition, ExportResult exportResult) {
exportStopwatch.stop();
long elapsedTimeMs = exportStopwatch.elapsed(TimeUnit.MILLISECONDS);
String details =
getString(R.string.export_completed, elapsedTimeMs / 1000.f, filePath);
Log.d(TAG, DebugTraceUtil.generateTraceSummary());
Log.i(TAG, details);
exportInformationTextView.setText(details);
try {
JSONObject resultJson =
JsonUtil.exportResultAsJsonObject(exportResult)
.put("elapsedTimeMs", elapsedTimeMs)
.put("device", JsonUtil.getDeviceDetailsAsJsonObject());
for (String line : Util.split(resultJson.toString(2), "\n")) {
Log.i(TAG, line);
}
} catch (JSONException e) {
Log.w(TAG, "Unable to convert exportResult to JSON", e);
}
}
@Override
public void onError(
Composition composition,
ExportResult exportResult,
ExportException exportException) {
exportStopwatch.stop();
Toast.makeText(
getApplicationContext(),
"Export error: " + exportException,
Toast.LENGTH_LONG)
.show();
Log.e(TAG, "Export error", exportException);
Log.d(TAG, DebugTraceUtil.generateTraceSummary());
exportInformationTextView.setText(R.string.export_error);
}
})
.build();
exportInformationTextView.setText(R.string.export_started);
exportStopwatch.reset();
exportStopwatch.start();
transformer.start(composition, filePath);
Log.i(TAG, "Export started");
}
private void releasePlayer() {
if (compositionPlayer != null) {
compositionPlayer.release();
compositionPlayer = null;
}
}
/** Cancels any ongoing export operation, and deletes output file contents. */
private void cancelExport() {
if (transformer != null) {
transformer.cancel();
transformer = null;
}
if (outputFile != null) {
outputFile.delete();
outputFile = null;
}
exportInformationTextView.setText("");
}
/**
* Creates a {@link File} of the {@code fileName} in the application cache directory.
*
* <p>If a file of that name already exists, it is overwritten.
*/
// TODO: b/320636291 - Refactor duplicate createExternalCacheFile functions.
private File createExternalCacheFile(String fileName) throws IOException {
File file = new File(getExternalCacheDir(), fileName);
if (file.exists() && !file.delete()) {
throw new IOException("Could not delete file: " + file.getAbsolutePath());
}
if (!file.createNewFile()) {
throw new IOException("Could not create file: " + file.getAbsolutePath());
}
return file;
}
}

View file

@ -1,93 +0,0 @@
/*
* Copyright 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.media3.demo.composition;
import android.graphics.Matrix;
import androidx.media3.common.C;
import androidx.media3.common.util.Util;
import androidx.media3.effect.GlMatrixTransformation;
import androidx.media3.effect.MatrixTransformation;
/**
* Factory for {@link GlMatrixTransformation GlMatrixTransformations} and {@link
* MatrixTransformation MatrixTransformations} that create video effects by applying transformation
* matrices to the individual video frames.
*/
/* package */ final class MatrixTransformationFactory {
/**
* Returns a {@link MatrixTransformation} that rescales the frames over the first {@link
* #ZOOM_DURATION_SECONDS} seconds, such that the rectangle filled with the input frame increases
* linearly in size from a single point to filling the full output frame.
*/
public static MatrixTransformation createZoomInTransition() {
return MatrixTransformationFactory::calculateZoomInTransitionMatrix;
}
/**
* Returns a {@link MatrixTransformation} that crops frames to a rectangle that moves on an
* ellipse.
*/
public static MatrixTransformation createDizzyCropEffect() {
return MatrixTransformationFactory::calculateDizzyCropMatrix;
}
/**
* Returns a {@link GlMatrixTransformation} that rotates a frame in 3D around the y-axis and
* applies perspective projection to 2D.
*/
public static GlMatrixTransformation createSpin3dEffect() {
return MatrixTransformationFactory::calculate3dSpinMatrix;
}
private static final float ZOOM_DURATION_SECONDS = 2f;
private static final float DIZZY_CROP_ROTATION_PERIOD_US = 5_000_000f;
private static Matrix calculateZoomInTransitionMatrix(long presentationTimeUs) {
Matrix transformationMatrix = new Matrix();
float scale = Math.min(1, presentationTimeUs / (C.MICROS_PER_SECOND * ZOOM_DURATION_SECONDS));
transformationMatrix.postScale(/* sx= */ scale, /* sy= */ scale);
return transformationMatrix;
}
private static android.graphics.Matrix calculateDizzyCropMatrix(long presentationTimeUs) {
double theta = presentationTimeUs * 2 * Math.PI / DIZZY_CROP_ROTATION_PERIOD_US;
float centerX = 0.5f * (float) Math.cos(theta);
float centerY = 0.5f * (float) Math.sin(theta);
android.graphics.Matrix transformationMatrix = new android.graphics.Matrix();
transformationMatrix.postTranslate(/* dx= */ centerX, /* dy= */ centerY);
transformationMatrix.postScale(/* sx= */ 2f, /* sy= */ 2f);
return transformationMatrix;
}
private static float[] calculate3dSpinMatrix(long presentationTimeUs) {
float[] transformationMatrix = new float[16];
android.opengl.Matrix.frustumM(
transformationMatrix,
/* offset= */ 0,
/* left= */ -1f,
/* right= */ 1f,
/* bottom= */ -1f,
/* top= */ 1f,
/* near= */ 3f,
/* far= */ 5f);
android.opengl.Matrix.translateM(
transformationMatrix, /* mOffset= */ 0, /* x= */ 0f, /* y= */ 0f, /* z= */ -4f);
float theta = Util.usToMs(presentationTimeUs) / 10f;
android.opengl.Matrix.rotateM(
transformationMatrix, /* mOffset= */ 0, theta, /* x= */ 0f, /* y= */ 1f, /* z= */ 0f);
return transformationMatrix;
}
}

View file

@ -1,178 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright 2024 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="16dp">
<com.google.android.material.card.MaterialCardView
android:id="@+id/composition_preview_card_view"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:cardCornerRadius="4dp"
app:cardElevation="2dp">
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content" >
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/input_text_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:padding="8dp"
android:textAppearance="@style/TextAppearance.AppCompat.Medium"
android:text="@string/preview_composition" />
<FrameLayout
android:layout_width="match_parent"
android:layout_height="200dp" >
<androidx.media3.ui.PlayerView
android:id="@+id/composition_player_view"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</FrameLayout>
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/sequence_header_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/video_sequence_items"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/composition_preview_card_view"
app:layout_constraintBottom_toTopOf="@id/composition_preset_list"/>
<androidx.appcompat.widget.AppCompatButton
android:id="@+id/edit_sequence_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="@style/TextAppearance.AppCompat.Small"
android:text="@string/edit"
app:layout_constraintStart_toEndOf="@id/sequence_header_text"
app:layout_constraintTop_toTopOf="@id/sequence_header_text"
app:layout_constraintBottom_toBottomOf="@id/sequence_header_text"/>
<androidx.appcompat.widget.AppCompatCheckBox
android:id="@+id/apply_video_effects_checkbox"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/add_effects"
app:layout_constraintStart_toEndOf="@id/edit_sequence_button"
app:layout_constraintTop_toTopOf="@id/sequence_header_text"
app:layout_constraintBottom_toBottomOf="@id/sequence_header_text" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/composition_preset_list"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_marginTop="16dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/edit_sequence_button"
app:layout_constraintBottom_toTopOf="@id/export_information_text"/>
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/export_information_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintBottom_toTopOf="@id/background_audio_checkbox"/>
<androidx.appcompat.widget.AppCompatCheckBox
android:id="@+id/background_audio_checkbox"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/add_background_audio"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toTopOf="@id/resolution_height_setting" />
<LinearLayout
android:id="@+id/resolution_height_setting"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:layout_marginBottom="8dp"
app:layout_constraintBottom_toTopOf="@id/hdr_mode_setting">
<TextView
android:layout_height="wrap_content"
android:layout_width="0dp"
android:layout_weight="1"
android:text="@string/output_video_resolution"/>
<Spinner
android:id="@+id/resolution_height_spinner"
android:layout_gravity="end|center_vertical"
android:gravity="end"
android:layout_height="wrap_content"
android:layout_width="wrap_content"/>
</LinearLayout>
<LinearLayout
android:id="@+id/hdr_mode_setting"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:layout_marginBottom="12dp"
app:layout_constraintBottom_toTopOf="@id/preview_button">
<TextView
android:layout_height="wrap_content"
android:layout_width="0dp"
android:layout_weight="1"
android:text="@string/hdr_mode" />
<Spinner
android:id="@+id/hdr_mode_spinner"
android:layout_gravity="end|center_vertical"
android:gravity="end"
android:layout_height="wrap_content"
android:layout_width="wrap_content"/>
</LinearLayout>
<androidx.appcompat.widget.AppCompatButton
android:id="@+id/composition_export_button"
android:text="@string/export"
android:layout_marginTop="16dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintStart_toEndOf="@id/preview_button"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toBottomOf="parent"/>
<androidx.appcompat.widget.AppCompatButton
android:id="@+id/preview_button"
android:text="@string/preview"
android:layout_marginTop="16dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toStartOf="@id/composition_export_button"
app:layout_constraintBottom_toBottomOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -1,110 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright 2024 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
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"
android:id="@+id/export_settings_list"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="8dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:layout_marginBottom="12dp"
android:layout_marginTop="12dp">
<TextView
android:layout_height="wrap_content"
android:layout_width="0dp"
android:layout_weight="1"
android:text="@string/output_audio_mime_type"/>
<Spinner
android:id="@+id/audio_mime_spinner"
android:layout_gravity="end|center_vertical"
android:gravity="end"
android:layout_height="wrap_content"
android:layout_width="wrap_content"/>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:layout_marginBottom="12dp">
<TextView
android:layout_height="wrap_content"
android:layout_width="0dp"
android:layout_weight="1"
android:text="@string/output_video_mime_type"/>
<Spinner
android:id="@+id/video_mime_spinner"
android:layout_gravity="end|center_vertical"
android:gravity="end"
android:layout_height="wrap_content"
android:layout_width="wrap_content"/>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical">
<TextView
android:layout_height="wrap_content"
android:layout_width="0dp"
android:layout_weight="1"
android:text="@string/enable_debug_tracing"/>
<CheckBox
android:id="@+id/enable_debug_tracing_checkbox"
android:layout_gravity="end"
android:checked="false"
android:layout_height="wrap_content"
android:layout_width="wrap_content"/>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical">
<TextView
android:text="@string/use_media3_muxer"
android:layout_height="wrap_content"
android:layout_width="0dp"
android:layout_weight="1" />
<CheckBox
android:id="@+id/use_media3_muxer_checkbox"
android:layout_gravity="end"
android:checked="false"
android:layout_height="wrap_content"
android:layout_width="wrap_content"/>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical">
<TextView
android:text="@string/produce_fragmented_mp4"
android:layout_height="wrap_content"
android:layout_width="0dp"
android:layout_weight="1" />
<CheckBox
android:id="@+id/produce_fragmented_mp4_checkbox"
android:layout_gravity="end"
android:checked="false"
android:layout_height="wrap_content"
android:layout_width="wrap_content"/>
</LinearLayout>
</LinearLayout>

View file

@ -1,30 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright 2024 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<androidx.appcompat.widget.LinearLayoutCompat
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:padding="8dp">
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/preset_name_text"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_weight="1"/>
</androidx.appcompat.widget.LinearLayoutCompat>

View file

@ -1,25 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright 2024 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
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.
-->
<TextView
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="32dp"
android:gravity="start|center_vertical"
android:paddingLeft="4dp"
android:paddingRight="4dp"
android:layout_marginLeft="4dp"
android:layout_marginRight="4dp"
android:textIsSelectable="false" />

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

View file

@ -1,31 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright 2024 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
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.Media3internal" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
<!-- Primary brand color. -->
<item name="colorPrimary">@color/purple_200</item>
<item name="colorPrimaryVariant">@color/purple_700</item>
<item name="colorOnPrimary">@color/black</item>
<!-- Secondary brand color. -->
<item name="colorSecondary">@color/teal_200</item>
<item name="colorSecondaryVariant">@color/teal_200</item>
<item name="colorOnSecondary">@color/black</item>
<!-- Status bar color. -->
<item name="android:statusBarColor">?attr/colorPrimaryVariant</item>
<!-- Customize your theme here. -->
</style>
</resources>

View file

@ -1,77 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright 2024 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
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-array name="preset_descriptions">
<item>720p H264 video and AAC audio</item>
<item>1080p H265 video and AAC audio</item>
<item>360p H264 video and AAC audio</item>
<item>360p VP8 video and Vorbis audio</item>
<item>4K H264 video and AAC audio (portrait, no B-frames)</item>
<item>8k H265 video and AAC audio</item>
<item>Short 1080p H265 video and AAC audio</item>
<item>Long 180p H264 video and AAC audio</item>
<item>H264 video and AAC audio (portrait, H &gt; W, 0°)</item>
<item>H264 video and AAC audio (portrait, H &lt; W, 90°)</item>
<item>SEF slow motion with 240 fps</item>
<item>480p DASH (non-square pixels)</item>
<item>HDR (HDR10+) H265 limited range video (encoding may fail)</item>
<item>HDR (HLG) H265 limited range video (encoding may fail)</item>
<item>720p H264 video with no audio</item>
<item>London JPG image (plays for 5 secs at 30 fps)</item>
<item>Tokyo JPG image (portrait, plays for 5 secs at 30 fps)</item>
<item>Pixel 7 shorter audio track</item>
</string-array>
<string-array name="preset_uris">
<item>https://storage.googleapis.com/exoplayer-test-media-1/mp4/android-screens-10s.mp4</item>
<item>https://storage.googleapis.com/exoplayer-test-media-0/android-block-1080-hevc.mp4</item>
<item>https://html5demos.com/assets/dizzy.mp4</item>
<item>https://html5demos.com/assets/dizzy.webm</item>
<item>https://storage.googleapis.com/exoplayer-test-media-1/mp4/portrait_4k60.mp4</item>
<item>https://storage.googleapis.com/exoplayer-test-media-1/mp4/8k24fps_4s.mp4</item>
<item>https://storage.googleapis.com/exoplayer-test-media-1/mp4/1920w_1080h_4s.mp4</item>
<item>https://storage.googleapis.com/exoplayer-test-media-0/BigBuckBunny_320x180.mp4</item>
<item>https://storage.googleapis.com/exoplayer-test-media-1/mp4/portrait_avc_aac.mp4</item>
<item>https://storage.googleapis.com/exoplayer-test-media-1/mp4/portrait_rotated_avc_aac.mp4</item>
<item>https://storage.googleapis.com/exoplayer-test-media-1/mp4/slow-motion/slowMotion_stopwatch_240fps_long.mp4</item>
<item>https://storage.googleapis.com/exoplayer-test-media-1/gen/screens/dash-vod-single-segment/manifest-baseline.mpd</item>
<item>https://storage.googleapis.com/exoplayer-test-media-1/mp4/samsung-s21-hdr-hdr10.mp4</item>
<item>https://storage.googleapis.com/exoplayer-test-media-1/mp4/Pixel7Pro_HLG_1080P.mp4</item>
<item>https://storage.googleapis.com/exoplayer-test-media-1/mp4/sample_video_track_only.mp4</item>
<item>https://storage.googleapis.com/exoplayer-test-media-1/jpg/london.jpg</item>
<item>https://storage.googleapis.com/exoplayer-test-media-1/jpg/tokyo.jpg</item>
<item>https://storage.googleapis.com/exoplayer-temp/audio-blip/metronome_selfie_pixel.mp4</item>
</string-array>
<integer-array name="preset_durations">
<item>10024000</item>
<item>23823000</item>
<item>25000000</item>
<item>25000000</item>
<item>3745000</item>
<item>4421000</item>
<item>3923000</item>
<item>596459000</item>
<item>3687000</item>
<item>2235000</item>
<item>47987000</item>
<item>128270000</item>
<item>4236000</item>
<item>5167000</item>
<item>1001000</item>
<item>5000000</item>
<item>5000000</item>
<item>2170000</item>
</integer-array>
</resources>

View file

@ -1,24 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright 2024 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
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>
</resources>

View file

@ -1,39 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright 2024 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
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">Composition Demo</string>
<string name="edit">Edit</string>
<string name="add_effects">Add effects</string>
<string name="preview" translatable="false">Preview</string>
<string name="preview_composition" translatable="false">Composition preview</string>
<string name="video_sequence_items" translatable="false">Video sequence items:</string>
<string name="select_preset_title" translatable="false">Choose preset input</string>
<string name="export" translatable="false">Export</string>
<string name="export_completed" translatable="false">Export completed in %.3f seconds.\nOutput: %s</string>
<string name="export_error" translatable="false">Export error</string>
<string name="export_started" translatable="false">Export started</string>
<string name="add_background_audio" translatable="false">Add background audio</string>
<string name="output_video_resolution" translatable="false">Output video resolution</string>
<string name="hdr_mode" translatable="false">HDR mode</string>
<string name="ok" translatable="false">OK</string>
<string name="cancel" translatable="false">Cancel</string>
<string name="export_settings" translatable="false">Export Settings</string>
<string name="output_audio_mime_type" translatable="false">Output audio MIME type</string>
<string name="output_video_mime_type" translatable="false">Output video MIME type</string>
<string name="enable_debug_tracing" translatable="false">Enable debug tracing</string>
<string name="use_media3_muxer" translatable="false">Use Media3 muxer</string>
<string name="produce_fragmented_mp4" translatable="false">Produce fragmented MP4</string>
</resources>

View file

@ -1,31 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright 2024 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
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.Media3internal" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
<!-- Primary brand color. -->
<item name="colorPrimary">@color/purple_500</item>
<item name="colorPrimaryVariant">@color/purple_700</item>
<item name="colorOnPrimary">@color/white</item>
<!-- Secondary brand color. -->
<item name="colorSecondary">@color/teal_200</item>
<item name="colorSecondaryVariant">@color/teal_700</item>
<item name="colorOnSecondary">@color/black</item>
<!-- Status bar color. -->
<item name="android:statusBarColor">?attr/colorPrimaryVariant</item>
<!-- Customize your theme here. -->
</style>
</resources>

View file

@ -17,7 +17,7 @@ apply plugin: 'com.android.application'
android { android {
namespace 'androidx.media3.demo.gl' namespace 'androidx.media3.demo.gl'
compileSdk project.ext.compileSdkVersion compileSdkVersion project.ext.compileSdkVersion
compileOptions { compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8 sourceCompatibility JavaVersion.VERSION_1_8
@ -29,6 +29,7 @@ android {
versionCode project.ext.releaseVersionCode versionCode project.ext.releaseVersionCode
minSdkVersion project.ext.minSdkVersion minSdkVersion project.ext.minSdkVersion
targetSdkVersion project.ext.appTargetSdkVersion targetSdkVersion project.ext.appTargetSdkVersion
multiDexEnabled true
} }
buildTypes { buildTypes {
@ -54,5 +55,6 @@ dependencies {
implementation project(modulePrefix + 'lib-exoplayer-smoothstreaming') implementation project(modulePrefix + 'lib-exoplayer-smoothstreaming')
implementation project(modulePrefix + 'lib-ui') implementation project(modulePrefix + 'lib-ui')
implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion
implementation 'androidx.multidex:multidex:' + androidxMultidexVersion
compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion
} }

View file

@ -22,6 +22,7 @@
<uses-sdk/> <uses-sdk/>
<application <application
android:name="androidx.multidex.MultiDexApplication"
android:allowBackup="false" android:allowBackup="false"
android:icon="@mipmap/ic_launcher" android:icon="@mipmap/ic_launcher"
android:label="@string/application_name"> android:label="@string/application_name">

View file

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

View file

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

View file

@ -19,7 +19,7 @@ apply plugin: 'kotlin-android'
android { android {
namespace 'androidx.media3.demo.main' namespace 'androidx.media3.demo.main'
compileSdk project.ext.compileSdkVersion compileSdkVersion project.ext.compileSdkVersion
compileOptions { compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8 sourceCompatibility JavaVersion.VERSION_1_8
@ -31,6 +31,7 @@ android {
versionCode project.ext.releaseVersionCode versionCode project.ext.releaseVersionCode
minSdkVersion project.ext.minSdkVersion minSdkVersion project.ext.minSdkVersion
targetSdkVersion project.ext.appTargetSdkVersion targetSdkVersion project.ext.appTargetSdkVersion
multiDexEnabled true
} }
buildTypes { buildTypes {
@ -54,9 +55,7 @@ android {
disable 'GoogleAppIndexingWarning','MissingTranslation','IconDensities' disable 'GoogleAppIndexingWarning','MissingTranslation','IconDensities'
} }
flavorDimensions = ["decoderExtensions"] flavorDimensions "decoderExtensions"
buildFeatures.buildConfig true
productFlavors { productFlavors {
noDecoderExtensions { noDecoderExtensions {
@ -74,6 +73,7 @@ dependencies {
compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion
implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion
implementation 'androidx.appcompat:appcompat:' + androidxAppCompatVersion implementation 'androidx.appcompat:appcompat:' + androidxAppCompatVersion
implementation 'androidx.multidex:multidex:' + androidxMultidexVersion
implementation 'com.google.android.material:material:' + androidxMaterialVersion implementation 'com.google.android.material:material:' + androidxMaterialVersion
implementation project(modulePrefix + 'lib-exoplayer') implementation project(modulePrefix + 'lib-exoplayer')
implementation project(modulePrefix + 'lib-exoplayer-dash') implementation project(modulePrefix + 'lib-exoplayer-dash')
@ -87,7 +87,6 @@ dependencies {
withDecoderExtensionsImplementation project(modulePrefix + 'lib-decoder-ffmpeg') withDecoderExtensionsImplementation project(modulePrefix + 'lib-decoder-ffmpeg')
withDecoderExtensionsImplementation project(modulePrefix + 'lib-decoder-flac') withDecoderExtensionsImplementation project(modulePrefix + 'lib-decoder-flac')
withDecoderExtensionsImplementation project(modulePrefix + 'lib-decoder-opus') withDecoderExtensionsImplementation project(modulePrefix + 'lib-decoder-opus')
withDecoderExtensionsImplementation project(modulePrefix + 'lib-decoder-iamf')
withDecoderExtensionsImplementation project(modulePrefix + 'lib-decoder-vp9') withDecoderExtensionsImplementation project(modulePrefix + 'lib-decoder-vp9')
withDecoderExtensionsImplementation project(modulePrefix + 'lib-decoder-midi') withDecoderExtensionsImplementation project(modulePrefix + 'lib-decoder-midi')
withDecoderExtensionsImplementation project(modulePrefix + 'lib-datasource-rtmp') withDecoderExtensionsImplementation project(modulePrefix + 'lib-datasource-rtmp')

View file

@ -40,6 +40,7 @@
android:largeHeap="true" android:largeHeap="true"
android:allowBackup="false" android:allowBackup="false"
android:supportsRtl="true" android:supportsRtl="true"
android:name="androidx.multidex.MultiDexApplication"
tools:targetApi="29"> tools:targetApi="29">
<activity android:name=".SampleChooserActivity" <activity android:name=".SampleChooserActivity"

View file

@ -593,27 +593,6 @@
"clip_start_position_ms": 10000 "clip_start_position_ms": 10000
} }
] ]
},
{
"name": "Image -> Video -> Image -> Image",
"playlist": [
{
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/jpg/tokyo.jpg",
"image_duration_ms": 2000
},
{
"uri": "https://html5demos.com/assets/dizzy.mp4",
"clip_end_position_ms": 2000
},
{
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/jpg/london.jpg",
"image_duration_ms": 2000
},
{
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/jpg/tokyo.jpg",
"image_duration_ms": 2000
}
]
} }
] ]
}, },
@ -705,7 +684,7 @@
] ]
}, },
{ {
"name": "Progressive", "name": "Misc",
"samples": [ "samples": [
{ {
"name": "Dizzy (MP4)", "name": "Dizzy (MP4)",
@ -758,44 +737,6 @@
{ {
"name": "One hour frame counter (MP4)", "name": "One hour frame counter (MP4)",
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/mp4/frame-counter-one-hour.mp4" "uri": "https://storage.googleapis.com/exoplayer-test-media-1/mp4/frame-counter-one-hour.mp4"
},
{
"name": "Immersive Audio Format Sample (MP4, IAMF)",
"uri": "https://github.com/AOMediaCodec/libiamf/raw/main/tests/test_000036_s.mp4"
}
]
},
{
"name": "Images",
"samples": [
{
"name": "JPEG (wide)",
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/jpg/london.jpg",
"image_duration_ms": 2000
},
{
"name": "JPEG (tall)",
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/jpg/tokyo.jpg",
"image_duration_ms": 2000
},
{
"name": "PNG",
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/png/media3test.png",
"image_duration_ms": 2000
},
{
"name": "JPEG motion photo (still)",
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/jpg/london_motion_photo.jpg",
"image_duration_ms": 2000
},
{
"name": "JPEG motion photo (motion)",
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/jpg/london_motion_photo.jpg"
},
{
"name": "JPEG (Ultra HDR)",
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/jpg/ultra_hdr.jpg",
"image_duration_ms": 2000
} }
] ]
} }

View file

@ -63,7 +63,7 @@ public class DemoDownloadService extends DownloadService {
@Override @Override
protected Scheduler getScheduler() { protected Scheduler getScheduler() {
return new PlatformScheduler(this, JOB_ID); return Util.SDK_INT >= 21 ? new PlatformScheduler(this, JOB_ID) : null;
} }
@Override @Override

View file

@ -16,16 +16,12 @@
package androidx.media3.demo.main; package androidx.media3.demo.main;
import android.content.Context; import android.content.Context;
import android.net.http.HttpEngine;
import android.os.Build;
import android.os.ext.SdkExtensions;
import androidx.annotation.OptIn; import androidx.annotation.OptIn;
import androidx.media3.database.DatabaseProvider; import androidx.media3.database.DatabaseProvider;
import androidx.media3.database.StandaloneDatabaseProvider; import androidx.media3.database.StandaloneDatabaseProvider;
import androidx.media3.datasource.DataSource; import androidx.media3.datasource.DataSource;
import androidx.media3.datasource.DefaultDataSource; import androidx.media3.datasource.DefaultDataSource;
import androidx.media3.datasource.DefaultHttpDataSource; import androidx.media3.datasource.DefaultHttpDataSource;
import androidx.media3.datasource.HttpEngineDataSource;
import androidx.media3.datasource.cache.Cache; import androidx.media3.datasource.cache.Cache;
import androidx.media3.datasource.cache.CacheDataSource; import androidx.media3.datasource.cache.CacheDataSource;
import androidx.media3.datasource.cache.NoOpCacheEvictor; import androidx.media3.datasource.cache.NoOpCacheEvictor;
@ -50,26 +46,25 @@ public final class DemoUtil {
public static final String DOWNLOAD_NOTIFICATION_CHANNEL_ID = "download_channel"; public static final String DOWNLOAD_NOTIFICATION_CHANNEL_ID = "download_channel";
/**
* Whether the demo application uses Cronet for networking. Note that Cronet does not provide
* automatic support for cookies (https://github.com/google/ExoPlayer/issues/5975).
*
* <p>If set to false, the platform's default network stack is used with a {@link CookieManager}
* configured in {@link #getHttpDataSourceFactory}.
*/
private static final boolean USE_CRONET_FOR_NETWORKING = true;
private static final String TAG = "DemoUtil"; private static final String TAG = "DemoUtil";
private static final String DOWNLOAD_CONTENT_DIRECTORY = "downloads"; private static final String DOWNLOAD_CONTENT_DIRECTORY = "downloads";
private static DataSource.@MonotonicNonNull Factory dataSourceFactory; private static DataSource.@MonotonicNonNull Factory dataSourceFactory;
private static DataSource.@MonotonicNonNull Factory httpDataSourceFactory; private static DataSource.@MonotonicNonNull Factory httpDataSourceFactory;
@OptIn(markerClass = androidx.media3.common.util.UnstableApi.class)
private static @MonotonicNonNull DatabaseProvider databaseProvider; private static @MonotonicNonNull DatabaseProvider databaseProvider;
private static @MonotonicNonNull File downloadDirectory; private static @MonotonicNonNull File downloadDirectory;
@OptIn(markerClass = androidx.media3.common.util.UnstableApi.class)
private static @MonotonicNonNull Cache downloadCache; private static @MonotonicNonNull Cache downloadCache;
@OptIn(markerClass = androidx.media3.common.util.UnstableApi.class)
private static @MonotonicNonNull DownloadManager downloadManager; private static @MonotonicNonNull DownloadManager downloadManager;
private static @MonotonicNonNull DownloadTracker downloadTracker; private static @MonotonicNonNull DownloadTracker downloadTracker;
@OptIn(markerClass = androidx.media3.common.util.UnstableApi.class)
private static @MonotonicNonNull DownloadNotificationHelper downloadNotificationHelper; private static @MonotonicNonNull DownloadNotificationHelper downloadNotificationHelper;
/** Returns whether extension renderers should be used. */ /** Returns whether extension renderers should be used. */
@ -91,30 +86,24 @@ public final class DemoUtil {
.setExtensionRendererMode(extensionRendererMode); .setExtensionRendererMode(extensionRendererMode);
} }
@OptIn(markerClass = androidx.media3.common.util.UnstableApi.class)
public static synchronized DataSource.Factory getHttpDataSourceFactory(Context context) { public static synchronized DataSource.Factory getHttpDataSourceFactory(Context context) {
if (httpDataSourceFactory != null) { if (httpDataSourceFactory == null) {
return httpDataSourceFactory; if (USE_CRONET_FOR_NETWORKING) {
context = context.getApplicationContext();
@Nullable CronetEngine cronetEngine = CronetUtil.buildCronetEngine(context);
if (cronetEngine != null) {
httpDataSourceFactory =
new CronetDataSource.Factory(cronetEngine, Executors.newSingleThreadExecutor());
}
}
if (httpDataSourceFactory == null) {
// We don't want to use Cronet, or we failed to instantiate a CronetEngine.
CookieManager cookieManager = new CookieManager();
cookieManager.setCookiePolicy(CookiePolicy.ACCEPT_ORIGINAL_SERVER);
CookieHandler.setDefault(cookieManager);
httpDataSourceFactory = new DefaultHttpDataSource.Factory();
}
} }
context = context.getApplicationContext();
if (Build.VERSION.SDK_INT >= 30
&& SdkExtensions.getExtensionVersion(Build.VERSION_CODES.S) >= 7) {
HttpEngine httpEngine = new HttpEngine.Builder(context).build();
httpDataSourceFactory =
new HttpEngineDataSource.Factory(httpEngine, Executors.newSingleThreadExecutor());
return httpDataSourceFactory;
}
@Nullable CronetEngine cronetEngine = CronetUtil.buildCronetEngine(context);
if (cronetEngine != null) {
httpDataSourceFactory =
new CronetDataSource.Factory(cronetEngine, Executors.newSingleThreadExecutor());
return httpDataSourceFactory;
}
// The device doesn't support HttpEngine and we failed to instantiate a CronetEngine.
CookieManager cookieManager = new CookieManager();
cookieManager.setCookiePolicy(CookiePolicy.ACCEPT_ORIGINAL_SERVER);
CookieHandler.setDefault(cookieManager);
httpDataSourceFactory = new DefaultHttpDataSource.Factory();
return httpDataSourceFactory; return httpDataSourceFactory;
} }
@ -139,7 +128,6 @@ public final class DemoUtil {
return downloadNotificationHelper; return downloadNotificationHelper;
} }
@OptIn(markerClass = androidx.media3.common.util.UnstableApi.class)
public static synchronized DownloadManager getDownloadManager(Context context) { public static synchronized DownloadManager getDownloadManager(Context context) {
ensureDownloadManagerInitialized(context); ensureDownloadManagerInitialized(context);
return downloadManager; return downloadManager;

View file

@ -16,16 +16,15 @@
package androidx.media3.demo.main; package androidx.media3.demo.main;
import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;
import android.content.Context; import android.content.Context;
import android.content.DialogInterface; import android.content.DialogInterface;
import android.net.Uri; import android.net.Uri;
import android.os.Handler; import android.os.AsyncTask;
import android.os.Looper;
import android.widget.Toast; import android.widget.Toast;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.annotation.OptIn; import androidx.annotation.OptIn;
import androidx.annotation.RequiresApi;
import androidx.fragment.app.FragmentManager; import androidx.fragment.app.FragmentManager;
import androidx.media3.common.C; import androidx.media3.common.C;
import androidx.media3.common.DrmInitData; import androidx.media3.common.DrmInitData;
@ -55,9 +54,6 @@ import java.io.IOException;
import java.util.HashMap; import java.util.HashMap;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.CopyOnWriteArraySet; 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. */ /** Tracks media that has been downloaded. */
@OptIn(markerClass = androidx.media3.common.util.UnstableApi.class) @OptIn(markerClass = androidx.media3.common.util.UnstableApi.class)
@ -186,7 +182,7 @@ public class DownloadTracker {
trackSelectionDialog.dismiss(); trackSelectionDialog.dismiss();
} }
if (widevineOfflineLicenseFetchTask != null) { if (widevineOfflineLicenseFetchTask != null) {
widevineOfflineLicenseFetchTask.cancel(); widevineOfflineLicenseFetchTask.cancel(false);
} }
} }
@ -201,7 +197,12 @@ public class DownloadTracker {
} }
// The content is DRM protected. We need to acquire an offline license. // 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. // TODO(internal b/163107948): Support cases where DrmInitData are not in the manifest.
if (!hasNonNullWidevineSchemaData(format.drmInitData)) { if (!hasNonNullWidevineSchemaData(format.drmInitData)) {
Toast.makeText(context, R.string.download_start_error_offline_license, Toast.LENGTH_LONG) Toast.makeText(context, R.string.download_start_error_offline_license, Toast.LENGTH_LONG)
@ -356,16 +357,15 @@ public class DownloadTracker {
} }
/** Downloads a Widevine offline license in a background thread. */ /** Downloads a Widevine offline license in a background thread. */
private static final class WidevineOfflineLicenseFetchTask { @RequiresApi(18)
private static final class WidevineOfflineLicenseFetchTask extends AsyncTask<Void, Void, Void> {
private final Format format; private final Format format;
private final MediaItem.DrmConfiguration drmConfiguration; private final MediaItem.DrmConfiguration drmConfiguration;
private final DataSource.Factory dataSourceFactory; private final DataSource.Factory dataSourceFactory;
private final StartDownloadDialogHelper dialogHelper; private final StartDownloadDialogHelper dialogHelper;
private final DownloadHelper downloadHelper; private final DownloadHelper downloadHelper;
private final ExecutorService executorService;
@Nullable Future<?> future;
@Nullable private byte[] keySetId; @Nullable private byte[] keySetId;
@Nullable private DrmSession.DrmSessionException drmSessionException; @Nullable private DrmSession.DrmSessionException drmSessionException;
@ -375,8 +375,6 @@ public class DownloadTracker {
DataSource.Factory dataSourceFactory, DataSource.Factory dataSourceFactory,
StartDownloadDialogHelper dialogHelper, StartDownloadDialogHelper dialogHelper,
DownloadHelper downloadHelper) { DownloadHelper downloadHelper) {
checkState(drmConfiguration.scheme.equals(C.WIDEVINE_UUID));
this.executorService = Executors.newSingleThreadExecutor();
this.format = format; this.format = format;
this.drmConfiguration = drmConfiguration; this.drmConfiguration = drmConfiguration;
this.dataSourceFactory = dataSourceFactory; this.dataSourceFactory = dataSourceFactory;
@ -384,41 +382,32 @@ public class DownloadTracker {
this.downloadHelper = downloadHelper; this.downloadHelper = downloadHelper;
} }
public void cancel() { @Override
if (future != null) { protected Void doInBackground(Void... voids) {
future.cancel(/* mayInterruptIfRunning= */ false); 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();
} }
return null;
} }
public void execute() { @Override
future = protected void onPostExecute(Void aVoid) {
executorService.submit( if (drmSessionException != null) {
() -> { dialogHelper.onOfflineLicenseFetchedError(drmSessionException);
OfflineLicenseHelper offlineLicenseHelper = } else {
OfflineLicenseHelper.newWidevineInstance( dialogHelper.onOfflineLicenseFetched(downloadHelper, checkNotNull(keySetId));
drmConfiguration.licenseUri.toString(), }
drmConfiguration.forceDefaultLicenseUri,
dataSourceFactory,
drmConfiguration.licenseRequestHeaders,
new DrmSessionEventListener.EventDispatcher());
try {
keySetId = offlineLicenseHelper.downloadLicense(format);
} catch (DrmSession.DrmSessionException e) {
drmSessionException = e;
} finally {
offlineLicenseHelper.release();
new Handler(Looper.getMainLooper())
.post(
() -> {
if (drmSessionException != null) {
dialogHelper.onOfflineLicenseFetchedError(drmSessionException);
} else {
dialogHelper.onOfflineLicenseFetched(
downloadHelper, checkNotNull(keySetId));
}
});
}
});
} }
} }
} }

View file

@ -22,14 +22,11 @@ import static com.google.common.base.Preconditions.checkState;
import android.content.Intent; import android.content.Intent;
import android.net.Uri; import android.net.Uri;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.annotation.OptIn;
import androidx.media3.common.C; import androidx.media3.common.C;
import androidx.media3.common.MediaItem; import androidx.media3.common.MediaItem;
import androidx.media3.common.MediaItem.ClippingConfiguration; import androidx.media3.common.MediaItem.ClippingConfiguration;
import androidx.media3.common.MediaItem.SubtitleConfiguration; import androidx.media3.common.MediaItem.SubtitleConfiguration;
import androidx.media3.common.MediaMetadata; import androidx.media3.common.MediaMetadata;
import androidx.media3.common.Player;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.common.util.Util; import androidx.media3.common.util.Util;
import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableList;
import java.util.ArrayList; import java.util.ArrayList;
@ -56,7 +53,6 @@ public class IntentUtil {
public static final String MIME_TYPE_EXTRA = "mime_type"; public static final String MIME_TYPE_EXTRA = "mime_type";
public static final String CLIP_START_POSITION_MS_EXTRA = "clip_start_position_ms"; public static final String CLIP_START_POSITION_MS_EXTRA = "clip_start_position_ms";
public static final String CLIP_END_POSITION_MS_EXTRA = "clip_end_position_ms"; public static final String CLIP_END_POSITION_MS_EXTRA = "clip_end_position_ms";
public static final String IMAGE_DURATION_MS = "image_duration_ms";
public static final String AD_TAG_URI_EXTRA = "ad_tag_uri"; public static final String AD_TAG_URI_EXTRA = "ad_tag_uri";
@ -70,21 +66,6 @@ public class IntentUtil {
public static final String SUBTITLE_URI_EXTRA = "subtitle_uri"; public static final String SUBTITLE_URI_EXTRA = "subtitle_uri";
public static final String SUBTITLE_MIME_TYPE_EXTRA = "subtitle_mime_type"; public static final String SUBTITLE_MIME_TYPE_EXTRA = "subtitle_mime_type";
public static final String SUBTITLE_LANGUAGE_EXTRA = "subtitle_language"; public static final String SUBTITLE_LANGUAGE_EXTRA = "subtitle_language";
public static final String REPEAT_MODE_EXTRA = "repeat_mode";
public static @Player.RepeatMode int parseRepeatModeExtra(String repeatMode) {
switch (repeatMode) {
case "OFF":
return Player.REPEAT_MODE_OFF;
case "ONE":
return Player.REPEAT_MODE_ONE;
case "ALL":
return Player.REPEAT_MODE_ALL;
default:
throw new IllegalArgumentException(
"Argument " + repeatMode + " does not match any of the repeat modes: OFF|ONE|ALL");
}
}
/** Creates a list of {@link MediaItem media items} from an {@link Intent}. */ /** Creates a list of {@link MediaItem media items} from an {@link Intent}. */
public static List<MediaItem> createMediaItemsFromIntent(Intent intent) { public static List<MediaItem> createMediaItemsFromIntent(Intent intent) {
@ -133,7 +114,6 @@ public class IntentUtil {
} }
} }
@OptIn(markerClass = UnstableApi.class) // Setting image duration.
private static MediaItem createMediaItemFromIntent( private static MediaItem createMediaItemFromIntent(
Uri uri, Intent intent, String extrasKeySuffix) { Uri uri, Intent intent, String extrasKeySuffix) {
@Nullable String mimeType = intent.getStringExtra(MIME_TYPE_EXTRA + extrasKeySuffix); @Nullable String mimeType = intent.getStringExtra(MIME_TYPE_EXTRA + extrasKeySuffix);
@ -142,7 +122,6 @@ public class IntentUtil {
@Nullable @Nullable
SubtitleConfiguration subtitleConfiguration = SubtitleConfiguration subtitleConfiguration =
createSubtitleConfiguration(intent, extrasKeySuffix); createSubtitleConfiguration(intent, extrasKeySuffix);
long imageDurationMs = intent.getLongExtra(IMAGE_DURATION_MS + extrasKeySuffix, C.TIME_UNSET);
MediaItem.Builder builder = MediaItem.Builder builder =
new MediaItem.Builder() new MediaItem.Builder()
.setUri(uri) .setUri(uri)
@ -155,8 +134,7 @@ public class IntentUtil {
.setEndPositionMs( .setEndPositionMs(
intent.getLongExtra( intent.getLongExtra(
CLIP_END_POSITION_MS_EXTRA + extrasKeySuffix, C.TIME_END_OF_SOURCE)) CLIP_END_POSITION_MS_EXTRA + extrasKeySuffix, C.TIME_END_OF_SOURCE))
.build()) .build());
.setImageDurationMs(imageDurationMs);
if (adTagUri != null) { if (adTagUri != null) {
builder.setAdsConfiguration( builder.setAdsConfiguration(
new MediaItem.AdsConfiguration.Builder(Uri.parse(adTagUri)).build()); new MediaItem.AdsConfiguration.Builder(Uri.parse(adTagUri)).build());
@ -217,7 +195,6 @@ public class IntentUtil {
return builder; return builder;
} }
@OptIn(markerClass = UnstableApi.class) // Accessing image duration.
private static void addLocalConfigurationToIntent( private static void addLocalConfigurationToIntent(
MediaItem.LocalConfiguration localConfiguration, Intent intent, String extrasKeySuffix) { MediaItem.LocalConfiguration localConfiguration, Intent intent, String extrasKeySuffix) {
intent intent
@ -238,9 +215,6 @@ public class IntentUtil {
intent.putExtra(SUBTITLE_MIME_TYPE_EXTRA + extrasKeySuffix, subtitleConfiguration.mimeType); intent.putExtra(SUBTITLE_MIME_TYPE_EXTRA + extrasKeySuffix, subtitleConfiguration.mimeType);
intent.putExtra(SUBTITLE_LANGUAGE_EXTRA + extrasKeySuffix, subtitleConfiguration.language); intent.putExtra(SUBTITLE_LANGUAGE_EXTRA + extrasKeySuffix, subtitleConfiguration.language);
} }
if (localConfiguration.imageDurationMs != C.TIME_UNSET) {
intent.putExtra(IMAGE_DURATION_MS + extrasKeySuffix, localConfiguration.imageDurationMs);
}
} }
private static void addDrmConfigurationToIntent( private static void addDrmConfigurationToIntent(

View file

@ -93,8 +93,11 @@ public class PlayerActivity extends AppCompatActivity
@Nullable private AdsLoader clientSideAdsLoader; @Nullable private AdsLoader clientSideAdsLoader;
@Nullable private ImaServerSideAdInsertionMediaSource.AdsLoader serverSideAdsLoader; @OptIn(markerClass = UnstableApi.class)
@Nullable
private ImaServerSideAdInsertionMediaSource.AdsLoader serverSideAdsLoader;
@OptIn(markerClass = UnstableApi.class)
private ImaServerSideAdInsertionMediaSource.AdsLoader.@MonotonicNonNull State private ImaServerSideAdInsertionMediaSource.AdsLoader.@MonotonicNonNull State
serverSideAdsLoaderState; serverSideAdsLoaderState;
@ -259,8 +262,8 @@ public class PlayerActivity extends AppCompatActivity
* @return Whether initialization was successful. * @return Whether initialization was successful.
*/ */
protected boolean initializePlayer() { protected boolean initializePlayer() {
Intent intent = getIntent();
if (player == null) { if (player == null) {
Intent intent = getIntent();
mediaItems = createMediaItems(intent); mediaItems = createMediaItems(intent);
if (mediaItems.isEmpty()) { if (mediaItems.isEmpty()) {
@ -290,15 +293,11 @@ public class PlayerActivity extends AppCompatActivity
} }
player.setMediaItems(mediaItems, /* resetPosition= */ !haveStartPosition); player.setMediaItems(mediaItems, /* resetPosition= */ !haveStartPosition);
player.prepare(); player.prepare();
String repeatModeExtra = intent.getStringExtra(IntentUtil.REPEAT_MODE_EXTRA);
if (repeatModeExtra != null) {
player.setRepeatMode(IntentUtil.parseRepeatModeExtra(repeatModeExtra));
}
updateButtonVisibility(); updateButtonVisibility();
return true; return true;
} }
@OptIn(markerClass = UnstableApi.class) // DRM configuration @OptIn(markerClass = UnstableApi.class) // SSAI configuration
private MediaSource.Factory createMediaSourceFactory() { private MediaSource.Factory createMediaSourceFactory() {
DefaultDrmSessionManagerProvider drmSessionManagerProvider = DefaultDrmSessionManagerProvider drmSessionManagerProvider =
new DefaultDrmSessionManagerProvider(); new DefaultDrmSessionManagerProvider();
@ -331,6 +330,7 @@ public class PlayerActivity extends AppCompatActivity
playerBuilder.setRenderersFactory(renderersFactory); playerBuilder.setRenderersFactory(renderersFactory);
} }
@OptIn(markerClass = UnstableApi.class)
private void configurePlayerWithServerSideAdsLoader() { private void configurePlayerWithServerSideAdsLoader() {
serverSideAdsLoader.setPlayer(player); serverSideAdsLoader.setPlayer(player);
} }
@ -361,7 +361,11 @@ public class PlayerActivity extends AppCompatActivity
MediaItem.DrmConfiguration drmConfiguration = mediaItem.localConfiguration.drmConfiguration; MediaItem.DrmConfiguration drmConfiguration = mediaItem.localConfiguration.drmConfiguration;
if (drmConfiguration != null) { if (drmConfiguration != null) {
if (!FrameworkMediaDrm.isCryptoSchemeSupported(drmConfiguration.scheme)) { 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)) {
showToast(R.string.error_drm_unsupported_scheme); showToast(R.string.error_drm_unsupported_scheme);
finish(); finish();
return Collections.emptyList(); return Collections.emptyList();
@ -399,6 +403,7 @@ public class PlayerActivity extends AppCompatActivity
} }
} }
@OptIn(markerClass = UnstableApi.class)
private void releaseServerSideAdsLoader() { private void releaseServerSideAdsLoader() {
serverSideAdsLoaderState = serverSideAdsLoader.release(); serverSideAdsLoaderState = serverSideAdsLoader.release();
serverSideAdsLoader = null; serverSideAdsLoader = null;
@ -412,17 +417,20 @@ public class PlayerActivity extends AppCompatActivity
} }
} }
@OptIn(markerClass = UnstableApi.class)
private void saveServerSideAdsLoaderState(Bundle outState) { private void saveServerSideAdsLoaderState(Bundle outState) {
if (serverSideAdsLoaderState != null) { if (serverSideAdsLoaderState != null) {
outState.putBundle(KEY_SERVER_SIDE_ADS_LOADER_STATE, serverSideAdsLoaderState.toBundle()); outState.putBundle(KEY_SERVER_SIDE_ADS_LOADER_STATE, serverSideAdsLoaderState.toBundle());
} }
} }
@OptIn(markerClass = UnstableApi.class)
private void restoreServerSideAdsLoaderState(Bundle savedInstanceState) { private void restoreServerSideAdsLoaderState(Bundle savedInstanceState) {
Bundle adsLoaderStateBundle = savedInstanceState.getBundle(KEY_SERVER_SIDE_ADS_LOADER_STATE); Bundle adsLoaderStateBundle = savedInstanceState.getBundle(KEY_SERVER_SIDE_ADS_LOADER_STATE);
if (adsLoaderStateBundle != null) { if (adsLoaderStateBundle != null) {
serverSideAdsLoaderState = serverSideAdsLoaderState =
ImaServerSideAdInsertionMediaSource.AdsLoader.State.fromBundle(adsLoaderStateBundle); ImaServerSideAdInsertionMediaSource.AdsLoader.State.CREATOR.fromBundle(
adsLoaderStateBundle);
} }
} }
@ -506,7 +514,7 @@ public class PlayerActivity extends AppCompatActivity
private class PlayerErrorMessageProvider implements ErrorMessageProvider<PlaybackException> { private class PlayerErrorMessageProvider implements ErrorMessageProvider<PlaybackException> {
@OptIn(markerClass = UnstableApi.class) // Using decoder exceptions @OptIn(markerClass = androidx.media3.common.util.UnstableApi.class)
@Override @Override
public Pair<Integer, String> getErrorMessage(PlaybackException e) { public Pair<Integer, String> getErrorMessage(PlaybackException e) {
String errorString = getString(R.string.error_generic); String errorString = getString(R.string.error_generic);
@ -547,7 +555,7 @@ public class PlayerActivity extends AppCompatActivity
return mediaItems; return mediaItems;
} }
@OptIn(markerClass = UnstableApi.class) // Using Download API @OptIn(markerClass = androidx.media3.common.util.UnstableApi.class)
private static MediaItem maybeSetDownloadProperties( private static MediaItem maybeSetDownloadProperties(
MediaItem item, @Nullable DownloadRequest downloadRequest) { MediaItem item, @Nullable DownloadRequest downloadRequest) {
if (downloadRequest == null) { if (downloadRequest == null) {

View file

@ -26,13 +26,11 @@ import android.content.SharedPreferences;
import android.content.pm.PackageManager; import android.content.pm.PackageManager;
import android.content.res.AssetManager; import android.content.res.AssetManager;
import android.net.Uri; import android.net.Uri;
import android.os.AsyncTask;
import android.os.Build; import android.os.Build;
import android.os.Bundle; import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.text.TextUtils; import android.text.TextUtils;
import android.util.JsonReader; import android.util.JsonReader;
import android.util.Log;
import android.view.Menu; import android.view.Menu;
import android.view.MenuInflater; import android.view.MenuInflater;
import android.view.MenuItem; import android.view.MenuItem;
@ -45,15 +43,15 @@ import android.widget.ExpandableListView.OnChildClickListener;
import android.widget.ImageButton; import android.widget.ImageButton;
import android.widget.TextView; import android.widget.TextView;
import android.widget.Toast; import android.widget.Toast;
import androidx.annotation.DoNotInline;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.annotation.OptIn; import androidx.annotation.OptIn;
import androidx.annotation.RequiresApi; import androidx.annotation.RequiresApi;
import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.app.AppCompatActivity;
import androidx.media3.common.C;
import androidx.media3.common.MediaItem; import androidx.media3.common.MediaItem;
import androidx.media3.common.MediaItem.ClippingConfiguration; import androidx.media3.common.MediaItem.ClippingConfiguration;
import androidx.media3.common.MediaMetadata; import androidx.media3.common.MediaMetadata;
import androidx.media3.common.util.UnstableApi; import androidx.media3.common.util.Log;
import androidx.media3.common.util.Util; import androidx.media3.common.util.Util;
import androidx.media3.datasource.DataSource; import androidx.media3.datasource.DataSource;
import androidx.media3.datasource.DataSourceInputStream; import androidx.media3.datasource.DataSourceInputStream;
@ -67,7 +65,6 @@ import com.google.common.collect.ImmutableMap;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.io.InputStreamReader; import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
@ -75,8 +72,6 @@ import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/** An activity for selecting from a list of media samples. */ /** An activity for selecting from a list of media samples. */
public class SampleChooserActivity extends AppCompatActivity public class SampleChooserActivity extends AppCompatActivity
@ -119,7 +114,6 @@ public class SampleChooserActivity extends AppCompatActivity
} }
} }
} catch (IOException e) { } catch (IOException e) {
Log.e(TAG, "One or more sample lists failed to load", e);
Toast.makeText(getApplicationContext(), R.string.sample_list_load_error, Toast.LENGTH_LONG) Toast.makeText(getApplicationContext(), R.string.sample_list_load_error, Toast.LENGTH_LONG)
.show(); .show();
} }
@ -260,7 +254,6 @@ public class SampleChooserActivity extends AppCompatActivity
} }
} }
@OptIn(markerClass = androidx.media3.common.util.UnstableApi.class)
private void toggleDownload(MediaItem mediaItem) { private void toggleDownload(MediaItem mediaItem) {
RenderersFactory renderersFactory = RenderersFactory renderersFactory =
DemoUtil.buildRenderersFactory( DemoUtil.buildRenderersFactory(
@ -277,10 +270,6 @@ public class SampleChooserActivity extends AppCompatActivity
if (localConfiguration.adsConfiguration != null) { if (localConfiguration.adsConfiguration != null) {
return R.string.download_ads_unsupported; 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(); String scheme = localConfiguration.uri.getScheme();
if (!("http".equals(scheme) || "https".equals(scheme))) { if (!("http".equals(scheme) || "https".equals(scheme))) {
return R.string.download_scheme_unsupported; return R.string.download_scheme_unsupported;
@ -293,43 +282,34 @@ public class SampleChooserActivity extends AppCompatActivity
return menuItem != null && menuItem.isChecked(); return menuItem != null && menuItem.isChecked();
} }
private final class SampleListLoader { private final class SampleListLoader extends AsyncTask<String, Void, List<PlaylistGroup>> {
private final ExecutorService executorService;
private boolean sawError; private boolean sawError;
public SampleListLoader() { @OptIn(markerClass = androidx.media3.common.util.UnstableApi.class)
executorService = Executors.newSingleThreadExecutor(); @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;
} }
@OptIn(markerClass = androidx.media3.common.util.UnstableApi.class) @Override
public void execute(String... uris) { protected void onPostExecute(List<PlaylistGroup> result) {
executorService.execute( onPlaylistGroups(result, sawError);
() -> {
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, StandardCharsets.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) private void readPlaylistGroups(JsonReader reader, List<PlaylistGroup> groups)
@ -373,7 +353,6 @@ public class SampleChooserActivity extends AppCompatActivity
group.playlists.addAll(playlistHolders); group.playlists.addAll(playlistHolders);
} }
@OptIn(markerClass = UnstableApi.class) // Setting image duration.
private PlaylistHolder readEntry(JsonReader reader, boolean insidePlaylist) throws IOException { private PlaylistHolder readEntry(JsonReader reader, boolean insidePlaylist) throws IOException {
Uri uri = null; Uri uri = null;
String extension = null; String extension = null;
@ -411,9 +390,6 @@ public class SampleChooserActivity extends AppCompatActivity
case "clip_end_position_ms": case "clip_end_position_ms":
clippingConfiguration.setEndPositionMs(reader.nextLong()); clippingConfiguration.setEndPositionMs(reader.nextLong());
break; break;
case "image_duration_ms":
mediaItem.setImageDurationMs(reader.nextLong());
break;
case "ad_tag_uri": case "ad_tag_uri":
mediaItem.setAdsConfiguration( mediaItem.setAdsConfiguration(
new MediaItem.AdsConfiguration.Builder(Uri.parse(reader.nextString())).build()); new MediaItem.AdsConfiguration.Builder(Uri.parse(reader.nextString())).build());
@ -666,6 +642,7 @@ public class SampleChooserActivity extends AppCompatActivity
@RequiresApi(33) @RequiresApi(33)
private static class Api33 { private static class Api33 {
@DoNotInline
public static String getPostNotificationPermissionString() { public static String getPostNotificationPermissionString() {
return Manifest.permission.POST_NOTIFICATIONS; return Manifest.permission.POST_NOTIFICATIONS;
} }

View file

@ -67,8 +67,7 @@ public final class TrackSelectionDialog extends DialogFragment {
} }
public static final ImmutableList<Integer> SUPPORTED_TRACK_TYPES = public static final ImmutableList<Integer> SUPPORTED_TRACK_TYPES =
ImmutableList.of( ImmutableList.of(C.TRACK_TYPE_VIDEO, C.TRACK_TYPE_AUDIO, C.TRACK_TYPE_TEXT);
C.TRACK_TYPE_VIDEO, C.TRACK_TYPE_AUDIO, C.TRACK_TYPE_TEXT, C.TRACK_TYPE_IMAGE);
private final SparseArray<TrackSelectionViewFragment> tabFragments; private final SparseArray<TrackSelectionViewFragment> tabFragments;
private final ArrayList<Integer> tabTrackTypes; private final ArrayList<Integer> tabTrackTypes;
@ -267,13 +266,11 @@ public final class TrackSelectionDialog extends DialogFragment {
private static String getTrackTypeString(Resources resources, @C.TrackType int trackType) { private static String getTrackTypeString(Resources resources, @C.TrackType int trackType) {
switch (trackType) { switch (trackType) {
case C.TRACK_TYPE_VIDEO: case C.TRACK_TYPE_VIDEO:
return resources.getString(R.string.track_selection_title_video); return resources.getString(R.string.exo_track_selection_title_video);
case C.TRACK_TYPE_AUDIO: case C.TRACK_TYPE_AUDIO:
return resources.getString(R.string.track_selection_title_audio); return resources.getString(R.string.exo_track_selection_title_audio);
case C.TRACK_TYPE_TEXT: case C.TRACK_TYPE_TEXT:
return resources.getString(R.string.track_selection_title_text); return resources.getString(R.string.exo_track_selection_title_text);
case C.TRACK_TYPE_IMAGE:
return resources.getString(R.string.track_selection_title_image);
default: default:
throw new IllegalArgumentException(); throw new IllegalArgumentException();
} }

View file

@ -25,6 +25,8 @@
<string name="error_generic">Playback failed</string> <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_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> <string name="error_no_decoder">This device does not provide a decoder for <xliff:g id="mime_type">%1$s</xliff:g></string>
@ -57,16 +59,6 @@
<string name="download_ads_unsupported">IMA does not support offline ads</string> <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> <string name="prefer_extension_decoders">Prefer extension decoders</string>
<string name="track_selection_title_video">Video</string>
<string name="track_selection_title_audio">Audio</string>
<string name="track_selection_title_text">Text</string>
<string name="track_selection_title_image">Image</string>
</resources> </resources>

View file

@ -18,7 +18,7 @@ apply plugin: 'kotlin-android'
android { android {
namespace 'androidx.media3.demo.session' namespace 'androidx.media3.demo.session'
compileSdk project.ext.compileSdkVersion compileSdkVersion project.ext.compileSdkVersion
compileOptions { compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8 sourceCompatibility JavaVersion.VERSION_1_8
@ -34,6 +34,7 @@ android {
versionCode project.ext.releaseVersionCode versionCode project.ext.releaseVersionCode
minSdkVersion project.ext.minSdkVersion minSdkVersion project.ext.minSdkVersion
targetSdkVersion project.ext.appTargetSdkVersion targetSdkVersion project.ext.appTargetSdkVersion
multiDexEnabled true
} }
buildTypes { buildTypes {
@ -59,13 +60,11 @@ android {
dependencies { dependencies {
// For detecting and debugging leaks only. LeakCanary is not needed for demo app to work. // For detecting and debugging leaks only. LeakCanary is not needed for demo app to work.
debugImplementation 'com.squareup.leakcanary:leakcanary-android:' + leakCanaryVersion debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.10'
implementation 'androidx.core:core-ktx:' + androidxCoreVersion implementation 'androidx.core:core-ktx:' + androidxCoreVersion
implementation 'androidx.lifecycle:lifecycle-common:' + androidxLifecycleVersion
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:' + androidxLifecycleVersion
implementation 'androidx.appcompat:appcompat:' + androidxAppCompatVersion implementation 'androidx.appcompat:appcompat:' + androidxAppCompatVersion
implementation 'androidx.multidex:multidex:' + androidxMultidexVersion
implementation 'com.google.android.material:material:' + androidxMaterialVersion implementation 'com.google.android.material:material:' + androidxMaterialVersion
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-guava:' + kotlinxCoroutinesVersion
implementation project(modulePrefix + 'lib-ui') implementation project(modulePrefix + 'lib-ui')
implementation project(modulePrefix + 'lib-session') implementation project(modulePrefix + 'lib-session')
implementation project(modulePrefix + 'demo-session-service') implementation project(modulePrefix + 'demo-session-service')

View file

@ -14,6 +14,7 @@
limitations under the License. limitations under the License.
--> -->
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="androidx.media3.demo.session"> package="androidx.media3.demo.session">
<uses-sdk/> <uses-sdk/>
@ -22,10 +23,12 @@
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" />
<application <application
android:name="androidx.multidex.MultiDexApplication"
android:allowBackup="false" android:allowBackup="false"
android:icon="@mipmap/ic_launcher" android:icon="@mipmap/ic_launcher"
android:label="@string/app_name" android:label="@string/app_name"
android:theme="@style/Theme.Media3Demo"> android:theme="@style/Theme.Media3Demo"
tools:replace="android:name">
<!-- Declare that this session demo supports Android Auto. --> <!-- Declare that this session demo supports Android Auto. -->
<meta-data <meta-data
@ -56,7 +59,7 @@
android:foregroundServiceType="mediaPlayback" android:foregroundServiceType="mediaPlayback"
android:exported="true"> android:exported="true">
<intent-filter> <intent-filter>
<action android:name="androidx.media3.session.MediaLibraryService"/> <action android:name="androidx.media3.session.MediaSessionService"/>
<action android:name="android.media.browse.MediaBrowserService"/> <action android:name="android.media.browse.MediaBrowserService"/>
<action android:name="android.media.action.MEDIA_PLAY_FROM_SEARCH"/> <action android:name="android.media.action.MEDIA_PLAY_FROM_SEARCH"/>
</intent-filter> </intent-filter>

View file

@ -18,7 +18,6 @@ package androidx.media3.demo.session
import android.content.ComponentName import android.content.ComponentName
import android.content.Context import android.content.Context
import android.os.Bundle import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
@ -29,9 +28,6 @@ import android.widget.TextView
import androidx.annotation.OptIn import androidx.annotation.OptIn
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import androidx.media3.common.C.TRACK_TYPE_TEXT import androidx.media3.common.C.TRACK_TYPE_TEXT
import androidx.media3.common.MediaItem import androidx.media3.common.MediaItem
import androidx.media3.common.Player import androidx.media3.common.Player
@ -44,15 +40,13 @@ import androidx.media3.session.MediaController
import androidx.media3.session.SessionToken import androidx.media3.session.SessionToken
import androidx.media3.ui.PlayerView import androidx.media3.ui.PlayerView
import com.google.common.util.concurrent.ListenableFuture import com.google.common.util.concurrent.ListenableFuture
import kotlinx.coroutines.awaitCancellation import com.google.common.util.concurrent.MoreExecutors
import kotlinx.coroutines.guava.await
import kotlinx.coroutines.launch
private const val TAG = "PlayerActivity"
class PlayerActivity : AppCompatActivity() { class PlayerActivity : AppCompatActivity() {
private lateinit var controllerFuture: ListenableFuture<MediaController> private lateinit var controllerFuture: ListenableFuture<MediaController>
private lateinit var controller: MediaController private val controller: MediaController?
get() =
if (controllerFuture.isDone && !controllerFuture.isCancelled) controllerFuture.get() else null
private lateinit var playerView: PlayerView private lateinit var playerView: PlayerView
private lateinit var mediaItemListView: ListView private lateinit var mediaItemListView: ListView
@ -60,21 +54,8 @@ class PlayerActivity : AppCompatActivity() {
private val mediaItemList: MutableList<MediaItem> = mutableListOf() private val mediaItemList: MutableList<MediaItem> = mutableListOf()
private var lastMediaItemId: String? = null private var lastMediaItemId: String? = null
@OptIn(UnstableApi::class) // PlayerView.hideController
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
lifecycleScope.launch {
lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
try {
initializeController()
awaitCancellation()
} finally {
playerView.player = null
releaseController()
}
}
}
setContentView(R.layout.activity_player) setContentView(R.layout.activity_player)
playerView = findViewById(R.id.player_view) playerView = findViewById(R.id.player_view)
@ -83,8 +64,10 @@ class PlayerActivity : AppCompatActivity() {
mediaItemListView.adapter = mediaItemListAdapter mediaItemListView.adapter = mediaItemListAdapter
mediaItemListView.setOnItemClickListener { _, _, position, _ -> mediaItemListView.setOnItemClickListener { _, _, position, _ ->
run { run {
val controller = this.controller ?: return@run
if (controller.currentMediaItemIndex == position) { if (controller.currentMediaItemIndex == position) {
controller.playWhenReady = !controller.playWhenReady controller.playWhenReady = !controller.playWhenReady
@OptIn(UnstableApi::class) // PlayerView.hideController
if (controller.playWhenReady) { if (controller.playWhenReady) {
playerView.hideController() playerView.hideController()
} }
@ -96,15 +79,26 @@ class PlayerActivity : AppCompatActivity() {
} }
} }
private suspend fun initializeController() { override fun onStart() {
super.onStart()
initializeController()
}
override fun onStop() {
super.onStop()
playerView.player = null
releaseController()
}
private fun initializeController() {
controllerFuture = controllerFuture =
MediaController.Builder( MediaController.Builder(
this, this,
SessionToken(this, ComponentName(this, PlaybackService::class.java)), SessionToken(this, ComponentName(this, PlaybackService::class.java))
) )
.buildAsync() .buildAsync()
updateMediaMetadataUI() updateMediaMetadataUI()
setController() controllerFuture.addListener({ setController() }, MoreExecutors.directExecutor())
} }
private fun releaseController() { private fun releaseController() {
@ -112,13 +106,9 @@ class PlayerActivity : AppCompatActivity() {
} }
@OptIn(UnstableApi::class) // PlayerView.setShowSubtitleButton @OptIn(UnstableApi::class) // PlayerView.setShowSubtitleButton
private suspend fun setController() { private fun setController() {
try { val controller = this.controller ?: return
controller = controllerFuture.await()
} catch (t: Throwable) {
Log.w(TAG, "Failed to connect to MediaController", t)
return
}
playerView.player = controller playerView.player = controller
updateCurrentPlaylistUI() updateCurrentPlaylistUI()
@ -147,7 +137,8 @@ class PlayerActivity : AppCompatActivity() {
} }
private fun updateMediaMetadataUI() { private fun updateMediaMetadataUI() {
if (!::controller.isInitialized || controller.mediaItemCount == 0) { 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_title).text = getString(R.string.waiting_for_metadata)
findViewById<TextView>(R.id.media_artist).text = "" findViewById<TextView>(R.id.media_artist).text = ""
return return
@ -161,9 +152,7 @@ class PlayerActivity : AppCompatActivity() {
} }
private fun updateCurrentPlaylistUI() { private fun updateCurrentPlaylistUI() {
if (!::controller.isInitialized) { val controller = this.controller ?: return
return
}
mediaItemList.clear() mediaItemList.clear()
for (i in 0 until controller.mediaItemCount) { for (i in 0 until controller.mediaItemCount) {
mediaItemList.add(controller.getMediaItemAt(i)) mediaItemList.add(controller.getMediaItemAt(i))
@ -174,7 +163,7 @@ class PlayerActivity : AppCompatActivity() {
private inner class MediaItemListAdapter( private inner class MediaItemListAdapter(
context: Context, context: Context,
viewID: Int, viewID: Int,
mediaItemList: List<MediaItem>, mediaItemList: List<MediaItem>
) : ArrayAdapter<MediaItem>(context, viewID, mediaItemList) { ) : ArrayAdapter<MediaItem>(context, viewID, mediaItemList) {
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View { override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
val mediaItem = getItem(position)!! val mediaItem = getItem(position)!!
@ -184,7 +173,7 @@ class PlayerActivity : AppCompatActivity() {
returnConvertView.findViewById<TextView>(R.id.media_item).text = mediaItem.mediaMetadata.title returnConvertView.findViewById<TextView>(R.id.media_item).text = mediaItem.mediaMetadata.title
val deleteButton = returnConvertView.findViewById<Button>(R.id.delete_button) val deleteButton = returnConvertView.findViewById<Button>(R.id.delete_button)
if (::controller.isInitialized && position == controller.currentMediaItemIndex) { if (position == controller?.currentMediaItemIndex) {
// Styles for the current media item list item. // Styles for the current media item list item.
returnConvertView.setBackgroundColor( returnConvertView.setBackgroundColor(
ContextCompat.getColor(context, R.color.playlist_item_background) ContextCompat.getColor(context, R.color.playlist_item_background)
@ -203,6 +192,7 @@ class PlayerActivity : AppCompatActivity() {
.setTextColor(ContextCompat.getColor(context, R.color.white)) .setTextColor(ContextCompat.getColor(context, R.color.white))
deleteButton.visibility = View.VISIBLE deleteButton.visibility = View.VISIBLE
deleteButton.setOnClickListener { deleteButton.setOnClickListener {
val controller = this@PlayerActivity.controller ?: return@setOnClickListener
controller.removeMediaItem(position) controller.removeMediaItem(position)
updateCurrentPlaylistUI() updateCurrentPlaylistUI()
} }

View file

@ -22,16 +22,21 @@
android:background="@color/player_background" android:background="@color/player_background"
tools:context=".PlayerActivity"> tools:context=".PlayerActivity">
<androidx.media3.ui.PlayerView <androidx.media3.ui.AspectRatioFrameLayout
android:id="@+id/player_view"
android:background="@color/player_background"
android:layout_width="match_parent"
android:layout_height="300dp" android:layout_height="300dp"
app:artwork_display_mode="fill" android:layout_width="match_parent"
app:default_artwork="@drawable/artwork_placeholder" >
app:repeat_toggle_modes="one|all" <androidx.media3.ui.PlayerView
app:show_shuffle_button="true" android:id="@+id/player_view"
app:shutter_background_color="@color/player_background" /> android:background="@color/player_background"
android:layout_width="match_parent"
android:layout_height="match_parent"
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 <TextView
android:id="@+id/media_artist" android:id="@+id/media_artist"

View file

@ -13,7 +13,7 @@
See the License for the specific language governing permissions and See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
--> -->
<resources> <resources xmlns:tools="http://schemas.android.com/tools">
<!-- Base application theme. --> <!-- Base application theme. -->
<style name="Theme.Media3Demo" parent="Theme.MaterialComponents.DayNight.DarkActionBar"> <style name="Theme.Media3Demo" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
<!-- Primary brand color. --> <!-- Primary brand color. -->
@ -25,7 +25,9 @@
<item name="colorSecondaryVariant">@color/teal_200</item> <item name="colorSecondaryVariant">@color/teal_200</item>
<item name="colorOnSecondary">@color/black</item> <item name="colorOnSecondary">@color/black</item>
<!-- Status bar color. --> <!-- Status bar color. -->
<item name="android:statusBarColor">?attr/colorPrimaryVariant</item> <item name="android:statusBarColor" tools:targetApi="l">
?attr/colorPrimaryVariant
</item>
<!-- Customize your theme here. --> <!-- Customize your theme here. -->
</style> </style>
</resources> </resources>

View file

@ -16,11 +16,11 @@
<resources> <resources>
<string name="app_name">Media3 Session Demo</string> <string name="app_name">Media3 Session Demo</string>
<string name="current_playlist_name">Current playlist</string> <string name="current_playlist_name">Current playlist</string>
<string name="open_player_content_description">Click to view your playlist</string> <string name="open_player_content_description">Click to view your play list</string>
<string name="added_media_item_format">Added %1$s to playlist</string> <string name="added_media_item_format">Added %1$s to playlist</string>
<string name="shuffle">Shuffle</string> <string name="shuffle">Shuffle</string>
<string name="play_button">Play</string> <string name="play_button">Play</string>
<string name="waiting_for_metadata">Connecting</string> <string name="waiting_for_metadata">Waiting for playlist to load</string>
<string name="notification_permission_denied"> <string name="notification_permission_denied">
"Without notification access the app can't warn about failed background operations"</string> "Without notification access the app can't warn about failed background operations"</string>
</resources> </resources>

View file

@ -13,7 +13,7 @@
See the License for the specific language governing permissions and See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
--> -->
<resources> <resources xmlns:tools="http://schemas.android.com/tools">
<!-- Base application theme. --> <!-- Base application theme. -->
<style name="Theme.Media3Demo" parent="Theme.MaterialComponents.DayNight.DarkActionBar"> <style name="Theme.Media3Demo" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
<!-- Primary brand color. --> <!-- Primary brand color. -->
@ -25,7 +25,9 @@
<item name="colorSecondaryVariant">@color/teal_700</item> <item name="colorSecondaryVariant">@color/teal_700</item>
<item name="colorOnSecondary">@color/black</item> <item name="colorOnSecondary">@color/black</item>
<!-- Status bar color. --> <!-- Status bar color. -->
<item name="android:statusBarColor">?attr/colorPrimaryVariant</item> <item name="android:statusBarColor" tools:targetApi="l">
?attr/colorPrimaryVariant
</item>
<!-- Customize your theme here. --> <!-- Customize your theme here. -->
</style> </style>
</resources> </resources>

View file

@ -18,7 +18,7 @@ apply plugin: 'kotlin-android'
android { android {
namespace 'androidx.media3.demo.session.automotive' namespace 'androidx.media3.demo.session.automotive'
compileSdk project.ext.compileSdkVersion compileSdkVersion project.ext.compileSdkVersion
compileOptions { compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8 sourceCompatibility JavaVersion.VERSION_1_8
@ -34,6 +34,7 @@ android {
versionCode project.ext.releaseVersionCode versionCode project.ext.releaseVersionCode
minSdkVersion project.ext.automotiveMinSdkVersion minSdkVersion project.ext.automotiveMinSdkVersion
targetSdkVersion project.ext.appTargetSdkVersion targetSdkVersion project.ext.appTargetSdkVersion
multiDexEnabled true
} }
buildTypes { buildTypes {
@ -59,6 +60,7 @@ android {
dependencies { dependencies {
implementation 'androidx.core:core-ktx:' + androidxCoreVersion implementation 'androidx.core:core-ktx:' + androidxCoreVersion
implementation 'androidx.appcompat:appcompat:' + androidxAppCompatVersion implementation 'androidx.appcompat:appcompat:' + androidxAppCompatVersion
implementation 'androidx.multidex:multidex:' + androidxMultidexVersion
implementation 'com.google.android.material:material:' + androidxMaterialVersion implementation 'com.google.android.material:material:' + androidxMaterialVersion
implementation project(modulePrefix + 'lib-session') implementation project(modulePrefix + 'lib-session')
implementation project(modulePrefix + 'demo-session-service') implementation project(modulePrefix + 'demo-session-service')

View file

@ -14,6 +14,7 @@
limitations under the License. limitations under the License.
--> -->
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="androidx.media3.demo.session.automotive"> package="androidx.media3.demo.session.automotive">
<uses-sdk/> <uses-sdk/>
@ -38,11 +39,13 @@
android:resource="@xml/automotive_app_desc"/> android:resource="@xml/automotive_app_desc"/>
<application <application
android:name="androidx.multidex.MultiDexApplication"
android:allowBackup="false" android:allowBackup="false"
android:taskAffinity="" android:taskAffinity=""
android:appCategory="audio" android:appCategory="audio"
android:icon="@mipmap/ic_launcher" android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"> android:label="@string/app_name"
tools:replace="android:name">
<meta-data <meta-data
android:name="androidx.car.app.TintableAttributionIcon" android:name="androidx.car.app.TintableAttributionIcon"

View file

@ -17,7 +17,7 @@ apply plugin: 'kotlin-android'
android { android {
namespace 'androidx.media3.demo.session.service' namespace 'androidx.media3.demo.session.service'
compileSdk project.ext.compileSdkVersion compileSdkVersion project.ext.compileSdkVersion
compileOptions { compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8 sourceCompatibility JavaVersion.VERSION_1_8
@ -33,6 +33,7 @@ android {
versionCode project.ext.releaseVersionCode versionCode project.ext.releaseVersionCode
minSdkVersion project.ext.minSdkVersion minSdkVersion project.ext.minSdkVersion
targetSdkVersion project.ext.appTargetSdkVersion targetSdkVersion project.ext.appTargetSdkVersion
multiDexEnabled true
} }
buildTypes { buildTypes {
@ -53,6 +54,7 @@ android {
dependencies { dependencies {
implementation 'androidx.core:core-ktx:' + androidxCoreVersion implementation 'androidx.core:core-ktx:' + androidxCoreVersion
implementation 'androidx.appcompat:appcompat:' + androidxAppCompatVersion implementation 'androidx.appcompat:appcompat:' + androidxAppCompatVersion
implementation 'androidx.multidex:multidex:' + androidxMultidexVersion
implementation project(modulePrefix + 'lib-exoplayer') implementation project(modulePrefix + 'lib-exoplayer')
implementation project(modulePrefix + 'lib-exoplayer-dash') implementation project(modulePrefix + 'lib-exoplayer-dash')
implementation project(modulePrefix + 'lib-exoplayer-hls') implementation project(modulePrefix + 'lib-exoplayer-hls')

View file

@ -27,7 +27,6 @@ import androidx.media3.session.MediaLibraryService
import androidx.media3.session.MediaSession import androidx.media3.session.MediaSession
import androidx.media3.session.MediaSession.MediaItemsWithStartPosition import androidx.media3.session.MediaSession.MediaItemsWithStartPosition
import androidx.media3.session.SessionCommand import androidx.media3.session.SessionCommand
import androidx.media3.session.SessionError
import androidx.media3.session.SessionResult import androidx.media3.session.SessionResult
import com.google.common.collect.ImmutableList import com.google.common.collect.ImmutableList
import com.google.common.util.concurrent.Futures import com.google.common.util.concurrent.Futures
@ -41,17 +40,18 @@ open class DemoMediaLibrarySessionCallback(context: Context) :
MediaItemTree.initialize(context.assets) MediaItemTree.initialize(context.assets)
} }
@OptIn(UnstableApi::class) // TODO: b/328238954 - Remove once new CommandButton icons are stable.
private val customLayoutCommandButtons: List<CommandButton> = private val customLayoutCommandButtons: List<CommandButton> =
listOf( listOf(
CommandButton.Builder(CommandButton.ICON_SHUFFLE_OFF) CommandButton.Builder()
.setDisplayName(context.getString(R.string.exo_controls_shuffle_on_description)) .setDisplayName(context.getString(R.string.exo_controls_shuffle_on_description))
.setSessionCommand(SessionCommand(CUSTOM_COMMAND_TOGGLE_SHUFFLE_MODE_ON, Bundle.EMPTY)) .setSessionCommand(SessionCommand(CUSTOM_COMMAND_TOGGLE_SHUFFLE_MODE_ON, Bundle.EMPTY))
.setIconResId(R.drawable.exo_icon_shuffle_off)
.build(), .build(),
CommandButton.Builder(CommandButton.ICON_SHUFFLE_ON) CommandButton.Builder()
.setDisplayName(context.getString(R.string.exo_controls_shuffle_off_description)) .setDisplayName(context.getString(R.string.exo_controls_shuffle_off_description))
.setSessionCommand(SessionCommand(CUSTOM_COMMAND_TOGGLE_SHUFFLE_MODE_OFF, Bundle.EMPTY)) .setSessionCommand(SessionCommand(CUSTOM_COMMAND_TOGGLE_SHUFFLE_MODE_OFF, Bundle.EMPTY))
.build(), .setIconResId(R.drawable.exo_icon_shuffle_on)
.build()
) )
@OptIn(UnstableApi::class) // MediaSession.ConnectionResult.DEFAULT_SESSION_AND_LIBRARY_COMMANDS @OptIn(UnstableApi::class) // MediaSession.ConnectionResult.DEFAULT_SESSION_AND_LIBRARY_COMMANDS
@ -70,7 +70,7 @@ open class DemoMediaLibrarySessionCallback(context: Context) :
@OptIn(UnstableApi::class) @OptIn(UnstableApi::class)
override fun onConnect( override fun onConnect(
session: MediaSession, session: MediaSession,
controller: MediaSession.ControllerInfo, controller: MediaSession.ControllerInfo
): MediaSession.ConnectionResult { ): MediaSession.ConnectionResult {
if ( if (
session.isMediaNotificationController(controller) || session.isMediaNotificationController(controller) ||
@ -93,7 +93,7 @@ open class DemoMediaLibrarySessionCallback(context: Context) :
session: MediaSession, session: MediaSession,
controller: MediaSession.ControllerInfo, controller: MediaSession.ControllerInfo,
customCommand: SessionCommand, customCommand: SessionCommand,
args: Bundle, args: Bundle
): ListenableFuture<SessionResult> { ): ListenableFuture<SessionResult> {
if (CUSTOM_COMMAND_TOGGLE_SHUFFLE_MODE_ON == customCommand.customAction) { if (CUSTOM_COMMAND_TOGGLE_SHUFFLE_MODE_ON == customCommand.customAction) {
// Enable shuffling. // Enable shuffling.
@ -101,7 +101,7 @@ open class DemoMediaLibrarySessionCallback(context: Context) :
// Change the custom layout to contain the `Disable shuffling` command. // Change the custom layout to contain the `Disable shuffling` command.
session.setCustomLayout( session.setCustomLayout(
session.mediaNotificationControllerInfo!!, session.mediaNotificationControllerInfo!!,
ImmutableList.of(customLayoutCommandButtons[1]), ImmutableList.of(customLayoutCommandButtons[1])
) )
return Futures.immediateFuture(SessionResult(SessionResult.RESULT_SUCCESS)) return Futures.immediateFuture(SessionResult(SessionResult.RESULT_SUCCESS))
} else if (CUSTOM_COMMAND_TOGGLE_SHUFFLE_MODE_OFF == customCommand.customAction) { } else if (CUSTOM_COMMAND_TOGGLE_SHUFFLE_MODE_OFF == customCommand.customAction) {
@ -110,53 +110,51 @@ open class DemoMediaLibrarySessionCallback(context: Context) :
// Change the custom layout to contain the `Enable shuffling` command. // Change the custom layout to contain the `Enable shuffling` command.
session.setCustomLayout( session.setCustomLayout(
session.mediaNotificationControllerInfo!!, session.mediaNotificationControllerInfo!!,
ImmutableList.of(customLayoutCommandButtons[0]), ImmutableList.of(customLayoutCommandButtons[0])
) )
return Futures.immediateFuture(SessionResult(SessionResult.RESULT_SUCCESS)) return Futures.immediateFuture(SessionResult(SessionResult.RESULT_SUCCESS))
} }
return Futures.immediateFuture(SessionResult(SessionError.ERROR_NOT_SUPPORTED)) return Futures.immediateFuture(SessionResult(SessionResult.RESULT_ERROR_NOT_SUPPORTED))
} }
override fun onGetLibraryRoot( override fun onGetLibraryRoot(
session: MediaLibraryService.MediaLibrarySession, session: MediaLibraryService.MediaLibrarySession,
browser: MediaSession.ControllerInfo, browser: MediaSession.ControllerInfo,
params: MediaLibraryService.LibraryParams?, params: MediaLibraryService.LibraryParams?
): ListenableFuture<LibraryResult<MediaItem>> { ): ListenableFuture<LibraryResult<MediaItem>> {
return Futures.immediateFuture(LibraryResult.ofItem(MediaItemTree.getRootItem(), params)) return Futures.immediateFuture(LibraryResult.ofItem(MediaItemTree.getRootItem(), params))
} }
@OptIn(UnstableApi::class) // SessionError.ERROR_BAD_VALUE
override fun onGetItem( override fun onGetItem(
session: MediaLibraryService.MediaLibrarySession, session: MediaLibraryService.MediaLibrarySession,
browser: MediaSession.ControllerInfo, browser: MediaSession.ControllerInfo,
mediaId: String, mediaId: String
): ListenableFuture<LibraryResult<MediaItem>> { ): ListenableFuture<LibraryResult<MediaItem>> {
MediaItemTree.getItem(mediaId)?.let { MediaItemTree.getItem(mediaId)?.let {
return Futures.immediateFuture(LibraryResult.ofItem(it, /* params= */ null)) return Futures.immediateFuture(LibraryResult.ofItem(it, /* params= */ null))
} }
return Futures.immediateFuture(LibraryResult.ofError(SessionError.ERROR_BAD_VALUE)) return Futures.immediateFuture(LibraryResult.ofError(LibraryResult.RESULT_ERROR_BAD_VALUE))
} }
@OptIn(UnstableApi::class) // SessionError.ERROR_BAD_VALUE
override fun onGetChildren( override fun onGetChildren(
session: MediaLibraryService.MediaLibrarySession, session: MediaLibraryService.MediaLibrarySession,
browser: MediaSession.ControllerInfo, browser: MediaSession.ControllerInfo,
parentId: String, parentId: String,
page: Int, page: Int,
pageSize: Int, pageSize: Int,
params: MediaLibraryService.LibraryParams?, params: MediaLibraryService.LibraryParams?
): ListenableFuture<LibraryResult<ImmutableList<MediaItem>>> { ): ListenableFuture<LibraryResult<ImmutableList<MediaItem>>> {
val children = MediaItemTree.getChildren(parentId) val children = MediaItemTree.getChildren(parentId)
if (children.isNotEmpty()) { if (children.isNotEmpty()) {
return Futures.immediateFuture(LibraryResult.ofItemList(children, params)) return Futures.immediateFuture(LibraryResult.ofItemList(children, params))
} }
return Futures.immediateFuture(LibraryResult.ofError(SessionError.ERROR_BAD_VALUE)) return Futures.immediateFuture(LibraryResult.ofError(LibraryResult.RESULT_ERROR_BAD_VALUE))
} }
override fun onAddMediaItems( override fun onAddMediaItems(
mediaSession: MediaSession, mediaSession: MediaSession,
controller: MediaSession.ControllerInfo, controller: MediaSession.ControllerInfo,
mediaItems: List<MediaItem>, mediaItems: List<MediaItem>
): ListenableFuture<List<MediaItem>> { ): ListenableFuture<List<MediaItem>> {
return Futures.immediateFuture(resolveMediaItems(mediaItems)) return Futures.immediateFuture(resolveMediaItems(mediaItems))
} }
@ -167,7 +165,7 @@ open class DemoMediaLibrarySessionCallback(context: Context) :
browser: MediaSession.ControllerInfo, browser: MediaSession.ControllerInfo,
mediaItems: List<MediaItem>, mediaItems: List<MediaItem>,
startIndex: Int, startIndex: Int,
startPositionMs: Long, startPositionMs: Long
): ListenableFuture<MediaItemsWithStartPosition> { ): ListenableFuture<MediaItemsWithStartPosition> {
if (mediaItems.size == 1) { if (mediaItems.size == 1) {
// Try to expand a single item to a playlist. // Try to expand a single item to a playlist.
@ -196,7 +194,7 @@ open class DemoMediaLibrarySessionCallback(context: Context) :
private fun maybeExpandSingleItemToPlaylist( private fun maybeExpandSingleItemToPlaylist(
mediaItem: MediaItem, mediaItem: MediaItem,
startIndex: Int, startIndex: Int,
startPositionMs: Long, startPositionMs: Long
): MediaItemsWithStartPosition? { ): MediaItemsWithStartPosition? {
var playlist = listOf<MediaItem>() var playlist = listOf<MediaItem>()
var indexInPlaylist = startIndex var indexInPlaylist = startIndex
@ -225,7 +223,7 @@ open class DemoMediaLibrarySessionCallback(context: Context) :
session: MediaLibraryService.MediaLibrarySession, session: MediaLibraryService.MediaLibrarySession,
browser: MediaSession.ControllerInfo, browser: MediaSession.ControllerInfo,
query: String, query: String,
params: MediaLibraryService.LibraryParams?, params: MediaLibraryService.LibraryParams?
): ListenableFuture<LibraryResult<Void>> { ): ListenableFuture<LibraryResult<Void>> {
session.notifySearchResultChanged(browser, query, MediaItemTree.search(query).size, params) session.notifySearchResultChanged(browser, query, MediaItemTree.search(query).size, params)
return Futures.immediateFuture(LibraryResult.ofVoid()) return Futures.immediateFuture(LibraryResult.ofVoid())
@ -237,7 +235,7 @@ open class DemoMediaLibrarySessionCallback(context: Context) :
query: String, query: String,
page: Int, page: Int,
pageSize: Int, pageSize: Int,
params: MediaLibraryService.LibraryParams?, params: MediaLibraryService.LibraryParams?
): ListenableFuture<LibraryResult<ImmutableList<MediaItem>>> { ): ListenableFuture<LibraryResult<ImmutableList<MediaItem>>> {
return Futures.immediateFuture(LibraryResult.ofItemList(MediaItemTree.search(query), params)) return Futures.immediateFuture(LibraryResult.ofItemList(MediaItemTree.search(query), params))
} }

View file

@ -19,18 +19,17 @@ import android.Manifest
import android.app.NotificationChannel import android.app.NotificationChannel
import android.app.NotificationManager import android.app.NotificationManager
import android.app.PendingIntent import android.app.PendingIntent
import android.content.Intent
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.os.Build import android.os.Build
import androidx.annotation.OptIn import androidx.annotation.OptIn
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat import androidx.core.app.NotificationManagerCompat
import androidx.core.os.bundleOf
import androidx.media3.common.AudioAttributes import androidx.media3.common.AudioAttributes
import androidx.media3.common.util.UnstableApi import androidx.media3.common.util.UnstableApi
import androidx.media3.demo.session.service.R import androidx.media3.demo.session.service.R
import androidx.media3.exoplayer.ExoPlayer import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.exoplayer.util.EventLogger import androidx.media3.exoplayer.util.EventLogger
import androidx.media3.session.MediaConstants
import androidx.media3.session.MediaLibraryService import androidx.media3.session.MediaLibraryService
import androidx.media3.session.MediaSession import androidx.media3.session.MediaSession
import androidx.media3.session.MediaSession.ControllerInfo import androidx.media3.session.MediaSession.ControllerInfo
@ -91,6 +90,13 @@ open class DemoPlaybackService : MediaLibraryService() {
return mediaLibrarySession return mediaLibrarySession
} }
override fun onTaskRemoved(rootIntent: Intent?) {
val player = mediaLibrarySession.player
if (!player.playWhenReady || player.mediaItemCount == 0) {
stopSelf()
}
}
// MediaSession.setSessionActivity // MediaSession.setSessionActivity
// MediaSessionService.clearListener // MediaSessionService.clearListener
@OptIn(UnstableApi::class) @OptIn(UnstableApi::class)
@ -113,17 +119,6 @@ open class DemoPlaybackService : MediaLibraryService() {
MediaLibrarySession.Builder(this, player, createLibrarySessionCallback()) MediaLibrarySession.Builder(this, player, createLibrarySessionCallback())
.also { builder -> getSingleTopActivity()?.let { builder.setSessionActivity(it) } } .also { builder -> getSingleTopActivity()?.let { builder.setSessionActivity(it) } }
.build() .build()
.also { mediaLibrarySession ->
// The media session always supports skip, except at the start and end of the playlist.
// Reserve the space for the skip action in these cases to avoid custom actions jumping
// around when the user skips.
mediaLibrarySession.setSessionExtras(
bundleOf(
MediaConstants.EXTRAS_KEY_SLOT_RESERVATION_SEEK_TO_PREV to true,
MediaConstants.EXTRAS_KEY_SLOT_RESERVATION_SEEK_TO_NEXT to true,
)
)
}
} }
@OptIn(UnstableApi::class) // MediaSessionService.Listener @OptIn(UnstableApi::class) // MediaSessionService.Listener
@ -171,7 +166,7 @@ open class DemoPlaybackService : MediaLibraryService() {
NotificationChannel( NotificationChannel(
CHANNEL_ID, CHANNEL_ID,
getString(R.string.notification_channel_name), getString(R.string.notification_channel_name),
NotificationManager.IMPORTANCE_DEFAULT, NotificationManager.IMPORTANCE_DEFAULT
) )
notificationManagerCompat.createNotificationChannel(channel) notificationManagerCompat.createNotificationChannel(channel)
} }

View file

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

View file

@ -1,94 +0,0 @@
// 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'
compileSdk project.ext.compileSdkVersion
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = '1.8'
}
defaultConfig {
versionName project.ext.releaseVersion
versionCode project.ext.releaseVersionCode
minSdkVersion project.ext.minSdkVersion
targetSdkVersion project.ext.appTargetSdkVersion
}
buildTypes {
release {
shrinkResources true
minifyEnabled true
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 'com.google.android.material:material:' + androidxMaterialVersion
implementation project(modulePrefix + 'lib-exoplayer')
implementation project(modulePrefix + 'lib-exoplayer-dash')
implementation project(modulePrefix + 'lib-exoplayer-hls')
implementation project(modulePrefix + 'lib-ui')
testImplementation 'androidx.test:core:' + androidxTestCoreVersion
testImplementation 'androidx.test.ext:junit:' + androidxTestJUnitVersion
testImplementation 'junit:junit:' + junitVersion
testImplementation 'com.google.truth:truth:' + truthVersion
testImplementation 'org.robolectric:robolectric:' + robolectricVersion
testImplementation 'org.robolectric:robolectric:' + robolectricVersion
}

View file

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

View file

@ -1,43 +0,0 @@
<?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.shortform">
<application
android:allowBackup="false"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:theme="@style/Theme.MaterialComponents.DayNight.NoActionBar"
android:taskAffinity="">
<activity
android:exported="true"
android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity
android:exported="false"
android:label="@string/title_activity_view_pager"
android:name=".viewpager.ViewPagerActivity"/>
</application>
<uses-permission android:name="android.permission.INTERNET" />
<uses-sdk />
</manifest>

View file

@ -1,64 +0,0 @@
/*
* 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))
}
}
}
)
findViewById<View>(R.id.view_pager_button).setOnClickListener {
startActivity(
Intent(this, ViewPagerActivity::class.java).putExtra(NUM_PLAYERS_EXTRA, numberOfPlayers)
)
}
}
companion object {
const val NUM_PLAYERS_EXTRA = "number_of_players"
}
}

View file

@ -1,35 +0,0 @@
/*
* 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
class MediaItemDatabase {
private val mediaUris =
mutableListOf(
"https://storage.googleapis.com/exoplayer-test-media-0/shortform_1.mp4",
"https://storage.googleapis.com/exoplayer-test-media-0/shortform_2.mp4",
"https://storage.googleapis.com/exoplayer-test-media-0/shortform_3.mp4",
"https://storage.googleapis.com/exoplayer-test-media-0/shortform_4.mp4",
"https://storage.googleapis.com/exoplayer-test-media-0/shortform_6.mp4",
)
fun get(index: Int): MediaItem {
val uri = mediaUris.get(index.mod(mediaUris.size))
return MediaItem.Builder().setUri(uri).setMediaId(index.toString()).build()
}
}

View file

@ -1,129 +0,0 @@
/*
* 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.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.source.preload.DefaultPreloadManager.Builder
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
@OptIn(UnstableApi::class)
class PlayerPool(private val numberOfPlayers: Int, preloadManagerBuilder: Builder) {
/** 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(preloadManagerBuilder)
fun acquirePlayer(token: Int, callback: (ExoPlayer) -> Unit) {
synchronized(playerMap) {
// 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 (playerMap.size < numberOfPlayers) {
val player = playerFactory.createPlayer()
playerMap[playerMap.size] = player
callback.invoke(player)
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.
*/
private fun pauseAllPlayers(keepOngoingPlayer: Player? = null) {
for (player in playerMap.values) {
if (player != keepOngoingPlayer) {
player.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 preloadManagerBuilder: Builder) : PlayerFactory {
private var playerCounter = 0
override fun createPlayer(): ExoPlayer {
val player = preloadManagerBuilder.buildExoPlayer()
player.addAnalyticsListener(EventLogger("player-$playerCounter"))
playerCounter++
player.repeatMode = ExoPlayer.REPEAT_MODE_ONE
return player
}
}
}

View file

@ -1,63 +0,0 @@
/*
* 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.demo.shortform.MainActivity
import androidx.media3.demo.shortform.MediaItemDatabase
import androidx.media3.demo.shortform.R
import androidx.viewpager2.widget.ViewPager2
class ViewPagerActivity : AppCompatActivity() {
private lateinit var viewPagerView: ViewPager2
private lateinit var onPageChangeCallback: ViewPager2.OnPageChangeCallback
private var numberOfPlayers = 3
private var mediaItemDatabase = MediaItemDatabase()
companion object {
private const val TAG = "ViewPagerActivity"
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_view_pager)
numberOfPlayers = intent.getIntExtra(MainActivity.NUM_PLAYERS_EXTRA, numberOfPlayers)
Log.d(TAG, "Using a pool of $numberOfPlayers players")
viewPagerView = findViewById(R.id.viewPager)
viewPagerView.offscreenPageLimit = 1
}
override fun onStart() {
super.onStart()
val adapter = ViewPagerMediaAdapter(mediaItemDatabase, numberOfPlayers, applicationContext)
viewPagerView.adapter = adapter
onPageChangeCallback =
object : ViewPager2.OnPageChangeCallback() {
override fun onPageSelected(position: Int) {
adapter.onPageSelected(position)
}
}
viewPagerView.registerOnPageChangeCallback(onPageChangeCallback)
}
override fun onStop() {
viewPagerView.unregisterOnPageChangeCallback(onPageChangeCallback)
viewPagerView.adapter = null
super.onStop()
}
}

View file

@ -1,187 +0,0 @@
/*
* 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.view.LayoutInflater
import android.view.ViewGroup
import androidx.annotation.OptIn
import androidx.media3.common.C
import androidx.media3.common.MediaItem
import androidx.media3.common.util.Log
import androidx.media3.common.util.UnstableApi
import androidx.media3.demo.shortform.MediaItemDatabase
import androidx.media3.demo.shortform.PlayerPool
import androidx.media3.demo.shortform.R
import androidx.media3.exoplayer.DefaultLoadControl
import androidx.media3.exoplayer.source.preload.DefaultPreloadManager
import androidx.media3.exoplayer.source.preload.DefaultPreloadManager.Status.STAGE_LOADED_FOR_DURATION_MS
import androidx.media3.exoplayer.source.preload.TargetPreloadStatusControl
import androidx.recyclerview.widget.RecyclerView
import kotlin.math.abs
@OptIn(UnstableApi::class)
class ViewPagerMediaAdapter(
private val mediaItemDatabase: MediaItemDatabase,
numberOfPlayers: Int,
context: Context,
) : RecyclerView.Adapter<ViewPagerMediaHolder>() {
private val preloadManager: DefaultPreloadManager
private val currentMediaItemsAndIndexes: ArrayDeque<Pair<MediaItem, Int>> = ArrayDeque()
private var playerPool: PlayerPool
private val holderMap: MutableMap<Int, ViewPagerMediaHolder>
private val preloadControl: DefaultPreloadControl
companion object {
private const val TAG = "ViewPagerMediaAdapter"
private const val LOAD_CONTROL_MIN_BUFFER_MS = 5_000
private const val LOAD_CONTROL_MAX_BUFFER_MS = 20_000
private const val LOAD_CONTROL_BUFFER_FOR_PLAYBACK_MS = 500
private const val MANAGED_ITEM_COUNT = 10
private const val ITEM_ADD_REMOVE_COUNT = 4
}
init {
val loadControl =
DefaultLoadControl.Builder()
.setBufferDurationsMs(
LOAD_CONTROL_MIN_BUFFER_MS,
LOAD_CONTROL_MAX_BUFFER_MS,
LOAD_CONTROL_BUFFER_FOR_PLAYBACK_MS,
DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS,
)
.setPrioritizeTimeOverSizeThresholds(true)
.build()
preloadControl = DefaultPreloadControl()
val preloadManagerBuilder =
DefaultPreloadManager.Builder(context.applicationContext, preloadControl)
.setLoadControl(loadControl)
playerPool = PlayerPool(numberOfPlayers, preloadManagerBuilder)
holderMap = mutableMapOf()
preloadManager = preloadManagerBuilder.build()
for (i in 0 until MANAGED_ITEM_COUNT) {
addMediaItem(index = i, isAddingToRightEnd = true)
}
preloadManager.invalidate()
}
override fun onDetachedFromRecyclerView(recyclerView: RecyclerView) {
playerPool.destroyPlayers()
preloadManager.release()
holderMap.clear()
super.onDetachedFromRecyclerView(recyclerView)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewPagerMediaHolder {
val view =
LayoutInflater.from(parent.context).inflate(R.layout.media_item_view_pager, parent, false)
val holder = ViewPagerMediaHolder(view, playerPool)
view.addOnAttachStateChangeListener(holder)
return holder
}
override fun onBindViewHolder(holder: ViewPagerMediaHolder, position: Int) {
val mediaItem = mediaItemDatabase.get(position)
Log.d(TAG, "onBindViewHolder: Getting item at position $position")
var currentMediaSource = preloadManager.getMediaSource(mediaItem)
if (currentMediaSource == null) {
preloadManager.add(mediaItem, position)
currentMediaSource = preloadManager.getMediaSource(mediaItem)!!
}
holder.bindData(currentMediaSource)
}
override fun onViewAttachedToWindow(holder: ViewPagerMediaHolder) {
val holderBindingAdapterPosition = holder.bindingAdapterPosition
holderMap[holderBindingAdapterPosition] = holder
if (!currentMediaItemsAndIndexes.isEmpty()) {
val leftMostIndex = currentMediaItemsAndIndexes.first().second
val rightMostIndex = currentMediaItemsAndIndexes.last().second
if (rightMostIndex - holderBindingAdapterPosition <= 2) {
Log.d(TAG, "onViewAttachedToWindow: Approaching to the rightmost item")
for (i in 1 until ITEM_ADD_REMOVE_COUNT + 1) {
addMediaItem(index = rightMostIndex + i, isAddingToRightEnd = true)
removeMediaItem(isRemovingFromRightEnd = false)
}
} else if (holderBindingAdapterPosition - leftMostIndex <= 2) {
Log.d(TAG, "onViewAttachedToWindow: Approaching to the leftmost item")
for (i in 1 until ITEM_ADD_REMOVE_COUNT + 1) {
addMediaItem(index = leftMostIndex - i, isAddingToRightEnd = false)
removeMediaItem(isRemovingFromRightEnd = true)
}
}
}
}
override fun onViewDetachedFromWindow(holder: ViewPagerMediaHolder) {
holderMap.remove(holder.bindingAdapterPosition)
}
override fun getItemCount(): Int {
// Effectively infinite scroll
return Int.MAX_VALUE
}
fun onPageSelected(position: Int) {
holderMap[position]?.playIfPossible()
preloadControl.currentPlayingIndex = position
preloadManager.setCurrentPlayingIndex(position)
preloadManager.invalidate()
}
private fun addMediaItem(index: Int, isAddingToRightEnd: Boolean) {
if (index < 0) {
return
}
Log.d(TAG, "addMediaItem: Adding item at index $index")
val mediaItem = mediaItemDatabase.get(index)
preloadManager.add(mediaItem, index)
if (isAddingToRightEnd) {
currentMediaItemsAndIndexes.addLast(Pair(mediaItem, index))
} else {
currentMediaItemsAndIndexes.addFirst(Pair(mediaItem, index))
}
}
private fun removeMediaItem(isRemovingFromRightEnd: Boolean) {
if (currentMediaItemsAndIndexes.size <= MANAGED_ITEM_COUNT) {
return
}
val itemAndIndex =
if (isRemovingFromRightEnd) {
currentMediaItemsAndIndexes.removeLast()
} else {
currentMediaItemsAndIndexes.removeFirst()
}
Log.d(TAG, "removeMediaItem: Removing item at index ${itemAndIndex.second}")
preloadManager.remove(itemAndIndex.first)
}
inner class DefaultPreloadControl(var currentPlayingIndex: Int = C.INDEX_UNSET) :
TargetPreloadStatusControl<Int> {
override fun getTargetPreloadStatus(rankingData: Int): DefaultPreloadManager.Status? {
if (abs(rankingData - currentPlayingIndex) == 2) {
return DefaultPreloadManager.Status(STAGE_LOADED_FOR_DURATION_MS, 500L)
} else if (abs(rankingData - currentPlayingIndex) == 1) {
return DefaultPreloadManager.Status(STAGE_LOADED_FOR_DURATION_MS, 1000L)
}
return null
}
}
}

View file

@ -1,111 +0,0 @@
/*
* 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.view.View
import androidx.annotation.OptIn
import androidx.media3.common.util.Log
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.MediaSource
import androidx.media3.ui.PlayerView
import androidx.recyclerview.widget.RecyclerView
@OptIn(UnstableApi::class)
class ViewPagerMediaHolder(itemView: View, 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 pendingPlayRequestUponSetupPlayer: Boolean = false
private lateinit var mediaSource: MediaSource
companion object {
private const val TAG = "ViewPagerMediaHolder"
}
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 }
}
}
}
private val player: ExoPlayer?
get() {
return exoPlayer
}
override fun onViewAttachedToWindow(view: View) {
Log.d(TAG, "onViewAttachedToWindow: $bindingAdapterPosition")
isInView = true
if (player == null) {
playerPool.acquirePlayer(bindingAdapterPosition, ::setupPlayer)
}
}
override fun onViewDetachedFromWindow(view: View) {
Log.d(TAG, "onViewDetachedFromWindow: $bindingAdapterPosition")
isInView = false
releasePlayer(exoPlayer)
}
fun bindData(mediaSource: MediaSource) {
this.mediaSource = mediaSource
}
fun playIfPossible() {
player?.let { playerPool.play(it) }
if (player == null) {
Log.d(TAG, "playIfPossible: The player hasn't been setup yet")
pendingPlayRequestUponSetupPlayer = true
}
}
private fun releasePlayer(player: ExoPlayer?) {
playerPool.releasePlayer(bindingAdapterPosition, player ?: exoPlayer)
this.exoPlayer = null
playerView.player = null
}
private 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
if (pendingPlayRequestUponSetupPlayer) {
playerPool.play(player)
pendingPlayRequestUponSetupPlayer = false
}
}
}
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

View file

@ -1,48 +0,0 @@
<?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="number"
android:textColorHint="@color/grey" />
</LinearLayout>

View file

@ -1,29 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright 2023 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".viewpager.ViewPagerActivity">
<androidx.viewpager2.widget.ViewPager2
android:id="@+id/viewPager"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</FrameLayout>

View file

@ -1,32 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright 2023 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".viewpager.ViewPagerActivity">
<androidx.media3.ui.PlayerView android:id="@+id/player_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:use_controller="false"
app:resize_mode="fill"
app:show_shuffle_button="true"
app:show_subtitle_button="true"/>
</LinearLayout>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

View file

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

View file

@ -1,21 +0,0 @@
<?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>
</resources>

View file

@ -1,31 +0,0 @@
<?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>
<!-- 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">?attr/colorPrimaryVariant</item>
<!-- Customize your theme here. -->
</style>
</resources>

View file

@ -17,7 +17,7 @@ apply plugin: 'com.android.application'
android { android {
namespace 'androidx.media3.demo.surface' namespace 'androidx.media3.demo.surface'
compileSdk project.ext.compileSdkVersion compileSdkVersion project.ext.compileSdkVersion
compileOptions { compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8 sourceCompatibility JavaVersion.VERSION_1_8

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