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
description: What version of Media3 (or ExoPlayer) are you using?
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.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.0 / ExoPlayer 2.19.0
- Media3 1.0.2 / ExoPlayer 2.18.7

41
.gitignore vendored
View file

@ -52,31 +52,30 @@ tmp
# External native builds
.externalNativeBuild
.cxx
# VP9 decoder extension
libraries/decoder_vp9/src/main/jni/libvpx
libraries/decoder_vp9/src/main/jni/libvpx_android_configs
libraries/decoder_vp9/src/main/jni/libyuv
# VP9 extension
extensions/vp9/src/main/jni/libvpx
extensions/vp9/src/main/jni/libvpx_android_configs
extensions/vp9/src/main/jni/libyuv
# AV1 decoder extension
libraries/decoder_av1/src/main/jni/cpu_features
libraries/decoder_av1/src/main/jni/libgav1
# AV1 extension
extensions/av1/src/main/jni/cpu_features
extensions/av1/src/main/jni/libgav1
# Opus decoder extension
libraries/decoder_opus/src/main/jni/libopus
# Opus extension
extensions/opus/src/main/jni/libopus
# FLAC decoder extension
libraries/decoder_flac/src/main/jni/flac
# FLAC extension
extensions/flac/src/main/jni/flac
# FFmpeg decoder extension
libraries/decoder_ffmpeg/src/main/jni/ffmpeg
# FFmpeg extension
extensions/ffmpeg/src/main/jni/ffmpeg
# Cronet datasource extension
libraries/datasource_cronet/jniLibs/*
!libraries/datasource_cronet/jniLibs/README.md
libraries/datasource_cronet/libs/*
!libraries/datasource_cronet/libs/README.md
# Cronet extension
extensions/cronet/jniLibs/*
!extensions/cronet/jniLibs/README.md
extensions/cronet/libs/*
!extensions/cronet/libs/README.md
# MIDI decoder extension
libraries/decoder_midi/lib
# MIDI extension
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
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
git clone https://github.com/androidx/media.git
cd media
```
Next, add the following to your project's `settings.gradle.kts` file, replacing
`path/to/media` with the path to your local copy:
```kotlin
(gradle as ExtensionAware).extra["androidxMediaModulePrefix"] = "media3-"
gradle.extra.apply {
set("androidxMediaModulePrefix", "media-")
}
apply(from = file("path/to/media/core_settings.gradle"))
```
Or in Gradle Groovy DSL `settings.gradle`:
```groovy
gradle.ext.androidxMediaModulePrefix = 'media3-'
gradle.ext.androidxMediaModulePrefix = 'media-'
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:
```kotlin
implementation(project(":media3-lib-exoplayer"))
implementation(project(":media3-lib-exoplayer-dash"))
implementation(project(":media3-lib-ui"))
implementation(project(":media-lib-exoplayer"))
implementation(project(":media-lib-exoplayer-dash"))
implementation(project(":media-lib-ui"))
```
Or in Gradle Groovy DSL `build.gradle`:
```groovy
implementation project(':media3-lib-exoplayer')
implementation project(':media3-lib-exoplayer-dash')
implementation project(':media3-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
implementation project(':media-lib-exoplayer')
implementation project(':media-lib-exoplayer-dash')
implementation project(':media-lib-ui')
```
## 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 {
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 @androidx.media3.common.C.AudioAllowedCapturePolicy public final int allowedCapturePolicy;
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;
}
public static final class AudioAttributes.AudioAttributesV21 {
@RequiresApi(21) public static final class AudioAttributes.AudioAttributesV21 {
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 float RATE_UNSET = -3.4028235E38f;
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_COMMENTARY = 8; // 0x8
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(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 {
@ -549,7 +548,6 @@ package androidx.media3.common {
field public static final String APPLICATION_PGS = "application/pgs";
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_SDP = "application/sdp";
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_TTML = "application/ttml+xml";
@ -623,20 +621,13 @@ package androidx.media3.common {
method public static String getErrorCodeName(@androidx.media3.common.PlaybackException.ErrorCode int);
field public static final int CUSTOM_ERROR_CODE_BASE = 1000000; // 0xf4240
field public static final int ERROR_CODE_AUDIO_TRACK_INIT_FAILED = 5001; // 0x1389
field public static final int ERROR_CODE_AUDIO_TRACK_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_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_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_QUERY_FAILED = 4002; // 0xfa2
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_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_DEVICE_REVOKED = 6007; // 0x1777
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_SYSTEM_ERROR = 6006; // 0x1776
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_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_CLEARTEXT_NOT_PERMITTED = 2007; // 0x7d7
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_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_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_UNSUPPORTED = 3003; // 0xbbb
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_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_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_UNSPECIFIED = 1000; // 0x3e8
field @androidx.media3.common.PlaybackException.ErrorCode public final int errorCode;
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 {
@ -781,7 +763,7 @@ package androidx.media3.common {
method @Deprecated public void setDeviceMuted(boolean);
method public void setDeviceMuted(boolean, @androidx.media3.common.C.VolumeFlags int);
method @Deprecated public void setDeviceVolume(@IntRange(from=0) int);
method public void setDeviceVolume(@IntRange(from=0) int, @androidx.media3.common.C.VolumeFlags int);
method public void setDeviceVolume(@IntRange(from=0) int, int);
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, 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_SEEK = 1; // 0x1
field public static final int DISCONTINUITY_REASON_SEEK_ADJUSTMENT = 2; // 0x2
field public static final int DISCONTINUITY_REASON_SILENCE_SKIP = 6; // 0x6
field public static final int DISCONTINUITY_REASON_SKIP = 3; // 0x3
field public static final int EVENT_AUDIO_ATTRIBUTES_CHANGED = 20; // 0x14
field public static final int EVENT_AUDIO_SESSION_ID = 21; // 0x15
@ -913,7 +894,7 @@ package androidx.media3.common {
field public static final androidx.media3.common.Player.Commands EMPTY;
}
@IntDef({androidx.media3.common.Player.DISCONTINUITY_REASON_AUTO_TRANSITION, androidx.media3.common.Player.DISCONTINUITY_REASON_SEEK, androidx.media3.common.Player.DISCONTINUITY_REASON_SEEK_ADJUSTMENT, androidx.media3.common.Player.DISCONTINUITY_REASON_SKIP, androidx.media3.common.Player.DISCONTINUITY_REASON_REMOVE, androidx.media3.common.Player.DISCONTINUITY_REASON_INTERNAL, 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 {
@ -1094,7 +1075,7 @@ package androidx.media3.common {
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 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 boolean forceHighestSupportedBitrate;
field public final boolean forceLowestBitrate;
@ -1192,7 +1173,7 @@ package androidx.media3.common {
field public static final androidx.media3.common.VideoSize UNKNOWN;
field @IntRange(from=0) public final int height;
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;
}
@ -1388,7 +1369,7 @@ package androidx.media3.exoplayer.analytics {
package androidx.media3.exoplayer.drm {
public final class FrameworkMediaDrm {
@RequiresApi(18) public final class FrameworkMediaDrm {
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();
}
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 {
@ -1438,7 +1396,6 @@ package androidx.media3.exoplayer.source {
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 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 {
@ -1527,7 +1484,7 @@ package androidx.media3.session {
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 {
@ -1803,7 +1760,6 @@ package androidx.media3.session {
method public int getControllerVersion();
method public String getPackageName();
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
}
@ -1852,7 +1808,6 @@ package androidx.media3.session {
ctor public SessionCommands.Builder();
method public androidx.media3.session.SessionCommands.Builder add(androidx.media3.session.SessionCommand);
method public androidx.media3.session.SessionCommands.Builder add(@androidx.media3.session.SessionCommand.CommandCode int);
method public androidx.media3.session.SessionCommands.Builder addSessionCommands(java.util.Collection<androidx.media3.session.SessionCommand>);
method public androidx.media3.session.SessionCommands build();
method public androidx.media3.session.SessionCommands.Builder remove(androidx.media3.session.SessionCommand);
method public androidx.media3.session.SessionCommands.Builder remove(@androidx.media3.session.SessionCommand.CommandCode int);
@ -1882,7 +1837,7 @@ package androidx.media3.session {
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 {

View file

@ -17,9 +17,9 @@ buildscript {
mavenCentral()
}
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 'org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.10'
classpath 'org.jetbrains.kotlin:kotlin-gradle-plugin:1.8.20'
}
}
allprojects {

View file

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

View file

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

View file

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

View file

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

View file

@ -23,6 +23,7 @@
<uses-sdk/>
<application
android:name="androidx.multidex.MultiDexApplication"
android:label="@string/application_name"
android:icon="@mipmap/ic_launcher"
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");
* 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
* limitations under the License.
*/
@NonNullApi
@OptIn(markerClass = UnstableApi.class)
package androidx.media3.demo.composition;
package androidx.media3.demo.cast;
import androidx.annotation.OptIn;
import androidx.media3.common.util.NonNullApi;
import androidx.media3.common.util.UnstableApi;
import androidx.multidex.MultiDexApplication;
// 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.CastContext;
import com.google.android.gms.dynamite.DynamiteModule;
import com.google.common.util.concurrent.MoreExecutors;
/**
* An activity that plays video using {@link ExoPlayer} and supports casting using ExoPlayer's Cast
@ -66,7 +65,7 @@ public class MainActivity extends AppCompatActivity
super.onCreate(savedInstanceState);
// Getting the cast context later than onStart can cause device discovery not to take place.
try {
castContext = CastContext.getSharedInstance(this, MoreExecutors.directExecutor()).getResult();
castContext = CastContext.getSharedInstance(this);
} catch (RuntimeException e) {
Throwable cause = e.getCause();
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 {
namespace 'androidx.media3.demo.gl'
compileSdk project.ext.compileSdkVersion
compileSdkVersion project.ext.compileSdkVersion
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
@ -29,6 +29,7 @@ android {
versionCode project.ext.releaseVersionCode
minSdkVersion project.ext.minSdkVersion
targetSdkVersion project.ext.appTargetSdkVersion
multiDexEnabled true
}
buildTypes {
@ -54,5 +55,6 @@ dependencies {
implementation project(modulePrefix + 'lib-exoplayer-smoothstreaming')
implementation project(modulePrefix + 'lib-ui')
implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion
implementation 'androidx.multidex:multidex:' + androidxMultidexVersion
compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion
}

View file

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

View file

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

View file

@ -143,7 +143,7 @@ public final class MainActivity extends Activity {
? Assertions.checkNotNull(intent.getData())
: Uri.parse(DEFAULT_MEDIA_URI);
DrmSessionManager drmSessionManager;
if (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 drmLicenseUrl = Assertions.checkNotNull(intent.getStringExtra(DRM_LICENSE_URL_EXTRA));
UUID drmSchemeUuid = Assertions.checkNotNull(Util.getDrmUuid(drmScheme));

View file

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

View file

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

View file

@ -593,27 +593,6 @@
"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": [
{
"name": "Dizzy (MP4)",
@ -758,44 +737,6 @@
{
"name": "One hour frame counter (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
protected Scheduler getScheduler() {
return new PlatformScheduler(this, JOB_ID);
return Util.SDK_INT >= 21 ? new PlatformScheduler(this, JOB_ID) : null;
}
@Override

View file

@ -16,16 +16,12 @@
package androidx.media3.demo.main;
import android.content.Context;
import android.net.http.HttpEngine;
import android.os.Build;
import android.os.ext.SdkExtensions;
import androidx.annotation.OptIn;
import androidx.media3.database.DatabaseProvider;
import androidx.media3.database.StandaloneDatabaseProvider;
import androidx.media3.datasource.DataSource;
import androidx.media3.datasource.DefaultDataSource;
import androidx.media3.datasource.DefaultHttpDataSource;
import androidx.media3.datasource.HttpEngineDataSource;
import androidx.media3.datasource.cache.Cache;
import androidx.media3.datasource.cache.CacheDataSource;
import androidx.media3.datasource.cache.NoOpCacheEvictor;
@ -50,26 +46,25 @@ public final class DemoUtil {
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 DOWNLOAD_CONTENT_DIRECTORY = "downloads";
private static DataSource.@MonotonicNonNull Factory dataSourceFactory;
private static DataSource.@MonotonicNonNull Factory httpDataSourceFactory;
@OptIn(markerClass = androidx.media3.common.util.UnstableApi.class)
private static @MonotonicNonNull DatabaseProvider databaseProvider;
private static @MonotonicNonNull File downloadDirectory;
@OptIn(markerClass = androidx.media3.common.util.UnstableApi.class)
private static @MonotonicNonNull Cache downloadCache;
@OptIn(markerClass = androidx.media3.common.util.UnstableApi.class)
private static @MonotonicNonNull DownloadManager downloadManager;
private static @MonotonicNonNull DownloadTracker downloadTracker;
@OptIn(markerClass = androidx.media3.common.util.UnstableApi.class)
private static @MonotonicNonNull DownloadNotificationHelper downloadNotificationHelper;
/** Returns whether extension renderers should be used. */
@ -91,30 +86,24 @@ public final class DemoUtil {
.setExtensionRendererMode(extensionRendererMode);
}
@OptIn(markerClass = androidx.media3.common.util.UnstableApi.class)
public static synchronized DataSource.Factory getHttpDataSourceFactory(Context context) {
if (httpDataSourceFactory != null) {
return httpDataSourceFactory;
if (httpDataSourceFactory == null) {
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;
}
@ -139,7 +128,6 @@ public final class DemoUtil {
return downloadNotificationHelper;
}
@OptIn(markerClass = androidx.media3.common.util.UnstableApi.class)
public static synchronized DownloadManager getDownloadManager(Context context) {
ensureDownloadManagerInitialized(context);
return downloadManager;

View file

@ -16,16 +16,15 @@
package androidx.media3.demo.main;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;
import android.content.Context;
import android.content.DialogInterface;
import android.net.Uri;
import android.os.Handler;
import android.os.Looper;
import android.os.AsyncTask;
import android.widget.Toast;
import androidx.annotation.Nullable;
import androidx.annotation.OptIn;
import androidx.annotation.RequiresApi;
import androidx.fragment.app.FragmentManager;
import androidx.media3.common.C;
import androidx.media3.common.DrmInitData;
@ -55,9 +54,6 @@ import java.io.IOException;
import java.util.HashMap;
import java.util.UUID;
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
/** Tracks media that has been downloaded. */
@OptIn(markerClass = androidx.media3.common.util.UnstableApi.class)
@ -186,7 +182,7 @@ public class DownloadTracker {
trackSelectionDialog.dismiss();
}
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.
if (Util.SDK_INT < 18) {
Toast.makeText(context, R.string.error_drm_unsupported_before_api_18, Toast.LENGTH_LONG)
.show();
Log.e(TAG, "Downloading DRM protected content is not supported on API versions below 18");
return;
}
// TODO(internal b/163107948): Support cases where DrmInitData are not in the manifest.
if (!hasNonNullWidevineSchemaData(format.drmInitData)) {
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. */
private static final class WidevineOfflineLicenseFetchTask {
@RequiresApi(18)
private static final class WidevineOfflineLicenseFetchTask extends AsyncTask<Void, Void, Void> {
private final Format format;
private final MediaItem.DrmConfiguration drmConfiguration;
private final DataSource.Factory dataSourceFactory;
private final StartDownloadDialogHelper dialogHelper;
private final DownloadHelper downloadHelper;
private final ExecutorService executorService;
@Nullable Future<?> future;
@Nullable private byte[] keySetId;
@Nullable private DrmSession.DrmSessionException drmSessionException;
@ -375,8 +375,6 @@ public class DownloadTracker {
DataSource.Factory dataSourceFactory,
StartDownloadDialogHelper dialogHelper,
DownloadHelper downloadHelper) {
checkState(drmConfiguration.scheme.equals(C.WIDEVINE_UUID));
this.executorService = Executors.newSingleThreadExecutor();
this.format = format;
this.drmConfiguration = drmConfiguration;
this.dataSourceFactory = dataSourceFactory;
@ -384,41 +382,32 @@ public class DownloadTracker {
this.downloadHelper = downloadHelper;
}
public void cancel() {
if (future != null) {
future.cancel(/* mayInterruptIfRunning= */ false);
@Override
protected Void doInBackground(Void... voids) {
OfflineLicenseHelper offlineLicenseHelper =
OfflineLicenseHelper.newWidevineInstance(
drmConfiguration.licenseUri.toString(),
drmConfiguration.forceDefaultLicenseUri,
dataSourceFactory,
drmConfiguration.licenseRequestHeaders,
new DrmSessionEventListener.EventDispatcher());
try {
keySetId = offlineLicenseHelper.downloadLicense(format);
} catch (DrmSession.DrmSessionException e) {
drmSessionException = e;
} finally {
offlineLicenseHelper.release();
}
return null;
}
public void execute() {
future =
executorService.submit(
() -> {
OfflineLicenseHelper offlineLicenseHelper =
OfflineLicenseHelper.newWidevineInstance(
drmConfiguration.licenseUri.toString(),
drmConfiguration.forceDefaultLicenseUri,
dataSourceFactory,
drmConfiguration.licenseRequestHeaders,
new DrmSessionEventListener.EventDispatcher());
try {
keySetId = offlineLicenseHelper.downloadLicense(format);
} catch (DrmSession.DrmSessionException e) {
drmSessionException = e;
} finally {
offlineLicenseHelper.release();
new Handler(Looper.getMainLooper())
.post(
() -> {
if (drmSessionException != null) {
dialogHelper.onOfflineLicenseFetchedError(drmSessionException);
} else {
dialogHelper.onOfflineLicenseFetched(
downloadHelper, checkNotNull(keySetId));
}
});
}
});
@Override
protected void onPostExecute(Void aVoid) {
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.net.Uri;
import androidx.annotation.Nullable;
import androidx.annotation.OptIn;
import androidx.media3.common.C;
import androidx.media3.common.MediaItem;
import androidx.media3.common.MediaItem.ClippingConfiguration;
import androidx.media3.common.MediaItem.SubtitleConfiguration;
import androidx.media3.common.MediaMetadata;
import androidx.media3.common.Player;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.common.util.Util;
import com.google.common.collect.ImmutableList;
import java.util.ArrayList;
@ -56,7 +53,6 @@ public class IntentUtil {
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_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";
@ -70,21 +66,6 @@ public class IntentUtil {
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_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}. */
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(
Uri uri, Intent intent, String extrasKeySuffix) {
@Nullable String mimeType = intent.getStringExtra(MIME_TYPE_EXTRA + extrasKeySuffix);
@ -142,7 +122,6 @@ public class IntentUtil {
@Nullable
SubtitleConfiguration subtitleConfiguration =
createSubtitleConfiguration(intent, extrasKeySuffix);
long imageDurationMs = intent.getLongExtra(IMAGE_DURATION_MS + extrasKeySuffix, C.TIME_UNSET);
MediaItem.Builder builder =
new MediaItem.Builder()
.setUri(uri)
@ -155,8 +134,7 @@ public class IntentUtil {
.setEndPositionMs(
intent.getLongExtra(
CLIP_END_POSITION_MS_EXTRA + extrasKeySuffix, C.TIME_END_OF_SOURCE))
.build())
.setImageDurationMs(imageDurationMs);
.build());
if (adTagUri != null) {
builder.setAdsConfiguration(
new MediaItem.AdsConfiguration.Builder(Uri.parse(adTagUri)).build());
@ -217,7 +195,6 @@ public class IntentUtil {
return builder;
}
@OptIn(markerClass = UnstableApi.class) // Accessing image duration.
private static void addLocalConfigurationToIntent(
MediaItem.LocalConfiguration localConfiguration, Intent intent, String extrasKeySuffix) {
intent
@ -238,9 +215,6 @@ public class IntentUtil {
intent.putExtra(SUBTITLE_MIME_TYPE_EXTRA + extrasKeySuffix, subtitleConfiguration.mimeType);
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(

View file

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

View file

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

View file

@ -25,6 +25,8 @@
<string name="error_generic">Playback failed</string>
<string name="error_drm_unsupported_before_api_18">DRM content not supported on API levels below 18</string>
<string name="error_drm_unsupported_scheme">This device does not support the required DRM scheme</string>
<string name="error_no_decoder">This device does not provide a decoder for <xliff:g id="mime_type">%1$s</xliff:g></string>
@ -57,16 +59,6 @@
<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="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>

View file

@ -18,7 +18,7 @@ apply plugin: 'kotlin-android'
android {
namespace 'androidx.media3.demo.session'
compileSdk project.ext.compileSdkVersion
compileSdkVersion project.ext.compileSdkVersion
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
@ -34,6 +34,7 @@ android {
versionCode project.ext.releaseVersionCode
minSdkVersion project.ext.minSdkVersion
targetSdkVersion project.ext.appTargetSdkVersion
multiDexEnabled true
}
buildTypes {
@ -59,13 +60,11 @@ android {
dependencies {
// 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.lifecycle:lifecycle-common:' + androidxLifecycleVersion
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:' + androidxLifecycleVersion
implementation 'androidx.appcompat:appcompat:' + androidxAppCompatVersion
implementation 'androidx.multidex:multidex:' + androidxMultidexVersion
implementation 'com.google.android.material:material:' + androidxMaterialVersion
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-guava:' + kotlinxCoroutinesVersion
implementation project(modulePrefix + 'lib-ui')
implementation project(modulePrefix + 'lib-session')
implementation project(modulePrefix + 'demo-session-service')

View file

@ -14,6 +14,7 @@
limitations under the License.
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="androidx.media3.demo.session">
<uses-sdk/>
@ -22,10 +23,12 @@
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" />
<application
android:name="androidx.multidex.MultiDexApplication"
android:allowBackup="false"
android:icon="@mipmap/ic_launcher"
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. -->
<meta-data
@ -56,7 +59,7 @@
android:foregroundServiceType="mediaPlayback"
android:exported="true">
<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.action.MEDIA_PLAY_FROM_SEARCH"/>
</intent-filter>

View file

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

View file

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

View file

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

View file

@ -16,11 +16,11 @@
<resources>
<string name="app_name">Media3 Session Demo</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="shuffle">Shuffle</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">
"Without notification access the app can't warn about failed background operations"</string>
</resources>

View file

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

View file

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

View file

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

View file

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

View file

@ -27,7 +27,6 @@ import androidx.media3.session.MediaLibraryService
import androidx.media3.session.MediaSession
import androidx.media3.session.MediaSession.MediaItemsWithStartPosition
import androidx.media3.session.SessionCommand
import androidx.media3.session.SessionError
import androidx.media3.session.SessionResult
import com.google.common.collect.ImmutableList
import com.google.common.util.concurrent.Futures
@ -41,17 +40,18 @@ open class DemoMediaLibrarySessionCallback(context: Context) :
MediaItemTree.initialize(context.assets)
}
@OptIn(UnstableApi::class) // TODO: b/328238954 - Remove once new CommandButton icons are stable.
private val customLayoutCommandButtons: List<CommandButton> =
listOf(
CommandButton.Builder(CommandButton.ICON_SHUFFLE_OFF)
CommandButton.Builder()
.setDisplayName(context.getString(R.string.exo_controls_shuffle_on_description))
.setSessionCommand(SessionCommand(CUSTOM_COMMAND_TOGGLE_SHUFFLE_MODE_ON, Bundle.EMPTY))
.setIconResId(R.drawable.exo_icon_shuffle_off)
.build(),
CommandButton.Builder(CommandButton.ICON_SHUFFLE_ON)
CommandButton.Builder()
.setDisplayName(context.getString(R.string.exo_controls_shuffle_off_description))
.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
@ -70,7 +70,7 @@ open class DemoMediaLibrarySessionCallback(context: Context) :
@OptIn(UnstableApi::class)
override fun onConnect(
session: MediaSession,
controller: MediaSession.ControllerInfo,
controller: MediaSession.ControllerInfo
): MediaSession.ConnectionResult {
if (
session.isMediaNotificationController(controller) ||
@ -93,7 +93,7 @@ open class DemoMediaLibrarySessionCallback(context: Context) :
session: MediaSession,
controller: MediaSession.ControllerInfo,
customCommand: SessionCommand,
args: Bundle,
args: Bundle
): ListenableFuture<SessionResult> {
if (CUSTOM_COMMAND_TOGGLE_SHUFFLE_MODE_ON == customCommand.customAction) {
// Enable shuffling.
@ -101,7 +101,7 @@ open class DemoMediaLibrarySessionCallback(context: Context) :
// Change the custom layout to contain the `Disable shuffling` command.
session.setCustomLayout(
session.mediaNotificationControllerInfo!!,
ImmutableList.of(customLayoutCommandButtons[1]),
ImmutableList.of(customLayoutCommandButtons[1])
)
return Futures.immediateFuture(SessionResult(SessionResult.RESULT_SUCCESS))
} 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.
session.setCustomLayout(
session.mediaNotificationControllerInfo!!,
ImmutableList.of(customLayoutCommandButtons[0]),
ImmutableList.of(customLayoutCommandButtons[0])
)
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(
session: MediaLibraryService.MediaLibrarySession,
browser: MediaSession.ControllerInfo,
params: MediaLibraryService.LibraryParams?,
params: MediaLibraryService.LibraryParams?
): ListenableFuture<LibraryResult<MediaItem>> {
return Futures.immediateFuture(LibraryResult.ofItem(MediaItemTree.getRootItem(), params))
}
@OptIn(UnstableApi::class) // SessionError.ERROR_BAD_VALUE
override fun onGetItem(
session: MediaLibraryService.MediaLibrarySession,
browser: MediaSession.ControllerInfo,
mediaId: String,
mediaId: String
): ListenableFuture<LibraryResult<MediaItem>> {
MediaItemTree.getItem(mediaId)?.let {
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(
session: MediaLibraryService.MediaLibrarySession,
browser: MediaSession.ControllerInfo,
parentId: String,
page: Int,
pageSize: Int,
params: MediaLibraryService.LibraryParams?,
params: MediaLibraryService.LibraryParams?
): ListenableFuture<LibraryResult<ImmutableList<MediaItem>>> {
val children = MediaItemTree.getChildren(parentId)
if (children.isNotEmpty()) {
return Futures.immediateFuture(LibraryResult.ofItemList(children, params))
}
return Futures.immediateFuture(LibraryResult.ofError(SessionError.ERROR_BAD_VALUE))
return Futures.immediateFuture(LibraryResult.ofError(LibraryResult.RESULT_ERROR_BAD_VALUE))
}
override fun onAddMediaItems(
mediaSession: MediaSession,
controller: MediaSession.ControllerInfo,
mediaItems: List<MediaItem>,
mediaItems: List<MediaItem>
): ListenableFuture<List<MediaItem>> {
return Futures.immediateFuture(resolveMediaItems(mediaItems))
}
@ -167,7 +165,7 @@ open class DemoMediaLibrarySessionCallback(context: Context) :
browser: MediaSession.ControllerInfo,
mediaItems: List<MediaItem>,
startIndex: Int,
startPositionMs: Long,
startPositionMs: Long
): ListenableFuture<MediaItemsWithStartPosition> {
if (mediaItems.size == 1) {
// Try to expand a single item to a playlist.
@ -196,7 +194,7 @@ open class DemoMediaLibrarySessionCallback(context: Context) :
private fun maybeExpandSingleItemToPlaylist(
mediaItem: MediaItem,
startIndex: Int,
startPositionMs: Long,
startPositionMs: Long
): MediaItemsWithStartPosition? {
var playlist = listOf<MediaItem>()
var indexInPlaylist = startIndex
@ -225,7 +223,7 @@ open class DemoMediaLibrarySessionCallback(context: Context) :
session: MediaLibraryService.MediaLibrarySession,
browser: MediaSession.ControllerInfo,
query: String,
params: MediaLibraryService.LibraryParams?,
params: MediaLibraryService.LibraryParams?
): ListenableFuture<LibraryResult<Void>> {
session.notifySearchResultChanged(browser, query, MediaItemTree.search(query).size, params)
return Futures.immediateFuture(LibraryResult.ofVoid())
@ -237,7 +235,7 @@ open class DemoMediaLibrarySessionCallback(context: Context) :
query: String,
page: Int,
pageSize: Int,
params: MediaLibraryService.LibraryParams?,
params: MediaLibraryService.LibraryParams?
): ListenableFuture<LibraryResult<ImmutableList<MediaItem>>> {
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.NotificationManager
import android.app.PendingIntent
import android.content.Intent
import android.content.pm.PackageManager
import android.os.Build
import androidx.annotation.OptIn
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import androidx.core.os.bundleOf
import androidx.media3.common.AudioAttributes
import androidx.media3.common.util.UnstableApi
import androidx.media3.demo.session.service.R
import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.exoplayer.util.EventLogger
import androidx.media3.session.MediaConstants
import androidx.media3.session.MediaLibraryService
import androidx.media3.session.MediaSession
import androidx.media3.session.MediaSession.ControllerInfo
@ -91,6 +90,13 @@ open class DemoPlaybackService : MediaLibraryService() {
return mediaLibrarySession
}
override fun onTaskRemoved(rootIntent: Intent?) {
val player = mediaLibrarySession.player
if (!player.playWhenReady || player.mediaItemCount == 0) {
stopSelf()
}
}
// MediaSession.setSessionActivity
// MediaSessionService.clearListener
@OptIn(UnstableApi::class)
@ -113,17 +119,6 @@ open class DemoPlaybackService : MediaLibraryService() {
MediaLibrarySession.Builder(this, player, createLibrarySessionCallback())
.also { builder -> getSingleTopActivity()?.let { builder.setSessionActivity(it) } }
.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
@ -171,7 +166,7 @@ open class DemoPlaybackService : MediaLibraryService() {
NotificationChannel(
CHANNEL_ID,
getString(R.string.notification_channel_name),
NotificationManager.IMPORTANCE_DEFAULT,
NotificationManager.IMPORTANCE_DEFAULT
)
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 {
namespace 'androidx.media3.demo.surface'
compileSdk project.ext.compileSdkVersion
compileSdkVersion project.ext.compileSdkVersion
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8

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