Merge pull request #123 from stoyicker:wrapping_media_source

PiperOrigin-RevId: 476376463
This commit is contained in:
Marc Baechinger 2022-09-30 18:15:30 +00:00
commit cac8c4f6e9
7 changed files with 263 additions and 89 deletions

View File

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

View File

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

View File

@ -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<Void> {
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<Void> {
}
}
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<Void> {
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<Void> {
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<Void> {
}
@Override
protected void onChildSourceInfoRefreshed(Void id, MediaSource mediaSource, Timeline timeline) {
protected void onChildSourceInfoRefreshed(Timeline timeline) {
if (clippingError != null) {
return;
}

View File

@ -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<Void> {
public final class LoopingMediaSource extends WrappingMediaSource {
private final MaskingMediaSource maskingMediaSource;
private final int loopCount;
private final Map<MediaPeriodId, MediaPeriodId> childMediaPeriodIdToMediaPeriodId;
private final Map<MediaPeriod, MediaPeriodId> mediaPeriodToChildMediaPeriodId;
@ -67,21 +64,17 @@ public final class LoopingMediaSource extends CompositeMediaSource<Void> {
* @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<Void> {
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<Void> {
}
@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<Void> {
@Override
@Nullable
protected MediaPeriodId getMediaPeriodIdForChildMediaPeriodId(
Void id, MediaPeriodId mediaPeriodId) {
protected MediaPeriodId getMediaPeriodIdForChildMediaPeriodId(MediaPeriodId mediaPeriodId) {
return loopCount != Integer.MAX_VALUE
? childMediaPeriodIdToMediaPeriodId.get(mediaPeriodId)
: mediaPeriodId;

View File

@ -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<Void> {
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<Void> {
* 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<Void> {
}
@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<Void> {
unpreparedMaskingMediaPeriod = mediaPeriod;
if (!hasStartedPreparing) {
hasStartedPreparing = true;
prepareChildSource(/* id= */ null, mediaSource);
prepareChildSource();
}
}
return mediaPeriod;
@ -137,8 +129,7 @@ public final class MaskingMediaSource extends CompositeMediaSource<Void> {
}
@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<Void> {
@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<Void> {
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);

View File

@ -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}.
*
* <p>The implementation may want to override the following methods as needed:
*
* <ul>
* <li>{@link #getMediaItem()}: Amend the {@link MediaItem} for this media source. This is only
* used before the child source is prepared.
* <li>{@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)}.
* <li>{@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}.
* </ul>
*
* <p>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<Void> {
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.
*
* <p>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}.
*
* <p>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}.
*
* <p>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.
*
* <p>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.
*
* <p>{@link #onChildSourceInfoRefreshed(Timeline)} will be called when the child source updates
* its timeline.
*
* <p>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);
}
}

View File

@ -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<Void> delegatingMediaSource =
new CompositeMediaSource<Void>() {
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() {