From 0e3b05c67dab6877a85d8efba45b3c6c4978693c Mon Sep 17 00:00:00 2001 From: bachinger Date: Thu, 25 Apr 2024 05:03:33 -0700 Subject: [PATCH] Add metadata field durationMs PiperOrigin-RevId: 628038241 --- .../androidx/media3/common/MediaMetadata.java | 42 ++++++++ .../media3/common/MediaMetadataTest.java | 4 +- .../media3/session/LegacyConversions.java | 99 +++++++++++-------- .../media3/session/LegacyConversionsTest.java | 24 +++-- 4 files changed, 119 insertions(+), 50 deletions(-) diff --git a/libraries/common/src/main/java/androidx/media3/common/MediaMetadata.java b/libraries/common/src/main/java/androidx/media3/common/MediaMetadata.java index ede0858e8b..6bb4cc7ecc 100644 --- a/libraries/common/src/main/java/androidx/media3/common/MediaMetadata.java +++ b/libraries/common/src/main/java/androidx/media3/common/MediaMetadata.java @@ -15,6 +15,7 @@ */ package androidx.media3.common; +import static androidx.media3.common.util.Assertions.checkArgument; import static java.lang.annotation.ElementType.FIELD; import static java.lang.annotation.ElementType.LOCAL_VARIABLE; import static java.lang.annotation.ElementType.METHOD; @@ -53,6 +54,7 @@ public final class MediaMetadata implements Bundleable { @Nullable private CharSequence displayTitle; @Nullable private CharSequence subtitle; @Nullable private CharSequence description; + @Nullable private Long durationMs; @Nullable private Rating userRating; @Nullable private Rating overallRating; @Nullable private byte[] artworkData; @@ -95,6 +97,7 @@ public final class MediaMetadata implements Bundleable { this.displayTitle = mediaMetadata.displayTitle; this.subtitle = mediaMetadata.subtitle; this.description = mediaMetadata.description; + this.durationMs = mediaMetadata.durationMs; this.userRating = mediaMetadata.userRating; this.overallRating = mediaMetadata.overallRating; this.artworkData = mediaMetadata.artworkData; @@ -176,6 +179,23 @@ public final class MediaMetadata implements Bundleable { return this; } + /** + * Sets the optional duration, non-negative and in milliseconds. + * + *

The duration is populated by the app when building the metadata object and is for + * informational purpose only. For retrieving the duration of the media item currently being + * played, use {@link Player#getDuration()} instead. + * + * @throws IllegalArgumentException if the duration is negative. + */ + @UnstableApi + @CanIgnoreReturnValue + public Builder setDurationMs(@Nullable Long durationMs) { + checkArgument(durationMs == null || durationMs >= 0); + this.durationMs = durationMs; + return this; + } + /** Sets the user {@link Rating}. */ @CanIgnoreReturnValue public Builder setUserRating(@Nullable Rating userRating) { @@ -496,6 +516,9 @@ public final class MediaMetadata implements Bundleable { if (mediaMetadata.description != null) { setDescription(mediaMetadata.description); } + if (mediaMetadata.durationMs != null) { + setDurationMs(mediaMetadata.durationMs); + } if (mediaMetadata.userRating != null) { setUserRating(mediaMetadata.userRating); } @@ -978,6 +1001,15 @@ public final class MediaMetadata implements Bundleable { /** Optional description. */ @Nullable public final CharSequence description; + /** + * Optional duration, non-negative and in milliseconds. + * + *

This field is populated by the app when building the metadata object and is for + * informational purpose only. For retrieving the duration of the media item currently being + * played, use {@link Player#getDuration()} instead. + */ + @UnstableApi @Nullable public final Long durationMs; + /** Optional user {@link Rating}. */ @Nullable public final Rating userRating; @@ -1116,6 +1148,7 @@ public final class MediaMetadata implements Bundleable { this.displayTitle = builder.displayTitle; this.subtitle = builder.subtitle; this.description = builder.description; + this.durationMs = builder.durationMs; this.userRating = builder.userRating; this.overallRating = builder.overallRating; this.artworkData = builder.artworkData; @@ -1168,6 +1201,7 @@ public final class MediaMetadata implements Bundleable { && Util.areEqual(displayTitle, that.displayTitle) && Util.areEqual(subtitle, that.subtitle) && Util.areEqual(description, that.description) + && Util.areEqual(durationMs, that.durationMs) && Util.areEqual(userRating, that.userRating) && Util.areEqual(overallRating, that.overallRating) && Arrays.equals(artworkData, that.artworkData) @@ -1207,6 +1241,7 @@ public final class MediaMetadata implements Bundleable { displayTitle, subtitle, description, + durationMs, userRating, overallRating, Arrays.hashCode(artworkData), @@ -1270,6 +1305,7 @@ public final class MediaMetadata implements Bundleable { private static final String FIELD_STATION = Util.intToStringMaxRadix(30); private static final String FIELD_MEDIA_TYPE = Util.intToStringMaxRadix(31); private static final String FIELD_IS_BROWSABLE = Util.intToStringMaxRadix(32); + private static final String FIELD_DURATION_MS = Util.intToStringMaxRadix(33); private static final String FIELD_EXTRAS = Util.intToStringMaxRadix(1000); @SuppressWarnings("deprecation") // Bundling deprecated fields. @@ -1298,6 +1334,9 @@ public final class MediaMetadata implements Bundleable { if (description != null) { bundle.putCharSequence(FIELD_DESCRIPTION, description); } + if (durationMs != null) { + bundle.putLong(FIELD_DURATION_MS, durationMs); + } if (artworkData != null) { bundle.putByteArray(FIELD_ARTWORK_DATA, artworkData); } @@ -1428,6 +1467,9 @@ public final class MediaMetadata implements Bundleable { builder.setOverallRating(Rating.fromBundle(fieldBundle)); } } + if (bundle.containsKey(FIELD_DURATION_MS)) { + builder.setDurationMs(bundle.getLong(FIELD_DURATION_MS)); + } if (bundle.containsKey(FIELD_TRACK_NUMBER)) { builder.setTrackNumber(bundle.getInt(FIELD_TRACK_NUMBER)); } diff --git a/libraries/common/src/test/java/androidx/media3/common/MediaMetadataTest.java b/libraries/common/src/test/java/androidx/media3/common/MediaMetadataTest.java index 416773f800..d3553fc9d9 100644 --- a/libraries/common/src/test/java/androidx/media3/common/MediaMetadataTest.java +++ b/libraries/common/src/test/java/androidx/media3/common/MediaMetadataTest.java @@ -42,6 +42,7 @@ public class MediaMetadataTest { assertThat(mediaMetadata.displayTitle).isNull(); assertThat(mediaMetadata.subtitle).isNull(); assertThat(mediaMetadata.description).isNull(); + assertThat(mediaMetadata.durationMs).isNull(); assertThat(mediaMetadata.userRating).isNull(); assertThat(mediaMetadata.overallRating).isNull(); assertThat(mediaMetadata.artworkData).isNull(); @@ -121,7 +122,7 @@ public class MediaMetadataTest { } @Test - public void populate_withArtworkDataOnly_updatesBothArtWorkUriAndArtworkData() { + public void populate_withArtworkDataOnly_updatesBothArtworkUriAndArtworkData() { byte[] artworkData = new byte[] {35, 12, 6, 77}; MediaMetadata mediaMetadata = new MediaMetadata.Builder() @@ -251,6 +252,7 @@ public class MediaMetadataTest { .setDisplayTitle("display title") .setSubtitle("subtitle") .setDescription("description") + .setDurationMs(10_000L) .setUserRating(new HeartRating(false)) .setOverallRating(new PercentageRating(87.4f)) .setArtworkData( diff --git a/libraries/session/src/main/java/androidx/media3/session/LegacyConversions.java b/libraries/session/src/main/java/androidx/media3/session/LegacyConversions.java index f5550cded4..2ec33b7475 100644 --- a/libraries/session/src/main/java/androidx/media3/session/LegacyConversions.java +++ b/libraries/session/src/main/java/androidx/media3/session/LegacyConversions.java @@ -315,49 +315,6 @@ import java.util.concurrent.TimeoutException; return period; } - /** Converts a {@link MediaItem} to a {@link MediaDescriptionCompat} */ - @SuppressWarnings("deprecation") // Converting deprecated fields. - public static MediaDescriptionCompat convertToMediaDescriptionCompat( - MediaItem item, @Nullable Bitmap artworkBitmap) { - MediaDescriptionCompat.Builder builder = - new MediaDescriptionCompat.Builder() - .setMediaId(item.mediaId.equals(MediaItem.DEFAULT_MEDIA_ID) ? null : item.mediaId); - MediaMetadata metadata = item.mediaMetadata; - if (artworkBitmap != null) { - builder.setIconBitmap(artworkBitmap); - } - @Nullable Bundle extras = metadata.extras; - boolean hasFolderType = - metadata.folderType != null && metadata.folderType != MediaMetadata.FOLDER_TYPE_NONE; - boolean hasMediaType = metadata.mediaType != null; - if (hasFolderType || hasMediaType) { - if (extras == null) { - extras = new Bundle(); - } else { - extras = new Bundle(extras); - } - if (hasFolderType) { - extras.putLong( - MediaDescriptionCompat.EXTRA_BT_FOLDER_TYPE, - convertToExtraBtFolderType(checkNotNull(metadata.folderType))); - } - if (hasMediaType) { - extras.putLong( - MediaConstants.EXTRAS_KEY_MEDIA_TYPE_COMPAT, checkNotNull(metadata.mediaType)); - } - } - return builder - .setTitle(metadata.title) - // The BT AVRPC service expects the subtitle of the media description to be the artist - // (see https://github.com/androidx/media/issues/148). - .setSubtitle(metadata.artist != null ? metadata.artist : metadata.subtitle) - .setDescription(metadata.description) - .setIconUri(metadata.artworkUri) - .setMediaUri(item.requestMetadata.mediaUri) - .setExtras(extras) - .build(); - } - /** Creates {@link MediaMetadata} from the {@link CharSequence queue title}. */ public static MediaMetadata convertToMediaMetadata(@Nullable CharSequence queueTitle) { if (queueTitle == null) { @@ -450,6 +407,15 @@ import java.util.concurrent.TimeoutException; .setOverallRating( convertToRating(metadataCompat.getRating(MediaMetadataCompat.METADATA_KEY_RATING))); + if (metadataCompat.containsKey(MediaMetadataCompat.METADATA_KEY_DURATION)) { + long durationMs = metadataCompat.getLong(MediaMetadataCompat.METADATA_KEY_DURATION); + if (durationMs >= 0) { + // Only set duration if a non-negative is set. Do not assert because we don't want the app + // to crash because an external app sends a negative value that is valid in media1. + builder.setDurationMs(durationMs); + } + } + @Nullable Rating userRating = convertToRating(metadataCompat.getRating(MediaMetadataCompat.METADATA_KEY_USER_RATING)); @@ -621,6 +587,10 @@ import java.util.concurrent.TimeoutException; convertToExtraBtFolderType(metadata.folderType)); } + if (durationMs == C.TIME_UNSET && metadata.durationMs != null) { + // If the actual media duration is unknown, use the manually declared value if available. + durationMs = metadata.durationMs; + } if (durationMs != C.TIME_UNSET) { builder.putLong(MediaMetadataCompat.METADATA_KEY_DURATION, durationMs); } @@ -656,6 +626,49 @@ import java.util.concurrent.TimeoutException; return builder.build(); } + /** Converts a {@link MediaItem} to a {@link MediaDescriptionCompat} */ + @SuppressWarnings("deprecation") // Converting deprecated fields. + public static MediaDescriptionCompat convertToMediaDescriptionCompat( + MediaItem item, @Nullable Bitmap artworkBitmap) { + MediaDescriptionCompat.Builder builder = + new MediaDescriptionCompat.Builder() + .setMediaId(item.mediaId.equals(MediaItem.DEFAULT_MEDIA_ID) ? null : item.mediaId); + MediaMetadata metadata = item.mediaMetadata; + if (artworkBitmap != null) { + builder.setIconBitmap(artworkBitmap); + } + @Nullable Bundle extras = metadata.extras; + boolean hasFolderType = + metadata.folderType != null && metadata.folderType != MediaMetadata.FOLDER_TYPE_NONE; + boolean hasMediaType = metadata.mediaType != null; + if (hasFolderType || hasMediaType) { + if (extras == null) { + extras = new Bundle(); + } else { + extras = new Bundle(extras); + } + if (hasFolderType) { + extras.putLong( + MediaDescriptionCompat.EXTRA_BT_FOLDER_TYPE, + convertToExtraBtFolderType(checkNotNull(metadata.folderType))); + } + if (hasMediaType) { + extras.putLong( + MediaConstants.EXTRAS_KEY_MEDIA_TYPE_COMPAT, checkNotNull(metadata.mediaType)); + } + } + return builder + .setTitle(metadata.title) + // The BT AVRPC service expects the subtitle of the media description to be the artist + // (see https://github.com/androidx/media/issues/148). + .setSubtitle(metadata.artist != null ? metadata.artist : metadata.subtitle) + .setDescription(metadata.description) + .setIconUri(metadata.artworkUri) + .setMediaUri(item.requestMetadata.mediaUri) + .setExtras(extras) + .build(); + } + @SuppressWarnings("deprecation") // Converting to deprecated constants. @MediaMetadata.FolderType private static int convertToFolderType(long extraBtFolderType) { diff --git a/libraries/session/src/test/java/androidx/media3/session/LegacyConversionsTest.java b/libraries/session/src/test/java/androidx/media3/session/LegacyConversionsTest.java index 7732395eb0..380b86d273 100644 --- a/libraries/session/src/test/java/androidx/media3/session/LegacyConversionsTest.java +++ b/libraries/session/src/test/java/androidx/media3/session/LegacyConversionsTest.java @@ -132,7 +132,7 @@ public final class LegacyConversionsTest { @Test public void convertToQueueItem_withArtworkData() throws Exception { - MediaItem mediaItem = createMediaItemWithArtworkData("testId"); + MediaItem mediaItem = createMediaItemWithArtworkData("testId", /* durationMs= */ 10_000L); MediaMetadata mediaMetadata = mediaItem.mediaMetadata; ListenableFuture bitmapFuture = bitmapLoader.decodeBitmap(mediaMetadata.artworkData); @Nullable Bitmap bitmap = bitmapFuture.get(10, SECONDS); @@ -158,9 +158,11 @@ public final class LegacyConversionsTest { .setTitle(title) .setDescription(description) .setMediaType(MediaMetadata.MEDIA_TYPE_MUSIC) + .setDurationMs(10_000L) .build(); MediaItem mediaItem = new MediaItem.Builder().setMediaId(mediaId).setMediaMetadata(metadata).build(); + MediaDescriptionCompat descriptionCompat = LegacyConversions.convertToMediaDescriptionCompat(mediaItem, /* artworkBitmap= */ null); @@ -212,7 +214,7 @@ public final class LegacyConversionsTest { @Test public void convertToMediaMetadata_roundTripViaMediaMetadataCompat_returnsEqualMediaItemMetadata() throws Exception { - MediaItem testMediaItem = createMediaItemWithArtworkData("testZZZ"); + MediaItem testMediaItem = createMediaItemWithArtworkData("testZZZ", /* durationMs= */ 10_000L); MediaMetadata testMediaMetadata = testMediaItem.mediaMetadata; @Nullable Bitmap testArtworkBitmap = null; @Nullable @@ -225,7 +227,7 @@ public final class LegacyConversionsTest { testMediaMetadata, "mediaId", Uri.parse("http://example.com"), - /* durationMs= */ 100L, + /* durationMs= */ C.TIME_UNSET, testArtworkBitmap); MediaMetadata mediaMetadata = @@ -239,7 +241,8 @@ public final class LegacyConversionsTest { public void convertToMediaMetadata_roundTripViaMediaDescriptionCompat_returnsEqualMediaItemMetadata() throws Exception { - MediaItem testMediaItem = createMediaItemWithArtworkData("testZZZ"); + MediaItem testMediaItem = + createMediaItemWithArtworkData("testZZZ", /* durationMs= */ C.TIME_UNSET); MediaMetadata testMediaMetadata = testMediaItem.mediaMetadata; @Nullable Bitmap testArtworkBitmap = null; @Nullable @@ -1152,12 +1155,21 @@ public final class LegacyConversionsTest { return list.build(); } - private static MediaItem createMediaItemWithArtworkData(String mediaId) { + private static MediaItem createMediaItemWithArtworkData(String mediaId, long durationMs) { + Bundle extras = new Bundle(); + extras.putLong( + MediaConstants.EXTRAS_KEY_IS_EXPLICIT, MediaConstants.EXTRAS_VALUE_ATTRIBUTE_PRESENT); MediaMetadata.Builder mediaMetadataBuilder = new MediaMetadata.Builder() .setMediaType(MediaMetadata.MEDIA_TYPE_PLAYLIST) .setIsBrowsable(false) - .setIsPlayable(true); + .setIsPlayable(true) + .setExtras(extras); + + if (durationMs != C.TIME_UNSET) { + mediaMetadataBuilder.setDurationMs(durationMs); + } + try { byte[] artworkData; Bitmap bitmap =