From e5010e3d71a70caea5c50881ea010e5628be96ea Mon Sep 17 00:00:00 2001 From: tofunmi Date: Fri, 6 Oct 2023 05:22:34 -0700 Subject: [PATCH] Support updating the mediaItem in ExternallyLoadedMediaSource PiperOrigin-RevId: 571308986 --- .../source/ExternallyLoadedMediaSource.java | 45 ++++-- .../ExternallyLoadedMediaSourceTest.java | 134 ++++++++++++++++++ 2 files changed, 169 insertions(+), 10 deletions(-) create mode 100644 libraries/exoplayer/src/test/java/androidx/media3/exoplayer/source/ExternallyLoadedMediaSourceTest.java diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/ExternallyLoadedMediaSource.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/ExternallyLoadedMediaSource.java index de73db744b..fc90b09fff 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/ExternallyLoadedMediaSource.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/ExternallyLoadedMediaSource.java @@ -16,7 +16,9 @@ package androidx.media3.exoplayer.source; import static androidx.media3.common.util.Assertions.checkNotNull; +import static androidx.media3.common.util.Util.msToUs; +import androidx.annotation.GuardedBy; import androidx.annotation.Nullable; import androidx.media3.common.C; import androidx.media3.common.MediaItem; @@ -27,6 +29,7 @@ import androidx.media3.exoplayer.drm.DrmSessionManagerProvider; import androidx.media3.exoplayer.upstream.Allocator; import androidx.media3.exoplayer.upstream.LoadErrorHandlingPolicy; import com.google.common.base.Charsets; +import java.util.Objects; /** * A {@link MediaSource} for media loaded outside of the usual ExoPlayer loading mechanism. @@ -81,23 +84,26 @@ public final class ExternallyLoadedMediaSource extends BaseMediaSource { } } - private final MediaItem mediaItem; - private final Timeline timeline; + private final long timelineDurationUs; + + @GuardedBy("this") + private MediaItem mediaItem; private ExternallyLoadedMediaSource(MediaItem mediaItem, long timelineDurationUs) { this.mediaItem = mediaItem; - this.timeline = + this.timelineDurationUs = timelineDurationUs; + } + + @Override + protected void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) { + Timeline timeline = new SinglePeriodTimeline( timelineDurationUs, /* isSeekable= */ true, /* isDynamic= */ false, /* useLiveConfiguration= */ false, /* manifest= */ null, - mediaItem); - } - - @Override - protected void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) { + getMediaItem()); refreshSourceInfo(timeline); } @@ -107,10 +113,26 @@ public final class ExternallyLoadedMediaSource extends BaseMediaSource { } @Override - public MediaItem getMediaItem() { + public synchronized MediaItem getMediaItem() { return mediaItem; } + @Override + public synchronized boolean canUpdateMediaItem(MediaItem mediaItem) { + @Nullable MediaItem.LocalConfiguration newConfiguration = mediaItem.localConfiguration; + MediaItem.LocalConfiguration oldConfiguration = checkNotNull(getMediaItem().localConfiguration); + return newConfiguration != null + && newConfiguration.uri.equals(oldConfiguration.uri) + && Objects.equals(newConfiguration.mimeType, oldConfiguration.mimeType) + && (newConfiguration.imageDurationMs == C.TIME_UNSET + || msToUs(newConfiguration.imageDurationMs) == timelineDurationUs); + } + + @Override + public synchronized void updateMediaItem(MediaItem mediaItem) { + this.mediaItem = mediaItem; + } + @Override public void maybeThrowSourceInfoRefreshError() { // Do nothing. @@ -118,6 +140,7 @@ public final class ExternallyLoadedMediaSource extends BaseMediaSource { @Override public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator, long startPositionUs) { + MediaItem mediaItem = getMediaItem(); checkNotNull(mediaItem.localConfiguration); checkNotNull( mediaItem.localConfiguration.mimeType, "Externally loaded mediaItems require a MIME type."); @@ -126,5 +149,7 @@ public final class ExternallyLoadedMediaSource extends BaseMediaSource { } @Override - public void releasePeriod(MediaPeriod mediaPeriod) {} + public void releasePeriod(MediaPeriod mediaPeriod) { + // Do nothing. + } } diff --git a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/source/ExternallyLoadedMediaSourceTest.java b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/source/ExternallyLoadedMediaSourceTest.java new file mode 100644 index 0000000000..894a5325f4 --- /dev/null +++ b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/source/ExternallyLoadedMediaSourceTest.java @@ -0,0 +1,134 @@ +/* + * 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.exoplayer.source; + +import static androidx.media3.common.util.Util.msToUs; +import static com.google.common.truth.Truth.assertThat; + +import androidx.media3.common.C; +import androidx.media3.common.MediaItem; +import androidx.media3.common.MimeTypes; +import androidx.media3.common.Timeline; +import androidx.media3.exoplayer.analytics.PlayerId; +import androidx.media3.test.utils.TestUtil; +import androidx.media3.test.utils.robolectric.RobolectricUtil; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import java.util.concurrent.atomic.AtomicReference; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** Unit test for {@link ProgressiveMediaSource}. */ +@RunWith(AndroidJUnit4.class) +public class ExternallyLoadedMediaSourceTest { + + @Test + public void canUpdateMediaItem_withIrrelevantFieldsChanged_returnsTrue() { + MediaItem initialMediaItem = + new MediaItem.Builder() + .setUri("http://test.test") + .setImageDurationMs(5 * C.MILLIS_PER_SECOND) + .setMimeType(MimeTypes.APPLICATION_EXTERNALLY_LOADED_IMAGE) + .build(); + MediaItem updatedMediaItem = + TestUtil.buildFullyCustomizedMediaItem() + .buildUpon() + .setUri("http://test.test") + .setImageDurationMs(5 * C.MILLIS_PER_SECOND) + .setMimeType(MimeTypes.APPLICATION_EXTERNALLY_LOADED_IMAGE) + .build(); + MediaSource mediaSource = buildMediaSource(initialMediaItem); + + boolean canUpdateMediaItem = mediaSource.canUpdateMediaItem(updatedMediaItem); + + assertThat(canUpdateMediaItem).isTrue(); + } + + @Test + public void canUpdateMediaItem_withNullLocalConfiguration_returnsFalse() { + MediaItem initialMediaItem = + new MediaItem.Builder() + .setImageDurationMs(5 * C.MILLIS_PER_SECOND) + .setUri("http://test.test") + .build(); + MediaItem updatedMediaItem = + new MediaItem.Builder() + .setImageDurationMs(5 * C.MILLIS_PER_SECOND) + .setMediaId("id") + .build(); + MediaSource mediaSource = buildMediaSource(initialMediaItem); + + boolean canUpdateMediaItem = mediaSource.canUpdateMediaItem(updatedMediaItem); + + assertThat(canUpdateMediaItem).isFalse(); + } + + @Test + public void canUpdateMediaItem_withChangedUri_returnsFalse() { + MediaItem initialMediaItem = + new MediaItem.Builder() + .setImageDurationMs(5 * C.MILLIS_PER_SECOND) + .setUri("http://test.test") + .build(); + MediaItem updatedMediaItem = + new MediaItem.Builder() + .setImageDurationMs(5 * C.MILLIS_PER_SECOND) + .setUri("http://test2.test") + .build(); + MediaSource mediaSource = buildMediaSource(initialMediaItem); + + boolean canUpdateMediaItem = mediaSource.canUpdateMediaItem(updatedMediaItem); + + assertThat(canUpdateMediaItem).isFalse(); + } + + @Test + public void updateMediaItem_createsTimelineWithUpdatedItem() throws Exception { + MediaItem initialMediaItem = + new MediaItem.Builder() + .setUri("http://test.test") + .setImageDurationMs(5 * C.MILLIS_PER_SECOND) + .setTag("tag1") + .build(); + MediaItem updatedMediaItem = + new MediaItem.Builder() + .setUri("http://test.test") + .setImageDurationMs(5 * C.MILLIS_PER_SECOND) + .setTag("tag2") + .build(); + MediaSource mediaSource = buildMediaSource(initialMediaItem); + AtomicReference timelineReference = new AtomicReference<>(); + + mediaSource.updateMediaItem(updatedMediaItem); + mediaSource.prepareSource( + (source, timeline) -> timelineReference.set(timeline), + /* mediaTransferListener= */ null, + PlayerId.UNSET); + RobolectricUtil.runMainLooperUntil(() -> timelineReference.get() != null); + + assertThat( + timelineReference + .get() + .getWindow(/* windowIndex= */ 0, new Timeline.Window()) + .mediaItem) + .isEqualTo(updatedMediaItem); + } + + private static MediaSource buildMediaSource(MediaItem mediaItem) { + return new ExternallyLoadedMediaSource.Factory( + msToUs(mediaItem.localConfiguration.imageDurationMs)) + .createMediaSource(mediaItem); + } +}