From 480ff93f85a22db6ca3bd64e095d2ab4015988e7 Mon Sep 17 00:00:00 2001 From: tianyifeng Date: Wed, 3 May 2023 14:00:32 +0100 Subject: [PATCH] Reselect track when renderer capabilities change * Implement RendererCapabilities.Listener in DefaultTrackSelector. * Add new methods TrackSelector.invalidateForRendererCapabilitiesChange and TrackSelector.InvalidateListener.onRendererCapabilitiesChanged. * Add new field allowInvalidateSelectionsOnRendererCapabilitiesChange to DefaultTrackSelector.Parameter to allow opt-in of the renderer capabilities detection feature. * Add logics of triggering track reselection when renderer capabilities change. PiperOrigin-RevId: 529067433 --- RELEASENOTES.md | 9 +++ .../exoplayer/DecoderReuseEvaluation.java | 5 +- .../exoplayer/ExoPlayerImplInternal.java | 9 +++ .../audio/MediaCodecAudioRenderer.java | 5 ++ .../mediacodec/MediaCodecRenderer.java | 15 ++++- .../trackselection/DefaultTrackSelector.java | 65 ++++++++++++++++++- .../trackselection/TrackSelector.java | 21 ++++++ .../DefaultTrackSelectorTest.java | 36 ++++++++++ 8 files changed, 162 insertions(+), 3 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 23d34af5d2..752b15952d 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -66,6 +66,12 @@ * Add Util methods `shouldShowPlayButton` and `handlePlayPauseButtonAction` to write custom UI elements with a play/pause button. +* Track selection: + * Add + `DefaultTrackSelector.Parameters.allowInvalidateSelectionsForRendererCapabilitiesChange` + which is disabled by default. When enabled, the `DefaultTrackSelector` + will trigger a new track selection when the renderer capabilities + changed. * Audio: * Fix bug where some playbacks fail when tunneling is enabled and `AudioProcessors` are active, e.g. for gapless trimming @@ -85,6 +91,9 @@ `onRendererCapabilitiesChanged` events. * Add `ChannelMixingAudioProcessor` for applying scaling/mixing to audio channels. + * Add new int value `DISCARD_REASON_AUDIO_BYPASS_POSSIBLE` to + `DecoderDiscardReasons` to discard audio decoder when bypass mode is + possible after audio capabilities change. * Metadata: * Deprecate `MediaMetadata.folderType` in favor of `isBrowsable` and `mediaType`. diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/DecoderReuseEvaluation.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/DecoderReuseEvaluation.java index 142555e985..e56b172b50 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/DecoderReuseEvaluation.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/DecoderReuseEvaluation.java @@ -80,7 +80,8 @@ public final class DecoderReuseEvaluation { DISCARD_REASON_VIDEO_COLOR_INFO_CHANGED, DISCARD_REASON_AUDIO_CHANNEL_COUNT_CHANGED, DISCARD_REASON_AUDIO_SAMPLE_RATE_CHANGED, - DISCARD_REASON_AUDIO_ENCODING_CHANGED + DISCARD_REASON_AUDIO_ENCODING_CHANGED, + DISCARD_REASON_AUDIO_BYPASS_POSSIBLE }) public @interface DecoderDiscardReasons {} @@ -114,6 +115,8 @@ public final class DecoderReuseEvaluation { public static final int DISCARD_REASON_AUDIO_SAMPLE_RATE_CHANGED = 1 << 13; /** The audio encoding is changing. */ public static final int DISCARD_REASON_AUDIO_ENCODING_CHANGED = 1 << 14; + /** The audio bypass mode is possible. */ + public static final int DISCARD_REASON_AUDIO_BYPASS_POSSIBLE = 1 << 15; /** The name of the decoder. */ public final String decoderName; 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 1cfa125c10..30d8206dfb 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayerImplInternal.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayerImplInternal.java @@ -165,6 +165,7 @@ import java.util.concurrent.atomic.AtomicBoolean; private static final int MSG_SET_PAUSE_AT_END_OF_WINDOW = 23; private static final int MSG_SET_OFFLOAD_SCHEDULING_ENABLED = 24; private static final int MSG_ATTEMPT_RENDERER_ERROR_RECOVERY = 25; + private static final int MSG_RENDERER_CAPABILITIES_CHANGED = 26; private static final int ACTIVE_INTERVAL_MS = 10; private static final int IDLE_INTERVAL_MS = 1000; @@ -481,6 +482,11 @@ import java.util.concurrent.atomic.AtomicBoolean; handler.sendEmptyMessage(MSG_TRACK_SELECTION_INVALIDATED); } + @Override + public void onRendererCapabilitiesChanged(Renderer renderer) { + handler.sendEmptyMessage(MSG_RENDERER_CAPABILITIES_CHANGED); + } + // DefaultMediaClock.PlaybackParametersListener implementation. @Override @@ -576,6 +582,9 @@ import java.util.concurrent.atomic.AtomicBoolean; case MSG_ATTEMPT_RENDERER_ERROR_RECOVERY: attemptRendererErrorRecovery(); break; + case MSG_RENDERER_CAPABILITIES_CHANGED: + reselectTracksInternalAndSeek(); + break; case MSG_RELEASE: releaseInternal(); // Return immediately to not send playback info updates after release. 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 461365629f..4eff06394d 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 @@ -444,6 +444,11 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media DecoderReuseEvaluation evaluation = codecInfo.canReuseCodec(oldFormat, newFormat); @DecoderDiscardReasons int discardReasons = evaluation.discardReasons; + if (isBypassPossible(newFormat)) { + // We prefer direct audio playback so that for multi-channel tracks the audio is not downmixed + // to stereo. + discardReasons |= DecoderReuseEvaluation.DISCARD_REASON_AUDIO_BYPASS_POSSIBLE; + } if (getCodecMaxInputSize(codecInfo, newFormat) > codecMaxInputSize) { discardReasons |= DISCARD_REASON_MAX_INPUT_SIZE_EXCEEDED; } diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/mediacodec/MediaCodecRenderer.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/mediacodec/MediaCodecRenderer.java index 6e79139171..c1ea60c3e9 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/mediacodec/MediaCodecRenderer.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/mediacodec/MediaCodecRenderer.java @@ -491,7 +491,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer { return; } - if (sourceDrmSession == null && shouldUseBypass(inputFormat)) { + if (isBypassPossible(inputFormat)) { initBypass(inputFormat); return; } @@ -546,6 +546,19 @@ public abstract class MediaCodecRenderer extends BaseRenderer { } } + /** + * Returns whether buffers in the input format can be processed without a codec. + * + *

