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 */ );