Add metadata field durationMs

PiperOrigin-RevId: 628038241
This commit is contained in:
bachinger 2024-04-25 05:03:33 -07:00 committed by Copybara-Service
parent ed1cf35f30
commit 0e3b05c67d
4 changed files with 119 additions and 50 deletions

View File

@ -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.
*
* <p>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.
*
* <p>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));
}

View File

@ -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(

View File

@ -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) {

View File

@ -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<Bitmap> 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 =