This method returns the possibility of bypass mode with checking both the renderer + * capabilities and DRM protection. + * + * @param format The input {@link Format}. + * @return Whether playback bypassing {@link MediaCodec} is possible. + */ + protected final boolean isBypassPossible(Format format) { + return sourceDrmSession == null && shouldUseBypass(inputFormat); + } + /** * Returns whether buffers in the input format can be processed without a codec. * diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/trackselection/DefaultTrackSelector.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/trackselection/DefaultTrackSelector.java index fcb61e3e8c..0e8738b14b 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/trackselection/DefaultTrackSelector.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/trackselection/DefaultTrackSelector.java @@ -111,7 +111,8 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; * } */ @UnstableApi -public class DefaultTrackSelector extends MappingTrackSelector { +public class DefaultTrackSelector extends MappingTrackSelector + implements RendererCapabilities.Listener { private static final String TAG = "DefaultTrackSelector"; private static final String AUDIO_CHANNEL_COUNT_CONSTRAINTS_WARN_MESSAGE = @@ -758,6 +759,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { private boolean exceedRendererCapabilitiesIfNecessary; private boolean tunnelingEnabled; private boolean allowMultipleAdaptiveSelections; + private boolean allowInvalidateSelectionsOnRendererCapabilitiesChange; // Overrides private final SparseArray> selectionOverrides; @@ -814,6 +816,8 @@ public class DefaultTrackSelector extends MappingTrackSelector { exceedRendererCapabilitiesIfNecessary = initialValues.exceedRendererCapabilitiesIfNecessary; tunnelingEnabled = initialValues.tunnelingEnabled; allowMultipleAdaptiveSelections = initialValues.allowMultipleAdaptiveSelections; + allowInvalidateSelectionsOnRendererCapabilitiesChange = + initialValues.allowInvalidateSelectionsOnRendererCapabilitiesChange; // Overrides selectionOverrides = cloneSelectionOverrides(initialValues.selectionOverrides); rendererDisabledFlags = initialValues.rendererDisabledFlags.clone(); @@ -877,6 +881,10 @@ public class DefaultTrackSelector extends MappingTrackSelector { bundle.getBoolean( Parameters.FIELD_ALLOW_MULTIPLE_ADAPTIVE_SELECTIONS, defaultValue.allowMultipleAdaptiveSelections)); + setAllowInvalidateSelectionsOnRendererCapabilitiesChange( + bundle.getBoolean( + Parameters.FIELD_ALLOW_INVALIDATE_SELECTIONS_ON_RENDERER_CAPABILITIES_CHANGE, + defaultValue.allowInvalidateSelectionsOnRendererCapabilitiesChange)); // Overrides selectionOverrides = new SparseArray<>(); setSelectionOverridesFromBundle(bundle); @@ -1290,6 +1298,21 @@ public class DefaultTrackSelector extends MappingTrackSelector { return this; } + /** + * Sets whether to allow to invalidate selections on renderer capabilities change. + * + * @param allowInvalidateSelectionsOnRendererCapabilitiesChange Whether to allow to invalidate + * selections. + * @return This builder. + */ + @CanIgnoreReturnValue + public Builder setAllowInvalidateSelectionsOnRendererCapabilitiesChange( + boolean allowInvalidateSelectionsOnRendererCapabilitiesChange) { + this.allowInvalidateSelectionsOnRendererCapabilitiesChange = + allowInvalidateSelectionsOnRendererCapabilitiesChange; + return this; + } + @CanIgnoreReturnValue @Override public Builder addOverride(TrackSelectionOverride override) { @@ -1547,6 +1570,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { exceedRendererCapabilitiesIfNecessary = true; tunnelingEnabled = false; allowMultipleAdaptiveSelections = true; + allowInvalidateSelectionsOnRendererCapabilitiesChange = false; } private static SparseArray> @@ -1719,6 +1743,12 @@ public class DefaultTrackSelector extends MappingTrackSelector { */ public final boolean allowMultipleAdaptiveSelections; + /** + * Whether to allow to invalidate selections on renderer capabilities change. The default value + * is {@code false}. + */ + public final boolean allowInvalidateSelectionsOnRendererCapabilitiesChange; + // Overrides private final SparseArray> selectionOverrides; @@ -1743,6 +1773,8 @@ public class DefaultTrackSelector extends MappingTrackSelector { exceedRendererCapabilitiesIfNecessary = builder.exceedRendererCapabilitiesIfNecessary; tunnelingEnabled = builder.tunnelingEnabled; allowMultipleAdaptiveSelections = builder.allowMultipleAdaptiveSelections; + allowInvalidateSelectionsOnRendererCapabilitiesChange = + builder.allowInvalidateSelectionsOnRendererCapabilitiesChange; // Overrides selectionOverrides = builder.selectionOverrides; rendererDisabledFlags = builder.rendererDisabledFlags; @@ -1833,6 +1865,8 @@ public class DefaultTrackSelector extends MappingTrackSelector { && exceedRendererCapabilitiesIfNecessary == other.exceedRendererCapabilitiesIfNecessary && tunnelingEnabled == other.tunnelingEnabled && allowMultipleAdaptiveSelections == other.allowMultipleAdaptiveSelections + && allowInvalidateSelectionsOnRendererCapabilitiesChange + == other.allowInvalidateSelectionsOnRendererCapabilitiesChange // Overrides && areRendererDisabledFlagsEqual(rendererDisabledFlags, other.rendererDisabledFlags) && areSelectionOverridesEqual(selectionOverrides, other.selectionOverrides); @@ -1858,6 +1892,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { result = 31 * result + (exceedRendererCapabilitiesIfNecessary ? 1 : 0); result = 31 * result + (tunnelingEnabled ? 1 : 0); result = 31 * result + (allowMultipleAdaptiveSelections ? 1 : 0); + result = 31 * result + (allowInvalidateSelectionsOnRendererCapabilitiesChange ? 1 : 0); // Overrides (omitted from hashCode). return result; } @@ -1898,6 +1933,8 @@ public class DefaultTrackSelector extends MappingTrackSelector { Util.intToStringMaxRadix(FIELD_CUSTOM_ID_BASE + 15); private static final String FIELD_CONSTRAIN_AUDIO_CHANNEL_COUNT_TO_DEVICE_CAPABILITIES = Util.intToStringMaxRadix(FIELD_CUSTOM_ID_BASE + 16); + private static final String FIELD_ALLOW_INVALIDATE_SELECTIONS_ON_RENDERER_CAPABILITIES_CHANGE = + Util.intToStringMaxRadix(FIELD_CUSTOM_ID_BASE + 17); @Override public Bundle toBundle() { @@ -1934,6 +1971,9 @@ public class DefaultTrackSelector extends MappingTrackSelector { FIELD_EXCEED_RENDERER_CAPABILITIES_IF_NECESSARY, exceedRendererCapabilitiesIfNecessary); bundle.putBoolean(FIELD_TUNNELING_ENABLED, tunnelingEnabled); bundle.putBoolean(FIELD_ALLOW_MULTIPLE_ADAPTIVE_SELECTIONS, allowMultipleAdaptiveSelections); + bundle.putBoolean( + FIELD_ALLOW_INVALIDATE_SELECTIONS_ON_RENDERER_CAPABILITIES_CHANGE, + allowInvalidateSelectionsOnRendererCapabilitiesChange); putSelectionOverridesToBundle(bundle, selectionOverrides); // Only true values are put into rendererDisabledFlags. @@ -2360,6 +2400,19 @@ public class DefaultTrackSelector extends MappingTrackSelector { } } + @Override + @Nullable + public RendererCapabilities.Listener getRendererCapabilitiesListener() { + return this; + } + + // RendererCapabilities.Listener implementation. + + @Override + public void onRendererCapabilitiesChanged(Renderer renderer) { + maybeInvalidateForRendererCapabilitiesChange(renderer); + } + // MappingTrackSelector implementation. @Override @@ -2772,6 +2825,16 @@ public class DefaultTrackSelector extends MappingTrackSelector { } } + private void maybeInvalidateForRendererCapabilitiesChange(Renderer renderer) { + boolean shouldInvalidate; + synchronized (lock) { + shouldInvalidate = parameters.allowInvalidateSelectionsOnRendererCapabilitiesChange; + } + if (shouldInvalidate) { + invalidateForRendererCapabilitiesChange(renderer); + } + } + // Utility methods. private static void applyTrackSelectionOverrides( 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 8ff8888cf2..47e72ffaf1 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 @@ -102,6 +102,15 @@ public abstract class TrackSelector { * longer valid. May be called from any thread. */ void onTrackSelectionsInvalidated(); + + /** + * Called by a {@link TrackSelector} to indicate that selections it has previously made may no + * longer be valid due to the renderer capabilities change. This method is called from playback + * thread. + * + * @param renderer The renderer whose capabilities changed. + */ + default void onRendererCapabilitiesChanged(Renderer renderer) {} } @Nullable private InvalidationListener listener; @@ -207,6 +216,18 @@ public abstract class TrackSelector { } } + /** + * Calls {@link InvalidationListener#onRendererCapabilitiesChanged(Renderer)} to invalidate all + * previously generated track selections because a renderer's capabilities have changed. + * + * @param renderer The renderer whose capabilities changed. + */ + protected final void invalidateForRendererCapabilitiesChange(Renderer renderer) { + if (listener != null) { + listener.onRendererCapabilitiesChanged(renderer); + } + } + /** * Returns a bandwidth meter which can be used by track selections to select tracks. Must only be * called when the track selector is {@linkplain #init(InvalidationListener, BandwidthMeter) diff --git a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/trackselection/DefaultTrackSelectorTest.java b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/trackselection/DefaultTrackSelectorTest.java index b791d44ae0..17154f7982 100644 --- a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/trackselection/DefaultTrackSelectorTest.java +++ b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/trackselection/DefaultTrackSelectorTest.java @@ -28,6 +28,7 @@ import static androidx.media3.exoplayer.RendererCapabilities.HARDWARE_ACCELERATI import static androidx.media3.exoplayer.RendererCapabilities.TUNNELING_NOT_SUPPORTED; import static androidx.media3.exoplayer.RendererConfiguration.DEFAULT; import static com.google.common.truth.Truth.assertThat; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; @@ -44,17 +45,21 @@ import androidx.media3.common.TrackGroup; import androidx.media3.common.TrackSelectionOverride; import androidx.media3.common.TrackSelectionParameters; import androidx.media3.common.Tracks; +import androidx.media3.common.util.HandlerWrapper; import androidx.media3.common.util.Util; import androidx.media3.exoplayer.ExoPlaybackException; +import androidx.media3.exoplayer.Renderer; import androidx.media3.exoplayer.RendererCapabilities; import androidx.media3.exoplayer.RendererCapabilities.Capabilities; import androidx.media3.exoplayer.RendererConfiguration; +import androidx.media3.exoplayer.audio.AudioRendererEventListener; import androidx.media3.exoplayer.source.MediaSource.MediaPeriodId; import androidx.media3.exoplayer.source.TrackGroupArray; import androidx.media3.exoplayer.trackselection.DefaultTrackSelector.Parameters; import androidx.media3.exoplayer.trackselection.DefaultTrackSelector.SelectionOverride; import androidx.media3.exoplayer.trackselection.TrackSelector.InvalidationListener; import androidx.media3.exoplayer.upstream.BandwidthMeter; +import androidx.media3.test.utils.FakeAudioRenderer; import androidx.media3.test.utils.FakeTimeline; import androidx.media3.test.utils.TestUtil; import androidx.test.core.app.ApplicationProvider; @@ -2454,6 +2459,36 @@ public final class DefaultTrackSelectorTest { } } + @Test + public void onRendererCapabilitiesChangedWithDefaultParameters_notNotifyInvalidateListener() { + Renderer renderer = + new FakeAudioRenderer( + /* handler= */ mock(HandlerWrapper.class), + /* eventListener= */ mock(AudioRendererEventListener.class)); + + trackSelector.onRendererCapabilitiesChanged(renderer); + + verify(invalidationListener, never()).onRendererCapabilitiesChanged(renderer); + } + + @Test + public void + onRendererCapabilitiesChangedWithInvalidateSelectionsForRendererCapabilitiesChangeEnabled_notifyInvalidateListener() { + trackSelector.setParameters( + defaultParameters + .buildUpon() + .setAllowInvalidateSelectionsOnRendererCapabilitiesChange(true) + .build()); + Renderer renderer = + new FakeAudioRenderer( + /* handler= */ mock(HandlerWrapper.class), + /* eventListener= */ mock(AudioRendererEventListener.class)); + + trackSelector.onRendererCapabilitiesChanged(renderer); + + verify(invalidationListener).onRendererCapabilitiesChanged(renderer); + } + private static void assertSelections(TrackSelectorResult result, TrackSelection[] expected) { assertThat(result.length).isEqualTo(expected.length); for (int i = 0; i < expected.length; i++) { @@ -2574,6 +2609,7 @@ public final class DefaultTrackSelectorTest { .setExceedRendererCapabilitiesIfNecessary(false) .setTunnelingEnabled(true) .setAllowMultipleAdaptiveSelections(true) + .setAllowInvalidateSelectionsOnRendererCapabilitiesChange(false) .setSelectionOverride( /* rendererIndex= */ 2, new TrackGroupArray(VIDEO_TRACK_GROUP),