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:
parent
a8520bdee6
commit
3d4bd7ce19
@ -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
|
||||
|
@ -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) {
|
||||
|
@ -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(),
|
||||
|
@ -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.
|
||||
|
@ -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() {}
|
||||
|
||||
|
@ -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 {
|
||||
|
||||
|
@ -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.
|
||||
*/
|
||||
|
@ -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(
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
|
||||
|
@ -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 =
|
||||
|
@ -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));
|
||||
|
@ -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);
|
||||
|
@ -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) {
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
@ -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;
|
||||
|
Loading…
x
Reference in New Issue
Block a user