diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 1d82331ccb..023d02d95f 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -25,6 +25,9 @@ * Fix `mediaMetadata` being reset when media is repeated ([#9458](https://github.com/google/ExoPlayer/issues/9458)). * Remove final dependency on `jcenter()`. + * Adjust `ExoPlayer` `MediaMetadata` update priority, such that values + input through the `MediaItem.MediaMetadata` are used above media + derived values. * Video: * Fix bug in `MediaCodecVideoRenderer` that resulted in re-using a released `Surface` when playing without an app-provided `Surface` diff --git a/library/common/src/main/java/com/google/android/exoplayer2/MediaMetadata.java b/library/common/src/main/java/com/google/android/exoplayer2/MediaMetadata.java index e76edee173..4dad2da4c3 100644 --- a/library/common/src/main/java/com/google/android/exoplayer2/MediaMetadata.java +++ b/library/common/src/main/java/com/google/android/exoplayer2/MediaMetadata.java @@ -383,6 +383,108 @@ public final class MediaMetadata implements Bundleable { return this; } + /** Populates all the fields from {@code mediaMetadata}, provided they are non-null. */ + public Builder populate(@Nullable MediaMetadata mediaMetadata) { + if (mediaMetadata == null) { + return this; + } + if (mediaMetadata.title != null) { + setTitle(mediaMetadata.title); + } + if (mediaMetadata.artist != null) { + setArtist(mediaMetadata.artist); + } + if (mediaMetadata.albumTitle != null) { + setAlbumTitle(mediaMetadata.albumTitle); + } + if (mediaMetadata.albumArtist != null) { + setAlbumArtist(mediaMetadata.albumArtist); + } + if (mediaMetadata.displayTitle != null) { + setDisplayTitle(mediaMetadata.displayTitle); + } + if (mediaMetadata.subtitle != null) { + setSubtitle(mediaMetadata.subtitle); + } + if (mediaMetadata.description != null) { + setDescription(mediaMetadata.description); + } + if (mediaMetadata.mediaUri != null) { + setMediaUri(mediaMetadata.mediaUri); + } + if (mediaMetadata.userRating != null) { + setUserRating(mediaMetadata.userRating); + } + if (mediaMetadata.overallRating != null) { + setOverallRating(mediaMetadata.overallRating); + } + if (mediaMetadata.artworkData != null) { + setArtworkData(mediaMetadata.artworkData, mediaMetadata.artworkDataType); + } + if (mediaMetadata.artworkUri != null) { + setArtworkUri(mediaMetadata.artworkUri); + } + if (mediaMetadata.trackNumber != null) { + setTrackNumber(mediaMetadata.trackNumber); + } + if (mediaMetadata.totalTrackCount != null) { + setTotalTrackCount(mediaMetadata.totalTrackCount); + } + if (mediaMetadata.folderType != null) { + setFolderType(mediaMetadata.folderType); + } + if (mediaMetadata.isPlayable != null) { + setIsPlayable(mediaMetadata.isPlayable); + } + if (mediaMetadata.year != null) { + setRecordingYear(mediaMetadata.year); + } + if (mediaMetadata.recordingYear != null) { + setRecordingYear(mediaMetadata.recordingYear); + } + if (mediaMetadata.recordingMonth != null) { + setRecordingMonth(mediaMetadata.recordingMonth); + } + if (mediaMetadata.recordingDay != null) { + setRecordingDay(mediaMetadata.recordingDay); + } + if (mediaMetadata.releaseYear != null) { + setReleaseYear(mediaMetadata.releaseYear); + } + if (mediaMetadata.releaseMonth != null) { + setReleaseMonth(mediaMetadata.releaseMonth); + } + if (mediaMetadata.releaseDay != null) { + setReleaseDay(mediaMetadata.releaseDay); + } + if (mediaMetadata.writer != null) { + setWriter(mediaMetadata.writer); + } + if (mediaMetadata.composer != null) { + setComposer(mediaMetadata.composer); + } + if (mediaMetadata.conductor != null) { + setConductor(mediaMetadata.conductor); + } + if (mediaMetadata.discNumber != null) { + setDiscNumber(mediaMetadata.discNumber); + } + if (mediaMetadata.totalDiscCount != null) { + setTotalDiscCount(mediaMetadata.totalDiscCount); + } + if (mediaMetadata.genre != null) { + setGenre(mediaMetadata.genre); + } + if (mediaMetadata.compilation != null) { + setCompilation(mediaMetadata.compilation); + } + if (mediaMetadata.extras != null) { + setExtras(mediaMetadata.extras); + } + + return this; + } + /** Returns a new {@link MediaMetadata} instance with the current builder values. */ public MediaMetadata build() { return new MediaMetadata(/* builder= */ this); diff --git a/library/common/src/main/java/com/google/android/exoplayer2/Player.java b/library/common/src/main/java/com/google/android/exoplayer2/Player.java index b2b80d0d81..34d94ede8b 100644 --- a/library/common/src/main/java/com/google/android/exoplayer2/Player.java +++ b/library/common/src/main/java/com/google/android/exoplayer2/Player.java @@ -2065,7 +2065,9 @@ public interface Player { * *
This {@link MediaMetadata} is a combination of the {@link MediaItem#mediaMetadata} and the * static and dynamic metadata from the {@link TrackSelection#getFormat(int) track selections' - * formats} and {@link Listener#onMetadata(Metadata)}. + * formats} and {@link Listener#onMetadata(Metadata)}. If a field is populated in the {@link + * MediaItem#mediaMetadata}, it will be prioritised above the same field coming from static or + * dynamic metadata. */ MediaMetadata getMediaMetadata(); diff --git a/library/common/src/test/java/com/google/android/exoplayer2/MediaMetadataTest.java b/library/common/src/test/java/com/google/android/exoplayer2/MediaMetadataTest.java index 769758647e..9d2d78f48e 100644 --- a/library/common/src/test/java/com/google/android/exoplayer2/MediaMetadataTest.java +++ b/library/common/src/test/java/com/google/android/exoplayer2/MediaMetadataTest.java @@ -27,6 +27,9 @@ import org.junit.runner.RunWith; @RunWith(AndroidJUnit4.class) public class MediaMetadataTest { + private static final String EXTRAS_KEY = "exampleKey"; + private static final String EXTRAS_VALUE = "exampleValue"; + @Test public void builder_minimal_correctDefaults() { MediaMetadata mediaMetadata = new MediaMetadata.Builder().build(); @@ -91,41 +94,62 @@ public class MediaMetadataTest { } @Test - public void roundTripViaBundle_yieldsEqualInstance() { - Bundle extras = new Bundle(); - extras.putString("exampleKey", "exampleValue"); + public void populate_populatesEveryField() { + MediaMetadata mediaMetadata = getFullyPopulatedMediaMetadata(); + MediaMetadata populated = new MediaMetadata.Builder().populate(mediaMetadata).build(); - MediaMetadata mediaMetadata = - new MediaMetadata.Builder() - .setTitle("title") - .setAlbumArtist("the artist") - .setMediaUri(Uri.parse("https://www.google.com")) - .setUserRating(new HeartRating(false)) - .setOverallRating(new PercentageRating(87.4f)) - .setArtworkData( - new byte[] {-88, 12, 3, 2, 124, -54, -33, 69}, MediaMetadata.PICTURE_TYPE_MEDIA) - .setTrackNumber(4) - .setTotalTrackCount(12) - .setFolderType(MediaMetadata.FOLDER_TYPE_PLAYLISTS) - .setIsPlayable(true) - .setRecordingYear(2000) - .setRecordingMonth(11) - .setRecordingDay(23) - .setReleaseYear(2001) - .setReleaseMonth(1) - .setReleaseDay(2) - .setComposer("Composer") - .setConductor("Conductor") - .setWriter("Writer") - .setDiscNumber(1) - .setTotalDiscCount(3) - .setGenre("Pop") - .setCompilation("Amazing songs.") - .setExtras(extras) // Extras is not implemented in MediaMetadata.equals(Object o). - .build(); + // If this assertion fails, it's likely that a field is not being updated in + // MediaMetadata.Builder#populate(MediaMetadata). + assertThat(populated).isEqualTo(mediaMetadata); + assertThat(populated.extras.getString(EXTRAS_KEY)).isEqualTo(EXTRAS_VALUE); + } + + @Test + public void roundTripViaBundle_yieldsEqualInstance() { + MediaMetadata mediaMetadata = getFullyPopulatedMediaMetadata(); MediaMetadata fromBundle = MediaMetadata.CREATOR.fromBundle(mediaMetadata.toBundle()); assertThat(fromBundle).isEqualTo(mediaMetadata); - assertThat(fromBundle.extras.getString("exampleKey")).isEqualTo("exampleValue"); + // Extras is not implemented in MediaMetadata.equals(Object o). + assertThat(fromBundle.extras.getString(EXTRAS_KEY)).isEqualTo(EXTRAS_VALUE); + } + + private static MediaMetadata getFullyPopulatedMediaMetadata() { + Bundle extras = new Bundle(); + extras.putString(EXTRAS_KEY, EXTRAS_VALUE); + + return new MediaMetadata.Builder() + .setTitle("title") + .setArtist("artist") + .setAlbumTitle("album title") + .setAlbumArtist("album artist") + .setDisplayTitle("display title") + .setSubtitle("subtitle") + .setDescription("description") + .setMediaUri(Uri.parse("https://www.google.com")) + .setUserRating(new HeartRating(false)) + .setOverallRating(new PercentageRating(87.4f)) + .setArtworkData( + new byte[] {-88, 12, 3, 2, 124, -54, -33, 69}, MediaMetadata.PICTURE_TYPE_MEDIA) + .setArtworkUri(Uri.parse("https://www.google.com")) + .setTrackNumber(4) + .setTotalTrackCount(12) + .setFolderType(MediaMetadata.FOLDER_TYPE_PLAYLISTS) + .setIsPlayable(true) + .setRecordingYear(2000) + .setRecordingMonth(11) + .setRecordingDay(23) + .setReleaseYear(2001) + .setReleaseMonth(1) + .setReleaseDay(2) + .setComposer("Composer") + .setConductor("Conductor") + .setWriter("Writer") + .setDiscNumber(1) + .setTotalDiscCount(3) + .setGenre("Pop") + .setCompilation("Amazing songs.") + .setExtras(extras) + .build(); } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java index bee9a64ea6..78f232e9a5 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java @@ -39,6 +39,7 @@ import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; import com.google.android.exoplayer2.source.MediaSourceFactory; import com.google.android.exoplayer2.source.ShuffleOrder; +import com.google.android.exoplayer2.source.TrackGroup; import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.text.Cue; import com.google.android.exoplayer2.trackselection.ExoTrackSelection; @@ -111,6 +112,10 @@ import java.util.concurrent.CopyOnWriteArraySet; private MediaMetadata mediaMetadata; private MediaMetadata playlistMetadata; + // MediaMetadata built from static (TrackGroup Format) and dynamic (onMetadata(Metadata)) metadata + // sources. + private MediaMetadata staticAndDynamicMediaMetadata; + // Playback information when there is no pending seek/set source operation. private PlaybackInfo playbackInfo; @@ -229,6 +234,7 @@ import java.util.concurrent.CopyOnWriteArraySet; .build(); mediaMetadata = MediaMetadata.EMPTY; playlistMetadata = MediaMetadata.EMPTY; + staticAndDynamicMediaMetadata = MediaMetadata.EMPTY; maskingWindowIndex = C.INDEX_UNSET; playbackInfoUpdateHandler = clock.createHandler(applicationLooper, /* callback= */ null); playbackInfoUpdateListener = @@ -986,8 +992,11 @@ import java.util.concurrent.CopyOnWriteArraySet; } public void onMetadata(Metadata metadata) { - MediaMetadata newMediaMetadata = - mediaMetadata.buildUpon().populateFromMetadata(metadata).build(); + staticAndDynamicMediaMetadata = + staticAndDynamicMediaMetadata.buildUpon().populateFromMetadata(metadata).build(); + + MediaMetadata newMediaMetadata = buildUpdatedMediaMetadata(); + if (newMediaMetadata.equals(mediaMetadata)) { return; } @@ -1235,12 +1244,17 @@ import java.util.concurrent.CopyOnWriteArraySet; .windowIndex; mediaItem = newPlaybackInfo.timeline.getWindow(windowIndex, window).mediaItem; } - newMediaMetadata = mediaItem != null ? mediaItem.mediaMetadata : MediaMetadata.EMPTY; + staticAndDynamicMediaMetadata = MediaMetadata.EMPTY; } if (mediaItemTransitioned || !previousPlaybackInfo.staticMetadata.equals(newPlaybackInfo.staticMetadata)) { - newMediaMetadata = - newMediaMetadata.buildUpon().populateFromMetadata(newPlaybackInfo.staticMetadata).build(); + staticAndDynamicMediaMetadata = + staticAndDynamicMediaMetadata + .buildUpon() + .populateFromMetadata(newPlaybackInfo.staticMetadata) + .build(); + + newMediaMetadata = buildUpdatedMediaMetadata(); } boolean metadataChanged = !newMediaMetadata.equals(mediaMetadata); mediaMetadata = newMediaMetadata; @@ -1794,6 +1808,24 @@ import java.util.concurrent.CopyOnWriteArraySet; return positionUs; } + /** + * Builds a {@link MediaMetadata} from the main sources. + * + *
{@link MediaItem} {@link MediaMetadata} is prioritized, with any gaps/missing fields + * populated by metadata from static ({@link TrackGroup} {@link Format}) and dynamic ({@link + * #onMetadata(Metadata)}) sources. + */ + private MediaMetadata buildUpdatedMediaMetadata() { + @Nullable MediaItem mediaItem = getCurrentMediaItem(); + + if (mediaItem == null) { + return staticAndDynamicMediaMetadata; + } + + // MediaItem metadata is prioritized over metadata within the media. + return staticAndDynamicMediaMetadata.buildUpon().populate(mediaItem.mediaMetadata).build(); + } + private static boolean isPlaying(PlaybackInfo playbackInfo) { return playbackInfo.playbackState == Player.STATE_READY && playbackInfo.playWhenReady diff --git a/library/core/src/test/java/com/google/android/exoplayer2/ExoPlayerTest.java b/library/core/src/test/java/com/google/android/exoplayer2/ExoPlayerTest.java index bde93310f1..07f61f4e5e 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/ExoPlayerTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/ExoPlayerTest.java @@ -11181,6 +11181,46 @@ public final class ExoPlayerTest { assertThat(videoRenderer.get().positionResetCount).isEqualTo(1); } + @Test + public void setMediaItem_withMediaMetadata_updatesMediaMetadata() { + MediaMetadata mediaMetadata = new MediaMetadata.Builder().setTitle("the title").build(); + + ExoPlayer player = new TestExoPlayerBuilder(context).build(); + player.setMediaItem( + new MediaItem.Builder() + .setMediaId("id") + .setUri(Uri.EMPTY) + .setMediaMetadata(mediaMetadata) + .build()); + + assertThat(player.getMediaMetadata()).isEqualTo(mediaMetadata); + } + + @Test + public void playingMedia_withNoMetadata_doesNotUpdateMediaMetadata() throws Exception { + MediaMetadata mediaMetadata = new MediaMetadata.Builder().setTitle("the title").build(); + + ExoPlayer player = new TestExoPlayerBuilder(context).build(); + MediaItem mediaItem = + new MediaItem.Builder() + .setMediaId("id") + .setUri(Uri.parse("asset://android_asset/media/mp4/sample.mp4")) + .setMediaMetadata(mediaMetadata) + .build(); + player.setMediaItem(mediaItem); + + assertThat(player.getMediaMetadata()).isEqualTo(mediaMetadata); + + player.prepare(); + TestPlayerRunHelper.playUntilPosition( + player, /* windowIndex= */ 0, /* positionMs= */ 5 * C.MILLIS_PER_SECOND); + player.stop(); + + shadowOf(Looper.getMainLooper()).idle(); + + assertThat(player.getMediaMetadata()).isEqualTo(mediaMetadata); + } + // Internal methods. private static ActionSchedule.Builder addSurfaceSwitch(ActionSchedule.Builder builder) {