diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 32ab94035b..6e81af2448 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -41,6 +41,10 @@ with which the `DefaultAudioSink` will register as the listener to the `AudioCapabilitiesReceiver` and update its `audioCapabilities` property when informed with a capabilities change. + * Propagate audio capabilities changes via a new event + `onAudioCapabilitiesChanged` in `AudioSink.Listener` interface, and a + new interface `RendererCapabilities.Listener` which triggers + `onRendererCapabilitiesChanged` events. * DRM: * Reduce the visibility of several internal-only methods on `DefaultDrmSession` that aren't expected to be called from outside the diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/BaseRenderer.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/BaseRenderer.java index 50effc32de..40c717378b 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/BaseRenderer.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/BaseRenderer.java @@ -18,6 +18,7 @@ package androidx.media3.exoplayer; import static androidx.media3.common.util.Assertions.checkNotNull; import static java.lang.Math.max; +import androidx.annotation.GuardedBy; import androidx.annotation.Nullable; import androidx.media3.common.C; import androidx.media3.common.Format; @@ -37,6 +38,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; @UnstableApi public abstract class BaseRenderer implements Renderer, RendererCapabilities { + private final Object lock; private final @C.TrackType int trackType; private final FormatHolder formatHolder; @@ -52,11 +54,16 @@ public abstract class BaseRenderer implements Renderer, RendererCapabilities { private boolean streamIsFinal; private boolean throwRendererExceptionIsExecuting; + @GuardedBy("lock") + @Nullable + protected RendererCapabilities.Listener rendererCapabilitiesListener; + /** * @param trackType The track type that the renderer handles. One of the {@link C} {@code * TRACK_TYPE_*} constants. */ public BaseRenderer(@C.TrackType int trackType) { + lock = new Object(); this.trackType = trackType; formatHolder = new FormatHolder(); readingPositionUs = C.TIME_END_OF_SOURCE; @@ -210,6 +217,27 @@ public abstract class BaseRenderer implements Renderer, RendererCapabilities { return ADAPTIVE_NOT_SUPPORTED; } + @Override + public final void setListener(RendererCapabilities.Listener listener) { + synchronized (lock) { + this.rendererCapabilitiesListener = listener; + } + } + + @Override + public final void clearListener() { + synchronized (lock) { + this.rendererCapabilitiesListener = null; + } + } + + @Nullable + private Listener getListener() { + synchronized (lock) { + return this.rendererCapabilitiesListener; + } + } + // PlayerMessage.Target implementation. @Override @@ -486,4 +514,12 @@ public abstract class BaseRenderer implements Renderer, RendererCapabilities { protected final boolean isSourceReady() { return hasReadStreamToEnd() ? streamIsFinal : Assertions.checkNotNull(stream).isReady(); } + + /** Called when the renderer capabilities are changed. */ + protected final void onRendererCapabilitiesChanged() { + @Nullable RendererCapabilities.Listener listener = getListener(); + if (listener != null) { + listener.onRendererCapabilitiesChanged(this); + } + } } 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 83915dc840..1cfa125c10 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayerImplInternal.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayerImplInternal.java @@ -268,9 +268,15 @@ import java.util.concurrent.atomic.AtomicBoolean; playbackInfo = PlaybackInfo.createDummy(emptyTrackSelectorResult); playbackInfoUpdate = new PlaybackInfoUpdate(playbackInfo); rendererCapabilities = new RendererCapabilities[renderers.length]; + @Nullable + RendererCapabilities.Listener rendererCapabilitiesListener = + trackSelector.getRendererCapabilitiesListener(); for (int i = 0; i < renderers.length; i++) { renderers[i].init(/* index= */ i, playerId); rendererCapabilities[i] = renderers[i].getCapabilities(); + if (rendererCapabilitiesListener != null) { + rendererCapabilities[i].setListener(rendererCapabilitiesListener); + } } mediaClock = new DefaultMediaClock(this, clock); pendingMessages = new ArrayList<>(); @@ -2558,8 +2564,9 @@ import java.util.concurrent.atomic.AtomicBoolean; } private void releaseRenderers() { - for (Renderer renderer : renderers) { - renderer.release(); + for (int i = 0; i < renderers.length; i++) { + rendererCapabilities[i].clearListener(); + renderers[i].release(); } } diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/RendererCapabilities.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/RendererCapabilities.java index dbc2fa059e..95e2250ecd 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/RendererCapabilities.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/RendererCapabilities.java @@ -31,6 +31,19 @@ import java.lang.annotation.Target; @UnstableApi public interface RendererCapabilities { + /** Listener for renderer capabilities events. */ + interface Listener { + + /** + * Called when the renderer capabilities are changed. + * + *

