From e5133e78f5e5c42ec72e8d329ee3400c0a052bb8 Mon Sep 17 00:00:00 2001 From: michaelkatz Date: Wed, 23 Oct 2024 06:01:15 -0700 Subject: [PATCH] Abstract EPII renderer logic into holder class Move EPII calls to renderers to a separate managing class. PiperOrigin-RevId: 688932712 --- .../exoplayer/ExoPlayerImplInternal.java | 242 ++++------ .../media3/exoplayer/RendererHolder.java | 446 ++++++++++++++++++ 2 files changed, 531 insertions(+), 157 deletions(-) create mode 100644 libraries/exoplayer/src/main/java/androidx/media3/exoplayer/RendererHolder.java 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 5948f7959b..34fcffbb20 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayerImplInternal.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayerImplInternal.java @@ -19,9 +19,6 @@ import static androidx.media3.common.util.Assertions.checkNotNull; import static androidx.media3.common.util.Assertions.checkState; import static androidx.media3.common.util.Util.castNonNull; import static androidx.media3.common.util.Util.msToUs; -import static androidx.media3.exoplayer.Renderer.STATE_DISABLED; -import static androidx.media3.exoplayer.Renderer.STATE_ENABLED; -import static androidx.media3.exoplayer.Renderer.STATE_STARTED; import static androidx.media3.exoplayer.audio.AudioSink.OFFLOAD_MODE_DISABLED; import static java.lang.Math.max; import static java.lang.Math.min; @@ -59,26 +56,21 @@ import androidx.media3.exoplayer.ExoPlayer.PreloadConfiguration; import androidx.media3.exoplayer.analytics.AnalyticsCollector; import androidx.media3.exoplayer.analytics.PlayerId; import androidx.media3.exoplayer.drm.DrmSession; -import androidx.media3.exoplayer.metadata.MetadataRenderer; import androidx.media3.exoplayer.source.BehindLiveWindowException; import androidx.media3.exoplayer.source.MediaPeriod; import androidx.media3.exoplayer.source.MediaSource.MediaPeriodId; -import androidx.media3.exoplayer.source.SampleStream; import androidx.media3.exoplayer.source.ShuffleOrder; import androidx.media3.exoplayer.source.TrackGroupArray; -import androidx.media3.exoplayer.text.TextRenderer; import androidx.media3.exoplayer.trackselection.ExoTrackSelection; import androidx.media3.exoplayer.trackselection.TrackSelector; import androidx.media3.exoplayer.trackselection.TrackSelectorResult; import androidx.media3.exoplayer.upstream.BandwidthMeter; import com.google.common.base.Supplier; import com.google.common.collect.ImmutableList; -import com.google.common.collect.Sets; import java.io.IOException; import java.util.ArrayList; import java.util.Collections; import java.util.List; -import java.util.Set; import java.util.concurrent.atomic.AtomicBoolean; /** Implements the internal behavior of {@link ExoPlayerImpl}. */ @@ -181,8 +173,7 @@ import java.util.concurrent.atomic.AtomicBoolean; */ private static final long PLAYBACK_BUFFER_EMPTY_THRESHOLD_US = 500_000; - private final Renderer[] renderers; - private final Set renderersToReset; + private final RendererHolder[] renderers; private final RendererCapabilities[] rendererCapabilities; private final boolean[] rendererReportedReady; private final TrackSelector trackSelector; @@ -258,7 +249,6 @@ import java.util.concurrent.atomic.AtomicBoolean; @Nullable PlaybackLooperProvider playbackLooperProvider, PreloadConfiguration preloadConfiguration) { this.playbackInfoUpdateListener = playbackInfoUpdateListener; - this.renderers = renderers; this.trackSelector = trackSelector; this.emptyTrackSelectorResult = emptyTrackSelectorResult; this.loadControl = loadControl; @@ -289,16 +279,18 @@ import java.util.concurrent.atomic.AtomicBoolean; @Nullable RendererCapabilities.Listener rendererCapabilitiesListener = trackSelector.getRendererCapabilitiesListener(); + + this.renderers = new RendererHolder[renderers.length]; for (int i = 0; i < renderers.length; i++) { renderers[i].init(/* index= */ i, playerId, clock); rendererCapabilities[i] = renderers[i].getCapabilities(); if (rendererCapabilitiesListener != null) { rendererCapabilities[i].setListener(rendererCapabilitiesListener); } + this.renderers[i] = new RendererHolder(renderers[i], /* index= */ i); } mediaClock = new DefaultMediaClock(this, clock); pendingMessages = new ArrayList<>(); - renderersToReset = Sets.newIdentityHashSet(); window = new Timeline.Window(); period = new Timeline.Period(); trackSelector.init(/* listener= */ this, bandwidthMeter); @@ -981,18 +973,17 @@ import java.util.concurrent.atomic.AtomicBoolean; } TrackSelectorResult trackSelectorResult = playingPeriodHolder.getTrackSelectorResult(); for (int i = 0; i < renderers.length; i++) { - if (trackSelectorResult.isRendererEnabled(i) && renderers[i].getState() == STATE_ENABLED) { - renderers[i].start(); + if (!trackSelectorResult.isRendererEnabled(i)) { + continue; } + renderers[i].start(); } } private void stopRenderers() throws ExoPlaybackException { mediaClock.stop(); - for (Renderer renderer : renderers) { - if (isRendererEnabled(renderer)) { - ensureStopped(renderer); - } + for (RendererHolder rendererHolder : renderers) { + rendererHolder.stop(); } } @@ -1127,8 +1118,8 @@ import java.util.concurrent.atomic.AtomicBoolean; playingPeriodHolder.mediaPeriod.discardBuffer( playbackInfo.positionUs - backBufferDurationUs, retainBackBufferFromKeyframe); for (int i = 0; i < renderers.length; i++) { - Renderer renderer = renderers[i]; - if (!isRendererEnabled(renderer)) { + RendererHolder renderer = renderers[i]; + if (renderer.getEnabledRendererCount() == 0) { maybeTriggerOnRendererReadyChanged(/* rendererIndex= */ i, /* allowsPlayback= */ false); continue; } @@ -1136,16 +1127,13 @@ import java.util.concurrent.atomic.AtomicBoolean; // again. The minimum of these values should then be used as the delay before the next // invocation of this method. renderer.render(rendererPositionUs, rendererPositionElapsedRealtimeUs); + // Determine whether the renderer allows playback to continue. Playback can + // continue if the renderer is ready or ended. Also continue playback if the renderer is + // reading ahead into the next stream or is waiting for the next stream. This is to avoid + // getting stuck if tracks in the current period have uneven durations and are still being + // read by another renderer. See: https://github.com/google/ExoPlayer/issues/1874. renderersEnded = renderersEnded && renderer.isEnded(); - // Determine whether the renderer allows playback to continue. Playback can continue if the - // renderer is ready or ended. Also continue playback if the renderer is reading ahead into - // the next stream or is waiting for the next stream. This is to avoid getting stuck if - // tracks in the current period have uneven durations and are still being read by another - // renderer. See: https://github.com/google/ExoPlayer/issues/1874. - boolean isReadingAhead = playingPeriodHolder.sampleStreams[i] != renderer.getStream(); - boolean isWaitingForNextStream = !isReadingAhead && renderer.hasReadStreamToEnd(); - boolean allowsPlayback = - isReadingAhead || isWaitingForNextStream || renderer.isReady() || renderer.isEnded(); + boolean allowsPlayback = renderer.allowsPlayback(playingPeriodHolder); maybeTriggerOnRendererReadyChanged(/* rendererIndex= */ i, allowsPlayback); renderersAllowPlayback = renderersAllowPlayback && allowsPlayback; if (!allowsPlayback) { @@ -1198,8 +1186,7 @@ import java.util.concurrent.atomic.AtomicBoolean; boolean playbackMaybeStuck = false; if (playbackInfo.playbackState == Player.STATE_BUFFERING) { for (int i = 0; i < renderers.length; i++) { - if (isRendererEnabled(renderers[i]) - && renderers[i].getStream() == playingPeriodHolder.sampleStreams[i]) { + if (renderers[i].isReadingFromPeriod(playingPeriodHolder)) { maybeThrowRendererStreamError(/* rendererIndex= */ i); } } @@ -1285,15 +1272,13 @@ import java.util.concurrent.atomic.AtomicBoolean; ? READY_MAXIMUM_INTERVAL_MS : BUFFERING_MAXIMUM_INTERVAL_MS; if (dynamicSchedulingEnabled && shouldPlayWhenReady()) { - for (Renderer renderer : renderers) { - if (isRendererEnabled(renderer)) { - wakeUpTimeIntervalMs = - min( - wakeUpTimeIntervalMs, - Util.usToMs( - renderer.getDurationToProgressUs( - rendererPositionUs, rendererPositionElapsedRealtimeUs))); - } + for (RendererHolder rendererHolder : renderers) { + wakeUpTimeIntervalMs = + min( + wakeUpTimeIntervalMs, + Util.usToMs( + rendererHolder.getMinDurationToProgressUs( + rendererPositionUs, rendererPositionElapsedRealtimeUs))); } } handler.sendEmptyMessageAtTime( @@ -1448,9 +1433,7 @@ import java.util.concurrent.atomic.AtomicBoolean; || oldPlayingPeriodHolder != newPlayingPeriodHolder || (newPlayingPeriodHolder != null && newPlayingPeriodHolder.toRendererTime(periodPositionUs) < 0)) { - for (int i = 0; i < renderers.length; i++) { - disableRenderer(/* rendererIndex= */ i); - } + disableRenderers(); if (newPlayingPeriodHolder != null) { // Update the queue and reenable renderers if the requested media period already exists. while (queue.getPlayingPeriod() != newPlayingPeriodHolder) { @@ -1494,10 +1477,8 @@ import java.util.concurrent.atomic.AtomicBoolean; ? MediaPeriodQueue.INITIAL_RENDERER_POSITION_OFFSET_US + periodPositionUs : playingMediaPeriod.toRendererTime(periodPositionUs); mediaClock.resetPosition(rendererPositionUs); - for (Renderer renderer : renderers) { - if (isRendererEnabled(renderer)) { - renderer.resetPosition(rendererPositionUs); - } + for (RendererHolder rendererHolder : renderers) { + rendererHolder.resetPosition(rendererPositionUs); } notifyTrackSelectionDiscontinuity(); } @@ -1517,9 +1498,9 @@ import java.util.concurrent.atomic.AtomicBoolean; if (this.foregroundMode != foregroundMode) { this.foregroundMode = foregroundMode; if (!foregroundMode) { - for (Renderer renderer : renderers) { - if (!isRendererEnabled(renderer) && renderersToReset.remove(renderer)) { - renderer.reset(); + for (RendererHolder rendererHolder : renderers) { + if (rendererHolder.getEnabledRendererCount() == 0) { + rendererHolder.reset(); } } } @@ -1572,23 +1553,19 @@ import java.util.concurrent.atomic.AtomicBoolean; updateRebufferingState(/* isRebuffering= */ false, /* resetLastRebufferRealtimeMs= */ true); mediaClock.stop(); rendererPositionUs = MediaPeriodQueue.INITIAL_RENDERER_POSITION_OFFSET_US; - for (int i = 0; i < renderers.length; i++) { - try { - disableRenderer(/* rendererIndex= */ i); - } catch (ExoPlaybackException | RuntimeException e) { - // There's nothing we can do. - Log.e(TAG, "Disable failed.", e); - } + try { + disableRenderers(); + } catch (RuntimeException e) { + // There's nothing we can do. + Log.e(TAG, "Disable failed.", e); } if (resetRenderers) { - for (Renderer renderer : renderers) { - if (renderersToReset.remove(renderer)) { - try { - renderer.reset(); - } catch (RuntimeException e) { - // There's nothing we can do. - Log.e(TAG, "Reset failed.", e); - } + for (RendererHolder rendererHolder : renderers) { + try { + rendererHolder.reset(); + } catch (RuntimeException e) { + // There's nothing we can do. + Log.e(TAG, "Reset failed.", e); } } } @@ -1842,22 +1819,17 @@ import java.util.concurrent.atomic.AtomicBoolean; nextPendingMessageIndexHint = nextPendingMessageIndex; } - private void ensureStopped(Renderer renderer) { - if (renderer.getState() == STATE_STARTED) { - renderer.stop(); + private void disableRenderers() { + for (int i = 0; i < renderers.length; i++) { + disableRenderer(i); } } - private void disableRenderer(int rendererIndex) throws ExoPlaybackException { - Renderer renderer = renderers[rendererIndex]; - if (!isRendererEnabled(renderer)) { - return; - } + private void disableRenderer(int rendererIndex) { + int holderEnabledRendererCount = renderers[rendererIndex].getEnabledRendererCount(); + renderers[rendererIndex].disable(mediaClock); maybeTriggerOnRendererReadyChanged(rendererIndex, /* allowsPlayback= */ false); - mediaClock.onRendererDisabled(renderer); - ensureStopped(renderer); - renderer.disable(); - enabledRendererCount--; + enabledRendererCount -= holderEnabledRendererCount; } private void reselectTracksInternalAndSeek() throws ExoPlaybackException { @@ -1925,16 +1897,12 @@ import java.util.concurrent.atomic.AtomicBoolean; boolean[] rendererWasEnabledFlags = new boolean[renderers.length]; for (int i = 0; i < renderers.length; i++) { - Renderer renderer = renderers[i]; - rendererWasEnabledFlags[i] = isRendererEnabled(renderer); - SampleStream sampleStream = playingPeriodHolder.sampleStreams[i]; + rendererWasEnabledFlags[i] = renderers[i].getEnabledRendererCount() > 0; if (rendererWasEnabledFlags[i]) { - if (sampleStream != renderer.getStream()) { - // We need to disable the renderer. - disableRenderer(/* rendererIndex= */ i); + if (!renderers[i].isReadingFromPeriod(playingPeriodHolder)) { + disableRenderer(i); } else if (streamResetFlags[i]) { - // The renderer will continue to consume from its current stream, but needs to be reset. - renderer.resetPosition(rendererPositionUs); + renderers[i].resetPosition(rendererPositionUs); } } } @@ -2000,7 +1968,7 @@ import java.util.concurrent.atomic.AtomicBoolean; ? livePlaybackSpeedControl.getTargetLiveOffsetUs() : C.TIME_UNSET; MediaPeriodHolder loadingHolder = queue.getLoadingPeriod(); - boolean isBufferedToEnd = loadingHolder.isFullyBuffered() && loadingHolder.info.isFinal; + boolean isBufferedToEnd = (loadingHolder.isFullyBuffered() && loadingHolder.info.isFinal); // Ad loader implementations may only load ad media once playback has nearly reached the ad, but // it is possible for playback to be stuck buffering waiting for this. Therefore, we start // playback regardless of buffered duration if we are waiting for an ad media period to prepare. @@ -2062,8 +2030,8 @@ import java.util.concurrent.atomic.AtomicBoolean; /* releaseMediaSourceList= */ false, /* resetError= */ true); } - for (Renderer renderer : renderers) { - renderer.setTimeline(timeline); + for (RendererHolder rendererHolder : renderers) { + rendererHolder.setTimeline(timeline); } if (!periodPositionChanged) { // We can keep the current playing period. Update the rest of the queued periods. @@ -2181,12 +2149,11 @@ import java.util.concurrent.atomic.AtomicBoolean; return maxReadPositionUs; } for (int i = 0; i < renderers.length; i++) { - if (!isRendererEnabled(renderers[i]) - || renderers[i].getStream() != readingHolder.sampleStreams[i]) { + if (!renderers[i].isReadingFromPeriod(readingHolder)) { // Ignore disabled renderers and renderers with sample streams from previous periods. continue; } - long readingPositionUs = renderers[i].getReadingPositionUs(); + long readingPositionUs = renderers[i].getReadingPositionUs(readingHolder); if (readingPositionUs == C.TIME_END_OF_SOURCE) { return C.TIME_END_OF_SOURCE; } else { @@ -2250,19 +2217,19 @@ import java.util.concurrent.atomic.AtomicBoolean; // intentionally to pause at the end of the period. if (readingPeriodHolder.info.isFinal || pendingPauseAtEndOfPeriod) { for (int i = 0; i < renderers.length; i++) { - Renderer renderer = renderers[i]; - SampleStream sampleStream = readingPeriodHolder.sampleStreams[i]; + RendererHolder renderer = renderers[i]; + if (!renderer.isReadingFromPeriod(readingPeriodHolder)) { + continue; + } // Defer setting the stream as final until the renderer has actually consumed the whole // stream in case of playlist changes that cause the stream to be no longer final. - if (sampleStream != null - && renderer.getStream() == sampleStream - && renderer.hasReadStreamToEnd()) { + if (renderer.hasReadStreamToEnd()) { long streamEndPositionUs = readingPeriodHolder.info.durationUs != C.TIME_UNSET && readingPeriodHolder.info.durationUs != C.TIME_END_OF_SOURCE ? readingPeriodHolder.getRendererOffset() + readingPeriodHolder.info.durationUs : C.TIME_UNSET; - setCurrentStreamFinal(renderer, streamEndPositionUs); + renderer.setCurrentStreamFinal(streamEndPositionUs); } } } @@ -2320,8 +2287,7 @@ import java.util.concurrent.atomic.AtomicBoolean; // it's a no-sample renderer for which rendererOffsetUs should be updated only when // starting to play the next period. Mark the SampleStream as final to play out any // remaining data. - setCurrentStreamFinal( - renderers[i], + renderers[i].setCurrentStreamFinal( /* streamEndPositionUs= */ readingPeriodHolder.getStartPositionRendererTime()); } } @@ -2384,12 +2350,11 @@ import java.util.concurrent.atomic.AtomicBoolean; TrackSelectorResult newTrackSelectorResult = readingPeriodHolder.getTrackSelectorResult(); boolean needsToWaitForRendererToEnd = false; for (int i = 0; i < renderers.length; i++) { - Renderer renderer = renderers[i]; - if (!isRendererEnabled(renderer)) { + RendererHolder renderer = renderers[i]; + if (renderer.getEnabledRendererCount() == 0) { continue; } - boolean rendererIsReadingOldStream = - renderer.getStream() != readingPeriodHolder.sampleStreams[i]; + boolean rendererIsReadingOldStream = !renderer.isReadingFromPeriod(readingPeriodHolder); boolean rendererShouldBeEnabled = newTrackSelectorResult.isRendererEnabled(i); if (rendererShouldBeEnabled && !rendererIsReadingOldStream) { // All done. @@ -2411,7 +2376,7 @@ import java.util.concurrent.atomic.AtomicBoolean; } } else if (renderer.isEnded()) { // The renderer has finished playback, so we can disable it now. - disableRenderer(/* rendererIndex= */ i); + disableRenderer(i); } else { // We need to wait until rendering finished before disabling the renderer. needsToWaitForRendererToEnd = true; @@ -2479,9 +2444,10 @@ import java.util.concurrent.atomic.AtomicBoolean; private void allowRenderersToRenderStartOfStreams() { TrackSelectorResult playingTracks = queue.getPlayingPeriod().getTrackSelectorResult(); for (int i = 0; i < renderers.length; i++) { - if (playingTracks.isRendererEnabled(i)) { - renderers[i].enableMayRenderStartOfStream(); + if (!playingTracks.isRendererEnabled(i)) { + continue; } + renderers[i].enableMayRenderStartOfStream(); } } @@ -2514,46 +2480,16 @@ import java.util.concurrent.atomic.AtomicBoolean; return false; } for (int i = 0; i < renderers.length; i++) { - Renderer renderer = renderers[i]; - SampleStream sampleStream = readingPeriodHolder.sampleStreams[i]; - if (renderer.getStream() != sampleStream - || (sampleStream != null - && !renderer.hasReadStreamToEnd() - && !hasReachedServerSideInsertedAdsTransition(renderer, readingPeriodHolder))) { - // The current reading period is still being read by at least one renderer. + if (!renderers[i].hasFinishedReadingFromPeriod(readingPeriodHolder)) { return false; } } return true; } - private boolean hasReachedServerSideInsertedAdsTransition( - Renderer renderer, MediaPeriodHolder reading) { - MediaPeriodHolder nextPeriod = reading.getNext(); - // We can advance the reading period early once we read beyond the transition point in a - // server-side inserted ads stream because we know the samples are read from the same underlying - // stream. This shortcut is helpful in case the transition point moved and renderers already - // read beyond the new transition point. But wait until the next period is actually prepared to - // allow a seamless transition. - return reading.info.isFollowedByTransitionToSameStream - && nextPeriod.prepared - && (renderer instanceof TextRenderer // [internal: b/181312195] - || renderer instanceof MetadataRenderer - || renderer.getReadingPositionUs() >= nextPeriod.getStartPositionRendererTime()); - } - private void setAllRendererStreamsFinal(long streamEndPositionUs) { - for (Renderer renderer : renderers) { - if (renderer.getStream() != null) { - setCurrentStreamFinal(renderer, streamEndPositionUs); - } - } - } - - private void setCurrentStreamFinal(Renderer renderer, long streamEndPositionUs) { - renderer.setCurrentStreamFinal(); - if (renderer instanceof TextRenderer) { - ((TextRenderer) renderer).setFinalStreamEndPositionUs(streamEndPositionUs); + for (RendererHolder renderer : renderers) { + renderer.setCurrentStreamFinal(streamEndPositionUs); } } @@ -2635,11 +2571,9 @@ import java.util.concurrent.atomic.AtomicBoolean; playbackInfo = playbackInfo.copyWithPlaybackParameters(playbackParameters); } updateTrackSelectionPlaybackSpeed(playbackParameters.speed); - for (Renderer renderer : renderers) { - if (renderer != null) { - renderer.setPlaybackSpeed( - currentPlaybackSpeed, /* targetPlaybackSpeed= */ playbackParameters.speed); - } + for (RendererHolder rendererHolder : renderers) { + rendererHolder.setPlaybackSpeed( + currentPlaybackSpeed, /* targetPlaybackSpeed= */ playbackParameters.speed); } } @@ -2799,7 +2733,7 @@ import java.util.concurrent.atomic.AtomicBoolean; // Reset all disabled renderers before enabling any new ones. This makes sure resources released // by the disabled renderers will be available to renderers that are being enabled. for (int i = 0; i < renderers.length; i++) { - if (!trackSelectorResult.isRendererEnabled(i) && renderersToReset.remove(renderers[i])) { + if (!trackSelectorResult.isRendererEnabled(i)) { renderers[i].reset(); } } @@ -2814,11 +2748,11 @@ import java.util.concurrent.atomic.AtomicBoolean; private void enableRenderer(int rendererIndex, boolean wasRendererEnabled, long startPositionUs) throws ExoPlaybackException { - Renderer renderer = renderers[rendererIndex]; - if (isRendererEnabled(renderer)) { + MediaPeriodHolder periodHolder = queue.getReadingPeriod(); + RendererHolder renderer = renderers[rendererIndex]; + if (renderer.getEnabledRendererCount() > 0) { return; } - MediaPeriodHolder periodHolder = queue.getReadingPeriod(); boolean arePlayingAndReadingTheSamePeriod = periodHolder == queue.getPlayingPeriod(); TrackSelectorResult trackSelectorResult = periodHolder.getTrackSelectorResult(); RendererConfiguration rendererConfiguration = @@ -2831,7 +2765,6 @@ import java.util.concurrent.atomic.AtomicBoolean; boolean joining = !wasRendererEnabled && playing; // Enable the renderer. enabledRendererCount++; - renderersToReset.add(renderer); renderer.enable( rendererConfiguration, formats, @@ -2841,7 +2774,8 @@ import java.util.concurrent.atomic.AtomicBoolean; /* mayRenderStartOfStream= */ arePlayingAndReadingTheSamePeriod, startPositionUs, periodHolder.getRendererOffset(), - periodHolder.info.id); + periodHolder.info.id, + mediaClock); renderer.handleMessage( Renderer.MSG_SET_WAKEUP_LISTENER, new Renderer.WakeupListener() { @@ -2857,8 +2791,6 @@ import java.util.concurrent.atomic.AtomicBoolean; } } }); - - mediaClock.onRendererEnabled(renderer); // Start the renderer if playing and the Playing and Reading periods are the same. if (playing && arePlayingAndReadingTheSamePeriod) { renderer.start(); @@ -2948,7 +2880,7 @@ import java.util.concurrent.atomic.AtomicBoolean; private void maybeThrowRendererStreamError(int rendererIndex) throws IOException, ExoPlaybackException { - Renderer renderer = renderers[rendererIndex]; + RendererHolder renderer = renderers[rendererIndex]; try { renderer.maybeThrowStreamError(); } catch (IOException | RuntimeException e) { @@ -3442,10 +3374,6 @@ import java.util.concurrent.atomic.AtomicBoolean; return formats; } - private static boolean isRendererEnabled(Renderer renderer) { - return renderer.getState() != STATE_DISABLED; - } - private static final class SeekPosition { public final Timeline timeline; diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/RendererHolder.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/RendererHolder.java new file mode 100644 index 0000000000..136767a2ef --- /dev/null +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/RendererHolder.java @@ -0,0 +1,446 @@ +/* + * Copyright 2024 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; + +import static androidx.media3.exoplayer.Renderer.STATE_DISABLED; +import static androidx.media3.exoplayer.Renderer.STATE_ENABLED; +import static androidx.media3.exoplayer.Renderer.STATE_STARTED; + +import androidx.annotation.Nullable; +import androidx.media3.common.C; +import androidx.media3.common.Format; +import androidx.media3.common.Timeline; +import androidx.media3.common.util.Assertions; +import androidx.media3.exoplayer.metadata.MetadataRenderer; +import androidx.media3.exoplayer.source.MediaPeriod; +import androidx.media3.exoplayer.source.MediaSource; +import androidx.media3.exoplayer.source.SampleStream; +import androidx.media3.exoplayer.text.TextRenderer; +import java.io.IOException; + +/** Holds a {@link Renderer renderer}. */ +/* package */ class RendererHolder { + private final Renderer renderer; + // Index of renderer in renderer list held by the {@link Player}. + private final int index; + private boolean requiresReset; + + public RendererHolder(Renderer renderer, int index) { + this.renderer = renderer; + this.index = index; + requiresReset = false; + } + + public int getEnabledRendererCount() { + return isRendererEnabled(renderer) ? 1 : 0; + } + + /** + * Returns the track type that the renderer handles. + * + * @see Renderer#getTrackType() + */ + public @C.TrackType int getTrackType() { + return renderer.getTrackType(); + } + + /** + * Returns reading position from the {@link Renderer} enabled on the {@link MediaPeriodHolder + * media period}. + * + *

Call requires that {@link Renderer} is enabled on the provided {@link MediaPeriodHolder + * media period}. + * + * @param period The {@link MediaPeriodHolder media period} + * @return The {@link Renderer#getReadingPositionUs()} from the {@link Renderer} enabled on the + * {@link MediaPeriodHolder media period}. + */ + public long getReadingPositionUs(@Nullable MediaPeriodHolder period) { + Assertions.checkState(isReadingFromPeriod(period)); + return renderer.getReadingPositionUs(); + } + + /** + * Invokes {@link Renderer#hasReadStreamToEnd()}. + * + * @see Renderer#hasReadStreamToEnd() + */ + public boolean hasReadStreamToEnd() { + return renderer.hasReadStreamToEnd(); + } + + /** + * Signals to the renderer that the current {@link SampleStream} will be the final one supplied + * before it is next disabled or reset. + * + * @see Renderer#setCurrentStreamFinal() + * @param streamEndPositionUs The position to stop rendering at or {@link C#LENGTH_UNSET} to + * render until the end of the current stream. + */ + public void setCurrentStreamFinal(long streamEndPositionUs) { + setCurrentStreamFinal(renderer, streamEndPositionUs); + } + + private void setCurrentStreamFinal(Renderer renderer, long streamEndPositionUs) { + renderer.setCurrentStreamFinal(); + if (renderer instanceof TextRenderer) { + ((TextRenderer) renderer).setFinalStreamEndPositionUs(streamEndPositionUs); + } + } + + /** + * Returns whether the current {@link SampleStream} will be the final one supplied before the + * renderer is next disabled or reset. + * + * @see Renderer#isCurrentStreamFinal() + */ + public boolean isCurrentStreamFinal() { + return renderer.isCurrentStreamFinal(); + } + + /** + * Invokes {@link Renderer#replaceStream}. + * + * @see Renderer#replaceStream + */ + public void replaceStream( + Format[] formats, + SampleStream stream, + long startPositionUs, + long offsetUs, + MediaSource.MediaPeriodId mediaPeriodId) + throws ExoPlaybackException { + renderer.replaceStream(formats, stream, startPositionUs, offsetUs, mediaPeriodId); + } + + /** + * Returns minimum amount of playback clock time that must pass in order for the {@link #render} + * call to make progress. + * + *

Returns {@code Long.MAX_VALUE} if {@link Renderer renderers} are not enabled. + * + * @see Renderer#getDurationToProgressUs + * @param rendererPositionUs The current render position in microseconds, measured at the start of + * the current iteration of the rendering loop. + * @param rendererPositionElapsedRealtimeUs {@link android.os.SystemClock#elapsedRealtime()} in + * microseconds, measured at the start of the current iteration of the rendering loop. + * @return Minimum amount of playback clock time that must pass before renderer is able to make + * progress. + */ + public long getMinDurationToProgressUs( + long rendererPositionUs, long rendererPositionElapsedRealtimeUs) { + return isRendererEnabled(renderer) + ? renderer.getDurationToProgressUs(rendererPositionUs, rendererPositionElapsedRealtimeUs) + : Long.MAX_VALUE; + } + + /** + * Calls {@link Renderer#enableMayRenderStartOfStream} on enabled {@link Renderer renderers}. + * + * @see Renderer#enableMayRenderStartOfStream + */ + public void enableMayRenderStartOfStream() { + if (isRendererEnabled(renderer)) { + renderer.enableMayRenderStartOfStream(); + } + } + + /** + * Calls {@link Renderer#setPlaybackSpeed} on the {@link Renderer renderers}. + * + * @see Renderer#setPlaybackSpeed + */ + public void setPlaybackSpeed(float currentPlaybackSpeed, float targetPlaybackSpeed) + throws ExoPlaybackException { + renderer.setPlaybackSpeed(currentPlaybackSpeed, targetPlaybackSpeed); + } + + /** + * Calls {@link Renderer#setTimeline} on the {@link Renderer renderers}. + * + * @see Renderer#setTimeline + */ + public void setTimeline(Timeline timeline) { + renderer.setTimeline(timeline); + } + + /** + * Returns true if all renderers have {@link Renderer#isEnded() ended}. + * + * @see Renderer#isEnded() + * @return if all renderers have {@link Renderer#isEnded() ended}. + */ + public boolean isEnded() { + return renderer.isEnded(); + } + + /** + * Returns whether {@link Renderer} is enabled on a {@link MediaPeriodHolder media period}. + * + * @param period The {@link MediaPeriodHolder media period} to check. + * @return Whether {@link Renderer} is enabled on a {@link MediaPeriodHolder media period}. + */ + public boolean isReadingFromPeriod(@Nullable MediaPeriodHolder period) { + return getRendererReadingFromPeriod(period) != null; + } + + /** + * Returns the {@link Renderer} that is enabled on the provided media {@link MediaPeriodHolder + * period}. + * + *

Returns null if the renderer is not enabled on the requested period. + * + * @param period The {@link MediaPeriodHolder period} with which to retrieve the linked {@link + * Renderer} + * @return {@link Renderer} enabled on the {@link MediaPeriodHolder period} or {@code null} if the + * renderer is not enabled on the provided period. + */ + @Nullable + private Renderer getRendererReadingFromPeriod(@Nullable MediaPeriodHolder period) { + if (period == null || period.sampleStreams[index] == null) { + return null; + } + if (renderer.getStream() == period.sampleStreams[index]) { + return renderer; + } + return null; + } + + /** + * Returns whether the {@link Renderer renderers} are still reading a {@link MediaPeriodHolder + * media period}. + * + * @param periodHolder The {@link MediaPeriodHolder media period} to check. + * @return true if {@link Renderer renderers} are reading the current reading period. + */ + public boolean hasFinishedReadingFromPeriod(MediaPeriodHolder periodHolder) { + return hasFinishedReadingFromPeriodInternal(periodHolder); + } + + private boolean hasFinishedReadingFromPeriodInternal(MediaPeriodHolder readingPeriodHolder) { + SampleStream sampleStream = readingPeriodHolder.sampleStreams[index]; + if (renderer.getStream() != sampleStream + || (sampleStream != null + && !renderer.hasReadStreamToEnd() + && !hasReachedServerSideInsertedAdsTransition(renderer, readingPeriodHolder))) { + // The current reading period is still being read by at least one renderer. + return false; + } + return true; + } + + private boolean hasReachedServerSideInsertedAdsTransition( + Renderer renderer, MediaPeriodHolder reading) { + MediaPeriodHolder nextPeriod = reading.getNext(); + // We can advance the reading period early once we read beyond the transition point in a + // server-side inserted ads stream because we know the samples are read from the same underlying + // stream. This shortcut is helpful in case the transition point moved and renderers already + // read beyond the new transition point. But wait until the next period is actually prepared to + // allow a seamless transition. + return reading.info.isFollowedByTransitionToSameStream + && nextPeriod != null + && nextPeriod.prepared + && (renderer instanceof TextRenderer // [internal: b/181312195] + || renderer instanceof MetadataRenderer + || renderer.getReadingPositionUs() >= nextPeriod.getStartPositionRendererTime()); + } + + /** + * Calls {@link Renderer#render} on all enabled {@link Renderer renderers}. + * + * @param rendererPositionUs The current media time in microseconds, measured at the start of the + * current iteration of the rendering loop. + * @param rendererPositionElapsedRealtimeUs {@link android.os.SystemClock#elapsedRealtime()} in + * microseconds, measured at the start of the current iteration of the rendering loop. + * @throws ExoPlaybackException If an error occurs. + */ + public void render(long rendererPositionUs, long rendererPositionElapsedRealtimeUs) + throws ExoPlaybackException { + if (isRendererEnabled(renderer)) { + renderer.render(rendererPositionUs, rendererPositionElapsedRealtimeUs); + } + } + + /** + * Returns whether the renderers allow playback to continue. + * + *

Determine whether the renderer allows playback to continue. Playback can continue if the + * renderer is ready or ended. Also continue playback if the renderer is reading ahead into the + * next stream or is waiting for the next stream. This is to avoid getting stuck if tracks in the + * current period have uneven durations and are still being read by another renderer. See: + * https://github.com/google/ExoPlayer/issues/1874. + * + * @param playingPeriodHolder The currently playing media {@link MediaPeriodHolder period}. + * @return whether renderer allows playback. + */ + public boolean allowsPlayback(MediaPeriodHolder playingPeriodHolder) throws IOException { + return allowsPlayback(renderer, playingPeriodHolder); + } + + private boolean allowsPlayback(Renderer renderer, MediaPeriodHolder playingPeriodHolder) { + boolean isReadingAhead = playingPeriodHolder.sampleStreams[index] != renderer.getStream(); + boolean isWaitingForNextStream = !isReadingAhead && renderer.hasReadStreamToEnd(); + return isReadingAhead || isWaitingForNextStream || renderer.isReady() || renderer.isEnded(); + } + + /** + * Invokes {Renderer#maybeThrowStreamError}. + * + * @see Renderer#maybeThrowStreamError() + */ + public void maybeThrowStreamError() throws IOException { + renderer.maybeThrowStreamError(); + } + + /** + * Calls {@link Renderer#start()} on all enabled {@link Renderer renderers}. + * + * @throws ExoPlaybackException If an error occurs. + */ + public void start() throws ExoPlaybackException { + if (renderer.getState() == STATE_ENABLED) { + renderer.start(); + } + } + + /** Calls {@link Renderer#stop()} on all enabled {@link Renderer renderers}. */ + public void stop() { + if (isRendererEnabled(renderer)) { + ensureStopped(renderer); + } + } + + private void ensureStopped(Renderer renderer) { + if (renderer.getState() == STATE_STARTED) { + renderer.stop(); + } + } + + /** + * Enables the renderer to consume from the specified {@link SampleStream}. + * + * @see Renderer#enable + * @param configuration The renderer configuration. + * @param formats The enabled formats. + * @param stream The {@link SampleStream} from which the renderer should consume. + * @param positionUs The player's current position. + * @param joining Whether this renderer is being enabled to join an ongoing playback. + * @param mayRenderStartOfStream Whether this renderer is allowed to render the start of the + * stream even if the state is not {@link Renderer#STATE_STARTED} yet. + * @param startPositionUs The start position of the stream in renderer time (microseconds). + * @param offsetUs The offset to be added to timestamps of buffers read from {@code stream} before + * they are rendered. + * @param mediaPeriodId The {@link MediaSource.MediaPeriodId} of the {@link MediaPeriod} producing + * the {@code stream}. + * @param mediaClock The {@link DefaultMediaClock} with which to call {@link + * DefaultMediaClock#onRendererEnabled(Renderer)}. + * @throws ExoPlaybackException If an error occurs. + */ + public void enable( + RendererConfiguration configuration, + Format[] formats, + SampleStream stream, + long positionUs, + boolean joining, + boolean mayRenderStartOfStream, + long startPositionUs, + long offsetUs, + MediaSource.MediaPeriodId mediaPeriodId, + DefaultMediaClock mediaClock) + throws ExoPlaybackException { + requiresReset = true; + renderer.enable( + configuration, + formats, + stream, + positionUs, + joining, + mayRenderStartOfStream, + startPositionUs, + offsetUs, + mediaPeriodId); + mediaClock.onRendererEnabled(renderer); + } + + /** + * Invokes {@link Renderer#handleMessage} on the {@link Renderer}. + * + * @see Renderer#handleMessage(int, Object) + */ + public void handleMessage(@Renderer.MessageType int messageType, @Nullable Object message) + throws ExoPlaybackException { + renderer.handleMessage(messageType, message); + } + + /** + * Stops and disables all {@link Renderer renderers}. + * + * @param mediaClock To call {@link DefaultMediaClock#onRendererDisabled} if disabling a {@link + * Renderer}. + */ + public void disable(DefaultMediaClock mediaClock) { + disableRenderer(renderer, mediaClock); + } + + /** + * Disable a {@link Renderer} if its enabled. + * + *

The {@link DefaultMediaClock#onRendererDisabled} callback will be invoked if the renderer is + * disabled. + * + * @param renderer The {@link Renderer} to disable. + * @param mediaClock The {@link DefaultMediaClock} to invoke {@link + * DefaultMediaClock#onRendererDisabled onRendererDisabled} with the provided {@code + * renderer}. + */ + private void disableRenderer(Renderer renderer, DefaultMediaClock mediaClock) { + if (!isRendererEnabled(renderer)) { + return; + } + mediaClock.onRendererDisabled(renderer); + ensureStopped(renderer); + renderer.disable(); + } + + /** + * Calls {@link Renderer#resetPosition} on the {@link Renderer} if its enabled. + * + * @see Renderer#resetPosition + */ + public void resetPosition(long positionUs) throws ExoPlaybackException { + if (isRendererEnabled(renderer)) { + renderer.resetPosition(positionUs); + } + } + + /** Calls {@link Renderer#reset()} on all renderers that must be reset. */ + public void reset() { + if (requiresReset) { + renderer.reset(); + requiresReset = false; + } + } + + /** Calls {@link Renderer#release()} on all {@link Renderer renderers}. */ + public void release() { + renderer.release(); + requiresReset = false; + } + + private static boolean isRendererEnabled(Renderer renderer) { + return renderer.getState() != STATE_DISABLED; + } +}