diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayerImplInternal.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayerImplInternal.java index 770011c586..30d1aa433b 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayerImplInternal.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayerImplInternal.java @@ -1494,11 +1494,21 @@ import java.util.concurrent.atomic.AtomicBoolean; Timeline timeline = playbackInfo.timeline; if (releaseMediaSourceList && timeline instanceof PlaylistTimeline) { - // Wrap the current live timeline to make sure the current period is marked as a placeholder - // to force resolving the default start position with the next timeline refresh. + // Wrap the current timeline to make sure the current period is marked as a placeholder to + // force resolving the default start position with the next timeline refresh. timeline = ((PlaylistTimeline) playbackInfo.timeline) .copyWithPlaceholderTimeline(mediaSourceList.getShuffleOrder()); + if (mediaPeriodId.adGroupIndex != C.INDEX_UNSET) { + timeline.getPeriodByUid(mediaPeriodId.periodUid, period); + if (timeline.getWindow(period.windowIndex, window).isLive()) { + // Drop ad metadata to allow live streams to reset the ad playback state. In case the ad + // playback state is not reset by the source, the first timeline refresh after + // re-preparation will add the ad metadata to the period again. + mediaPeriodId = + new MediaPeriodId(mediaPeriodId.periodUid, mediaPeriodId.windowSequenceNumber); + } + } } playbackInfo = new PlaybackInfo( diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/PlaylistTimeline.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/PlaylistTimeline.java index 75a7ec2d14..63e450672b 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/PlaylistTimeline.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/PlaylistTimeline.java @@ -15,7 +15,9 @@ */ package androidx.media3.exoplayer; +import androidx.media3.common.AdPlaybackState; import androidx.media3.common.C; +import androidx.media3.common.Player; import androidx.media3.common.Timeline; import androidx.media3.common.util.Util; import androidx.media3.exoplayer.source.ForwardingTimeline; @@ -117,15 +119,43 @@ import java.util.List; return periodCount; } + /** + * Creates a copy of the timeline and wraps each child timeline with a {@link ForwardingTimeline} + * that overrides {@link Timeline#getPeriod(int, Period, boolean)} to set the {@link + * Period#isPlaceholder} flag. + * + *
For periods of a live window, the {@link AdPlaybackState} is set to {@link + * AdPlaybackState#NONE} to allow a live source with ad support to drop the ad playback state. + * + *
This method should be used when the player is reset (for instance when a playback error + * occurs or {@link Player#stop()} is called) to make the player resolve the start position like + * when prepared initially. In this state, each source needs to be prepared again at which point + * the first timeline delivered by the source will replace the wrapped source to continue + * playback. + */ public PlaylistTimeline copyWithPlaceholderTimeline(ShuffleOrder shuffleOrder) { Timeline[] newTimelines = new Timeline[timelines.length]; for (int i = 0; i < timelines.length; i++) { newTimelines[i] = new ForwardingTimeline(timelines[i]) { + private final Window window = new Window(); + @Override public Period getPeriod(int periodIndex, Period period, boolean setIds) { Period superPeriod = super.getPeriod(periodIndex, period, setIds); - superPeriod.isPlaceholder = true; + if (super.getWindow(superPeriod.windowIndex, window).isLive()) { + // Reset the ad playback state for placeholder period of a live streams. + superPeriod.set( + period.id, + period.uid, + period.windowIndex, + period.durationUs, + period.positionInWindowUs, + AdPlaybackState.NONE, + /* isPlaceholder= */ true); + } else { + superPeriod.isPlaceholder = true; + } return superPeriod; } };