Make MediaItems updateable

This changes all MediaSources in our library to allow updates to
their MediaItems (if supported).

Issue: google/ExoPlayer#9978
Issue: androidx/media#33
PiperOrigin-RevId: 546808812
This commit is contained in:
tonihei 2023-07-10 10:35:44 +01:00 committed by Rohit Singh
parent a8520bdee6
commit 3d4bd7ce19
21 changed files with 1233 additions and 82 deletions

View File

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

View File

@ -22,6 +22,7 @@ import static java.lang.annotation.ElementType.TYPE_USE;
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;
@ -194,6 +195,12 @@ public final class ClippingMediaSource extends WrappingMediaSource {
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) {

View File

@ -25,6 +25,7 @@ import android.net.Uri;
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;
@ -211,13 +212,15 @@ public final class ConcatenatingMediaSource2 extends CompositeMediaSource<Intege
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;
@ -232,10 +235,20 @@ public final class ConcatenatingMediaSource2 extends CompositeMediaSource<Intege
}
@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);
@ -426,7 +439,7 @@ public final class ConcatenatingMediaSource2 extends CompositeMediaSource<Intege
}
}
return new ConcatenatedTimeline(
mediaItem,
getMediaItem(),
timelinesBuilder.build(),
firstPeriodIndicesBuilder.build(),
periodOffsetsInWindowUsBuilder.build(),

View File

@ -19,11 +19,13 @@ import static androidx.media3.common.util.Assertions.checkNotNull;
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;
@ -226,8 +228,6 @@ public final class ProgressiveMediaSource extends BaseMediaSource
*/
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;
@ -240,6 +240,9 @@ public final class ProgressiveMediaSource extends BaseMediaSource
private boolean timelineIsLive;
@Nullable private TransferListener transferListener;
@GuardedBy("this")
private MediaItem mediaItem;
private ProgressiveMediaSource(
MediaItem mediaItem,
DataSource.Factory dataSourceFactory,
@ -247,7 +250,6 @@ public final class ProgressiveMediaSource extends BaseMediaSource
DrmSessionManager drmSessionManager,
LoadErrorHandlingPolicy loadableLoadErrorHandlingPolicy,
int continueLoadingCheckIntervalBytes) {
this.localConfiguration = checkNotNull(mediaItem.localConfiguration);
this.mediaItem = mediaItem;
this.dataSourceFactory = dataSourceFactory;
this.progressiveMediaExtractorFactory = progressiveMediaExtractorFactory;
@ -259,10 +261,24 @@ public final class ProgressiveMediaSource extends BaseMediaSource
}
@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;
@ -283,6 +299,7 @@ public final class ProgressiveMediaSource extends BaseMediaSource
if (transferListener != null) {
dataSource.addTransferListener(transferListener);
}
MediaItem.LocalConfiguration localConfiguration = getLocalConfiguration();
return new ProgressiveMediaPeriod(
localConfiguration.uri,
dataSource,
@ -329,6 +346,10 @@ public final class ProgressiveMediaSource extends BaseMediaSource
// 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].
@ -339,7 +360,7 @@ public final class ProgressiveMediaSource extends BaseMediaSource
/* 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.

View File

@ -18,6 +18,7 @@ package androidx.media3.exoplayer.source;
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;
@ -108,7 +109,9 @@ public final class SilenceMediaSource extends BaseMediaSource {
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.
@ -140,7 +143,7 @@ public final class SilenceMediaSource extends BaseMediaSource {
/* isDynamic= */ false,
/* useLiveConfiguration= */ false,
/* manifest= */ null,
mediaItem));
getMediaItem()));
}
@Override
@ -155,10 +158,20 @@ public final class SilenceMediaSource extends BaseMediaSource {
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() {}

View File

@ -193,7 +193,8 @@ public final class AdsMediaSource extends CompositeMediaSource<MediaPeriodId> {
@Override
public boolean canUpdateMediaItem(MediaItem mediaItem) {
return contentMediaSource.canUpdateMediaItem(mediaItem);
return Util.areEqual(getAdsConfiguration(getMediaItem()), getAdsConfiguration(mediaItem))
&& contentMediaSource.canUpdateMediaItem(mediaItem);
}
@Override
@ -370,6 +371,13 @@ public final class AdsMediaSource extends CompositeMediaSource<MediaPeriodId> {
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 {

View File

@ -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;
@ -25,6 +26,7 @@ import androidx.media3.common.Player;
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;
@ -32,9 +34,12 @@ 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;
@ -479,6 +484,83 @@ public final class ClippingMediaSourceTest {
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.
*/

View File

@ -46,6 +46,8 @@ import androidx.media3.exoplayer.util.EventLogger;
import androidx.media3.test.utils.FakeMediaSource;
import androidx.media3.test.utils.FakeTimeline;
import androidx.media3.test.utils.TestExoPlayerBuilder;
import androidx.media3.test.utils.TestUtil;
import androidx.media3.test.utils.robolectric.RobolectricUtil;
import androidx.test.core.app.ApplicationProvider;
import com.google.common.base.Supplier;
import com.google.common.collect.ImmutableList;
@ -691,6 +693,38 @@ public final class ConcatenatingMediaSource2Test {
}
}
@Test
public void canUpdateMediaItem_withFieldsChanged_returnsTrue() {
MediaItem updatedMediaItem =
TestUtil.buildFullyCustomizedMediaItem().buildUpon().setUri("http://test.test").build();
MediaSource mediaSource = config.mediaSourceSupplier.get();
boolean canUpdateMediaItem = mediaSource.canUpdateMediaItem(updatedMediaItem);
assertThat(canUpdateMediaItem).isTrue();
}
@Test
public void updateMediaItem_createsTimelineWithUpdatedItem() throws Exception {
MediaItem updatedMediaItem = new MediaItem.Builder().setUri("http://test.test").build();
MediaSource mediaSource = config.mediaSourceSupplier.get();
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 void blockingPrepareMediaPeriod(MediaPeriod mediaPeriod) {
ConditionVariable mediaPeriodPrepared = new ConditionVariable();
mediaPeriod.prepare(

View File

@ -0,0 +1,117 @@
/*
* 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 com.google.common.truth.Truth.assertThat;
import androidx.media3.common.MediaItem;
import androidx.media3.common.Timeline;
import androidx.media3.datasource.DefaultDataSource;
import androidx.media3.exoplayer.analytics.PlayerId;
import androidx.media3.test.utils.TestUtil;
import androidx.media3.test.utils.robolectric.RobolectricUtil;
import androidx.test.core.app.ApplicationProvider;
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 ProgressiveMediaSourceTest {
@Test
public void canUpdateMediaItem_withIrrelevantFieldsChanged_returnsTrue() {
MediaItem initialMediaItem =
new MediaItem.Builder().setUri("http://test.test").setCustomCacheKey("cache").build();
MediaItem updatedMediaItem =
TestUtil.buildFullyCustomizedMediaItem()
.buildUpon()
.setUri("http://test.test")
.setCustomCacheKey("cache")
.build();
MediaSource mediaSource = buildMediaSource(initialMediaItem);
boolean canUpdateMediaItem = mediaSource.canUpdateMediaItem(updatedMediaItem);
assertThat(canUpdateMediaItem).isTrue();
}
@Test
public void canUpdateMediaItem_withNullLocalConfiguration_returnsFalse() {
MediaItem initialMediaItem = new MediaItem.Builder().setUri("http://test.test").build();
MediaItem updatedMediaItem = new MediaItem.Builder().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().setUri("http://test.test").build();
MediaItem updatedMediaItem = new MediaItem.Builder().setUri("http://test2.test").build();
MediaSource mediaSource = buildMediaSource(initialMediaItem);
boolean canUpdateMediaItem = mediaSource.canUpdateMediaItem(updatedMediaItem);
assertThat(canUpdateMediaItem).isFalse();
}
@Test
public void canUpdateMediaItem_withChangedCustomCacheKey_returnsFalse() {
MediaItem initialMediaItem =
new MediaItem.Builder().setUri("http://test.test").setCustomCacheKey("old").build();
MediaItem updatedMediaItem =
new MediaItem.Builder().setUri("http://test.test").setCustomCacheKey("new").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").setTag("tag1").build();
MediaItem updatedMediaItem =
new MediaItem.Builder().setUri("http://test.test").setTag("tag2").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) {
return new ProgressiveMediaSource.Factory(
new DefaultDataSource.Factory(ApplicationProvider.getApplicationContext()))
.createMediaSource(mediaItem);
}
}

View File

@ -21,7 +21,12 @@ import static org.junit.Assert.assertThrows;
import android.net.Uri;
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;
@ -85,4 +90,39 @@ public class SilenceMediaSourceTest {
assertThat(mediaSource.getMediaItem().localConfiguration.uri).isEqualTo(Uri.EMPTY);
assertThat(mediaItem.localConfiguration.mimeType).isEqualTo(MimeTypes.AUDIO_RAW);
}
@Test
public void canUpdateMediaItem_withFieldsChanged_returnsTrue() {
MediaItem updatedMediaItem = TestUtil.buildFullyCustomizedMediaItem();
MediaSource mediaSource = buildMediaSource();
boolean canUpdateMediaItem = mediaSource.canUpdateMediaItem(updatedMediaItem);
assertThat(canUpdateMediaItem).isTrue();
}
@Test
public void updateMediaItem_createsTimelineWithUpdatedItem() throws Exception {
MediaItem updatedMediaItem = new MediaItem.Builder().setUri("http://test.test").build();
MediaSource mediaSource = buildMediaSource();
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() {
return new SilenceMediaSource.Factory().setDurationUs(1234).setTag("tag").createMediaSource();
}
}

View File

@ -18,11 +18,13 @@ package androidx.media3.exoplayer.source.ads;
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.robolectric.Shadows.shadowOf;
import android.content.Context;
import android.net.Uri;
import android.os.Looper;
import androidx.media3.common.AdPlaybackState;
@ -32,6 +34,7 @@ import androidx.media3.common.MediaItem;
import androidx.media3.common.Timeline;
import androidx.media3.datasource.DataSpec;
import androidx.media3.exoplayer.analytics.PlayerId;
import androidx.media3.exoplayer.source.DefaultMediaSourceFactory;
import androidx.media3.exoplayer.source.MediaPeriod;
import androidx.media3.exoplayer.source.MediaSource;
import androidx.media3.exoplayer.source.MediaSource.MediaPeriodId;
@ -40,7 +43,11 @@ import androidx.media3.exoplayer.source.SinglePeriodTimeline;
import androidx.media3.exoplayer.source.ads.AdsLoader.EventListener;
import androidx.media3.exoplayer.upstream.Allocator;
import androidx.media3.test.utils.FakeMediaSource;
import androidx.media3.test.utils.TestUtil;
import androidx.media3.test.utils.robolectric.RobolectricUtil;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import java.util.concurrent.atomic.AtomicReference;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
@ -238,4 +245,90 @@ public final class AdsMediaSourceTest {
prerollAdMediaSource.assertReleased();
contentMediaSource.assertReleased();
}
@Test
public void canUpdateMediaItem_withIrrelevantFieldsChanged_returnsTrue() {
MediaItem initialMediaItem =
new MediaItem.Builder()
.setUri("http://test.uri")
.setAdsConfiguration(
new MediaItem.AdsConfiguration.Builder(Uri.parse("http://ad.tag.test")).build())
.build();
MediaItem updatedMediaItem =
TestUtil.buildFullyCustomizedMediaItem()
.buildUpon()
.setAdsConfiguration(
new MediaItem.AdsConfiguration.Builder(Uri.parse("http://ad.tag.test")).build())
.build();
MediaSource mediaSource = buildMediaSource(initialMediaItem);
boolean canUpdateMediaItem = mediaSource.canUpdateMediaItem(updatedMediaItem);
assertThat(canUpdateMediaItem).isTrue();
}
@Test
public void canUpdateMediaItem_withChangedAdsConfiguration_returnsFalse() {
MediaItem initialMediaItem =
new MediaItem.Builder()
.setUri("http://test.uri")
.setAdsConfiguration(
new MediaItem.AdsConfiguration.Builder(Uri.parse("http://ad.tag.test")).build())
.build();
MediaItem updatedMediaItem =
new MediaItem.Builder()
.setUri("http://test.uri")
.setAdsConfiguration(
new MediaItem.AdsConfiguration.Builder(Uri.parse("http://other.tag.test")).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);
AdsLoader adsLoader = mock(AdsLoader.class);
doAnswer(
method -> {
((EventListener) method.getArgument(4))
.onAdPlaybackState(new AdPlaybackState(TEST_ADS_ID));
return null;
})
.when(adsLoader)
.start(any(), any(), any(), any(), any());
return new AdsMediaSource(
fakeMediaSource,
TEST_ADS_DATA_SPEC,
TEST_ADS_ID,
new DefaultMediaSourceFactory((Context) ApplicationProvider.getApplicationContext()),
adsLoader,
/* adViewProvider= */ () -> null);
}
}

View File

@ -28,6 +28,7 @@ import android.os.Looper;
import android.os.SystemClock;
import android.text.TextUtils;
import android.util.SparseArray;
import androidx.annotation.GuardedBy;
import androidx.annotation.Nullable;
import androidx.media3.common.C;
import androidx.media3.common.MediaItem;
@ -391,7 +392,6 @@ public final class DashMediaSource extends BaseMediaSource {
private static final String TAG = "DashMediaSource";
private final MediaItem mediaItem;
private final boolean sideloadedManifest;
private final DataSource.Factory manifestDataSourceFactory;
private final DashChunkSource.Factory chunkSourceFactory;
@ -433,6 +433,9 @@ public final class DashMediaSource extends BaseMediaSource {
private int firstPeriodId;
@GuardedBy("this")
private MediaItem mediaItem;
private DashMediaSource(
MediaItem mediaItem,
@Nullable DashManifest manifest,
@ -496,10 +499,28 @@ public final class DashMediaSource extends BaseMediaSource {
// MediaSource implementation.
@Override
public MediaItem getMediaItem() {
public synchronized MediaItem getMediaItem() {
return mediaItem;
}
@Override
public boolean canUpdateMediaItem(MediaItem mediaItem) {
MediaItem existingMediaItem = getMediaItem();
MediaItem.LocalConfiguration existingConfiguration =
checkNotNull(existingMediaItem.localConfiguration);
@Nullable MediaItem.LocalConfiguration newConfiguration = mediaItem.localConfiguration;
return newConfiguration != null
&& newConfiguration.uri.equals(existingConfiguration.uri)
&& newConfiguration.streamKeys.equals(existingConfiguration.streamKeys)
&& Util.areEqual(newConfiguration.drmConfiguration, existingConfiguration.drmConfiguration)
&& existingMediaItem.liveConfiguration.equals(mediaItem.liveConfiguration);
}
@Override
public synchronized void updateMediaItem(MediaItem mediaItem) {
this.mediaItem = mediaItem;
}
@Override
protected void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) {
this.mediaTransferListener = mediaTransferListener;
@ -902,7 +923,7 @@ public final class DashMediaSource extends BaseMediaSource {
windowDurationUs,
windowDefaultPositionUs,
manifest,
mediaItem,
getMediaItem(),
manifest.dynamic ? liveConfiguration : null);
refreshSourceInfo(timeline);
@ -938,12 +959,13 @@ public final class DashMediaSource extends BaseMediaSource {
}
private void updateLiveConfiguration(long nowInWindowUs, long windowDurationUs) {
MediaItem.LiveConfiguration mediaItemLiveConfiguration = getMediaItem().liveConfiguration;
// Default maximum offset: start of window.
long maxPossibleLiveOffsetMs = usToMs(nowInWindowUs);
long maxLiveOffsetMs = maxPossibleLiveOffsetMs;
// Override maximum offset with user or media defined values if they are smaller.
if (mediaItem.liveConfiguration.maxOffsetMs != C.TIME_UNSET) {
maxLiveOffsetMs = min(maxLiveOffsetMs, mediaItem.liveConfiguration.maxOffsetMs);
if (mediaItemLiveConfiguration.maxOffsetMs != C.TIME_UNSET) {
maxLiveOffsetMs = min(maxLiveOffsetMs, mediaItemLiveConfiguration.maxOffsetMs);
} else if (manifest.serviceDescription != null
&& manifest.serviceDescription.maxOffsetMs != C.TIME_UNSET) {
maxLiveOffsetMs = min(maxLiveOffsetMs, manifest.serviceDescription.maxOffsetMs);
@ -961,10 +983,10 @@ public final class DashMediaSource extends BaseMediaSource {
}
// Override minimum offset with user and media defined values if they are larger, but don't
// exceed the maximum possible offset.
if (mediaItem.liveConfiguration.minOffsetMs != C.TIME_UNSET) {
if (mediaItemLiveConfiguration.minOffsetMs != C.TIME_UNSET) {
minLiveOffsetMs =
constrainValue(
mediaItem.liveConfiguration.minOffsetMs, minLiveOffsetMs, maxPossibleLiveOffsetMs);
mediaItemLiveConfiguration.minOffsetMs, minLiveOffsetMs, maxPossibleLiveOffsetMs);
} else if (manifest.serviceDescription != null
&& manifest.serviceDescription.minOffsetMs != C.TIME_UNSET) {
minLiveOffsetMs =
@ -1000,14 +1022,14 @@ public final class DashMediaSource extends BaseMediaSource {
maxTargetOffsetForSafeDistanceToWindowStartMs, minLiveOffsetMs, maxLiveOffsetMs);
}
float minPlaybackSpeed = C.RATE_UNSET;
if (mediaItem.liveConfiguration.minPlaybackSpeed != C.RATE_UNSET) {
minPlaybackSpeed = mediaItem.liveConfiguration.minPlaybackSpeed;
if (mediaItemLiveConfiguration.minPlaybackSpeed != C.RATE_UNSET) {
minPlaybackSpeed = mediaItemLiveConfiguration.minPlaybackSpeed;
} else if (manifest.serviceDescription != null) {
minPlaybackSpeed = manifest.serviceDescription.minPlaybackSpeed;
}
float maxPlaybackSpeed = C.RATE_UNSET;
if (mediaItem.liveConfiguration.maxPlaybackSpeed != C.RATE_UNSET) {
maxPlaybackSpeed = mediaItem.liveConfiguration.maxPlaybackSpeed;
if (mediaItemLiveConfiguration.maxPlaybackSpeed != C.RATE_UNSET) {
maxPlaybackSpeed = mediaItemLiveConfiguration.maxPlaybackSpeed;
} else if (manifest.serviceDescription != null) {
maxPlaybackSpeed = manifest.serviceDescription.maxPlaybackSpeed;
}

View File

@ -16,7 +16,6 @@
package androidx.media3.exoplayer.dash;
import static com.google.common.truth.Truth.assertThat;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static org.junit.Assert.fail;
import android.net.Uri;
@ -24,6 +23,7 @@ import androidx.media3.common.C;
import androidx.media3.common.MediaItem;
import androidx.media3.common.MediaItem.LiveConfiguration;
import androidx.media3.common.ParserException;
import androidx.media3.common.StreamKey;
import androidx.media3.common.Timeline;
import androidx.media3.common.Timeline.Window;
import androidx.media3.common.util.Util;
@ -32,18 +32,17 @@ import androidx.media3.datasource.DataSource;
import androidx.media3.datasource.FileDataSource;
import androidx.media3.exoplayer.analytics.PlayerId;
import androidx.media3.exoplayer.source.MediaSource;
import androidx.media3.exoplayer.source.MediaSource.MediaSourceCaller;
import androidx.media3.exoplayer.upstream.ParsingLoadable;
import androidx.media3.test.utils.TestUtil;
import androidx.media3.test.utils.robolectric.RobolectricUtil;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.common.collect.ImmutableList;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicReference;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.shadows.ShadowLooper;
/** Unit test for {@link DashMediaSource}. */
@RunWith(AndroidJUnit4.class)
@ -142,7 +141,7 @@ public final class DashMediaSourceTest {
@Test
public void prepare_withoutLiveConfiguration_withoutMediaItemLiveConfiguration_usesUnitSpeed()
throws InterruptedException {
throws Exception {
DashMediaSource mediaSource =
new DashMediaSource.Factory(
() -> createSampleMpdDataSource(SAMPLE_MPD_LIVE_WITHOUT_LIVE_CONFIGURATION))
@ -161,7 +160,7 @@ public final class DashMediaSourceTest {
@Test
public void prepare_withoutLiveConfiguration_withOnlyMediaItemTargetOffset_usesUnitSpeed()
throws InterruptedException {
throws Exception {
DashMediaSource mediaSource =
new DashMediaSource.Factory(
() -> createSampleMpdDataSource(SAMPLE_MPD_LIVE_WITHOUT_LIVE_CONFIGURATION))
@ -184,7 +183,7 @@ public final class DashMediaSourceTest {
@Test
public void prepare_withoutLiveConfiguration_withMediaItemSpeedLimits_usesDefaultFallbackValues()
throws InterruptedException {
throws Exception {
DashMediaSource mediaSource =
new DashMediaSource.Factory(
() -> createSampleMpdDataSource(SAMPLE_MPD_LIVE_WITHOUT_LIVE_CONFIGURATION))
@ -209,7 +208,7 @@ public final class DashMediaSourceTest {
@Test
public void
prepare_withoutLiveConfiguration_withoutMediaItemTargetOffset_usesDefinedFallbackTargetOffset()
throws InterruptedException {
throws Exception {
DashMediaSource mediaSource =
new DashMediaSource.Factory(
() -> createSampleMpdDataSource(SAMPLE_MPD_LIVE_WITHOUT_LIVE_CONFIGURATION))
@ -233,7 +232,7 @@ public final class DashMediaSourceTest {
@Test
public void prepare_withoutLiveConfiguration_withMediaItemLiveProperties_usesMediaItem()
throws InterruptedException {
throws Exception {
MediaItem mediaItem =
new MediaItem.Builder()
.setUri(Uri.EMPTY)
@ -260,7 +259,7 @@ public final class DashMediaSourceTest {
@Test
public void prepare_withSuggestedPresentationDelayAndMinBufferTime_usesManifestValue()
throws InterruptedException {
throws Exception {
DashMediaSource mediaSource =
new DashMediaSource.Factory(
() ->
@ -287,7 +286,7 @@ public final class DashMediaSourceTest {
@Test
public void
prepare_withSuggestedPresentationDelayAndMinBufferTime_withMediaItemLiveProperties_usesMediaItem()
throws InterruptedException {
throws Exception {
MediaItem mediaItem =
new MediaItem.Builder()
.setUri(Uri.EMPTY)
@ -319,8 +318,7 @@ public final class DashMediaSourceTest {
}
@Test
public void prepare_withCompleteServiceDescription_usesManifestValue()
throws InterruptedException {
public void prepare_withCompleteServiceDescription_usesManifestValue() throws Exception {
DashMediaSource mediaSource =
new DashMediaSource.Factory(
() -> createSampleMpdDataSource(SAMPLE_MPD_LIVE_WITH_COMPLETE_SERVICE_DESCRIPTION))
@ -339,7 +337,7 @@ public final class DashMediaSourceTest {
@Test
public void prepare_withCompleteServiceDescription_withMediaItemLiveProperties_usesMediaItem()
throws InterruptedException {
throws Exception {
MediaItem mediaItem =
new MediaItem.Builder()
.setUri(Uri.EMPTY)
@ -397,7 +395,7 @@ public final class DashMediaSourceTest {
@Test
public void prepare_targetLiveOffsetInWindow_manifestTargetOffsetAndAlignedWindowStartPosition()
throws InterruptedException {
throws Exception {
DashMediaSource mediaSource =
new DashMediaSource.Factory(
() -> createSampleMpdDataSource(SAMPLE_MPD_LIVE_WITH_OFFSET_INSIDE_WINDOW))
@ -413,7 +411,7 @@ public final class DashMediaSourceTest {
@Test
public void prepare_targetLiveOffsetTooLong_correctedTargetOffsetAndAlignedWindowStartPosition()
throws InterruptedException {
throws Exception {
DashMediaSource mediaSource =
new DashMediaSource.Factory(
() -> createSampleMpdDataSource(SAMPLE_MPD_LIVE_WITH_OFFSET_TOO_LONG))
@ -429,7 +427,7 @@ public final class DashMediaSourceTest {
@Test
public void prepare_targetLiveOffsetTooShort_correctedTargetOffsetAndAlignedWindowStartPosition()
throws InterruptedException {
throws Exception {
// Load manifest with now time far behind the start of the window.
DashMediaSource mediaSource =
new DashMediaSource.Factory(
@ -444,21 +442,156 @@ public final class DashMediaSourceTest {
assertThat(window.liveConfiguration.targetOffsetMs).isEqualTo(60_000 - 16_000);
}
private static Window prepareAndWaitForTimelineRefresh(MediaSource mediaSource)
throws InterruptedException {
AtomicReference<Window> windowReference = new AtomicReference<>();
CountDownLatch countDownLatch = new CountDownLatch(/* count= */ 1);
MediaSourceCaller caller =
(MediaSource source, Timeline timeline) -> {
if (windowReference.get() == null) {
windowReference.set(timeline.getWindow(0, new Timeline.Window()));
countDownLatch.countDown();
}
};
mediaSource.prepareSource(caller, /* mediaTransferListener= */ null, PlayerId.UNSET);
while (!countDownLatch.await(/* timeout= */ 10, MILLISECONDS)) {
ShadowLooper.idleMainLooper();
}
@Test
public void canUpdateMediaItem_withIrrelevantFieldsChanged_returnsTrue() {
MediaItem initialMediaItem =
new MediaItem.Builder()
.setUri("http://test.test")
.setStreamKeys(
ImmutableList.of(new StreamKey(/* groupIndex= */ 1, /* streamIndex= */ 0)))
.setDrmConfiguration(new MediaItem.DrmConfiguration.Builder(C.WIDEVINE_UUID).build())
.setLiveConfiguration(new LiveConfiguration.Builder().setTargetOffsetMs(2000).build())
.build();
MediaItem updatedMediaItem =
TestUtil.buildFullyCustomizedMediaItem()
.buildUpon()
.setUri("http://test.test")
.setStreamKeys(
ImmutableList.of(new StreamKey(/* groupIndex= */ 1, /* streamIndex= */ 0)))
.setDrmConfiguration(new MediaItem.DrmConfiguration.Builder(C.WIDEVINE_UUID).build())
.setLiveConfiguration(new LiveConfiguration.Builder().setTargetOffsetMs(2000).build())
.build();
MediaSource mediaSource =
new DashMediaSource.Factory(
() -> createSampleMpdDataSource(SAMPLE_MPD_LIVE_WITHOUT_LIVE_CONFIGURATION))
.createMediaSource(initialMediaItem);
boolean canUpdateMediaItem = mediaSource.canUpdateMediaItem(updatedMediaItem);
assertThat(canUpdateMediaItem).isTrue();
}
@Test
public void canUpdateMediaItem_withNullLocalConfiguration_returnsFalse() {
MediaItem initialMediaItem = new MediaItem.Builder().setUri("http://test.test").build();
MediaItem updatedMediaItem = new MediaItem.Builder().setMediaId("id").build();
MediaSource mediaSource =
new DashMediaSource.Factory(
() -> createSampleMpdDataSource(SAMPLE_MPD_LIVE_WITHOUT_LIVE_CONFIGURATION))
.createMediaSource(initialMediaItem);
boolean canUpdateMediaItem = mediaSource.canUpdateMediaItem(updatedMediaItem);
assertThat(canUpdateMediaItem).isFalse();
}
@Test
public void canUpdateMediaItem_withChangedUri_returnsFalse() {
MediaItem initialMediaItem = new MediaItem.Builder().setUri("http://test.test").build();
MediaItem updatedMediaItem = new MediaItem.Builder().setUri("http://test2.test").build();
MediaSource mediaSource =
new DashMediaSource.Factory(
() -> createSampleMpdDataSource(SAMPLE_MPD_LIVE_WITHOUT_LIVE_CONFIGURATION))
.createMediaSource(initialMediaItem);
boolean canUpdateMediaItem = mediaSource.canUpdateMediaItem(updatedMediaItem);
assertThat(canUpdateMediaItem).isFalse();
}
@Test
public void canUpdateMediaItem_withChangedStreamKeys_returnsFalse() {
MediaItem initialMediaItem =
new MediaItem.Builder()
.setUri("http://test.test")
.setStreamKeys(
ImmutableList.of(new StreamKey(/* groupIndex= */ 1, /* streamIndex= */ 0)))
.build();
MediaItem updatedMediaItem =
new MediaItem.Builder()
.setUri("http://test.test")
.setStreamKeys(
ImmutableList.of(new StreamKey(/* groupIndex= */ 2, /* streamIndex= */ 2)))
.build();
MediaSource mediaSource =
new DashMediaSource.Factory(
() -> createSampleMpdDataSource(SAMPLE_MPD_LIVE_WITHOUT_LIVE_CONFIGURATION))
.createMediaSource(initialMediaItem);
boolean canUpdateMediaItem = mediaSource.canUpdateMediaItem(updatedMediaItem);
assertThat(canUpdateMediaItem).isFalse();
}
@Test
public void canUpdateMediaItem_withChangedDrmConfiguration_returnsFalse() {
MediaItem initialMediaItem =
new MediaItem.Builder()
.setUri("http://test.test")
.setDrmConfiguration(new MediaItem.DrmConfiguration.Builder(C.WIDEVINE_UUID).build())
.build();
MediaItem updatedMediaItem =
new MediaItem.Builder()
.setUri("http://test.test")
.setDrmConfiguration(new MediaItem.DrmConfiguration.Builder(C.CLEARKEY_UUID).build())
.build();
MediaSource mediaSource =
new DashMediaSource.Factory(
() -> createSampleMpdDataSource(SAMPLE_MPD_LIVE_WITHOUT_LIVE_CONFIGURATION))
.createMediaSource(initialMediaItem);
boolean canUpdateMediaItem = mediaSource.canUpdateMediaItem(updatedMediaItem);
assertThat(canUpdateMediaItem).isFalse();
}
@Test
public void canUpdateMediaItem_withChangedLiveConfiguration_returnsFalse() {
MediaItem initialMediaItem =
new MediaItem.Builder()
.setUri("http://test.test")
.setLiveConfiguration(new LiveConfiguration.Builder().setTargetOffsetMs(2000).build())
.build();
MediaItem updatedMediaItem =
new MediaItem.Builder()
.setUri("http://test.test")
.setLiveConfiguration(new LiveConfiguration.Builder().setTargetOffsetMs(5000).build())
.build();
MediaSource mediaSource =
new DashMediaSource.Factory(
() -> createSampleMpdDataSource(SAMPLE_MPD_LIVE_WITHOUT_LIVE_CONFIGURATION))
.createMediaSource(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").setTag("tag1").build();
MediaItem updatedMediaItem =
new MediaItem.Builder().setUri("http://test.test").setTag("tag2").build();
MediaSource mediaSource =
new DashMediaSource.Factory(
() -> createSampleMpdDataSource(SAMPLE_MPD_LIVE_WITHOUT_LIVE_CONFIGURATION))
.createMediaSource(initialMediaItem);
mediaSource.updateMediaItem(updatedMediaItem);
Timeline.Window window = prepareAndWaitForTimelineRefresh(mediaSource);
assertThat(window.mediaItem).isEqualTo(updatedMediaItem);
}
private static Window prepareAndWaitForTimelineRefresh(MediaSource mediaSource) throws Exception {
AtomicReference<Timeline.Window> windowReference = new AtomicReference<>();
mediaSource.prepareSource(
(source, timeline) ->
windowReference.set(timeline.getWindow(/* windowIndex= */ 0, new Timeline.Window())),
/* mediaTransferListener= */ null,
PlayerId.UNSET);
RobolectricUtil.runMainLooperUntil(() -> windowReference.get() != null);
return windowReference.get();
}

View File

@ -21,6 +21,7 @@ import static java.lang.annotation.RetentionPolicy.SOURCE;
import android.os.Looper;
import android.os.SystemClock;
import androidx.annotation.GuardedBy;
import androidx.annotation.IntDef;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
@ -400,7 +401,6 @@ public final class HlsMediaSource extends BaseMediaSource
}
private final HlsExtractorFactory extractorFactory;
private final MediaItem.LocalConfiguration localConfiguration;
private final HlsDataSourceFactory dataSourceFactory;
private final CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory;
@Nullable private final CmcdConfiguration cmcdConfiguration;
@ -411,12 +411,14 @@ public final class HlsMediaSource extends BaseMediaSource
private final boolean useSessionKeys;
private final HlsPlaylistTracker playlistTracker;
private final long elapsedRealTimeOffsetMs;
private final MediaItem mediaItem;
private final long timestampAdjusterInitializationTimeoutMs;
private MediaItem.LiveConfiguration liveConfiguration;
@Nullable private TransferListener mediaTransferListener;
@GuardedBy("this")
private MediaItem mediaItem;
private HlsMediaSource(
MediaItem mediaItem,
HlsDataSourceFactory dataSourceFactory,
@ -431,7 +433,6 @@ public final class HlsMediaSource extends BaseMediaSource
@MetadataType int metadataType,
boolean useSessionKeys,
long timestampAdjusterInitializationTimeoutMs) {
this.localConfiguration = checkNotNull(mediaItem.localConfiguration);
this.mediaItem = mediaItem;
this.liveConfiguration = mediaItem.liveConfiguration;
this.dataSourceFactory = dataSourceFactory;
@ -449,10 +450,28 @@ public final class HlsMediaSource extends BaseMediaSource
}
@Override
public MediaItem getMediaItem() {
public synchronized MediaItem getMediaItem() {
return mediaItem;
}
@Override
public boolean canUpdateMediaItem(MediaItem mediaItem) {
MediaItem existingMediaItem = getMediaItem();
MediaItem.LocalConfiguration existingConfiguration =
checkNotNull(existingMediaItem.localConfiguration);
@Nullable MediaItem.LocalConfiguration newConfiguration = mediaItem.localConfiguration;
return newConfiguration != null
&& newConfiguration.uri.equals(existingConfiguration.uri)
&& newConfiguration.streamKeys.equals(existingConfiguration.streamKeys)
&& Util.areEqual(newConfiguration.drmConfiguration, existingConfiguration.drmConfiguration)
&& existingMediaItem.liveConfiguration.equals(mediaItem.liveConfiguration);
}
@Override
public synchronized void updateMediaItem(MediaItem mediaItem) {
this.mediaItem = mediaItem;
}
@Override
protected void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) {
this.mediaTransferListener = mediaTransferListener;
@ -462,7 +481,9 @@ public final class HlsMediaSource extends BaseMediaSource
MediaSourceEventListener.EventDispatcher eventDispatcher =
createEventDispatcher(/* mediaPeriodId= */ null);
playlistTracker.start(
localConfiguration.uri, eventDispatcher, /* primaryPlaylistListener= */ this);
checkNotNull(getMediaItem().localConfiguration).uri,
eventDispatcher,
/* primaryPlaylistListener= */ this);
}
@Override
@ -567,7 +588,7 @@ public final class HlsMediaSource extends BaseMediaSource
/* isDynamic= */ !playlist.hasEndTag,
suppressPositionProjection,
manifest,
mediaItem,
getMediaItem(),
liveConfiguration);
}
@ -600,7 +621,7 @@ public final class HlsMediaSource extends BaseMediaSource
/* isDynamic= */ false,
/* suppressPositionProjection= */ true,
manifest,
mediaItem,
getMediaItem(),
/* liveConfiguration= */ null);
}
@ -640,9 +661,10 @@ public final class HlsMediaSource extends BaseMediaSource
}
private void updateLiveConfiguration(HlsMediaPlaylist playlist, long targetLiveOffsetUs) {
MediaItem.LiveConfiguration mediaItemLiveConfiguration = getMediaItem().liveConfiguration;
boolean disableSpeedAdjustment =
mediaItem.liveConfiguration.minPlaybackSpeed == C.RATE_UNSET
&& mediaItem.liveConfiguration.maxPlaybackSpeed == C.RATE_UNSET
mediaItemLiveConfiguration.minPlaybackSpeed == C.RATE_UNSET
&& mediaItemLiveConfiguration.maxPlaybackSpeed == C.RATE_UNSET
&& playlist.serverControl.holdBackUs == C.TIME_UNSET
&& playlist.serverControl.partHoldBackUs == C.TIME_UNSET;
liveConfiguration =

View File

@ -23,6 +23,7 @@ import android.os.SystemClock;
import androidx.media3.common.C;
import androidx.media3.common.MediaItem;
import androidx.media3.common.ParserException;
import androidx.media3.common.StreamKey;
import androidx.media3.common.Timeline;
import androidx.media3.common.util.Util;
import androidx.media3.exoplayer.analytics.PlayerId;
@ -31,7 +32,9 @@ import androidx.media3.exoplayer.hls.playlist.HlsPlaylistParser;
import androidx.media3.exoplayer.source.MediaSource;
import androidx.media3.test.utils.FakeDataSet;
import androidx.media3.test.utils.FakeDataSource;
import androidx.media3.test.utils.TestUtil;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.common.collect.ImmutableList;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.util.ArrayList;
@ -775,6 +778,186 @@ public class HlsMediaSourceTest {
.isEqualTo(8000);
}
@Test
public void canUpdateMediaItem_withIrrelevantFieldsChanged_returnsTrue() {
String playlistUri = "http://test.test";
MediaItem initialMediaItem =
new MediaItem.Builder()
.setUri(playlistUri)
.setStreamKeys(
ImmutableList.of(new StreamKey(/* groupIndex= */ 1, /* streamIndex= */ 0)))
.setDrmConfiguration(new MediaItem.DrmConfiguration.Builder(C.WIDEVINE_UUID).build())
.setLiveConfiguration(
new MediaItem.LiveConfiguration.Builder().setTargetOffsetMs(2000).build())
.build();
MediaItem updatedMediaItem =
TestUtil.buildFullyCustomizedMediaItem()
.buildUpon()
.setUri(playlistUri)
.setStreamKeys(
ImmutableList.of(new StreamKey(/* groupIndex= */ 1, /* streamIndex= */ 0)))
.setDrmConfiguration(new MediaItem.DrmConfiguration.Builder(C.WIDEVINE_UUID).build())
.setLiveConfiguration(
new MediaItem.LiveConfiguration.Builder().setTargetOffsetMs(2000).build())
.build();
String playlist =
"#EXTM3U\n"
+ "#EXT-X-PLAYLIST-TYPE:VOD\n"
+ "#EXT-X-TARGETDURATION:10\n"
+ "#EXT-X-VERSION:4\n"
+ "#EXT-X-ENDLIST";
HlsMediaSource.Factory factory = createHlsMediaSourceFactory(playlistUri, playlist);
HlsMediaSource mediaSource = factory.createMediaSource(initialMediaItem);
boolean canUpdateMediaItem = mediaSource.canUpdateMediaItem(updatedMediaItem);
assertThat(canUpdateMediaItem).isTrue();
}
@Test
public void canUpdateMediaItem_withNullLocalConfiguration_returnsFalse() {
String playlistUri = "http://test.test";
MediaItem initialMediaItem = new MediaItem.Builder().setUri(playlistUri).build();
MediaItem updatedMediaItem = new MediaItem.Builder().setMediaId("id").build();
String playlist =
"#EXTM3U\n"
+ "#EXT-X-PLAYLIST-TYPE:VOD\n"
+ "#EXT-X-TARGETDURATION:10\n"
+ "#EXT-X-VERSION:4\n"
+ "#EXT-X-ENDLIST";
HlsMediaSource.Factory factory = createHlsMediaSourceFactory(playlistUri, playlist);
HlsMediaSource mediaSource = factory.createMediaSource(initialMediaItem);
boolean canUpdateMediaItem = mediaSource.canUpdateMediaItem(updatedMediaItem);
assertThat(canUpdateMediaItem).isFalse();
}
@Test
public void canUpdateMediaItem_withChangedUri_returnsFalse() {
String playlistUri = "http://test.test";
MediaItem initialMediaItem = new MediaItem.Builder().setUri(playlistUri).build();
MediaItem updatedMediaItem = new MediaItem.Builder().setUri("http://test2.test").build();
String playlist =
"#EXTM3U\n"
+ "#EXT-X-PLAYLIST-TYPE:VOD\n"
+ "#EXT-X-TARGETDURATION:10\n"
+ "#EXT-X-VERSION:4\n"
+ "#EXT-X-ENDLIST";
HlsMediaSource.Factory factory = createHlsMediaSourceFactory(playlistUri, playlist);
HlsMediaSource mediaSource = factory.createMediaSource(initialMediaItem);
boolean canUpdateMediaItem = mediaSource.canUpdateMediaItem(updatedMediaItem);
assertThat(canUpdateMediaItem).isFalse();
}
@Test
public void canUpdateMediaItem_withChangedStreamKeys_returnsFalse() {
String playlistUri = "http://test.test";
MediaItem initialMediaItem =
new MediaItem.Builder()
.setUri(playlistUri)
.setStreamKeys(
ImmutableList.of(new StreamKey(/* groupIndex= */ 1, /* streamIndex= */ 0)))
.build();
MediaItem updatedMediaItem =
new MediaItem.Builder()
.setUri(playlistUri)
.setStreamKeys(
ImmutableList.of(new StreamKey(/* groupIndex= */ 2, /* streamIndex= */ 2)))
.build();
String playlist =
"#EXTM3U\n"
+ "#EXT-X-PLAYLIST-TYPE:VOD\n"
+ "#EXT-X-TARGETDURATION:10\n"
+ "#EXT-X-VERSION:4\n"
+ "#EXT-X-ENDLIST";
HlsMediaSource.Factory factory = createHlsMediaSourceFactory(playlistUri, playlist);
HlsMediaSource mediaSource = factory.createMediaSource(initialMediaItem);
boolean canUpdateMediaItem = mediaSource.canUpdateMediaItem(updatedMediaItem);
assertThat(canUpdateMediaItem).isFalse();
}
@Test
public void canUpdateMediaItem_withChangedDrmConfiguration_returnsFalse() {
String playlistUri = "http://test.test";
MediaItem initialMediaItem =
new MediaItem.Builder()
.setUri(playlistUri)
.setDrmConfiguration(new MediaItem.DrmConfiguration.Builder(C.WIDEVINE_UUID).build())
.build();
MediaItem updatedMediaItem =
new MediaItem.Builder()
.setUri(playlistUri)
.setDrmConfiguration(new MediaItem.DrmConfiguration.Builder(C.CLEARKEY_UUID).build())
.build();
String playlist =
"#EXTM3U\n"
+ "#EXT-X-PLAYLIST-TYPE:VOD\n"
+ "#EXT-X-TARGETDURATION:10\n"
+ "#EXT-X-VERSION:4\n"
+ "#EXT-X-ENDLIST";
HlsMediaSource.Factory factory = createHlsMediaSourceFactory(playlistUri, playlist);
HlsMediaSource mediaSource = factory.createMediaSource(initialMediaItem);
boolean canUpdateMediaItem = mediaSource.canUpdateMediaItem(updatedMediaItem);
assertThat(canUpdateMediaItem).isFalse();
}
@Test
public void canUpdateMediaItem_withChangedLiveConfiguration_returnsFalse() {
String playlistUri = "http://test.test";
MediaItem initialMediaItem =
new MediaItem.Builder()
.setUri(playlistUri)
.setLiveConfiguration(
new MediaItem.LiveConfiguration.Builder().setTargetOffsetMs(2000).build())
.build();
MediaItem updatedMediaItem =
new MediaItem.Builder()
.setUri(playlistUri)
.setLiveConfiguration(
new MediaItem.LiveConfiguration.Builder().setTargetOffsetMs(5000).build())
.build();
String playlist =
"#EXTM3U\n"
+ "#EXT-X-PLAYLIST-TYPE:VOD\n"
+ "#EXT-X-TARGETDURATION:10\n"
+ "#EXT-X-VERSION:4\n"
+ "#EXT-X-ENDLIST";
HlsMediaSource.Factory factory = createHlsMediaSourceFactory(playlistUri, playlist);
HlsMediaSource mediaSource = factory.createMediaSource(initialMediaItem);
boolean canUpdateMediaItem = mediaSource.canUpdateMediaItem(updatedMediaItem);
assertThat(canUpdateMediaItem).isFalse();
}
@Test
public void updateMediaItem_createsTimelineWithUpdatedItem() throws Exception {
String playlistUri = "http://test.test";
MediaItem initialMediaItem = new MediaItem.Builder().setUri(playlistUri).setTag("tag1").build();
MediaItem updatedMediaItem = new MediaItem.Builder().setUri(playlistUri).setTag("tag2").build();
String playlist =
"#EXTM3U\n"
+ "#EXT-X-PLAYLIST-TYPE:VOD\n"
+ "#EXT-X-TARGETDURATION:10\n"
+ "#EXT-X-VERSION:4\n"
+ "#EXT-X-ENDLIST";
HlsMediaSource.Factory factory = createHlsMediaSourceFactory(playlistUri, playlist);
HlsMediaSource mediaSource = factory.createMediaSource(initialMediaItem);
mediaSource.updateMediaItem(updatedMediaItem);
Timeline timeline = prepareAndWaitForTimeline(mediaSource);
assertThat(timeline.getWindow(/* windowIndex= */ 0, new Timeline.Window()).mediaItem)
.isEqualTo(updatedMediaItem);
}
private static HlsMediaSource.Factory createHlsMediaSourceFactory(
String playlistUri, String playlist) {
FakeDataSet fakeDataSet = new FakeDataSet().setData(playlistUri, Util.getUtf8Bytes(playlist));

View File

@ -44,6 +44,7 @@ import android.os.Handler;
import android.os.Looper;
import android.util.Pair;
import android.view.ViewGroup;
import androidx.annotation.GuardedBy;
import androidx.annotation.MainThread;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
@ -499,7 +500,6 @@ public final class ImaServerSideAdInsertionMediaSource extends CompositeMediaSou
private static final String TAG = "ImaSSAIMediaSource";
private final MediaItem mediaItem;
private final Player player;
private final MediaSource.Factory contentMediaSourceFactory;
private final AdsLoader adsLoader;
@ -521,6 +521,9 @@ public final class ImaServerSideAdInsertionMediaSource extends CompositeMediaSou
@Nullable private Timeline contentTimeline;
private AdPlaybackState adPlaybackState;
@GuardedBy("this")
private MediaItem mediaItem;
private ImaServerSideAdInsertionMediaSource(
Player player,
MediaItem mediaItem,
@ -559,10 +562,29 @@ public final class ImaServerSideAdInsertionMediaSource extends CompositeMediaSou
}
@Override
public MediaItem getMediaItem() {
public synchronized MediaItem getMediaItem() {
return mediaItem;
}
@Override
public boolean canUpdateMediaItem(MediaItem mediaItem) {
MediaItem existingMediaItem = getMediaItem();
MediaItem.LocalConfiguration existingConfiguration =
checkNotNull(existingMediaItem.localConfiguration);
@Nullable MediaItem.LocalConfiguration newConfiguration = mediaItem.localConfiguration;
return newConfiguration != null
&& newConfiguration.uri.equals(existingConfiguration.uri)
&& newConfiguration.streamKeys.equals(existingConfiguration.streamKeys)
&& Util.areEqual(newConfiguration.customCacheKey, existingConfiguration.customCacheKey)
&& Util.areEqual(newConfiguration.drmConfiguration, existingConfiguration.drmConfiguration)
&& existingMediaItem.liveConfiguration.equals(mediaItem.liveConfiguration);
}
@Override
public synchronized void updateMediaItem(MediaItem mediaItem) {
this.mediaItem = mediaItem;
}
@Override
public void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) {
mainHandler.post(() -> assertSingleInstanceInPlaylist(checkNotNull(player)));
@ -588,6 +610,7 @@ public final class ImaServerSideAdInsertionMediaSource extends CompositeMediaSou
@Override
protected void onChildSourceInfoRefreshed(
Void childSourceId, MediaSource mediaSource, Timeline newTimeline) {
MediaItem mediaItem = getMediaItem();
refreshSourceInfo(
new ForwardingTimeline(newTimeline) {
@Override
@ -728,6 +751,7 @@ public final class ImaServerSideAdInsertionMediaSource extends CompositeMediaSou
private void setContentUri(Uri contentUri) {
if (serverSideAdInsertionMediaSource == null) {
MediaItem mediaItem = getMediaItem();
MediaItem contentMediaItem =
new MediaItem.Builder()
.setUri(contentUri)
@ -835,6 +859,7 @@ public final class ImaServerSideAdInsertionMediaSource extends CompositeMediaSou
return;
}
MediaItem mediaItem = getMediaItem();
if (mediaItem.equals(oldPosition.mediaItem) && !mediaItem.equals(newPosition.mediaItem)) {
// Playback automatically transitioned to the next media item. Notify the SDK.
streamPlayer.onContentCompleted();
@ -906,7 +931,7 @@ public final class ImaServerSideAdInsertionMediaSource extends CompositeMediaSou
@Override
public void onMetadata(Metadata metadata) {
if (!isCurrentAdPlaying(player, mediaItem, adsId)) {
if (!isCurrentAdPlaying(player, getMediaItem(), adsId)) {
return;
}
for (int i = 0; i < metadata.length(); i++) {
@ -926,14 +951,14 @@ public final class ImaServerSideAdInsertionMediaSource extends CompositeMediaSou
@Override
public void onPlaybackStateChanged(@Player.State int state) {
if (state == Player.STATE_ENDED && isCurrentAdPlaying(player, mediaItem, adsId)) {
if (state == Player.STATE_ENDED && isCurrentAdPlaying(player, getMediaItem(), adsId)) {
streamPlayer.onContentCompleted();
}
}
@Override
public void onVolumeChanged(float volume) {
if (!isCurrentAdPlaying(player, mediaItem, adsId)) {
if (!isCurrentAdPlaying(player, getMediaItem(), adsId)) {
return;
}
int volumePct = (int) Math.floor(volume * 100);

View File

@ -20,6 +20,7 @@ import static androidx.media3.common.util.Assertions.checkArgument;
import static androidx.media3.common.util.Assertions.checkNotNull;
import android.net.Uri;
import androidx.annotation.GuardedBy;
import androidx.annotation.IntRange;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
@ -213,7 +214,6 @@ public final class RtspMediaSource extends BaseMediaSource {
}
}
private final MediaItem mediaItem;
private final RtpDataChannel.Factory rtpDataChannelFactory;
private final String userAgent;
private final Uri uri;
@ -225,6 +225,9 @@ public final class RtspMediaSource extends BaseMediaSource {
private boolean timelineIsLive;
private boolean timelineIsPlaceholder;
@GuardedBy("this")
private MediaItem mediaItem;
@VisibleForTesting
/* package */ RtspMediaSource(
MediaItem mediaItem,
@ -235,7 +238,7 @@ public final class RtspMediaSource extends BaseMediaSource {
this.mediaItem = mediaItem;
this.rtpDataChannelFactory = rtpDataChannelFactory;
this.userAgent = userAgent;
this.uri = checkNotNull(this.mediaItem.localConfiguration).uri;
this.uri = checkNotNull(mediaItem.localConfiguration).uri;
this.socketFactory = socketFactory;
this.debugLoggingEnabled = debugLoggingEnabled;
this.timelineDurationUs = C.TIME_UNSET;
@ -253,10 +256,21 @@ public final class RtspMediaSource extends BaseMediaSource {
}
@Override
public MediaItem getMediaItem() {
public synchronized MediaItem getMediaItem() {
return mediaItem;
}
@Override
public boolean canUpdateMediaItem(MediaItem mediaItem) {
@Nullable MediaItem.LocalConfiguration newConfiguration = mediaItem.localConfiguration;
return newConfiguration != null && newConfiguration.uri.equals(this.uri);
}
@Override
public synchronized void updateMediaItem(MediaItem mediaItem) {
this.mediaItem = mediaItem;
}
@Override
public void maybeThrowSourceInfoRefreshError() {
// Do nothing.
@ -304,7 +318,7 @@ public final class RtspMediaSource extends BaseMediaSource {
/* isDynamic= */ false,
/* useLiveConfiguration= */ timelineIsLive,
/* manifest= */ null,
mediaItem);
getMediaItem());
if (timelineIsPlaceholder) {
timeline =
new ForwardingTimeline(timeline) {

View File

@ -0,0 +1,96 @@
/*
* 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.rtsp;
import static com.google.common.truth.Truth.assertThat;
import androidx.media3.common.MediaItem;
import androidx.media3.common.Timeline;
import androidx.media3.exoplayer.analytics.PlayerId;
import androidx.media3.exoplayer.source.MediaSource;
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 RtspMediaSource}. */
@RunWith(AndroidJUnit4.class)
public class RtspMediaSourceTest {
@Test
public void canUpdateMediaItem_withIrrelevantFieldsChanged_returnsTrue() {
MediaItem initialMediaItem = new MediaItem.Builder().setUri("http://test.test").build();
MediaItem updatedMediaItem =
TestUtil.buildFullyCustomizedMediaItem().buildUpon().setUri("http://test.test").build();
MediaSource mediaSource = buildMediaSource(initialMediaItem);
boolean canUpdateMediaItem = mediaSource.canUpdateMediaItem(updatedMediaItem);
assertThat(canUpdateMediaItem).isTrue();
}
@Test
public void canUpdateMediaItem_withNullLocalConfiguration_returnsFalse() {
MediaItem initialMediaItem = new MediaItem.Builder().setUri("http://test.test").build();
MediaItem updatedMediaItem = new MediaItem.Builder().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().setUri("http://test.test").build();
MediaItem updatedMediaItem = new MediaItem.Builder().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").setTag("tag1").build();
MediaItem updatedMediaItem =
new MediaItem.Builder().setUri("http://test.test").setTag("tag2").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) {
return new RtspMediaSource.Factory().createMediaSource(mediaItem);
}
}

View File

@ -23,6 +23,7 @@ import android.net.Uri;
import android.os.Handler;
import android.os.Looper;
import android.os.SystemClock;
import androidx.annotation.GuardedBy;
import androidx.annotation.Nullable;
import androidx.media3.common.C;
import androidx.media3.common.MediaItem;
@ -334,8 +335,6 @@ public final class SsMediaSource extends BaseMediaSource
private final boolean sideloadedManifest;
private final Uri manifestUri;
private final MediaItem.LocalConfiguration localConfiguration;
private final MediaItem mediaItem;
private final DataSource.Factory manifestDataSourceFactory;
private final SsChunkSource.Factory chunkSourceFactory;
private final CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory;
@ -351,12 +350,13 @@ public final class SsMediaSource extends BaseMediaSource
private Loader manifestLoader;
private LoaderErrorThrower manifestLoaderErrorThrower;
@Nullable private TransferListener mediaTransferListener;
private long manifestLoadStartTimestamp;
private SsManifest manifest;
private Handler manifestRefreshHandler;
@GuardedBy("this")
private MediaItem mediaItem;
private SsMediaSource(
MediaItem mediaItem,
@Nullable SsManifest manifest,
@ -370,7 +370,7 @@ public final class SsMediaSource extends BaseMediaSource
long livePresentationDelayMs) {
Assertions.checkState(manifest == null || !manifest.isLive);
this.mediaItem = mediaItem;
localConfiguration = checkNotNull(mediaItem.localConfiguration);
MediaItem.LocalConfiguration localConfiguration = checkNotNull(mediaItem.localConfiguration);
this.manifest = manifest;
this.manifestUri =
localConfiguration.uri.equals(Uri.EMPTY)
@ -392,10 +392,26 @@ public final class SsMediaSource extends BaseMediaSource
// MediaSource implementation.
@Override
public MediaItem getMediaItem() {
public synchronized MediaItem getMediaItem() {
return mediaItem;
}
@Override
public boolean canUpdateMediaItem(MediaItem mediaItem) {
MediaItem.LocalConfiguration existingConfiguration =
checkNotNull(getMediaItem().localConfiguration);
@Nullable MediaItem.LocalConfiguration newConfiguration = mediaItem.localConfiguration;
return newConfiguration != null
&& newConfiguration.uri.equals(existingConfiguration.uri)
&& newConfiguration.streamKeys.equals(existingConfiguration.streamKeys)
&& Util.areEqual(newConfiguration.drmConfiguration, existingConfiguration.drmConfiguration);
}
@Override
public synchronized void updateMediaItem(MediaItem mediaItem) {
this.mediaItem = mediaItem;
}
@Override
protected void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) {
this.mediaTransferListener = mediaTransferListener;
@ -567,7 +583,7 @@ public final class SsMediaSource extends BaseMediaSource
/* isDynamic= */ manifest.isLive,
/* useLiveConfiguration= */ manifest.isLive,
manifest,
mediaItem);
getMediaItem());
} else if (manifest.isLive) {
if (manifest.dvrWindowLengthUs != C.TIME_UNSET && manifest.dvrWindowLengthUs > 0) {
startTimeUs = max(startTimeUs, endTimeUs - manifest.dvrWindowLengthUs);
@ -590,7 +606,7 @@ public final class SsMediaSource extends BaseMediaSource
/* isDynamic= */ true,
/* useLiveConfiguration= */ true,
manifest,
mediaItem);
getMediaItem());
} else {
long durationUs =
manifest.durationUs != C.TIME_UNSET ? manifest.durationUs : endTimeUs - startTimeUs;
@ -604,7 +620,7 @@ public final class SsMediaSource extends BaseMediaSource
/* isDynamic= */ false,
/* useLiveConfiguration= */ false,
manifest,
mediaItem);
getMediaItem());
}
refreshSourceInfo(timeline);
}

View File

@ -0,0 +1,178 @@
/*
* 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.smoothstreaming;
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.fail;
import androidx.media3.common.C;
import androidx.media3.common.MediaItem;
import androidx.media3.common.StreamKey;
import androidx.media3.common.Timeline;
import androidx.media3.datasource.ByteArrayDataSource;
import androidx.media3.datasource.DataSource;
import androidx.media3.exoplayer.analytics.PlayerId;
import androidx.media3.exoplayer.source.MediaSource;
import androidx.media3.test.utils.TestUtil;
import androidx.media3.test.utils.robolectric.RobolectricUtil;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.common.collect.ImmutableList;
import java.io.IOException;
import java.util.concurrent.atomic.AtomicReference;
import org.junit.Test;
import org.junit.runner.RunWith;
/** Unit test for {@link SsMediaSource}. */
@RunWith(AndroidJUnit4.class)
public class SsMediaSourceTest {
private static final String SAMPLE_MANIFEST = "media/smooth-streaming/sample_ismc_1";
@Test
public void canUpdateMediaItem_withIrrelevantFieldsChanged_returnsTrue() {
MediaItem initialMediaItem =
new MediaItem.Builder()
.setUri("http://test.test")
.setStreamKeys(
ImmutableList.of(new StreamKey(/* groupIndex= */ 1, /* streamIndex= */ 0)))
.setDrmConfiguration(new MediaItem.DrmConfiguration.Builder(C.WIDEVINE_UUID).build())
.build();
MediaItem updatedMediaItem =
TestUtil.buildFullyCustomizedMediaItem()
.buildUpon()
.setUri("http://test.test")
.setStreamKeys(
ImmutableList.of(new StreamKey(/* groupIndex= */ 1, /* streamIndex= */ 0)))
.setDrmConfiguration(new MediaItem.DrmConfiguration.Builder(C.WIDEVINE_UUID).build())
.build();
MediaSource mediaSource =
new SsMediaSource.Factory(() -> createSampleDataSource(SAMPLE_MANIFEST))
.createMediaSource(initialMediaItem);
boolean canUpdateMediaItem = mediaSource.canUpdateMediaItem(updatedMediaItem);
assertThat(canUpdateMediaItem).isTrue();
}
@Test
public void canUpdateMediaItem_withNullLocalConfiguration_returnsFalse() {
MediaItem initialMediaItem = new MediaItem.Builder().setUri("http://test.test").build();
MediaItem updatedMediaItem = new MediaItem.Builder().setMediaId("id").build();
MediaSource mediaSource =
new SsMediaSource.Factory(() -> createSampleDataSource(SAMPLE_MANIFEST))
.createMediaSource(initialMediaItem);
boolean canUpdateMediaItem = mediaSource.canUpdateMediaItem(updatedMediaItem);
assertThat(canUpdateMediaItem).isFalse();
}
@Test
public void canUpdateMediaItem_withChangedUri_returnsFalse() {
MediaItem initialMediaItem = new MediaItem.Builder().setUri("http://test.test").build();
MediaItem updatedMediaItem = new MediaItem.Builder().setUri("http://test2.test").build();
MediaSource mediaSource =
new SsMediaSource.Factory(() -> createSampleDataSource(SAMPLE_MANIFEST))
.createMediaSource(initialMediaItem);
boolean canUpdateMediaItem = mediaSource.canUpdateMediaItem(updatedMediaItem);
assertThat(canUpdateMediaItem).isFalse();
}
@Test
public void canUpdateMediaItem_withChangedStreamKeys_returnsFalse() {
MediaItem initialMediaItem =
new MediaItem.Builder()
.setUri("http://test.test")
.setStreamKeys(
ImmutableList.of(new StreamKey(/* groupIndex= */ 1, /* streamIndex= */ 0)))
.build();
MediaItem updatedMediaItem =
new MediaItem.Builder()
.setUri("http://test.test")
.setStreamKeys(
ImmutableList.of(new StreamKey(/* groupIndex= */ 2, /* streamIndex= */ 2)))
.build();
MediaSource mediaSource =
new SsMediaSource.Factory(() -> createSampleDataSource(SAMPLE_MANIFEST))
.createMediaSource(initialMediaItem);
boolean canUpdateMediaItem = mediaSource.canUpdateMediaItem(updatedMediaItem);
assertThat(canUpdateMediaItem).isFalse();
}
@Test
public void canUpdateMediaItem_withChangedDrmConfiguration_returnsFalse() {
MediaItem initialMediaItem =
new MediaItem.Builder()
.setUri("http://test.test")
.setDrmConfiguration(new MediaItem.DrmConfiguration.Builder(C.WIDEVINE_UUID).build())
.build();
MediaItem updatedMediaItem =
new MediaItem.Builder()
.setUri("http://test.test")
.setDrmConfiguration(new MediaItem.DrmConfiguration.Builder(C.CLEARKEY_UUID).build())
.build();
MediaSource mediaSource =
new SsMediaSource.Factory(() -> createSampleDataSource(SAMPLE_MANIFEST))
.createMediaSource(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").setTag("tag1").build();
MediaItem updatedMediaItem =
new MediaItem.Builder().setUri("http://test.test").setTag("tag2").build();
MediaSource mediaSource =
new SsMediaSource.Factory(() -> createSampleDataSource(SAMPLE_MANIFEST))
.createMediaSource(initialMediaItem);
mediaSource.updateMediaItem(updatedMediaItem);
Timeline.Window window = prepareAndWaitForTimelineRefresh(mediaSource);
assertThat(window.mediaItem).isEqualTo(updatedMediaItem);
}
private static Timeline.Window prepareAndWaitForTimelineRefresh(MediaSource mediaSource)
throws Exception {
AtomicReference<Timeline.Window> windowReference = new AtomicReference<>();
mediaSource.prepareSource(
(source, timeline) ->
windowReference.set(timeline.getWindow(/* windowIndex= */ 0, new Timeline.Window())),
/* mediaTransferListener= */ null,
PlayerId.UNSET);
RobolectricUtil.runMainLooperUntil(() -> windowReference.get() != null);
return windowReference.get();
}
private static DataSource createSampleDataSource(String fileName) {
byte[] manifestData = new byte[0];
try {
manifestData = TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), fileName);
} catch (IOException e) {
fail(e.getMessage());
}
return new ByteArrayDataSource(manifestData);
}
}

View File

@ -27,6 +27,9 @@ import android.media.MediaCodec;
import android.net.Uri;
import androidx.annotation.Nullable;
import androidx.media3.common.C;
import androidx.media3.common.MediaItem;
import androidx.media3.common.MediaMetadata;
import androidx.media3.common.StreamKey;
import androidx.media3.common.Timeline;
import androidx.media3.common.util.Assertions;
import androidx.media3.common.util.UnstableApi;
@ -63,6 +66,7 @@ import java.util.List;
import java.util.Queue;
import java.util.Random;
import java.util.Set;
import java.util.UUID;
/** Utility methods for tests. */
@UnstableApi
@ -562,6 +566,33 @@ public class TestUtil {
return list;
}
/** Returns a {@link MediaItem} that has all fields set to non-default values. */
public static MediaItem buildFullyCustomizedMediaItem() {
return new MediaItem.Builder()
.setUri("http://custom.uri.test")
.setCustomCacheKey("custom.cache")
.setMediaId("custom.id")
.setMediaMetadata(new MediaMetadata.Builder().setTitle("custom.title").build())
.setClippingConfiguration(
new MediaItem.ClippingConfiguration.Builder().setStartPositionMs(123).build())
.setAdsConfiguration(
new MediaItem.AdsConfiguration.Builder(Uri.parse("http:://custom.ad.test")).build())
.setDrmConfiguration(new MediaItem.DrmConfiguration.Builder(UUID.randomUUID()).build())
.setLiveConfiguration(
new MediaItem.LiveConfiguration.Builder().setTargetOffsetMs(234).build())
.setMimeType("mime")
.setRequestMetadata(
new MediaItem.RequestMetadata.Builder().setSearchQuery("custom.query").build())
.setStreamKeys(ImmutableList.of(new StreamKey(/* groupIndex= */ 0, /* streamIndex= */ 0)))
.setTag("tag")
.setSubtitleConfigurations(
ImmutableList.of(
new MediaItem.SubtitleConfiguration.Builder(
Uri.parse("http://custom.subtitle.test"))
.build()))
.build();
}
private static final class NoUidOrShufflingTimeline extends Timeline {
private final Timeline delegate;