diff --git a/RELEASENOTES.md b/RELEASENOTES.md
index 48976897bd..c5e0ae25ff 100644
--- a/RELEASENOTES.md
+++ b/RELEASENOTES.md
@@ -39,6 +39,8 @@
example, a Bluetooth headset
([#167](https://github.com/androidx/media/issues/167)).
* UI:
+ * Deprecate `PlayerView.setUseArtwork(boolean)` and replace it with
+ `PlayerView.setArtworkDisplayMode(@ArtworkDisplayMode)`.
* Downloads:
* OkHttp Extension:
* Cronet Extension:
diff --git a/demos/session/src/main/assets/catalog.json b/demos/session/src/main/assets/catalog.json
index e336999b1d..d5b9286d7c 100644
--- a/demos/session/src/main/assets/catalog.json
+++ b/demos/session/src/main/assets/catalog.json
@@ -501,6 +501,94 @@
"totalTrackCount": 2,
"duration": 160,
"site": "https://www.youtube.com/audiolibrary/music"
+ },
+ {
+ "id": "mixed_media_01",
+ "title": "Tear of steal - DASH",
+ "album": "Mixed media",
+ "artist": "Mixed artists",
+ "genre": "Mixed",
+ "source": "https://storage.googleapis.com/wvmedia/clear/h264/tears/tears.mpd",
+ "image": "https://storage.googleapis.com/wvmedia/clear/h264/tears/tears.mpd"
+ },
+ {
+ "id": "mixed_media_02",
+ "title": "Intro - The Way Of Waking Up (feat. Alan Watts - MP3)",
+ "album": "Mixed media",
+ "artist": "Mixed artists",
+ "genre": "Mixed",
+ "source": "https://storage.googleapis.com/uamp/The_Kyoto_Connection_-_Wake_Up/01_-_Intro_-_The_Way_Of_Waking_Up_feat_Alan_Watts.mp3",
+ "image": "https://storage.googleapis.com/uamp/The_Kyoto_Connection_-_Wake_Up/art.jpg"
+ },
+ {
+ "id": "mixed_media_03",
+ "title": "TTML Netflix Japanese examples (MP4)",
+ "source": "https://storage.googleapis.com/exoplayer-test-media-1/gen-3/screens/dash-vod-single-segment/video-avc-baseline-480.mp4",
+ "album": "Mixed media",
+ "artist": "Mixed artists",
+ "genre": "Mixed",
+ "image": "https://cdn.pixabay.com/photo/2014/10/09/13/14/video-481821_960_720.png",
+ "subtitles": [
+ {
+ "subtitle_uri": "https://storage.googleapis.com/exoplayer-test-media-1/ttml/netflix_japanese_ttml.xml",
+ "subtitle_mime_type": "application/ttml+xml",
+ "subtitle_lang": "ja"
+ }
+ ]
+ },
+ {
+ "id": "mixed_media_04",
+ "title": "The Coldest Shoulder",
+ "album": "Mixed media",
+ "artist": "Mixed artists",
+ "genre": "Mixed",
+ "source": "https://storage.googleapis.com/automotive-media/The_Coldest_Shoulder.mp3",
+ "image": "https://storage.googleapis.com/automotive-media/album_art_3.jpg"
+ },
+ {
+ "id": "mixed_media_05",
+ "title": "Dizzy - MPEG-4 Timed Text",
+ "album": "Mixed media",
+ "artist": "Mixed artists",
+ "genre": "Mixed",
+ "source": "https://storage.googleapis.com/exoplayer-test-media-1/mp4/dizzy-with-tx3g.mp4",
+ "image": "https://cdn.pixabay.com/photo/2014/10/09/13/14/video-481821_960_720.png"
+ },
+ {
+ "id": "mixed_media_06",
+ "title": "Apple 4x3 basic stream (TS)",
+ "album": "Mixed media",
+ "artist": "Mixed artists",
+ "genre": "Mixed",
+ "source": "https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_4x3/bipbop_4x3_variant.m3u8",
+ "image": "https://storage.googleapis.com/uamp/The_Kyoto_Connection_-_Wake_Up/art.jpg"
+ },
+ {
+ "id": "mixed_media_07",
+ "title": "The Calm Before The Storm",
+ "album": "Mixed media",
+ "artist": "Mixed artists",
+ "genre": "Mixed",
+ "source": "https://storage.googleapis.com/uamp/The_Kyoto_Connection_-_Wake_Up/05_-_The_Calm_Before_The_Storm.mp3",
+ "image": "https://storage.googleapis.com/uamp/The_Kyoto_Connection_-_Wake_Up/art.jpg"
+ },
+ {
+ "id": "mixed_media_08",
+ "title": "Android screens (MKV)",
+ "album": "Mixed media",
+ "artist": "Mixed artists",
+ "genre": "Mixed",
+ "source": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv",
+ "image": "https://storage.googleapis.com/uamp/The_Kyoto_Connection_-_Wake_Up/art.jpg"
+ },
+ {
+ "id": "mixed_media_09",
+ "title": "No Pain, No Gain",
+ "album": "Mixed media",
+ "artist": "Mixed artists",
+ "genre": "Mixed",
+ "source": "https://storage.googleapis.com/uamp/The_Kyoto_Connection_-_Wake_Up/06_-_No_Pain_No_Gain.mp3",
+ "image": "https://storage.googleapis.com/uamp/The_Kyoto_Connection_-_Wake_Up/art.jpg"
}
]
}
diff --git a/libraries/ui/src/main/java/androidx/media3/ui/PlayerView.java b/libraries/ui/src/main/java/androidx/media3/ui/PlayerView.java
index 6b6b9a7cb3..d732c2a753 100644
--- a/libraries/ui/src/main/java/androidx/media3/ui/PlayerView.java
+++ b/libraries/ui/src/main/java/androidx/media3/ui/PlayerView.java
@@ -93,10 +93,11 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
* The following attributes can be set on a PlayerView when used in a layout XML file:
*
*
- * - {@code use_artwork} - Whether artwork is used if available in audio streams.
+ *
- {@code artwork_display_mode} - Whether artwork is used if available in audio streams
+ * and {@link ArtworkDisplayMode how it is displayed}.
*
- * - Corresponding method: {@link #setUseArtwork(boolean)}
- *
- Default: {@code true}
+ *
- Corresponding method: {@link #setArtworkDisplayMode(int)}
+ *
- Default: {@link #ARTWORK_DISPLAY_MODE_FIT}
*
* - {@code default_artwork} - Default artwork to use if no artwork available in audio
* streams.
@@ -201,6 +202,27 @@ public class PlayerView extends FrameLayout implements AdViewProvider {
void onFullscreenButtonClick(boolean isFullScreen);
}
+ /**
+ * Determines the artwork display mode. One of {@link #ARTWORK_DISPLAY_MODE_OFF}, {@link
+ * #ARTWORK_DISPLAY_MODE_FIT} or {@link #ARTWORK_DISPLAY_MODE_FILL}.
+ */
+ @UnstableApi
+ @Documented
+ @Retention(RetentionPolicy.SOURCE)
+ @Target(TYPE_USE)
+ @IntDef({ARTWORK_DISPLAY_MODE_OFF, ARTWORK_DISPLAY_MODE_FIT, ARTWORK_DISPLAY_MODE_FILL})
+ public @interface ArtworkDisplayMode {}
+
+ /** No artwork is shown. */
+ @UnstableApi public static final int ARTWORK_DISPLAY_MODE_OFF = 0;
+ /** The artwork is fit into the player view and centered creating a letterbox style. */
+ @UnstableApi public static final int ARTWORK_DISPLAY_MODE_FIT = 1;
+ /**
+ * The artwork covers the entire space of the player view. If the aspect ratio of the image is
+ * different than the player view some areas of the image are cropped.
+ */
+ @UnstableApi public static final int ARTWORK_DISPLAY_MODE_FILL = 2;
+
/**
* Determines when the buffering view is shown. One of {@link #SHOW_BUFFERING_NEVER}, {@link
* #SHOW_BUFFERING_WHEN_PLAYING} or {@link #SHOW_BUFFERING_ALWAYS}.
@@ -255,7 +277,8 @@ public class PlayerView extends FrameLayout implements AdViewProvider {
@Nullable private FullscreenButtonClickListener fullscreenButtonClickListener;
- private boolean useArtwork;
+ private @ArtworkDisplayMode int artworkDisplayMode;
+
@Nullable private Drawable defaultArtwork;
private @ShowBuffering int showBuffering;
private boolean keepContentOnPlayerReset;
@@ -308,6 +331,7 @@ public class PlayerView extends FrameLayout implements AdViewProvider {
int shutterColor = 0;
int playerLayoutId = R.layout.exo_player_view;
boolean useArtwork = true;
+ int artworkDisplayMode = ARTWORK_DISPLAY_MODE_FIT;
int defaultArtworkId = 0;
boolean useController = true;
int surfaceType = SURFACE_TYPE_SURFACE_VIEW;
@@ -328,6 +352,8 @@ public class PlayerView extends FrameLayout implements AdViewProvider {
shutterColor = a.getColor(R.styleable.PlayerView_shutter_background_color, shutterColor);
playerLayoutId = a.getResourceId(R.styleable.PlayerView_player_layout_id, playerLayoutId);
useArtwork = a.getBoolean(R.styleable.PlayerView_use_artwork, useArtwork);
+ artworkDisplayMode =
+ a.getInt(R.styleable.PlayerView_artwork_display_mode, artworkDisplayMode);
defaultArtworkId =
a.getResourceId(R.styleable.PlayerView_default_artwork, defaultArtworkId);
useController = a.getBoolean(R.styleable.PlayerView_use_controller, useController);
@@ -419,7 +445,9 @@ public class PlayerView extends FrameLayout implements AdViewProvider {
// Artwork view.
artworkView = findViewById(R.id.exo_artwork);
- this.useArtwork = useArtwork && artworkView != null;
+ boolean isArtworkEnabled =
+ useArtwork && artworkDisplayMode != ARTWORK_DISPLAY_MODE_OFF && artworkView != null;
+ this.artworkDisplayMode = isArtworkEnabled ? artworkDisplayMode : ARTWORK_DISPLAY_MODE_OFF;
if (defaultArtworkId != 0) {
defaultArtwork = ContextCompat.getDrawable(getContext(), defaultArtworkId);
}
@@ -556,7 +584,10 @@ public class PlayerView extends FrameLayout implements AdViewProvider {
} else if (surfaceView instanceof SurfaceView) {
player.setVideoSurfaceView((SurfaceView) surfaceView);
}
- updateAspectRatio();
+ if (player.getCurrentTracks().isTypeSupported(C.TRACK_TYPE_VIDEO)) {
+ // If the player already is or was playing a video, onVideoSizeChanged isn't called.
+ updateAspectRatio();
+ }
}
if (subtitleView != null && player.isCommandAvailable(COMMAND_GET_TEXT)) {
subtitleView.setCues(player.getCurrentCues().cues);
@@ -595,26 +626,40 @@ public class PlayerView extends FrameLayout implements AdViewProvider {
return contentFrame.getResizeMode();
}
- /** Returns whether artwork is displayed if present in the media. */
+ /**
+ * @deprecated Use {@link #getArtworkDisplayMode()} instead.
+ */
@UnstableApi
+ @Deprecated
public boolean getUseArtwork() {
- return useArtwork;
+ return this.artworkDisplayMode != ARTWORK_DISPLAY_MODE_OFF;
}
/**
- * Sets whether artwork is displayed if present in the media.
- *
- * @param useArtwork Whether artwork is displayed.
+ * @deprecated Use {@link #setArtworkDisplayMode(int)} instead.
*/
@UnstableApi
+ @Deprecated
public void setUseArtwork(boolean useArtwork) {
- Assertions.checkState(!useArtwork || artworkView != null);
- if (this.useArtwork != useArtwork) {
- this.useArtwork = useArtwork;
+ setArtworkDisplayMode(useArtwork ? ARTWORK_DISPLAY_MODE_OFF : ARTWORK_DISPLAY_MODE_FIT);
+ }
+
+ /** Sets whether and how artwork is displayed if present in the media. */
+ @UnstableApi
+ public void setArtworkDisplayMode(@ArtworkDisplayMode int artworkDisplayMode) {
+ Assertions.checkState(artworkDisplayMode == ARTWORK_DISPLAY_MODE_OFF || artworkView != null);
+ if (this.artworkDisplayMode != artworkDisplayMode) {
+ this.artworkDisplayMode = artworkDisplayMode;
updateForCurrentTrackSelections(/* isNewPlayer= */ false);
}
}
+ /** Returns the {@link ArtworkDisplayMode artwork display mode}. */
+ @UnstableApi
+ public @ArtworkDisplayMode int getArtworkDisplayMode() {
+ return artworkDisplayMode;
+ }
+
/** Returns the default artwork to display. */
@UnstableApi
@Nullable
@@ -1243,7 +1288,7 @@ public class PlayerView extends FrameLayout implements AdViewProvider {
@EnsuresNonNullIf(expression = "artworkView", result = true)
private boolean useArtwork() {
- if (useArtwork) {
+ if (artworkDisplayMode != ARTWORK_DISPLAY_MODE_OFF) {
Assertions.checkStateNotNull(artworkView);
return true;
}
@@ -1364,8 +1409,14 @@ public class PlayerView extends FrameLayout implements AdViewProvider {
int drawableWidth = drawable.getIntrinsicWidth();
int drawableHeight = drawable.getIntrinsicHeight();
if (drawableWidth > 0 && drawableHeight > 0) {
- float artworkAspectRatio = (float) drawableWidth / drawableHeight;
- onContentAspectRatioChanged(contentFrame, artworkAspectRatio);
+ float artworkLayoutAspectRatio = (float) drawableWidth / drawableHeight;
+ ImageView.ScaleType scaleStyle = ImageView.ScaleType.FIT_XY;
+ if (artworkDisplayMode == ARTWORK_DISPLAY_MODE_FILL) {
+ artworkLayoutAspectRatio = (float) getWidth() / getHeight();
+ scaleStyle = ImageView.ScaleType.CENTER_CROP;
+ }
+ onContentAspectRatioChanged(contentFrame, artworkLayoutAspectRatio);
+ artworkView.setScaleType(scaleStyle);
artworkView.setImageDrawable(drawable);
artworkView.setVisibility(VISIBLE);
return true;
diff --git a/libraries/ui/src/main/res/values/attrs.xml b/libraries/ui/src/main/res/values/attrs.xml
index d944b28bf1..36cd681105 100644
--- a/libraries/ui/src/main/res/values/attrs.xml
+++ b/libraries/ui/src/main/res/values/attrs.xml
@@ -42,6 +42,11 @@
+
+
+
+
+
@@ -93,6 +98,7 @@
+