This method will be called on the playback thread. + * + * @param renderer The renderer that has its capabilities changed. + */ + void onRendererCapabilitiesChanged(Renderer renderer); + } + /** * @deprecated Use {@link C.FormatSupport} instead. */ @@ -355,4 +368,18 @@ public interface RendererCapabilities { */ @AdaptiveSupport int supportsMixedMimeTypeAdaptation() throws ExoPlaybackException; + + /** + * Sets the {@link Listener}. + * + * @param listener The listener to be set. + */ + default void setListener(Listener listener) { + // Do nothing. + } + + /** Clears the {@link Listener}. */ + default void clearListener() { + // Do nothing. + } } diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/AudioSink.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/AudioSink.java index f73f9635cc..3e781b1c07 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/AudioSink.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/AudioSink.java @@ -134,6 +134,9 @@ public interface AudioSink { * a {@link WriteException}, or an {@link UnexpectedDiscontinuityException}. */ default void onAudioSinkError(Exception audioSinkError) {} + + /** Called when audio capabilities changed. */ + default void onAudioCapabilitiesChanged() {} } /** Thrown when a failure occurs configuring the sink. */ diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/DefaultAudioSink.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/DefaultAudioSink.java index 859b98f215..a1907c6ad7 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/DefaultAudioSink.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/DefaultAudioSink.java @@ -1469,6 +1469,9 @@ public final class DefaultAudioSink implements AudioSink { checkState(playbackLooper == Looper.myLooper()); if (!audioCapabilities.equals(getAudioCapabilities())) { this.audioCapabilities = audioCapabilities; + if (listener != null) { + listener.onAudioCapabilitiesChanged(); + } } } diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/MediaCodecAudioRenderer.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/MediaCodecAudioRenderer.java index eb74f06c42..82e5276683 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/MediaCodecAudioRenderer.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/MediaCodecAudioRenderer.java @@ -960,6 +960,11 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media Log.e(TAG, "Audio sink error", audioSinkError); eventDispatcher.audioSinkError(audioSinkError); } + + @Override + public void onAudioCapabilitiesChanged() { + MediaCodecAudioRenderer.this.onRendererCapabilitiesChanged(); + } } @RequiresApi(23) diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/trackselection/TrackSelector.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/trackselection/TrackSelector.java index f6ca0f3eee..8ff8888cf2 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/trackselection/TrackSelector.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/trackselection/TrackSelector.java @@ -187,6 +187,16 @@ public abstract class TrackSelector { // Default implementation is no-op. } + /** + * Returns the {@link RendererCapabilities.Listener} that the concrete instance uses to listen to + * the renderer capabilities changes. May be {@code null} if the implementation does not listen to + * the renderer capabilities changes. + */ + @Nullable + public RendererCapabilities.Listener getRendererCapabilitiesListener() { + return null; + } + /** * Calls {@link InvalidationListener#onTrackSelectionsInvalidated()} to invalidate all previously * generated track selections. diff --git a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/audio/MediaCodecAudioRendererTest.java b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/audio/MediaCodecAudioRendererTest.java index f7cd34f10c..2ba87a3522 100644 --- a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/audio/MediaCodecAudioRendererTest.java +++ b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/audio/MediaCodecAudioRendererTest.java @@ -83,6 +83,7 @@ public class MediaCodecAudioRendererTest { @Mock private AudioSink audioSink; @Mock private AudioRendererEventListener audioRendererEventListener; + @Mock private RendererCapabilities.Listener rendererCapabilitiesListener; @Before public void setUp() throws Exception { @@ -326,6 +327,21 @@ public class MediaCodecAudioRendererTest { verify(audioRendererEventListener).onAudioSinkError(error); } + @Test + public void + renderer_callsRendererCapabilitiesListener_whenAudioSinkListenerOnAudioCapabilitiesChangedIsCalled() { + final ArgumentCaptor listenerCaptor = + ArgumentCaptor.forClass(AudioSink.Listener.class); + verify(audioSink, atLeastOnce()).setListener(listenerCaptor.capture()); + AudioSink.Listener audioSinkListener = listenerCaptor.getValue(); + mediaCodecAudioRenderer.getCapabilities().setListener(rendererCapabilitiesListener); + + audioSinkListener.onAudioCapabilitiesChanged(); + + shadowOf(Looper.getMainLooper()).idle(); + verify(rendererCapabilitiesListener).onRendererCapabilitiesChanged(mediaCodecAudioRenderer); + } + @Test public void render_callsAudioSinkSetOutputStreamOffset_whenReplaceStream() throws Exception { FakeSampleStream fakeSampleStream1 =