Skip to content

Commit

Permalink
Make MediaItems updateable
Browse files Browse the repository at this point in the history
This changes all MediaSources in our library to allow updates to
their MediaItems (if supported).

Issue: google/ExoPlayer#9978
Issue: #33
PiperOrigin-RevId: 546808812
  • Loading branch information
tonihei authored and rohitjoins committed Jul 13, 2023
1 parent a8520bd commit 3d4bd7c
Show file tree
Hide file tree
Showing 21 changed files with 1,233 additions and 82 deletions.
3 changes: 3 additions & 0 deletions RELEASENOTES.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@
* Add `MediaSource.canUpdateMediaItem` and `MediaSource.updateMediaItem`
to accept `MediaItem` updates after creation via
`Player.replaceMediaItem(s)`.
* Allow `MediaItem` updates for all `MediaSource` classes provided by the
library via `Player.replaceMediaItem(s)`
(([#33](https://github.com/androidx/media/issues/33)),([#9978](https://github.com/google/ExoPlayer/issues/9978))).
* Transformer:
* Parse EXIF rotation data for image inputs.
* Remove `TransformationRequest.HdrMode` annotation type and its
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import androidx.annotation.IntDef;
import androidx.annotation.Nullable;
import androidx.media3.common.C;
import androidx.media3.common.MediaItem;
import androidx.media3.common.Timeline;
import androidx.media3.common.util.Assertions;
import androidx.media3.common.util.UnstableApi;
Expand Down Expand Up @@ -194,6 +195,12 @@ public ClippingMediaSource(
window = new Timeline.Window();
}

@Override
public boolean canUpdateMediaItem(MediaItem mediaItem) {
return getMediaItem().clippingConfiguration.equals(mediaItem.clippingConfiguration)
&& mediaSource.canUpdateMediaItem(mediaItem);
}

@Override
public void maybeThrowSourceInfoRefreshError() throws IOException {
if (clippingError != null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
import android.os.Handler;
import android.os.Message;
import android.util.Pair;
import androidx.annotation.GuardedBy;
import androidx.annotation.Nullable;
import androidx.media3.common.C;
import androidx.media3.common.MediaItem;
Expand Down Expand Up @@ -211,13 +212,15 @@ public ConcatenatingMediaSource2 build() {

private static final int MSG_UPDATE_TIMELINE = 0;

private final MediaItem mediaItem;
private final ImmutableList<MediaSourceHolder> mediaSourceHolders;
private final IdentityHashMap<MediaPeriod, MediaSourceHolder> mediaSourceByMediaPeriod;

@Nullable private Handler playbackThreadHandler;
private boolean timelineUpdateScheduled;

@GuardedBy("this")
private MediaItem mediaItem;

private ConcatenatingMediaSource2(
MediaItem mediaItem, ImmutableList<MediaSourceHolder> mediaSourceHolders) {
this.mediaItem = mediaItem;
Expand All @@ -232,10 +235,20 @@ public Timeline getInitialTimeline() {
}

@Override
public MediaItem getMediaItem() {
public synchronized MediaItem getMediaItem() {
return mediaItem;
}

@Override
public boolean canUpdateMediaItem(MediaItem mediaItem) {
return true;
}

@Override
public synchronized void updateMediaItem(MediaItem mediaItem) {
this.mediaItem = mediaItem;
}

@Override
protected void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) {
super.prepareSourceInternal(mediaTransferListener);
Expand Down Expand Up @@ -426,7 +439,7 @@ private ConcatenatedTimeline maybeCreateConcatenatedTimeline() {
}
}
return new ConcatenatedTimeline(
mediaItem,
getMediaItem(),
timelinesBuilder.build(),
firstPeriodIndicesBuilder.build(),
periodOffsetsInWindowUsBuilder.build(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,13 @@

import android.net.Uri;
import android.os.Looper;
import androidx.annotation.GuardedBy;
import androidx.annotation.Nullable;
import androidx.media3.common.C;
import androidx.media3.common.MediaItem;
import androidx.media3.common.Timeline;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.common.util.Util;
import androidx.media3.datasource.DataSource;
import androidx.media3.datasource.TransferListener;
import androidx.media3.exoplayer.drm.DefaultDrmSessionManagerProvider;
Expand Down Expand Up @@ -226,8 +228,6 @@ public ProgressiveMediaSource createMediaSource(MediaItem mediaItem) {
*/
public static final int DEFAULT_LOADING_CHECK_INTERVAL_BYTES = 1024 * 1024;

private final MediaItem mediaItem;
private final MediaItem.LocalConfiguration localConfiguration;
private final DataSource.Factory dataSourceFactory;
private final ProgressiveMediaExtractor.Factory progressiveMediaExtractorFactory;
private final DrmSessionManager drmSessionManager;
Expand All @@ -240,14 +240,16 @@ public ProgressiveMediaSource createMediaSource(MediaItem mediaItem) {
private boolean timelineIsLive;
@Nullable private TransferListener transferListener;

@GuardedBy("this")
private MediaItem mediaItem;

private ProgressiveMediaSource(
MediaItem mediaItem,
DataSource.Factory dataSourceFactory,
ProgressiveMediaExtractor.Factory progressiveMediaExtractorFactory,
DrmSessionManager drmSessionManager,
LoadErrorHandlingPolicy loadableLoadErrorHandlingPolicy,
int continueLoadingCheckIntervalBytes) {
this.localConfiguration = checkNotNull(mediaItem.localConfiguration);
this.mediaItem = mediaItem;
this.dataSourceFactory = dataSourceFactory;
this.progressiveMediaExtractorFactory = progressiveMediaExtractorFactory;
Expand All @@ -259,10 +261,24 @@ private ProgressiveMediaSource(
}

@Override
public MediaItem getMediaItem() {
public synchronized MediaItem getMediaItem() {
return mediaItem;
}

@Override
public boolean canUpdateMediaItem(MediaItem mediaItem) {
MediaItem.LocalConfiguration existingConfiguration = getLocalConfiguration();
@Nullable MediaItem.LocalConfiguration newConfiguration = mediaItem.localConfiguration;
return newConfiguration != null
&& newConfiguration.uri.equals(existingConfiguration.uri)
&& Util.areEqual(newConfiguration.customCacheKey, existingConfiguration.customCacheKey);
}

@Override
public synchronized void updateMediaItem(MediaItem mediaItem) {
this.mediaItem = mediaItem;
}

@Override
protected void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) {
transferListener = mediaTransferListener;
Expand All @@ -283,6 +299,7 @@ public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator, long star
if (transferListener != null) {
dataSource.addTransferListener(transferListener);
}
MediaItem.LocalConfiguration localConfiguration = getLocalConfiguration();
return new ProgressiveMediaPeriod(
localConfiguration.uri,
dataSource,
Expand Down Expand Up @@ -329,6 +346,10 @@ public void onSourceInfoRefreshed(long durationUs, boolean isSeekable, boolean i

// Internal methods.

private MediaItem.LocalConfiguration getLocalConfiguration() {
return checkNotNull(getMediaItem().localConfiguration);
}

private void notifySourceInfoRefreshed() {
// TODO: Split up isDynamic into multiple fields to indicate which values may change. Then
// indicate that the duration may change until it's known. See [internal: b/69703223].
Expand All @@ -339,7 +360,7 @@ private void notifySourceInfoRefreshed() {
/* isDynamic= */ false,
/* useLiveConfiguration= */ timelineIsLive,
/* manifest= */ null,
mediaItem);
getMediaItem());
if (timelineIsPlaceholder) {
// TODO: Actually prepare the extractors during preparation so that we don't need a
// placeholder. See https://github.com/google/ExoPlayer/issues/4727.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import static java.lang.Math.min;

import android.net.Uri;
import androidx.annotation.GuardedBy;
import androidx.annotation.IntRange;
import androidx.annotation.Nullable;
import androidx.media3.common.C;
Expand Down Expand Up @@ -108,7 +109,9 @@ public SilenceMediaSource createMediaSource() {
new byte[Util.getPcmFrameSize(PCM_ENCODING, CHANNEL_COUNT) * 1024];

private final long durationUs;
private final MediaItem mediaItem;

@GuardedBy("this")
private MediaItem mediaItem;

/**
* Creates a new media source providing silent audio of the given duration.
Expand Down Expand Up @@ -140,7 +143,7 @@ protected void prepareSourceInternal(@Nullable TransferListener mediaTransferLis
/* isDynamic= */ false,
/* useLiveConfiguration= */ false,
/* manifest= */ null,
mediaItem));
getMediaItem()));
}

@Override
Expand All @@ -155,10 +158,20 @@ public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator, long star
public void releasePeriod(MediaPeriod mediaPeriod) {}

@Override
public MediaItem getMediaItem() {
public synchronized MediaItem getMediaItem() {
return mediaItem;
}

@Override
public boolean canUpdateMediaItem(MediaItem mediaItem) {
return true;
}

@Override
public synchronized void updateMediaItem(MediaItem mediaItem) {
this.mediaItem = mediaItem;
}

@Override
protected void releaseSourceInternal() {}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,8 @@ public MediaItem getMediaItem() {

@Override
public boolean canUpdateMediaItem(MediaItem mediaItem) {
return contentMediaSource.canUpdateMediaItem(mediaItem);
return Util.areEqual(getAdsConfiguration(getMediaItem()), getAdsConfiguration(mediaItem))
&& contentMediaSource.canUpdateMediaItem(mediaItem);
}

@Override
Expand Down Expand Up @@ -370,6 +371,13 @@ private long[][] getAdDurationsUs() {
return adDurationsUs;
}

@Nullable
private static MediaItem.AdsConfiguration getAdsConfiguration(MediaItem mediaItem) {
return mediaItem.localConfiguration == null
? null
: mediaItem.localConfiguration.adsConfiguration;
}

/** Listener for component events. All methods are called on the main thread. */
private final class ComponentListener implements AdsLoader.EventListener {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
*/
package androidx.media3.exoplayer.source;

import static androidx.media3.common.util.Util.msToUs;
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.fail;

Expand All @@ -25,16 +26,20 @@
import androidx.media3.common.Timeline;
import androidx.media3.common.Timeline.Period;
import androidx.media3.common.Timeline.Window;
import androidx.media3.exoplayer.analytics.PlayerId;
import androidx.media3.exoplayer.source.ClippingMediaSource.IllegalClippingException;
import androidx.media3.exoplayer.source.MaskingMediaSource.PlaceholderTimeline;
import androidx.media3.exoplayer.source.MediaSource.MediaPeriodId;
import androidx.media3.test.utils.FakeMediaSource;
import androidx.media3.test.utils.FakeTimeline;
import androidx.media3.test.utils.FakeTimeline.TimelineWindowDefinition;
import androidx.media3.test.utils.MediaSourceTestRunner;
import androidx.media3.test.utils.TestUtil;
import androidx.media3.test.utils.TimelineAsserts;
import androidx.media3.test.utils.robolectric.RobolectricUtil;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import java.io.IOException;
import java.util.concurrent.atomic.AtomicReference;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
Expand Down Expand Up @@ -479,6 +484,83 @@ public void windowAndPeriodIndices() throws IOException {
TimelineAsserts.assertNextWindowIndices(clippedTimeline, Player.REPEAT_MODE_ALL, false, 0);
}

@Test
public void canUpdateMediaItem_withIrrelevantFieldsChanged_returnsTrue() {
MediaItem initialMediaItem =
new MediaItem.Builder()
.setMediaId("id")
.setClippingConfiguration(
new MediaItem.ClippingConfiguration.Builder().setStartPositionMs(1).build())
.build();
MediaItem updatedMediaItem =
TestUtil.buildFullyCustomizedMediaItem()
.buildUpon()
.setClippingConfiguration(
new MediaItem.ClippingConfiguration.Builder().setStartPositionMs(1).build())
.build();
MediaSource mediaSource = buildMediaSource(initialMediaItem);

boolean canUpdateMediaItem = mediaSource.canUpdateMediaItem(updatedMediaItem);

assertThat(canUpdateMediaItem).isTrue();
}

@Test
public void canUpdateMediaItem_withChangedClippingConfiguration_returnsFalse() {
MediaItem initialMediaItem =
new MediaItem.Builder()
.setMediaId("id")
.setClippingConfiguration(
new MediaItem.ClippingConfiguration.Builder().setStartPositionMs(1).build())
.build();
MediaItem updatedMediaItem =
new MediaItem.Builder()
.setMediaId("id")
.setClippingConfiguration(
new MediaItem.ClippingConfiguration.Builder().setStartPositionMs(2).build())
.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").build();
MediaItem updatedMediaItem = new MediaItem.Builder().setUri("http://test2.test").build();
MediaSource mediaSource = buildMediaSource(initialMediaItem);
AtomicReference<Timeline> 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) {
FakeMediaSource fakeMediaSource = new FakeMediaSource();
fakeMediaSource.setCanUpdateMediaItems(true);
fakeMediaSource.updateMediaItem(mediaItem);
return new ClippingMediaSource(
fakeMediaSource,
msToUs(mediaItem.clippingConfiguration.startPositionMs),
msToUs(mediaItem.clippingConfiguration.endPositionMs),
mediaItem.clippingConfiguration.startsAtKeyFrame,
mediaItem.clippingConfiguration.relativeToLiveWindow,
mediaItem.clippingConfiguration.relativeToDefaultPosition);
}

/**
* Wraps the specified timeline in a {@link ClippingMediaSource} and returns the clipped timeline.
*/
Expand Down
Loading

0 comments on commit 3d4bd7c

Please sign in to comment.