From c9a1c053b85b10ae4e3929a50337b1b24ff24733 Mon Sep 17 00:00:00 2001 From: Jorge Ruesga Date: Thu, 29 Jan 2026 09:59:24 +0000 Subject: [PATCH] Allow renderers capacity reevaluation It allows external implementation to reevalute the number of reevaluate the number of renderers allocated mid-stream through a new `RenderersFactory#reevaluateRenderers` method, that allow to provide new renderers based on the information from next tracks available in the stream. The implementation only allows to increase the number of renderers, as old renderers could potentially be in use. A new shared `RenderersCoordinator` class has been added to centralize the use of all renderers-related instances, so they can be growth safely mid-stream. Signed-off-by: Jorge Ruesga --- .../media3/exoplayer/ExoPlayerImpl.java | 137 ++++----- .../exoplayer/ExoPlayerImplInternal.java | 126 ++++---- .../media3/exoplayer/MediaPeriodHolder.java | 47 ++- .../exoplayer/RenderersCoordinator.java | 290 ++++++++++++++++++ .../media3/exoplayer/RenderersFactory.java | 27 ++ .../trackselection/TrackSelectorResult.java | 9 + .../exoplayer/MediaPeriodQueueTest.java | 25 +- 7 files changed, 511 insertions(+), 150 deletions(-) create mode 100644 libraries/exoplayer/src/main/java/androidx/media3/exoplayer/RenderersCoordinator.java diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayerImpl.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayerImpl.java index bf28187d4ca..3e243616d25 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayerImpl.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayerImpl.java @@ -91,7 +91,6 @@ import androidx.media3.common.util.HandlerWrapper; import androidx.media3.common.util.ListenerSet; import androidx.media3.common.util.Log; -import androidx.media3.common.util.NullableType; import androidx.media3.common.util.Size; import androidx.media3.common.util.StuckPlayerDetector; import androidx.media3.common.util.StuckPlayerException; @@ -116,10 +115,8 @@ import androidx.media3.exoplayer.source.TimelineWithUpdatedMediaItem; import androidx.media3.exoplayer.source.TrackGroupArray; import androidx.media3.exoplayer.text.TextOutput; -import androidx.media3.exoplayer.trackselection.ExoTrackSelection; import androidx.media3.exoplayer.trackselection.TrackSelectionArray; import androidx.media3.exoplayer.trackselection.TrackSelector; -import androidx.media3.exoplayer.trackselection.TrackSelectorResult; import androidx.media3.exoplayer.upstream.BandwidthMeter; import androidx.media3.exoplayer.video.VideoDecoderOutputBufferRenderer; import androidx.media3.exoplayer.video.VideoFrameMetadataListener; @@ -149,22 +146,12 @@ private static final String TAG = "ExoPlayerImpl"; - /** - * This empty track selector result can only be used for {@link PlaybackInfo#trackSelectorResult} - * when the player does not have any track selection made (such as when player is reset, or when - * player seeks to an unprepared period). It will not be used as result of any {@link - * TrackSelector#selectTracks(RendererCapabilities[], TrackGroupArray, MediaPeriodId, Timeline)} - * operation. - */ - /* package */ final TrackSelectorResult emptyTrackSelectorResult; - /* package */ final Commands permanentAvailableCommands; private final ConditionVariable constructorFinished; private final Context applicationContext; private final Player wrappingPlayer; - private final Renderer[] renderers; - private final @NullableType Renderer[] secondaryRenderers; + private final RenderersCoordinator renderersCoordinator; private final TrackSelector trackSelector; private final HandlerWrapper playbackInfoUpdateHandler; private final ExoPlayerImplInternal.PlaybackInfoUpdateListener playbackInfoUpdateListener; @@ -279,28 +266,20 @@ public ExoPlayerImpl(ExoPlayer.Builder builder, @Nullable Player wrappingPlayer) componentListener = new ComponentListener(); frameMetadataListener = new FrameMetadataListener(); Handler eventHandler = new Handler(builder.looper); - RenderersFactory renderersFactory = builder.renderersFactorySupplier.get(); - renderers = - renderersFactory.createRenderers( - eventHandler, - componentListener, - componentListener, - componentListener, - componentListener); - checkState(renderers.length > 0); - secondaryRenderers = new Renderer[renderers.length]; - for (int i = 0; i < secondaryRenderers.length; i++) { - // TODO(b/377671489): Fix DefaultAnalyticsCollector logic to still work with pre-warming. - secondaryRenderers[i] = - renderersFactory.createSecondaryRenderer( - renderers[i], - eventHandler, - componentListener, - componentListener, - componentListener, - componentListener); - } this.trackSelector = builder.trackSelectorSupplier.get(); + final PlayerId playerId = new PlayerId(builder.playerName); + RenderersFactory renderersFactory = builder.renderersFactorySupplier.get(); + renderersCoordinator = new RenderersCoordinator( + playerId, + eventHandler, + trackSelector, + builder.clock, + builder.renderersFactorySupplier, + componentListener, + componentListener, + componentListener, + componentListener + ); this.mediaSourceFactory = builder.mediaSourceFactorySupplier.get(); this.bandwidthMeter = builder.bandwidthMeterSupplier.get(); this.useLazyPreparation = builder.useLazyPreparation; @@ -322,12 +301,6 @@ public ExoPlayerImpl(ExoPlayer.Builder builder, @Nullable Player wrappingPlayer) mediaSourceHolderSnapshots = new ArrayList<>(); shuffleOrder = new ShuffleOrder.DefaultShuffleOrder(/* length= */ 0); preloadConfiguration = PreloadConfiguration.DEFAULT; - emptyTrackSelectorResult = - new TrackSelectorResult( - new RendererConfiguration[renderers.length], - new ExoTrackSelection[renderers.length], - Tracks.EMPTY, - /* info= */ null); period = new Timeline.Period(); permanentAvailableCommands = new Commands.Builder() @@ -370,16 +343,14 @@ public ExoPlayerImpl(ExoPlayer.Builder builder, @Nullable Player wrappingPlayer) playbackInfoUpdateListener = playbackInfoUpdate -> playbackInfoUpdateHandler.post(() -> handlePlaybackInfo(playbackInfoUpdate)); - playbackInfo = PlaybackInfo.createDummy(emptyTrackSelectorResult); + playbackInfo = PlaybackInfo.createDummy(renderersCoordinator.emptyTrackSelectorResult); analyticsCollector.setPlayer(this.wrappingPlayer, applicationLooper); - PlayerId playerId = new PlayerId(builder.playerName); + internalPlayer = new ExoPlayerImplInternal( applicationContext, - renderers, - secondaryRenderers, + renderersCoordinator, trackSelector, - emptyTrackSelectorResult, builder.loadControlSupplier.get(), bandwidthMeter, repeatMode, @@ -1281,26 +1252,26 @@ public long getContentBufferedPosition() { @Override public int getRendererCount() { verifyApplicationThread(); - return renderers.length; + return renderersCoordinator.getRendererCount(); } @Override public @C.TrackType int getRendererType(int index) { verifyApplicationThread(); - return renderers[index].getTrackType(); + return renderersCoordinator.getRendererType(index); } @Override public Renderer getRenderer(int index) { verifyApplicationThread(); - return renderers[index]; + return renderersCoordinator.getRenderer(index); } @Override @Nullable public Renderer getSecondaryRenderer(int index) { verifyApplicationThread(); - return secondaryRenderers[index]; + return renderersCoordinator.getSecondaryRenderer(index); } @Override @@ -1457,7 +1428,7 @@ public Size getSurfaceSize() { public void clearVideoSurface() { verifyApplicationThread(); removeSurfaceCallbacks(); - setVideoOutputInternal(/* videoOutput= */ null); + setVideoOutputInternalLocked(/* videoOutput= */ null); maybeNotifySurfaceSizeChanged(/* width= */ 0, /* height= */ 0); } @@ -1473,7 +1444,7 @@ public void clearVideoSurface(@Nullable Surface surface) { public void setVideoSurface(@Nullable Surface surface) { verifyApplicationThread(); removeSurfaceCallbacks(); - setVideoOutputInternal(surface); + setVideoOutputInternalLocked(surface); int newSurfaceSize = surface == null ? 0 : C.LENGTH_UNSET; maybeNotifySurfaceSizeChanged(/* width= */ newSurfaceSize, /* height= */ newSurfaceSize); } @@ -1490,11 +1461,11 @@ public void setVideoSurfaceHolder(@Nullable SurfaceHolder surfaceHolder) { surfaceHolder.addCallback(componentListener); Surface surface = surfaceHolder.getSurface(); if (surface != null && surface.isValid()) { - setVideoOutputInternal(surface); + setVideoOutputInternalLocked(surface); Rect surfaceSize = surfaceHolder.getSurfaceFrame(); maybeNotifySurfaceSizeChanged(surfaceSize.width(), surfaceSize.height()); } else { - setVideoOutputInternal(/* videoOutput= */ null); + setVideoOutputInternalLocked(/* videoOutput= */ null); maybeNotifySurfaceSizeChanged(/* width= */ 0, /* height= */ 0); } } @@ -1513,7 +1484,7 @@ public void setVideoSurfaceView(@Nullable SurfaceView surfaceView) { verifyApplicationThread(); if (surfaceView instanceof VideoDecoderOutputBufferRenderer) { removeSurfaceCallbacks(); - setVideoOutputInternal(surfaceView); + setVideoOutputInternalLocked(surfaceView); setNonVideoOutputSurfaceHolderInternal(surfaceView.getHolder()); } else if (surfaceView instanceof SphericalGLSurfaceView) { removeSurfaceCallbacks(); @@ -1523,7 +1494,7 @@ public void setVideoSurfaceView(@Nullable SurfaceView surfaceView) { .setPayload(sphericalGLSurfaceView) .send(); sphericalGLSurfaceView.addVideoSurfaceListener(componentListener); - setVideoOutputInternal(sphericalGLSurfaceView.getVideoSurface()); + setVideoOutputInternalLocked(sphericalGLSurfaceView.getVideoSurface()); setNonVideoOutputSurfaceHolderInternal(surfaceView.getHolder()); } else { setVideoSurfaceHolder(surfaceView == null ? null : surfaceView.getHolder()); @@ -1552,7 +1523,7 @@ public void setVideoTextureView(@Nullable TextureView textureView) { SurfaceTexture surfaceTexture = textureView.isAvailable() ? textureView.getSurfaceTexture() : null; if (surfaceTexture == null) { - setVideoOutputInternal(/* videoOutput= */ null); + setVideoOutputInternalLocked(/* videoOutput= */ null); maybeNotifySurfaceSizeChanged(/* width= */ 0, /* height= */ 0); } else { setSurfaceTextureInternal(surfaceTexture); @@ -2740,7 +2711,7 @@ private PlaybackInfo maskTimelineAndPosition( /* discontinuityStartPositionUs= */ positionUs, /* totalBufferedDurationUs= */ 0, TrackGroupArray.EMPTY, - emptyTrackSelectorResult, + renderersCoordinator.getEmptyTrackSelectorResult(), /* staticMetadata= */ ImmutableList.of()); playbackInfo = playbackInfo.copyWithLoadingMediaPeriodId(dummyMediaPeriodId); playbackInfo.bufferedPositionUs = playbackInfo.positionUs; @@ -2777,7 +2748,9 @@ private PlaybackInfo maskTimelineAndPosition( /* discontinuityStartPositionUs= */ newContentPositionUs, /* totalBufferedDurationUs= */ 0, playingPeriodChanged ? TrackGroupArray.EMPTY : playbackInfo.trackGroups, - playingPeriodChanged ? emptyTrackSelectorResult : playbackInfo.trackSelectorResult, + playingPeriodChanged + ? renderersCoordinator.getEmptyTrackSelectorResult() + : playbackInfo.trackSelectorResult, playingPeriodChanged ? ImmutableList.of() : playbackInfo.staticMetadata); playbackInfo = playbackInfo.copyWithLoadingMediaPeriodId(newPeriodId); playbackInfo.bufferedPositionUs = newContentPositionUs; @@ -2959,10 +2932,20 @@ private void removeSurfaceCallbacks() { private void setSurfaceTextureInternal(SurfaceTexture surfaceTexture) { Surface surface = new Surface(surfaceTexture); - setVideoOutputInternal(surface); + setVideoOutputInternalLocked(surface); ownedSurface = surface; } + private void setVideoOutputInternalLocked(@Nullable Object videoOutput) { + try { + renderersCoordinator.withLock(() -> { + setVideoOutputInternal(videoOutput); + }); + } catch (Exception ex) { + // Ignore. setVideoOutputInternal doesn't throw any exception. + } + } + private void setVideoOutputInternal(@Nullable Object videoOutput) { boolean isReplacingVideoOutput = this.videoOutput != null && this.videoOutput != videoOutput; long timeoutMs = isReplacingVideoOutput ? detachSurfaceTimeoutMs : C.TIME_UNSET; @@ -3116,15 +3099,21 @@ private void sendRendererMessage(int messageType, @Nullable Object payload) { private void sendRendererMessage( @C.TrackType int trackType, int messageType, @Nullable Object payload) { - for (Renderer renderer : renderers) { - if (trackType == -1 || renderer.getTrackType() == trackType) { - createMessageInternal(renderer).setType(messageType).setPayload(payload).send(); - } - } - for (@Nullable Renderer renderer : secondaryRenderers) { - if (renderer != null && (trackType == -1 || renderer.getTrackType() == trackType)) { - createMessageInternal(renderer).setType(messageType).setPayload(payload).send(); - } + try { + renderersCoordinator.withLock(() -> { + for (Renderer renderer : renderersCoordinator.renderers) { + if (trackType == -1 || renderer.getTrackType() == trackType) { + createMessageInternal(renderer).setType(messageType).setPayload(payload).send(); + } + } + for (@Nullable Renderer renderer : renderersCoordinator.secondaryRenderers) { + if (renderer != null && (trackType == -1 || renderer.getTrackType() == trackType)) { + createMessageInternal(renderer).setType(messageType).setPayload(payload).send(); + } + } + }); + } catch (Exception ex) { + // Ignore. There should not be errors in the the block. } } @@ -3515,7 +3504,7 @@ public void onMetadata(Metadata metadata) { @Override public void surfaceCreated(SurfaceHolder holder) { if (surfaceHolderSurfaceIsVideoOutput) { - setVideoOutputInternal(holder.getSurface()); + setVideoOutputInternalLocked(holder.getSurface()); } } @@ -3527,7 +3516,7 @@ public void surfaceChanged(SurfaceHolder holder, int format, int width, int heig @Override public void surfaceDestroyed(SurfaceHolder holder) { if (surfaceHolderSurfaceIsVideoOutput) { - setVideoOutputInternal(/* videoOutput= */ null); + setVideoOutputInternalLocked(/* videoOutput= */ null); } maybeNotifySurfaceSizeChanged(/* width= */ 0, /* height= */ 0); } @@ -3547,7 +3536,7 @@ public void onSurfaceTextureSizeChanged(SurfaceTexture surfaceTexture, int width @Override public boolean onSurfaceTextureDestroyed(SurfaceTexture surfaceTexture) { - setVideoOutputInternal(/* videoOutput= */ null); + setVideoOutputInternalLocked(/* videoOutput= */ null); maybeNotifySurfaceSizeChanged(/* width= */ 0, /* height= */ 0); return true; } @@ -3561,12 +3550,12 @@ public void onSurfaceTextureUpdated(SurfaceTexture surfaceTexture) { @Override public void onVideoSurfaceCreated(Surface surface) { - setVideoOutputInternal(surface); + setVideoOutputInternalLocked(surface); } @Override public void onVideoSurfaceDestroyed(Surface surface) { - setVideoOutputInternal(/* videoOutput= */ null); + setVideoOutputInternalLocked(/* videoOutput= */ null); } // AudioBecomingNoisyManager.EventListener implementation. 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 dc00a0f8848..56e524224fe 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayerImplInternal.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayerImplInternal.java @@ -205,11 +205,8 @@ public interface PlaybackInfoUpdateListener { */ private static final long DURATION_TO_ADVANCE_READING_THRESHOLD_US = 10 * C.MICROS_PER_SECOND; - private final RendererHolder[] renderers; - private final RendererCapabilities[] rendererCapabilities; - private final boolean[] rendererReportedReady; + private final RenderersCoordinator renderersCoordinator; private final TrackSelector trackSelector; - private final TrackSelectorResult emptyTrackSelectorResult; private final LoadControl loadControl; private final BandwidthMeter bandwidthMeter; private final HandlerWrapper handler; @@ -231,7 +228,6 @@ public interface PlaybackInfoUpdateListener { private final boolean dynamicSchedulingEnabled; private final AnalyticsCollector analyticsCollector; private final HandlerWrapper applicationLooperHandler; - private final boolean hasSecondaryRenderers; private final AudioFocusManager audioFocusManager; private final boolean avoidLoadingWhileEnded; @@ -272,10 +268,8 @@ public interface PlaybackInfoUpdateListener { public ExoPlayerImplInternal( Context context, - Renderer[] renderers, - Renderer[] secondaryRenderers, + RenderersCoordinator renderersCoordinator, TrackSelector trackSelector, - TrackSelectorResult emptyTrackSelectorResult, LoadControl loadControl, BandwidthMeter bandwidthMeter, @Player.RepeatMode int repeatMode, @@ -295,8 +289,8 @@ public ExoPlayerImplInternal( VideoFrameMetadataListener videoFrameMetadataListener, boolean avoidLoadingWhileEnded) { this.playbackInfoUpdateListener = playbackInfoUpdateListener; + this.renderersCoordinator = renderersCoordinator; this.trackSelector = trackSelector; - this.emptyTrackSelectorResult = emptyTrackSelectorResult; this.loadControl = loadControl; this.bandwidthMeter = bandwidthMeter; this.repeatMode = repeatMode; @@ -321,30 +315,8 @@ public ExoPlayerImplInternal( retainBackBufferFromKeyframe = loadControl.retainBackBufferFromKeyframe(playerId); lastPreloadPoolInvalidationTimeline = Timeline.EMPTY; - playbackInfo = PlaybackInfo.createDummy(emptyTrackSelectorResult); + playbackInfo = PlaybackInfo.createDummy(renderersCoordinator.emptyTrackSelectorResult); playbackInfoUpdate = new PlaybackInfoUpdate(playbackInfo); - rendererCapabilities = new RendererCapabilities[renderers.length]; - rendererReportedReady = new boolean[renderers.length]; - @Nullable - RendererCapabilities.Listener rendererCapabilitiesListener = - trackSelector.getRendererCapabilitiesListener(); - - boolean hasSecondaryRenderers = false; - 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); - } - if (secondaryRenderers[i] != null) { - secondaryRenderers[i].init(/* index= */ i, playerId, clock); - hasSecondaryRenderers = true; - } - this.renderers[i] = new RendererHolder(renderers[i], secondaryRenderers[i], /* index= */ i); - } - this.hasSecondaryRenderers = hasSecondaryRenderers; - mediaClock = new DefaultMediaClock(this, clock); pendingMessages = new ArrayList<>(); window = new Timeline.Window(); @@ -384,13 +356,12 @@ public ExoPlayerImplInternal( private MediaPeriodHolder createMediaPeriodHolder( MediaPeriodInfo mediaPeriodInfo, long rendererPositionOffsetUs) { return new MediaPeriodHolder( - rendererCapabilities, + renderersCoordinator, rendererPositionOffsetUs, trackSelector, loadControl.getAllocator(playerId), mediaSourceList, mediaPeriodInfo, - emptyTrackSelectorResult, preloadConfiguration.targetPreloadDurationUs); } @@ -534,7 +505,7 @@ private void handleAudioFocusVolumeMultiplierChange() throws ExoPlaybackExceptio private void setVideoFrameMetadataListenerInternal( VideoFrameMetadataListener videoFrameMetadataListener) throws ExoPlaybackException { - for (RendererHolder renderer : renderers) { + for (RendererHolder renderer : renderersCoordinator.rendererHolders) { renderer.setVideoFrameMetadataListener(videoFrameMetadataListener); } } @@ -708,7 +679,7 @@ public boolean handleMessage(Message msg) { setPreloadConfigurationInternal((PreloadConfiguration) msg.obj); break; case MSG_DO_SOME_WORK: - doSomeWork(); + renderersCoordinator.withLock(this::doSomeWork); break; case MSG_SEEK_TO: seekToInternal((SeekPosition) msg.obj); @@ -748,13 +719,15 @@ public boolean handleMessage(Message msg) { stopInternal(/* forceResetRenderers= */ false, /* acknowledgeStop= */ true); break; case MSG_PERIOD_PREPARED: - handlePeriodPrepared((MediaPeriod) msg.obj); + renderersCoordinator.withLock(() -> { + handlePeriodPrepared((MediaPeriod) msg.obj); + }); break; case MSG_SOURCE_CONTINUE_LOADING_REQUESTED: handleContinueLoadingRequested((MediaPeriod) msg.obj); break; case MSG_TRACK_SELECTION_INVALIDATED: - reselectTracksInternal(); + renderersCoordinator.withLock(this::reselectTracksInternal); break; case MSG_PLAYBACK_PARAMETERS_CHANGED_INTERNAL: handlePlaybackParameters((PlaybackParameters) msg.obj, /* acknowledgeCommand= */ false); @@ -787,7 +760,7 @@ public boolean handleMessage(Message msg) { setPauseAtEndOfWindowInternal(msg.arg1 != 0); break; case MSG_ATTEMPT_RENDERER_ERROR_RECOVERY: - attemptRendererErrorRecovery(); + renderersCoordinator.withLock(this::attemptRendererErrorRecovery); break; case MSG_RENDERER_CAPABILITIES_CHANGED: reselectTracksInternalAndSeek(); @@ -1057,7 +1030,7 @@ private void setAudioAttributesInternal(AudioAttributes audioAttributes, boolean private void setVolumeInternal(float volume) throws ExoPlaybackException { this.volume = volume; float scaledVolume = volume * audioFocusManager.getVolumeMultiplier(); - for (RendererHolder renderer : renderers) { + for (RendererHolder renderer : renderersCoordinator.rendererHolders) { renderer.setVolume(scaledVolume); } } @@ -1225,6 +1198,7 @@ private void startRenderers() throws ExoPlaybackException { return; } TrackSelectorResult trackSelectorResult = playingPeriodHolder.getTrackSelectorResult(); + final RendererHolder[] renderers = renderersCoordinator.rendererHolders; for (int i = 0; i < renderers.length; i++) { if (!trackSelectorResult.isRendererEnabled(i)) { continue; @@ -1235,6 +1209,7 @@ private void startRenderers() throws ExoPlaybackException { private void stopRenderers() throws ExoPlaybackException { mediaClock.stop(); + final RendererHolder[] renderers = renderersCoordinator.rendererHolders; for (RendererHolder rendererHolder : renderers) { rendererHolder.stop(); } @@ -1376,6 +1351,7 @@ private void doSomeWork() throws ExoPlaybackException, IOException { rendererPositionElapsedRealtimeUs = msToUs(clock.elapsedRealtime()); playingPeriodHolder.mediaPeriod.discardBuffer( playbackInfo.positionUs - backBufferDurationUs, retainBackBufferFromKeyframe); + final RendererHolder[] renderers = renderersCoordinator.rendererHolders; for (int i = 0; i < renderers.length; i++) { RendererHolder renderer = renderers[i]; if (renderer.getEnabledRendererCount() == 0) { @@ -1444,6 +1420,7 @@ && shouldTransitionToReadyState(renderersAllowPlayback)) { boolean playbackMaybeStuck = false; if (playbackInfo.playbackState == Player.STATE_BUFFERING) { + final RendererHolder[] renderers = renderersCoordinator.rendererHolders; for (int i = 0; i < renderers.length; i++) { if (renderers[i].isReadingFromPeriod(playingPeriodHolder)) { maybeThrowRendererStreamError(/* rendererIndex= */ i); @@ -1492,8 +1469,9 @@ && shouldPlayWhenReady()) { } private void maybeTriggerOnRendererReadyChanged(int rendererIndex, boolean allowsPlayback) { - if (rendererReportedReady[rendererIndex] != allowsPlayback) { - rendererReportedReady[rendererIndex] = allowsPlayback; + final RendererHolder[] renderers = renderersCoordinator.rendererHolders; + if (renderersCoordinator.rendererReportedReady[rendererIndex] != allowsPlayback) { + renderersCoordinator.rendererReportedReady[rendererIndex] = allowsPlayback; applicationLooperHandler.post( () -> analyticsCollector.onRendererReadyChanged( @@ -1540,6 +1518,7 @@ private long getDynamicSchedulingWakeUpIntervalMs() { playbackInfo.playbackState == Player.STATE_READY ? READY_MAXIMUM_INTERVAL_MS : BUFFERING_MAXIMUM_INTERVAL_MS; + final RendererHolder[] renderers = renderersCoordinator.rendererHolders; for (RendererHolder rendererHolder : renderers) { wakeUpTimeIntervalMs = min( @@ -1662,6 +1641,7 @@ private void seekToInternal(SeekPosition seekPosition) throws ExoPlaybackExcepti } if (scrubbingModeEnabled) { + final RendererHolder[] renderers = renderersCoordinator.rendererHolders; for (RendererHolder renderer : renderers) { // TODO: b/451939261 - Remove video-only condition once image-playback scrubbing mode // supports skipping intermittent seeks. @@ -1814,6 +1794,7 @@ private boolean shouldSkipKeyFrameReset(MediaPeriodHolder playingPeriod, long pe } long rendererPositionUs = playingPeriod.toRendererTime(periodPositionUs); boolean renderersSupportSkipKeyFrameReset = true; + final RendererHolder[] renderers = renderersCoordinator.rendererHolders; for (RendererHolder renderer : renderers) { if (renderer.isRendererEnabled()) { renderersSupportSkipKeyFrameReset &= @@ -1840,6 +1821,7 @@ private void resetRendererPosition(long periodPositionUs, boolean sampleStreamIs ? MediaPeriodQueue.INITIAL_RENDERER_POSITION_OFFSET_US + periodPositionUs : playingMediaPeriod.toRendererTime(periodPositionUs); mediaClock.resetPosition(rendererPositionUs); + final RendererHolder[] renderers = renderersCoordinator.rendererHolders; for (RendererHolder rendererHolder : renderers) { rendererHolder.resetPosition( playingMediaPeriod, rendererPositionUs, sampleStreamIsResetToKeyFrame); @@ -1893,6 +1875,7 @@ private void setScrubbingModeParametersInternal(ScrubbingModeParameters scrubbin } private void applyScrubbingModeParameters() throws ExoPlaybackException { + final RendererHolder[] renderers = renderersCoordinator.rendererHolders; for (RendererHolder renderer : renderers) { renderer.setScrubbingMode(scrubbingModeEnabled ? scrubbingModeParameters : null); } @@ -1903,6 +1886,7 @@ private void setForegroundModeInternal( if (this.foregroundMode != foregroundMode) { this.foregroundMode = foregroundMode; if (!foregroundMode) { + final RendererHolder[] renderers = renderersCoordinator.rendererHolders; for (RendererHolder rendererHolder : renderers) { rendererHolder.reset(); } @@ -1916,6 +1900,7 @@ private void setForegroundModeInternal( private void setVideoOutputInternal( @Nullable Object videoOutput, @Nullable ConditionVariable processedCondition) throws ExoPlaybackException { + final RendererHolder[] renderers = renderersCoordinator.rendererHolders; for (RendererHolder renderer : renderers) { renderer.setVideoOutput(videoOutput); } @@ -1981,6 +1966,7 @@ private void resetInternal( Log.e(TAG, "Disable failed.", e); } if (resetRenderers) { + final RendererHolder[] renderers = renderersCoordinator.rendererHolders; for (RendererHolder rendererHolder : renderers) { try { rendererHolder.reset(); @@ -2042,7 +2028,9 @@ private void resetInternal( resetError ? null : playbackInfo.playbackError, /* isLoading= */ false, resetTrackInfo ? TrackGroupArray.EMPTY : playbackInfo.trackGroups, - resetTrackInfo ? emptyTrackSelectorResult : playbackInfo.trackSelectorResult, + resetTrackInfo + ? renderersCoordinator.emptyTrackSelectorResult + : playbackInfo.trackSelectorResult, resetTrackInfo ? ImmutableList.of() : playbackInfo.staticMetadata, mediaPeriodId, playbackInfo.playWhenReady, @@ -2241,6 +2229,7 @@ private void maybeTriggerPendingMessages(long oldPeriodPositionUs, long newPerio } private void disableRenderers() throws ExoPlaybackException { + final RendererHolder[] renderers = renderersCoordinator.rendererHolders; for (int i = 0; i < renderers.length; i++) { disableRenderer(/* rendererIndex= */ i); } @@ -2248,6 +2237,7 @@ private void disableRenderers() throws ExoPlaybackException { } private void disableRenderer(int rendererIndex) throws ExoPlaybackException { + final RendererHolder[] renderers = renderersCoordinator.rendererHolders; int enabledRendererCountBeforeDisabling = renderers[rendererIndex].getEnabledRendererCount(); renderers[rendererIndex].disable(mediaClock); maybeTriggerOnRendererReadyChanged(rendererIndex, /* allowsPlayback= */ false); @@ -2255,9 +2245,10 @@ private void disableRenderer(int rendererIndex) throws ExoPlaybackException { } private void disableAndResetPrewarmingRenderers() { - if (!hasSecondaryRenderers || !areRenderersPrewarming()) { + if (!renderersCoordinator.hasSecondaryRenderers || !areRenderersPrewarming()) { return; } + final RendererHolder[] renderers = renderersCoordinator.rendererHolders; for (RendererHolder renderer : renderers) { int enabledRendererCountBeforeDisabling = renderer.getEnabledRendererCount(); renderer.disablePrewarming(mediaClock); @@ -2272,6 +2263,7 @@ private boolean isRendererPrewarmingMediaPeriod(int rendererIndex, MediaPeriodId || !queue.getPrewarmingPeriod().info.id.equals(mediaPeriodId)) { return false; } + final RendererHolder[] renderers = renderersCoordinator.rendererHolders; return renderers[rendererIndex].isPrewarmingPeriod(queue.getPrewarmingPeriod()); } @@ -2294,6 +2286,7 @@ private void reselectTracksInternal() throws ExoPlaybackException { // The reselection did not change any prepared periods. return; } + renderersCoordinator.reevaluate(periodHolder.getTrackGroups()); newTrackSelectorResult = periodHolder.selectTracks( playbackSpeed, playbackInfo.timeline, playbackInfo.playWhenReady); @@ -2319,6 +2312,7 @@ private void reselectTracksInternal() throws ExoPlaybackException { boolean recreateStreams = (removeAfterResult & UPDATE_PERIOD_QUEUE_ALTERED_READING_PERIOD) != 0; + final RendererHolder[] renderers = renderersCoordinator.rendererHolders; boolean[] streamResetFlags = new boolean[renderers.length]; long periodPositionUs = playingPeriodHolder.applyTrackSelection( @@ -2369,7 +2363,7 @@ private void reselectTracksInternal() throws ExoPlaybackException { if (periodHolder.prepared) { long loadingPeriodPositionUs = max(periodHolder.info.startPositionUs, periodHolder.toPeriodTime(rendererPositionUs)); - if (hasSecondaryRenderers + if (renderersCoordinator.hasSecondaryRenderers && areRenderersPrewarming() && queue.getPrewarmingPeriod() == periodHolder) { // If renderers are enabled early and track reselection is on the enabled-early period @@ -2497,6 +2491,7 @@ private void handleMediaSourceListInfoRefreshed(Timeline timeline, boolean isSou /* releaseMediaSourceList= */ false, /* resetError= */ true); } + final RendererHolder[] renderers = renderersCoordinator.rendererHolders; for (RendererHolder rendererHolder : renderers) { rendererHolder.setTimeline(timeline); } @@ -2631,6 +2626,7 @@ private long getMaxRendererReadPositionUs(MediaPeriodHolder periodHolder) { if (!periodHolder.prepared) { return maxReadPositionUs; } + final RendererHolder[] renderers = renderersCoordinator.rendererHolders; for (int i = 0; i < renderers.length; i++) { if (!renderers[i].isReadingFromPeriod(periodHolder)) { // Ignore disabled renderers and renderers with sample streams from previous periods. @@ -2693,7 +2689,7 @@ private boolean maybeUpdateLoadingPeriod() throws ExoPlaybackException { private void maybeUpdatePrewarmingPeriod() throws ExoPlaybackException { // TODO: Add limit as to not enable waiting renderer too early if (pendingPauseAtEndOfPeriod - || !hasSecondaryRenderers + || !renderersCoordinator.hasSecondaryRenderers || isPrewarmingDisabledUntilNextTransition || areRenderersPrewarming()) { return; @@ -2722,6 +2718,7 @@ private void maybePrewarmRenderers() throws ExoPlaybackException { return; } TrackSelectorResult trackSelectorResult = prewarmingPeriod.getTrackSelectorResult(); + final RendererHolder[] renderers = renderersCoordinator.rendererHolders; for (int i = 0; i < renderers.length; i++) { if (trackSelectorResult.isRendererEnabled(i) && renderers[i].hasSecondary() @@ -2757,6 +2754,7 @@ private void maybeUpdateReadingPeriod() throws ExoPlaybackException { // We don't have a successor to advance the reading period to or we want to let them end // intentionally to pause at the end of the period. if (readingPeriodHolder.info.isFinal || pendingPauseAtEndOfPeriod) { + final RendererHolder[] renderers = renderersCoordinator.rendererHolders; for (RendererHolder renderer : renderers) { if (!renderer.isReadingFromPeriod(readingPeriodHolder)) { continue; @@ -2811,15 +2809,17 @@ && getDurationToMediaPeriodUs(readingPeriodHolder.getNext()) /* forceSetTargetOffsetOverride= */ false); if (readingPeriodHolder.prepared - && ((hasSecondaryRenderers && prewarmingMediaPeriodDiscontinuity != C.TIME_UNSET) + && ((renderersCoordinator.hasSecondaryRenderers + && prewarmingMediaPeriodDiscontinuity != C.TIME_UNSET) || readingPeriodHolder.mediaPeriod.readDiscontinuity() != C.TIME_UNSET)) { prewarmingMediaPeriodDiscontinuity = C.TIME_UNSET; // The new period starts with a discontinuity, so unless a pre-warming renderer is handling // the discontinuity, the renderers will play out all data, then // be disabled and re-enabled when they start playing the next period. boolean arePrewarmingRenderersHandlingDiscontinuity = - hasSecondaryRenderers && !isPrewarmingDisabledUntilNextTransition; + renderersCoordinator.hasSecondaryRenderers && !isPrewarmingDisabledUntilNextTransition; if (arePrewarmingRenderersHandlingDiscontinuity) { + final RendererHolder[] renderers = renderersCoordinator.rendererHolders; for (int i = 0; i < renderers.length; i++) { if (!newTrackSelectorResult.isRendererEnabled(i) || renderers[i].getTrackType() == C.TRACK_TYPE_NONE) { @@ -2850,6 +2850,7 @@ && getDurationToMediaPeriodUs(readingPeriodHolder.getNext()) } } + final RendererHolder[] renderers = renderersCoordinator.rendererHolders; for (RendererHolder renderer : renderers) { renderer.maybeSetOldStreamToFinal( oldTrackSelectorResult, @@ -2876,6 +2877,7 @@ private boolean updateRenderersForTransition() throws ExoPlaybackException { MediaPeriodHolder readingMediaPeriod = queue.getReadingPeriod(); TrackSelectorResult newTrackSelectorResult = readingMediaPeriod.getTrackSelectorResult(); boolean allUpdated = true; + final RendererHolder[] renderers = renderersCoordinator.rendererHolders; for (int i = 0; i < renderers.length; i++) { int enabledRendererCountPreTransition = renderers[i].getEnabledRendererCount(); int result = @@ -2984,6 +2986,7 @@ private void maybeUpdatePlayingPeriod() throws ExoPlaybackException { } private void maybeHandlePrewarmingTransition() throws ExoPlaybackException { + final RendererHolder[] renderers = renderersCoordinator.rendererHolders; for (RendererHolder renderer : renderers) { renderer.maybeHandlePrewarmingTransition(); } @@ -3001,6 +3004,7 @@ private void maybeUpdateOffloadScheduling() { TrackSelectorResult trackSelectorResult = playingPeriodHolder.getTrackSelectorResult(); boolean isAudioRendererEnabledAndOffloadPreferred = false; boolean isAudioOnly = true; + final RendererHolder[] renderers = renderersCoordinator.rendererHolders; for (int i = 0; i < renderers.length; i++) { if (trackSelectorResult.isRendererEnabled(i)) { if (renderers[i].getTrackType() != C.TRACK_TYPE_AUDIO) { @@ -3019,6 +3023,7 @@ private void maybeUpdateOffloadScheduling() { private void allowRenderersToRenderStartOfStreams() { TrackSelectorResult playingTracks = queue.getPlayingPeriod().getTrackSelectorResult(); + final RendererHolder[] renderers = renderersCoordinator.rendererHolders; for (int i = 0; i < renderers.length; i++) { if (!playingTracks.isRendererEnabled(i)) { continue; @@ -3055,6 +3060,7 @@ private boolean hasReadingPeriodFinishedReading() { if (!readingPeriodHolder.prepared) { return false; } + final RendererHolder[] renderers = renderersCoordinator.rendererHolders; for (int i = 0; i < renderers.length; i++) { if (!renderers[i].hasFinishedReadingFromPeriod(readingPeriodHolder)) { return false; @@ -3064,6 +3070,7 @@ private boolean hasReadingPeriodFinishedReading() { } private void setAllNonPrewarmingRendererStreamsFinal(long streamEndPositionUs) { + final RendererHolder[] renderers = renderersCoordinator.rendererHolders; for (RendererHolder renderer : renderers) { renderer.setAllNonPrewarmingRendererStreamsFinal(streamEndPositionUs); } @@ -3071,7 +3078,7 @@ private void setAllNonPrewarmingRendererStreamsFinal(long streamEndPositionUs) { private void handlePeriodPrepared(MediaPeriod mediaPeriod) throws ExoPlaybackException { if (queue.isLoading(mediaPeriod)) { - handleLoadingPeriodPrepared(checkNotNull(queue.getLoadingPeriod())); + handleLoadingPeriodPrepared(mediaPeriod, checkNotNull(queue.getLoadingPeriod())); } else { @Nullable MediaPeriodHolder preloadHolder = queue.getPreloadHolderByMediaPeriod(mediaPeriod); if (preloadHolder != null) { @@ -3087,9 +3094,12 @@ private void handlePeriodPrepared(MediaPeriod mediaPeriod) throws ExoPlaybackExc } } - private void handleLoadingPeriodPrepared(MediaPeriodHolder loadingPeriodHolder) + private void handleLoadingPeriodPrepared( + MediaPeriod mediaPeriod, MediaPeriodHolder loadingPeriodHolder) throws ExoPlaybackException { if (!loadingPeriodHolder.prepared) { + // Reevaluate the numbers of renderers needed onwards + renderersCoordinator.reevaluate(mediaPeriod.getTrackGroups()); loadingPeriodHolder.handlePrepared( mediaClock.getPlaybackParameters().speed, playbackInfo.timeline, @@ -3149,6 +3159,7 @@ private void handlePlaybackParameters( playbackInfo = playbackInfo.copyWithPlaybackParameters(playbackParameters); } updateTrackSelectionPlaybackSpeed(playbackParameters.speed); + final RendererHolder[] renderers = renderersCoordinator.rendererHolders; for (RendererHolder rendererHolder : renderers) { rendererHolder.setPlaybackSpeed( currentPlaybackSpeed, /* targetPlaybackSpeed= */ playbackParameters.speed); @@ -3251,7 +3262,7 @@ private PlaybackInfo handlePositionDiscontinuity( : playingPeriodHolder.getTrackGroups(); trackSelectorResult = playingPeriodHolder == null - ? emptyTrackSelectorResult + ? renderersCoordinator.emptyTrackSelectorResult : playingPeriodHolder.getTrackSelectorResult(); staticMetadata = extractMetadataFromTrackSelectionArray(trackSelectorResult.selections); // Ensure the media period queue requested content position matches the new playback info. @@ -3264,7 +3275,7 @@ private PlaybackInfo handlePositionDiscontinuity( } else if (!mediaPeriodId.equals(playbackInfo.periodId)) { // Reset previously kept track info if unprepared and the period changes. trackGroupArray = TrackGroupArray.EMPTY; - trackSelectorResult = emptyTrackSelectorResult; + trackSelectorResult = renderersCoordinator.emptyTrackSelectorResult; staticMetadata = ImmutableList.of(); } if (reportDiscontinuity) { @@ -3300,6 +3311,7 @@ private ImmutableList extractMetadataFromTrackSelectionArray( } private void enableRenderers() throws ExoPlaybackException { + final RendererHolder[] renderers = renderersCoordinator.rendererHolders; enableRenderers( /* rendererWasEnabledFlags= */ new boolean[renderers.length], queue.getReadingPeriod().getStartPositionRendererTime()); @@ -3311,6 +3323,7 @@ private void enableRenderers(boolean[] rendererWasEnabledFlags, long startPositi TrackSelectorResult trackSelectorResult = readingMediaPeriod.getTrackSelectorResult(); // 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. + final RendererHolder[] renderers = renderersCoordinator.rendererHolders; for (int i = 0; i < renderers.length; i++) { if (!trackSelectorResult.isRendererEnabled(i)) { renderers[i].reset(); @@ -3334,6 +3347,7 @@ private void enableRenderer( boolean wasRendererEnabled, long startPositionUs) throws ExoPlaybackException { + final RendererHolder[] renderers = renderersCoordinator.rendererHolders; RendererHolder renderer = renderers[rendererIndex]; if (renderer.isRendererEnabled()) { return; @@ -3384,8 +3398,9 @@ public void onWakeup() { } private void releaseRenderers() { + final RendererHolder[] renderers = renderersCoordinator.rendererHolders; for (int i = 0; i < renderers.length; i++) { - rendererCapabilities[i].clearListener(); + renderersCoordinator.rendererCapabilities[i].clearListener(); renderers[i].release(); } } @@ -3474,7 +3489,7 @@ private boolean shouldPlayWhenReady() { private void maybeThrowRendererStreamError(int rendererIndex) throws IOException, ExoPlaybackException { - RendererHolder renderer = renderers[rendererIndex]; + RendererHolder renderer = renderersCoordinator.rendererHolders[rendererIndex]; try { renderer.maybeThrowStreamError(checkNotNull(queue.getPlayingPeriod())); } catch (IOException | RuntimeException e) { @@ -3513,9 +3528,10 @@ private void maybeThrowRendererStreamError(int rendererIndex) } private boolean areRenderersPrewarming() { - if (!hasSecondaryRenderers) { + if (!renderersCoordinator.hasSecondaryRenderers) { return false; } + final RendererHolder[] renderers = renderersCoordinator.rendererHolders; for (RendererHolder renderer : renderers) { if (renderer.isPrewarming()) { return true; diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/MediaPeriodHolder.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/MediaPeriodHolder.java index 1861469ec97..81dc9f82ac9 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/MediaPeriodHolder.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/MediaPeriodHolder.java @@ -38,7 +38,7 @@ import java.io.IOException; /** Holds a {@link MediaPeriod} with information required to play it as part of a timeline. */ -/* package */ final class MediaPeriodHolder { +/* package */ final class MediaPeriodHolder implements RenderersCoordinator.OnRenderersReevaluated { private static final String TAG = "MediaPeriodHolder"; @@ -51,7 +51,7 @@ /** * The sample streams for each renderer associated with this period. May contain null elements. */ - public final @NullableType SampleStream[] sampleStreams; + public @NullableType SampleStream[] sampleStreams; /** The target buffer duration to preload. */ public final long targetPreloadBufferDurationUs; @@ -80,8 +80,8 @@ */ public boolean allRenderersInCorrectState; - private final boolean[] mayRetainStreamFlags; - private final RendererCapabilities[] rendererCapabilities; + private boolean[] mayRetainStreamFlags; + private final RenderersCoordinator renderersCoordinator; private final TrackSelector trackSelector; private final MediaSourceList mediaSourceList; @@ -93,25 +93,22 @@ /** * Creates a new holder with information required to play it as part of a timeline. * - * @param rendererCapabilities The renderer capabilities. + * @param renderersCoordinator A coordinator that allows dynamic allocation of renderers. * @param rendererPositionOffsetUs The renderer time of the start of the period, in microseconds. * @param trackSelector The track selector. * @param allocator The allocator. * @param mediaSourceList The playlist. * @param info Information used to identify this media period in its timeline period. - * @param emptyTrackSelectorResult A {@link TrackSelectorResult} with empty selections for each - * renderer. */ public MediaPeriodHolder( - RendererCapabilities[] rendererCapabilities, + RenderersCoordinator renderersCoordinator, long rendererPositionOffsetUs, TrackSelector trackSelector, Allocator allocator, MediaSourceList mediaSourceList, MediaPeriodInfo info, - TrackSelectorResult emptyTrackSelectorResult, long targetPreloadBufferDurationUs) { - this.rendererCapabilities = rendererCapabilities; + this.renderersCoordinator = renderersCoordinator; this.rendererPositionOffsetUs = rendererPositionOffsetUs; this.trackSelector = trackSelector; this.mediaSourceList = mediaSourceList; @@ -119,7 +116,8 @@ public MediaPeriodHolder( this.info = info; this.targetPreloadBufferDurationUs = targetPreloadBufferDurationUs; this.trackGroups = TrackGroupArray.EMPTY; - this.trackSelectorResult = emptyTrackSelectorResult; + this.trackSelectorResult = renderersCoordinator.emptyTrackSelectorResult; + final RendererCapabilities[] rendererCapabilities = renderersCoordinator.rendererCapabilities; sampleStreams = new SampleStream[rendererCapabilities.length]; mayRetainStreamFlags = new boolean[rendererCapabilities.length]; mediaPeriod = @@ -130,6 +128,7 @@ public MediaPeriodHolder( info.startPositionUs, info.endPositionUs, info.isPrecededByTransitionFromSameStream); + renderersCoordinator.addListener(this); } /** @@ -267,6 +266,7 @@ public void continueLoading(LoadingInfo loadingInfo) { */ public TrackSelectorResult selectTracks( float playbackSpeed, Timeline timeline, boolean playWhenReady) throws ExoPlaybackException { + final RendererCapabilities[] rendererCapabilities = renderersCoordinator.rendererCapabilities; TrackSelectorResult selectorResult = trackSelector.selectTracks(rendererCapabilities, getTrackGroups(), info.id, timeline); for (int i = 0; i < selectorResult.length; i++) { @@ -303,7 +303,19 @@ public long applyTrackSelection( trackSelectorResult, positionUs, forceRecreateStreams, - new boolean[rendererCapabilities.length]); + new boolean[renderersCoordinator.rendererCapabilities.length]); + } + + @Override + public void onRenderersReevaluated() { + int size = renderersCoordinator.renderers.length; + SampleStream[] newSampleStreams = new SampleStream[size]; + boolean[] newMayRetainStreamFlags = new boolean[size]; + System.arraycopy(sampleStreams, 0, newSampleStreams, 0, sampleStreams.length); + System.arraycopy( + mayRetainStreamFlags, 0, newMayRetainStreamFlags, 0, mayRetainStreamFlags.length); + sampleStreams = newSampleStreams; + mayRetainStreamFlags = newMayRetainStreamFlags; } /** @@ -325,7 +337,9 @@ public long applyTrackSelection( boolean[] streamResetFlags) { for (int i = 0; i < newTrackSelectorResult.length; i++) { mayRetainStreamFlags[i] = - !forceRecreateStreams && newTrackSelectorResult.isEquivalent(trackSelectorResult, i); + !forceRecreateStreams + && i < trackSelectorResult.length + && newTrackSelectorResult.isEquivalent(trackSelectorResult, i); } // Undo the effect of previous call to associate no-sample renderers with empty tracks @@ -350,10 +364,10 @@ public long applyTrackSelection( if (sampleStreams[i] != null) { checkState(newTrackSelectorResult.isRendererEnabled(i)); // hasEnabledTracks should be true only when non-empty streams exists. - if (rendererCapabilities[i].getTrackType() != C.TRACK_TYPE_NONE) { + if (renderersCoordinator.rendererCapabilities[i].getTrackType() != C.TRACK_TYPE_NONE) { hasEnabledTracks = true; } - } else { + } else if (newTrackSelectorResult.selections.length > 0) { checkState(newTrackSelectorResult.selections[i] == null); } } @@ -362,6 +376,7 @@ public long applyTrackSelection( /** Releases the media period. No other method should be called after the release. */ public void release() { + renderersCoordinator.removeListener(this); disableTrackSelectionsInResult(); releaseMediaPeriod(mediaSourceList, mediaPeriod); } @@ -462,6 +477,7 @@ private void disableTrackSelectionsInResult() { */ private void disassociateNoSampleRenderersWithEmptySampleStream( @NullableType SampleStream[] sampleStreams) { + final RendererCapabilities[] rendererCapabilities = renderersCoordinator.rendererCapabilities; for (int i = 0; i < rendererCapabilities.length; i++) { if (rendererCapabilities[i].getTrackType() == C.TRACK_TYPE_NONE) { sampleStreams[i] = null; @@ -475,6 +491,7 @@ private void disassociateNoSampleRenderersWithEmptySampleStream( */ private void associateNoSampleRenderersWithEmptySampleStream( @NullableType SampleStream[] sampleStreams) { + final RendererCapabilities[] rendererCapabilities = renderersCoordinator.rendererCapabilities; for (int i = 0; i < rendererCapabilities.length; i++) { if (rendererCapabilities[i].getTrackType() == C.TRACK_TYPE_NONE && trackSelectorResult.isRendererEnabled(i)) { diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/RenderersCoordinator.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/RenderersCoordinator.java new file mode 100644 index 00000000000..8a311bd88b7 --- /dev/null +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/RenderersCoordinator.java @@ -0,0 +1,290 @@ +package androidx.media3.exoplayer; + +import static androidx.media3.common.util.Assertions.checkState; +import static androidx.media3.common.util.Util.castNonNull; + +import android.os.Handler; +import androidx.annotation.Nullable; +import androidx.media3.common.C; +import androidx.media3.common.Timeline; +import androidx.media3.common.Tracks; +import androidx.media3.common.util.Clock; +import androidx.media3.common.util.NullableType; +import androidx.media3.exoplayer.analytics.PlayerId; +import androidx.media3.exoplayer.audio.AudioRendererEventListener; +import androidx.media3.exoplayer.metadata.MetadataOutput; +import androidx.media3.exoplayer.source.MediaSource.MediaPeriodId; +import androidx.media3.exoplayer.source.TrackGroupArray; +import androidx.media3.exoplayer.text.TextOutput; +import androidx.media3.exoplayer.trackselection.ExoTrackSelection; +import androidx.media3.exoplayer.trackselection.TrackSelector; +import androidx.media3.exoplayer.trackselection.TrackSelectorResult; +import androidx.media3.exoplayer.video.VideoRendererEventListener; +import com.google.common.base.Supplier; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +/* package */ class RenderersCoordinator { + + @FunctionalInterface + /*package*/ interface WithLock { + void consume() throws ExoPlaybackException, IOException, RuntimeException; + } + + /*package*/ interface OnRenderersReevaluated { + void onRenderersReevaluated(); + } + + private final PlayerId playerId; + private final Handler eventHandler; + private final TrackSelector trackSelector; + private final Clock clock; + + private final Object lock = new Object(); + /*package*/ RendererHolder[] rendererHolders; + /*package*/ RendererCapabilities[] rendererCapabilities; + /*package*/ boolean[] rendererReportedReady; + /*package*/ Renderer[] renderers; + /*package*/ @NullableType Renderer[] secondaryRenderers; + private final Supplier renderersFactorySupplier; + private final VideoRendererEventListener videoRendererEventListener; + private final AudioRendererEventListener audioRendererEventListener; + private final TextOutput textRendererOutput; + private final MetadataOutput metadataRendererOutput; + boolean hasSecondaryRenderers; + + private final List listeners; + + /** + * This empty track selector result can only be used for {@link PlaybackInfo#trackSelectorResult} + * when the player does not have any track selection made (such as when player is reset, or when + * player seeks to an unprepared period). It will not be used as result of any {@link + * TrackSelector#selectTracks(RendererCapabilities[], TrackGroupArray, MediaPeriodId, Timeline)} + * operation. + */ + /* package */ TrackSelectorResult emptyTrackSelectorResult; + + public RenderersCoordinator( + PlayerId playerId, + Handler eventHandler, + TrackSelector trackSelector, + Clock clock, + Supplier renderersFactorySupplier, + VideoRendererEventListener videoRendererEventListener, + AudioRendererEventListener audioRendererEventListener, + TextOutput textRendererOutput, + MetadataOutput metadataRendererOutput) { + this.playerId = playerId; + this.eventHandler = eventHandler; + this.trackSelector = trackSelector; + this.clock = clock; + + RenderersFactory renderersFactory = renderersFactorySupplier.get(); + + renderers = + renderersFactory + .createRenderers( + eventHandler, + videoRendererEventListener, + audioRendererEventListener, + textRendererOutput, + metadataRendererOutput); + checkState(renderers.length > 0); + secondaryRenderers = new Renderer[renderers.length]; + for (int i = 0; i < secondaryRenderers.length; i++) { + // TODO(b/377671489): Fix DefaultAnalyticsCollector logic to still work with pre-warming. + secondaryRenderers[i] = + renderersFactory.createSecondaryRenderer( + renderers[i], + eventHandler, + videoRendererEventListener, + audioRendererEventListener, + textRendererOutput, + metadataRendererOutput); + } + + rendererCapabilities = new RendererCapabilities[renderers.length]; + rendererReportedReady = new boolean[renderers.length]; + @Nullable + RendererCapabilities.Listener rendererCapabilitiesListener = + trackSelector.getRendererCapabilitiesListener(); + + boolean hasSecondaryRenderers = false; + this.rendererHolders = 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); + } + if (secondaryRenderers[i] != null) { + castNonNull(secondaryRenderers[i]).init(/* index= */ i, playerId, clock); + hasSecondaryRenderers = true; + } + this.rendererHolders[i] = new RendererHolder( + renderers[i], secondaryRenderers[i], /* index= */ i); + } + this.hasSecondaryRenderers = hasSecondaryRenderers; + + this.renderersFactorySupplier = renderersFactorySupplier; + this.videoRendererEventListener = videoRendererEventListener; + this.audioRendererEventListener = audioRendererEventListener; + this.textRendererOutput = textRendererOutput; + this.metadataRendererOutput = metadataRendererOutput; + this.emptyTrackSelectorResult = newEmptyTrackSelectorResult(); + this.listeners = new ArrayList<>(); + } + + public void addListener(OnRenderersReevaluated listener) { + this.listeners.add(listener); + } + + public void removeListener(OnRenderersReevaluated listener) { + this.listeners.remove(listener); + } + + public void reevaluate(TrackGroupArray trackGroups) { + // Obtain the new list of renderers + Renderer[] newRenderers = renderersFactorySupplier.get() + .reevaluateRenderers( + renderers, + trackGroups, + eventHandler, + videoRendererEventListener, + audioRendererEventListener, + textRendererOutput, + metadataRendererOutput + ); + Renderer[] newSecondaryRenderers = new Renderer[newRenderers.length]; + RendererHolder[] newRendererHolders = new RendererHolder[newRenderers.length]; + + // WARNING!! Previous renderers cannot be destroyed and must be kept in the same order. + // Otherwise the new allocation cannot be accepted. + int curlen = renderers.length; + if (curlen > newRenderers.length) return; + for (int i = 0; i < curlen; i++) { + if (renderers[i] != newRenderers[i]) return; + } + + renderers = newRenderers; + rendererReportedReady = newRendererReportedReady(); + rendererCapabilities = newRendererCapabilitiesList(); + + for (int i = 0; i < curlen; i++) { + newSecondaryRenderers[i] = secondaryRenderers[i]; + newRendererHolders[i] = rendererHolders[i]; + } + + // Init new allocated renderers + for (int i = curlen; i < renderers.length; i++) { + renderers[i].init(/* index= */ i, playerId, clock); + rendererCapabilities[i] = renderers[i].getCapabilities(); + + @Nullable + RendererCapabilities.Listener listener = trackSelector.getRendererCapabilitiesListener(); + if (listener != null) { + rendererCapabilities[i].setListener(listener); + } + + initSecondaryRendererOnReevaluate(i); + + newRendererHolders[i] = new RendererHolder( + renderers[i], newSecondaryRenderers[i], /* index= */ i); + } + + secondaryRenderers = newSecondaryRenderers; + rendererHolders = newRendererHolders; + + emptyTrackSelectorResult = newEmptyTrackSelectorResult(); + + // Update period holders + for (OnRenderersReevaluated listener : listeners) { + listener.onRenderersReevaluated(); + } + } + + public int getRendererCount() { + synchronized (lock) { + return renderers.length; + } + } + + public @C.TrackType int getRendererType(int index) { + synchronized (lock) { + return renderers[index].getTrackType(); + } + } + + public Renderer getRenderer(int index) { + synchronized (lock) { + return renderers[index]; + } + } + + @Nullable + public Renderer getSecondaryRenderer(int index) { + synchronized (lock) { + return secondaryRenderers[index]; + } + } + + public TrackSelectorResult getEmptyTrackSelectorResult() { + synchronized (lock) { + return emptyTrackSelectorResult; + } + } + + /*package*/ void withLock(WithLock action) + throws ExoPlaybackException, IOException, RuntimeException { + synchronized (lock) { + action.consume(); + } + } + + private RendererCapabilities[] newRendererCapabilitiesList() { + RendererCapabilities.Listener rendererCapabilitiesListener = + trackSelector.getRendererCapabilitiesListener(); + RendererCapabilities[] rendererCapabilities = new RendererCapabilities[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); + } + } + return rendererCapabilities; + } + + private boolean[] newRendererReportedReady() { + boolean[] newRendererReportedReady = new boolean[renderers.length]; + System.arraycopy( + rendererReportedReady, 0, newRendererReportedReady, 0, rendererReportedReady.length); + return newRendererReportedReady; + } + + private TrackSelectorResult newEmptyTrackSelectorResult() { + return new TrackSelectorResult( + new RendererConfiguration[renderers.length], + new ExoTrackSelection[renderers.length], + Tracks.EMPTY, + /* info= */ null); + } + + private void initSecondaryRendererOnReevaluate(int i) { + // TODO(b/377671489): Fix DefaultAnalyticsCollector logic to still work with pre-warming. + secondaryRenderers[i] = + renderersFactorySupplier.get().createSecondaryRenderer( + renderers[i], + eventHandler, + videoRendererEventListener, + audioRendererEventListener, + textRendererOutput, + metadataRendererOutput); + + if (secondaryRenderers[i] != null) { + castNonNull(secondaryRenderers[i]).init(/* index= */ i + renderers.length, playerId, clock); + this.hasSecondaryRenderers = true; + } + } +} \ No newline at end of file diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/RenderersFactory.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/RenderersFactory.java index ecaec1141ee..39ffb9d9c67 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/RenderersFactory.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/RenderersFactory.java @@ -20,6 +20,7 @@ import androidx.media3.common.util.UnstableApi; import androidx.media3.exoplayer.audio.AudioRendererEventListener; import androidx.media3.exoplayer.metadata.MetadataOutput; +import androidx.media3.exoplayer.source.TrackGroupArray; import androidx.media3.exoplayer.text.TextOutput; import androidx.media3.exoplayer.video.VideoRendererEventListener; @@ -68,4 +69,30 @@ default Renderer createSecondaryRenderer( MetadataOutput metadataRendererOutput) { return null; } + + /** + * Allows to reevaluate the number of {@link Renderer} instances to adapt to new track groups. + *

+ * WARNING!! Previous renderers cannot be destroyed and must be kept in the same order. + * Otherwise the new allocation cannot be accepted. + * + * @param prevRenderers The previous available renderers. + * @param newTrackGroups The new track groups. + * @param eventHandler A handler to use when invoking event listeners and outputs. + * @param videoRendererEventListener An event listener for video renderers. + * @param audioRendererEventListener An event listener for audio renderers. + * @param textRendererOutput An output for text renderers. + * @param metadataRendererOutput An output for metadata renderers. + * @return The {@link Renderer instances}. + */ + default Renderer[] reevaluateRenderers( + Renderer[] prevRenderers, + TrackGroupArray newTrackGroups, + Handler eventHandler, + VideoRendererEventListener videoRendererEventListener, + AudioRendererEventListener audioRendererEventListener, + TextOutput textRendererOutput, + MetadataOutput metadataRendererOutput) { + return prevRenderers; + } } diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/trackselection/TrackSelectorResult.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/trackselection/TrackSelectorResult.java index c7185211436..d5e31f0482c 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/trackselection/TrackSelectorResult.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/trackselection/TrackSelectorResult.java @@ -96,6 +96,9 @@ public TrackSelectorResult( /** Returns whether the renderer at the specified index is enabled. */ public boolean isRendererEnabled(int index) { + if (index >= rendererConfigurations.length) { + return false; + } return rendererConfigurations[index] != null; } @@ -133,6 +136,12 @@ public boolean isEquivalent(@Nullable TrackSelectorResult other, int index) { if (other == null) { return false; } + if (index >= rendererConfigurations.length || index >= other.rendererConfigurations.length) { + return false; + } + if (index >= selections.length || index >= other.selections.length) { + return false; + } return Objects.equals(rendererConfigurations[index], other.rendererConfigurations[index]) && Objects.equals(selections[index], other.selections[index]); } diff --git a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/MediaPeriodQueueTest.java b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/MediaPeriodQueueTest.java index b57ca4d81b1..925e29544f8 100644 --- a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/MediaPeriodQueueTest.java +++ b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/MediaPeriodQueueTest.java @@ -32,6 +32,7 @@ import static org.mockito.Mockito.when; import static org.robolectric.Shadows.shadowOf; +import android.os.Handler; import android.os.Looper; import android.util.Pair; import androidx.annotation.Nullable; @@ -48,6 +49,7 @@ import androidx.media3.exoplayer.analytics.AnalyticsCollector; import androidx.media3.exoplayer.analytics.DefaultAnalyticsCollector; import androidx.media3.exoplayer.analytics.PlayerId; +import androidx.media3.exoplayer.audio.AudioRendererEventListener; import androidx.media3.exoplayer.source.MediaPeriod; import androidx.media3.exoplayer.source.MediaSource.MediaPeriodId; import androidx.media3.exoplayer.source.MediaSource.MediaSourceCaller; @@ -56,10 +58,12 @@ import androidx.media3.exoplayer.source.TrackGroupArray; import androidx.media3.exoplayer.source.ads.ServerSideAdInsertionMediaSource; import androidx.media3.exoplayer.source.ads.SinglePeriodAdTimeline; +import androidx.media3.exoplayer.trackselection.DefaultTrackSelector; import androidx.media3.exoplayer.trackselection.ExoTrackSelection; import androidx.media3.exoplayer.trackselection.TrackSelector; import androidx.media3.exoplayer.trackselection.TrackSelectorResult; import androidx.media3.exoplayer.upstream.Allocator; +import androidx.media3.exoplayer.video.VideoRendererEventListener; import androidx.media3.test.utils.FakeMediaSource; import androidx.media3.test.utils.FakeMultiPeriodLiveTimeline; import androidx.media3.test.utils.FakeShuffleOrder; @@ -128,17 +132,12 @@ public void setUp() { mediaPeriodHolderFactoryInfos.add(info); mediaPeriodHolderFactoryRendererPositionOffsets.add(rendererPositionOffsetUs); return new MediaPeriodHolder( - rendererCapabilities, + createRenderersCoordinator(), rendererPositionOffsetUs, trackSelector, allocator, mediaSourceList, info, - new TrackSelectorResult( - new RendererConfiguration[0], - new ExoTrackSelection[0], - Tracks.EMPTY, - /* info= */ null), /* targetPreloadBufferDurationUs= */ 5_000_000L); }, PreloadConfiguration.DEFAULT); @@ -154,6 +153,20 @@ public void setUp() { fakeMediaSources = new ArrayList<>(); } + private RenderersCoordinator createRenderersCoordinator() { + return new RenderersCoordinator( + PlayerId.UNSET, + new Handler(Looper.getMainLooper(), /* callback= */ null), + new DefaultTrackSelector(ApplicationProvider.getApplicationContext()), + Clock.DEFAULT, + () -> new DefaultRenderersFactory(ApplicationProvider.getApplicationContext()), + new VideoRendererEventListener() {}, + new AudioRendererEventListener() {}, + cueGroup -> {}, + metadata -> {} + ); + } + @Test public void getNextMediaPeriodInfo_withoutAds_returnsLastMediaPeriodInfo() { setupAdTimeline(/* no ad groups */ );