diff --git a/RELEASENOTES.md b/RELEASENOTES.md index a336506014..a7df132482 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -25,6 +25,8 @@ AudioTrack offload state. ([#134](https://github.com/androidx/media/issues/134)). * Make `AudioTrackBufferSizeProvider` a public interface. + * Add `WrappingMediaSource` to simplify wrapping a single `MediaSource` + ([#7279](https://github.com/google/ExoPlayer/issues/7279)). * Metadata: * `MetadataRenderer` can now be configured to render metadata as soon as they are available. Create an instance with diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/BaseMediaSource.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/BaseMediaSource.java index 1e8c9eab45..3fb3b9d4cd 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/BaseMediaSource.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/BaseMediaSource.java @@ -200,6 +200,13 @@ public abstract class BaseMediaSource implements MediaSource { drmEventDispatcher.removeEventListener(eventListener); } + @SuppressWarnings("deprecation") // Overriding deprecated method to make it final. + @Override + public final void prepareSource( + MediaSourceCaller caller, @Nullable TransferListener mediaTransferListener) { + prepareSource(caller, mediaTransferListener, PlayerId.UNSET); + } + @Override public final void prepareSource( MediaSourceCaller caller, diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/ClippingMediaSource.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/ClippingMediaSource.java index fc63321d45..20b0fb649f 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/ClippingMediaSource.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/ClippingMediaSource.java @@ -22,12 +22,10 @@ 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; import androidx.media3.common.util.Util; -import androidx.media3.datasource.TransferListener; import androidx.media3.exoplayer.upstream.Allocator; import java.io.IOException; import java.lang.annotation.Documented; @@ -41,7 +39,7 @@ import java.util.ArrayList; * positions. The wrapped source must consist of a single period. */ @UnstableApi -public final class ClippingMediaSource extends CompositeMediaSource { +public final class ClippingMediaSource extends WrappingMediaSource { /** Thrown when a {@link ClippingMediaSource} cannot clip its wrapped source. */ public static final class IllegalClippingException extends IOException { @@ -87,7 +85,6 @@ public final class ClippingMediaSource extends CompositeMediaSource { } } - private final MediaSource mediaSource; private final long startUs; private final long endUs; private final boolean enableInitialDiscontinuity; @@ -183,8 +180,8 @@ public final class ClippingMediaSource extends CompositeMediaSource { boolean enableInitialDiscontinuity, boolean allowDynamicClippingUpdates, boolean relativeToDefaultPosition) { + super(Assertions.checkNotNull(mediaSource)); Assertions.checkArgument(startPositionUs >= 0); - this.mediaSource = Assertions.checkNotNull(mediaSource); startUs = startPositionUs; endUs = endPositionUs; this.enableInitialDiscontinuity = enableInitialDiscontinuity; @@ -194,17 +191,6 @@ public final class ClippingMediaSource extends CompositeMediaSource { window = new Timeline.Window(); } - @Override - public MediaItem getMediaItem() { - return mediaSource.getMediaItem(); - } - - @Override - protected void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) { - super.prepareSourceInternal(mediaTransferListener); - prepareChildSource(/* id= */ null, mediaSource); - } - @Override public void maybeThrowSourceInfoRefreshError() throws IOException { if (clippingError != null) { @@ -242,7 +228,7 @@ public final class ClippingMediaSource extends CompositeMediaSource { } @Override - protected void onChildSourceInfoRefreshed(Void id, MediaSource mediaSource, Timeline timeline) { + protected void onChildSourceInfoRefreshed(Timeline timeline) { if (clippingError != null) { return; } diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/LoopingMediaSource.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/LoopingMediaSource.java index b3280a4c4e..491acca3e8 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/LoopingMediaSource.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/LoopingMediaSource.java @@ -17,12 +17,10 @@ package androidx.media3.exoplayer.source; import androidx.annotation.Nullable; import androidx.media3.common.C; -import androidx.media3.common.MediaItem; import androidx.media3.common.Player; import androidx.media3.common.Timeline; import androidx.media3.common.util.Assertions; import androidx.media3.common.util.UnstableApi; -import androidx.media3.datasource.TransferListener; import androidx.media3.exoplayer.AbstractConcatenatedTimeline; import androidx.media3.exoplayer.ExoPlayer; import androidx.media3.exoplayer.source.ShuffleOrder.UnshuffledShuffleOrder; @@ -43,9 +41,8 @@ import java.util.Map; */ @Deprecated @UnstableApi -public final class LoopingMediaSource extends CompositeMediaSource { +public final class LoopingMediaSource extends WrappingMediaSource { - private final MaskingMediaSource maskingMediaSource; private final int loopCount; private final Map childMediaPeriodIdToMediaPeriodId; private final Map mediaPeriodToChildMediaPeriodId; @@ -67,21 +64,17 @@ public final class LoopingMediaSource extends CompositeMediaSource { * @param loopCount The desired number of loops. Must be strictly positive. */ public LoopingMediaSource(MediaSource childSource, int loopCount) { + super(new MaskingMediaSource(childSource, /* useLazyPreparation= */ false)); Assertions.checkArgument(loopCount > 0); - this.maskingMediaSource = new MaskingMediaSource(childSource, /* useLazyPreparation= */ false); this.loopCount = loopCount; childMediaPeriodIdToMediaPeriodId = new HashMap<>(); mediaPeriodToChildMediaPeriodId = new HashMap<>(); } - @Override - public MediaItem getMediaItem() { - return maskingMediaSource.getMediaItem(); - } - @Override @Nullable public Timeline getInitialTimeline() { + MaskingMediaSource maskingMediaSource = (MaskingMediaSource) mediaSource; return loopCount != Integer.MAX_VALUE ? new LoopingTimeline(maskingMediaSource.getTimeline(), loopCount) : new InfinitelyLoopingTimeline(maskingMediaSource.getTimeline()); @@ -92,29 +85,23 @@ public final class LoopingMediaSource extends CompositeMediaSource { return false; } - @Override - protected void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) { - super.prepareSourceInternal(mediaTransferListener); - prepareChildSource(/* id= */ null, maskingMediaSource); - } - @Override public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator, long startPositionUs) { if (loopCount == Integer.MAX_VALUE) { - return maskingMediaSource.createPeriod(id, allocator, startPositionUs); + return mediaSource.createPeriod(id, allocator, startPositionUs); } Object childPeriodUid = LoopingTimeline.getChildPeriodUidFromConcatenatedUid(id.periodUid); MediaPeriodId childMediaPeriodId = id.copyWithPeriodUid(childPeriodUid); childMediaPeriodIdToMediaPeriodId.put(childMediaPeriodId, id); MediaPeriod mediaPeriod = - maskingMediaSource.createPeriod(childMediaPeriodId, allocator, startPositionUs); + mediaSource.createPeriod(childMediaPeriodId, allocator, startPositionUs); mediaPeriodToChildMediaPeriodId.put(mediaPeriod, childMediaPeriodId); return mediaPeriod; } @Override public void releasePeriod(MediaPeriod mediaPeriod) { - maskingMediaSource.releasePeriod(mediaPeriod); + mediaSource.releasePeriod(mediaPeriod); @Nullable MediaPeriodId childMediaPeriodId = mediaPeriodToChildMediaPeriodId.remove(mediaPeriod); if (childMediaPeriodId != null) { @@ -123,7 +110,7 @@ public final class LoopingMediaSource extends CompositeMediaSource { } @Override - protected void onChildSourceInfoRefreshed(Void id, MediaSource mediaSource, Timeline timeline) { + protected void onChildSourceInfoRefreshed(Timeline timeline) { Timeline loopingTimeline = loopCount != Integer.MAX_VALUE ? new LoopingTimeline(timeline, loopCount) @@ -133,8 +120,7 @@ public final class LoopingMediaSource extends CompositeMediaSource { @Override @Nullable - protected MediaPeriodId getMediaPeriodIdForChildMediaPeriodId( - Void id, MediaPeriodId mediaPeriodId) { + protected MediaPeriodId getMediaPeriodIdForChildMediaPeriodId(MediaPeriodId mediaPeriodId) { return loopCount != Integer.MAX_VALUE ? childMediaPeriodIdToMediaPeriodId.get(mediaPeriodId) : mediaPeriodId; diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/MaskingMediaSource.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/MaskingMediaSource.java index 88cf290892..ae1266c96c 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/MaskingMediaSource.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/MaskingMediaSource.java @@ -28,7 +28,6 @@ import androidx.media3.common.Timeline.Window; import androidx.media3.common.util.Assertions; import androidx.media3.common.util.UnstableApi; import androidx.media3.common.util.Util; -import androidx.media3.datasource.TransferListener; import androidx.media3.exoplayer.upstream.Allocator; import org.checkerframework.checker.nullness.qual.RequiresNonNull; @@ -37,9 +36,8 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; * structure is known. */ @UnstableApi -public final class MaskingMediaSource extends CompositeMediaSource { +public final class MaskingMediaSource extends WrappingMediaSource { - private final MediaSource mediaSource; private final boolean useLazyPreparation; private final Timeline.Window window; private final Timeline.Period period; @@ -59,7 +57,7 @@ public final class MaskingMediaSource extends CompositeMediaSource { * initial preparations are triggered only when the player starts buffering the media. */ public MaskingMediaSource(MediaSource mediaSource, boolean useLazyPreparation) { - this.mediaSource = mediaSource; + super(mediaSource); this.useLazyPreparation = useLazyPreparation && mediaSource.isSingleWindow(); window = new Timeline.Window(); period = new Timeline.Period(); @@ -80,19 +78,13 @@ public final class MaskingMediaSource extends CompositeMediaSource { } @Override - public void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) { - super.prepareSourceInternal(mediaTransferListener); + public void prepareSourceInternal() { if (!useLazyPreparation) { hasStartedPreparing = true; - prepareChildSource(/* id= */ null, mediaSource); + prepareChildSource(); } } - @Override - public MediaItem getMediaItem() { - return mediaSource.getMediaItem(); - } - @Override @SuppressWarnings("MissingSuperCall") public void maybeThrowSourceInfoRefreshError() { @@ -115,7 +107,7 @@ public final class MaskingMediaSource extends CompositeMediaSource { unpreparedMaskingMediaPeriod = mediaPeriod; if (!hasStartedPreparing) { hasStartedPreparing = true; - prepareChildSource(/* id= */ null, mediaSource); + prepareChildSource(); } } return mediaPeriod; @@ -137,8 +129,7 @@ public final class MaskingMediaSource extends CompositeMediaSource { } @Override - protected void onChildSourceInfoRefreshed( - Void id, MediaSource mediaSource, Timeline newTimeline) { + protected void onChildSourceInfoRefreshed(Timeline newTimeline) { @Nullable MediaPeriodId idForMaskingPeriodPreparation = null; if (isPrepared) { timeline = timeline.cloneWithUpdatedTimeline(newTimeline); @@ -208,8 +199,7 @@ public final class MaskingMediaSource extends CompositeMediaSource { @Override @Nullable - protected MediaPeriodId getMediaPeriodIdForChildMediaPeriodId( - Void id, MediaPeriodId mediaPeriodId) { + protected MediaPeriodId getMediaPeriodIdForChildMediaPeriodId(MediaPeriodId mediaPeriodId) { return mediaPeriodId.copyWithPeriodUid(getExternalPeriodUid(mediaPeriodId.periodUid)); } @@ -304,11 +294,6 @@ public final class MaskingMediaSource extends CompositeMediaSource { return new MaskingTimeline(timeline, replacedInternalWindowUid, replacedInternalPeriodUid); } - /** Returns the wrapped timeline. */ - public Timeline getTimeline() { - return timeline; - } - @Override public Window getWindow(int windowIndex, Window window, long defaultPositionProjectionUs) { timeline.getWindow(windowIndex, window, defaultPositionProjectionUs); diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/WrappingMediaSource.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/WrappingMediaSource.java new file mode 100644 index 0000000000..99a367d944 --- /dev/null +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/WrappingMediaSource.java @@ -0,0 +1,231 @@ +/* + * Copyright (C) 2022 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 androidx.annotation.Nullable; +import androidx.media3.common.MediaItem; +import androidx.media3.common.Timeline; +import androidx.media3.common.util.UnstableApi; +import androidx.media3.datasource.TransferListener; +import androidx.media3.exoplayer.analytics.PlayerId; +import androidx.media3.exoplayer.upstream.Allocator; + +/** + * An abstract {@link MediaSource} wrapping a single child {@link MediaSource}. + * + *

The implementation may want to override the following methods as needed: + * + *

    + *
  • {@link #getMediaItem()}: Amend the {@link MediaItem} for this media source. This is only + * used before the child source is prepared. + *
  • {@link #onChildSourceInfoRefreshed(Timeline)}: Called whenever the child source's {@link + * Timeline} changed. This {@link Timeline} can be amended if needed, for example using {@link + * ForwardingTimeline}. The {@link Timeline} for the wrapping source needs to be published + * with {@link #refreshSourceInfo(Timeline)}. + *
  • {@link #createPeriod}/{@link #releasePeriod}: These methods create and release {@link + * MediaPeriod} instances. They typically forward to the wrapped media source and optionally + * wrap the returned {@link MediaPeriod}. + *
+ * + *

Other methods like {@link #prepareSourceInternal}, {@link #enableInternal}, {@link + * #disableInternal} or {@link #releaseSourceInternal} only need to be overwritten if required for + * resource management. + */ +@UnstableApi +public abstract class WrappingMediaSource extends CompositeMediaSource { + + private static final Void CHILD_SOURCE_ID = null; + + /** The wrapped child {@link MediaSource}. */ + protected final MediaSource mediaSource; + + /** + * Creates the wrapping {@link MediaSource}. + * + * @param mediaSource The wrapped child {@link MediaSource}. + */ + protected WrappingMediaSource(MediaSource mediaSource) { + this.mediaSource = mediaSource; + } + + @Override + protected final void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) { + super.prepareSourceInternal(mediaTransferListener); + prepareSourceInternal(); + } + + /** + * Starts source preparation and enables the source, see {@link #prepareSource(MediaSourceCaller, + * TransferListener, PlayerId)}. This method is called at most once until the next call to {@link + * #releaseSourceInternal()}. + */ + protected void prepareSourceInternal() { + prepareChildSource(); + } + + @Nullable + @Override + public Timeline getInitialTimeline() { + return mediaSource.getInitialTimeline(); + } + + @Override + public boolean isSingleWindow() { + return mediaSource.isSingleWindow(); + } + + /** + * Returns the {@link MediaItem} for this media source. + * + *

This method can be overridden to amend the {@link MediaItem} of the child source. It is only + * used before the child source is prepared. + * + * @see MediaSource#getMediaItem() + */ + @Override + public MediaItem getMediaItem() { + return mediaSource.getMediaItem(); + } + + /** + * Creates the requested {@link MediaPeriod}. + * + *

This method typically forwards to the wrapped media source and optionally wraps the returned + * {@link MediaPeriod}. + * + * @see MediaSource#createPeriod(MediaPeriodId, Allocator, long) + */ + @Override + public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator, long startPositionUs) { + return mediaSource.createPeriod(id, allocator, startPositionUs); + } + + /** + * Releases a {@link MediaPeriod}. + * + *

This method typically forwards to the wrapped media source and optionally unwraps the + * provided {@link MediaPeriod}. + * + * @see MediaSource#releasePeriod(MediaPeriod) + */ + @Override + public void releasePeriod(MediaPeriod mediaPeriod) { + mediaSource.releasePeriod(mediaPeriod); + } + + @Override + protected final void onChildSourceInfoRefreshed( + Void id, MediaSource mediaSource, Timeline timeline) { + onChildSourceInfoRefreshed(timeline); + } + + /** + * Called when the child source info has been refreshed. + * + *

This {@link Timeline} can be amended if needed, for example using {@link + * ForwardingTimeline}. The {@link Timeline} for the wrapping source needs to be published with + * {@link #refreshSourceInfo(Timeline)}. + * + * @param timeline The timeline of the child source. + */ + protected void onChildSourceInfoRefreshed(Timeline timeline) { + refreshSourceInfo(timeline); + } + + @Override + protected final int getWindowIndexForChildWindowIndex(Void id, int windowIndex) { + return getWindowIndexForChildWindowIndex(windowIndex); + } + + /** + * Returns the window index in the wrapping source corresponding to the specified window index in + * a child source. The default implementation does not change the window index. + * + * @param windowIndex A window index of the child source. + * @return The corresponding window index in the wrapping source. + */ + protected int getWindowIndexForChildWindowIndex(int windowIndex) { + return windowIndex; + } + + @Nullable + @Override + protected final MediaPeriodId getMediaPeriodIdForChildMediaPeriodId( + Void id, MediaPeriodId mediaPeriodId) { + return getMediaPeriodIdForChildMediaPeriodId(mediaPeriodId); + } + + /** + * Returns the {@link MediaPeriodId} in the wrapping source corresponding to the specified {@link + * MediaPeriodId} in a child source. The default implementation does not change the media period + * id. + * + * @param mediaPeriodId A {@link MediaPeriodId} of the child source. + * @return The corresponding {@link MediaPeriodId} in the wrapping source. Null if no + * corresponding media period id can be determined. + */ + @Nullable + protected MediaPeriodId getMediaPeriodIdForChildMediaPeriodId(MediaPeriodId mediaPeriodId) { + return mediaPeriodId; + } + + @Override + protected final long getMediaTimeForChildMediaTime(Void id, long mediaTimeMs) { + return getMediaTimeForChildMediaTime(mediaTimeMs); + } + + /** + * Returns the media time in the {@link MediaPeriod} of the wrapping source corresponding to the + * specified media time in the {@link MediaPeriod} of the child source. The default implementation + * does not change the media time. + * + * @param mediaTimeMs A media time in the {@link MediaPeriod} of the child source, in + * milliseconds. + * @return The corresponding media time in the {@link MediaPeriod} of the wrapping source, in + * milliseconds. + */ + protected long getMediaTimeForChildMediaTime(long mediaTimeMs) { + return mediaTimeMs; + } + + /** + * Prepares the wrapped child source. + * + *

{@link #onChildSourceInfoRefreshed(Timeline)} will be called when the child source updates + * its timeline. + * + *

If sources aren't explicitly released with {@link #releaseChildSource()} they will be + * released in {@link #releaseSourceInternal()}. + */ + protected final void prepareChildSource() { + prepareChildSource(CHILD_SOURCE_ID, mediaSource); + } + + /** Enables the child source. */ + protected final void enableChildSource() { + enableChildSource(CHILD_SOURCE_ID); + } + + /** Disables the child source. */ + protected final void disableChildSource() { + disableChildSource(CHILD_SOURCE_ID); + } + + /** Releases the child source. */ + protected final void releaseChildSource() { + releaseChildSource(CHILD_SOURCE_ID); + } +} diff --git a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/ExoPlayerTest.java b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/ExoPlayerTest.java index 86c31bba04..e4e69c55a8 100644 --- a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/ExoPlayerTest.java +++ b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/ExoPlayerTest.java @@ -118,7 +118,6 @@ import androidx.media3.exoplayer.analytics.AnalyticsListener; import androidx.media3.exoplayer.drm.DrmSessionEventListener; import androidx.media3.exoplayer.drm.DrmSessionManager; import androidx.media3.exoplayer.source.ClippingMediaSource; -import androidx.media3.exoplayer.source.CompositeMediaSource; import androidx.media3.exoplayer.source.ConcatenatingMediaSource; import androidx.media3.exoplayer.source.MaskingMediaSource; import androidx.media3.exoplayer.source.MediaPeriod; @@ -128,6 +127,7 @@ import androidx.media3.exoplayer.source.MediaSourceEventListener; import androidx.media3.exoplayer.source.ShuffleOrder; import androidx.media3.exoplayer.source.SinglePeriodTimeline; import androidx.media3.exoplayer.source.TrackGroupArray; +import androidx.media3.exoplayer.source.WrappingMediaSource; import androidx.media3.exoplayer.source.ads.ServerSideAdInsertionMediaSource; import androidx.media3.exoplayer.trackselection.DefaultTrackSelector; import androidx.media3.exoplayer.upstream.Allocation; @@ -3942,33 +3942,15 @@ public final class ExoPlayerTest { new TimelineWindowDefinition( /* isSeekable= */ true, /* isDynamic= */ false, /* durationUs= */ 10_000_000)); final ConcatenatingMediaSource underlyingSource = new ConcatenatingMediaSource(); - CompositeMediaSource delegatingMediaSource = - new CompositeMediaSource() { + WrappingMediaSource delegatingMediaSource = + new WrappingMediaSource(underlyingSource) { @Override - public void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) { - super.prepareSourceInternal(mediaTransferListener); + public void prepareSourceInternal() { underlyingSource.addMediaSource( new FakeMediaSource(fakeTimeline, ExoPlayerTestRunner.VIDEO_FORMAT)); underlyingSource.addMediaSource( new FakeMediaSource(fakeTimeline, ExoPlayerTestRunner.VIDEO_FORMAT)); - prepareChildSource(null, underlyingSource); - } - - @Override - public MediaPeriod createPeriod( - MediaPeriodId id, Allocator allocator, long startPositionUs) { - return underlyingSource.createPeriod(id, allocator, startPositionUs); - } - - @Override - public void releasePeriod(MediaPeriod mediaPeriod) { - underlyingSource.releasePeriod(mediaPeriod); - } - - @Override - protected void onChildSourceInfoRefreshed( - Void id, MediaSource mediaSource, Timeline timeline) { - refreshSourceInfo(timeline); + super.prepareSourceInternal(); } @Override @@ -3976,11 +3958,6 @@ public final class ExoPlayerTest { return false; } - @Override - public MediaItem getMediaItem() { - return underlyingSource.getMediaItem(); - } - @Override @Nullable public Timeline getInitialTimeline() {