diff --git a/RELEASENOTES.md b/RELEASENOTES.md index a354df1ebd..b26b93173e 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -28,6 +28,11 @@ `TrackSelectionParameters`. If the set preference is to enable, the device supports offload for the format, and the track selection is a single audio track, then audio offload will be enabled. + * If `audioOffloadModePreference` is set to + `AUDIO_OFFLOAD_MODE_PREFERENCE_REQUIRED`, then the + `DefaultTrackSelector` will only select an audio track and only if that + track's format is supported in offload. If no audio track is supported + in offload, then no track will be selected. * Remove parameter `enableOffload` from `DefaultRenderersFactory.buildAudioSink` method signature. * Remove method `DefaultAudioSink.Builder.setOffloadMode`. diff --git a/libraries/common/src/main/java/androidx/media3/common/TrackSelectionParameters.java b/libraries/common/src/main/java/androidx/media3/common/TrackSelectionParameters.java index e96ee4084b..432335d0b6 100644 --- a/libraries/common/src/main/java/androidx/media3/common/TrackSelectionParameters.java +++ b/libraries/common/src/main/java/androidx/media3/common/TrackSelectionParameters.java @@ -72,19 +72,27 @@ import org.checkerframework.checker.nullness.qual.EnsuresNonNull; public class TrackSelectionParameters implements Bundleable { /** - * The preference level for enabling audio offload on the audio sink. Either {@link - * #AUDIO_OFFLOAD_MODE_PREFERENCE_ENABLED} or {@link #AUDIO_OFFLOAD_MODE_PREFERENCE_DISABLED}. + * The preference level for enabling audio offload on the audio sink. One of {@link + * #AUDIO_OFFLOAD_MODE_PREFERENCE_REQUIRED}, {@link #AUDIO_OFFLOAD_MODE_PREFERENCE_ENABLED}, or + * {@link #AUDIO_OFFLOAD_MODE_PREFERENCE_DISABLED}. */ @UnstableApi @Documented @Retention(RetentionPolicy.SOURCE) @Target(TYPE_USE) @IntDef({ + AUDIO_OFFLOAD_MODE_PREFERENCE_REQUIRED, AUDIO_OFFLOAD_MODE_PREFERENCE_ENABLED, AUDIO_OFFLOAD_MODE_PREFERENCE_DISABLED, }) public @interface AudioOffloadModePreference {} + /** + * The track selector will only select tracks that with the renderer capabilities provide an audio + * offload compatible playback scenario. If it is impossible to create an offload-compatible track + * selection, then no tracks will be selected. + */ + @UnstableApi public static final int AUDIO_OFFLOAD_MODE_PREFERENCE_REQUIRED = 2; /** * The track selector will enable audio offload if the selected tracks and renderer capabilities * are compatible. @@ -987,9 +995,8 @@ public class TrackSelectionParameters implements Bundleable { public final ImmutableList preferredAudioMimeTypes; /** - * The preferred offload mode setting for audio playback. Either {@link - * #AUDIO_OFFLOAD_MODE_PREFERENCE_ENABLED} or {@link #AUDIO_OFFLOAD_MODE_PREFERENCE_DISABLED}. The - * default is {@code AUDIO_OFFLOAD_MODE_PREFERENCE_DISABLED}. + * The preferred offload mode setting for audio playback. The default is {@link + * #AUDIO_OFFLOAD_MODE_PREFERENCE_DISABLED}. */ public final @AudioOffloadModePreference int audioOffloadModePreference; /** 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 276544f405..3aa891a539 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 @@ -16,6 +16,7 @@ package androidx.media3.exoplayer.trackselection; import static androidx.media3.common.TrackSelectionParameters.AUDIO_OFFLOAD_MODE_PREFERENCE_DISABLED; +import static androidx.media3.common.TrackSelectionParameters.AUDIO_OFFLOAD_MODE_PREFERENCE_REQUIRED; import static androidx.media3.common.util.Assertions.checkStateNotNull; import static androidx.media3.common.util.Util.castNonNull; import static androidx.media3.exoplayer.RendererCapabilities.AUDIO_OFFLOAD_GAPLESS_SUPPORTED; @@ -2514,6 +2515,10 @@ public class DefaultTrackSelector extends MappingTrackSelector *

The implementation should not account for overrides and disabled flags. Track selections * generated by this method will be overridden to account for these properties. * + *

If selection parameters include {@link Parameters#audioOffloadModePreference} with {@link + * TrackSelectionParameters#AUDIO_OFFLOAD_MODE_PREFERENCE_REQUIRED}, then only audio tracks will + * be selected. If no audio track is supported in offload, then no track will be selected. + * * @param mappedTrackInfo Mapped track information. * @param rendererFormatSupports The {@link Capabilities} for each mapped track, indexed by * renderer, track group and track (in that order). @@ -2578,7 +2583,6 @@ public class DefaultTrackSelector extends MappingTrackSelector trackType, mappedTrackInfo.getTrackGroups(i), rendererFormatSupports[i], params); } } - return definitions; } @@ -2605,6 +2609,9 @@ public class DefaultTrackSelector extends MappingTrackSelector @AdaptiveSupport int[] mixedMimeTypeSupports, Parameters params) throws ExoPlaybackException { + if (params.audioOffloadModePreference == AUDIO_OFFLOAD_MODE_PREFERENCE_REQUIRED) { + return null; + } return selectTracksForType( C.TRACK_TYPE_VIDEO, mappedTrackInfo, @@ -2720,6 +2727,9 @@ public class DefaultTrackSelector extends MappingTrackSelector Parameters params, @Nullable String selectedAudioLanguage) throws ExoPlaybackException { + if (params.audioOffloadModePreference == AUDIO_OFFLOAD_MODE_PREFERENCE_REQUIRED) { + return null; + } return selectTracksForType( C.TRACK_TYPE_TEXT, mappedTrackInfo, @@ -2748,6 +2758,9 @@ public class DefaultTrackSelector extends MappingTrackSelector protected ExoTrackSelection.Definition selectOtherTrack( int trackType, TrackGroupArray groups, @Capabilities int[][] formatSupport, Parameters params) throws ExoPlaybackException { + if (params.audioOffloadModePreference == AUDIO_OFFLOAD_MODE_PREFERENCE_REQUIRED) { + return null; + } @Nullable TrackGroup selectedGroup = null; int selectedTrackIndex = 0; @Nullable OtherTrackScore selectedTrackScore = null; @@ -3069,7 +3082,8 @@ public class DefaultTrackSelector extends MappingTrackSelector @Capabilities int trackFormatSupport = rendererFormatSupports[i][trackGroupIndex][trackSelection.getIndexInTrackGroup(0)]; - if (rendererSupportsOffload(parameters, trackFormatSupport, trackSelection)) { + if (rendererSupportsOffload( + parameters, trackFormatSupport, trackSelection.getSelectedFormat())) { audioRendererIndex = i; audioRenderersSupportingOffload++; } @@ -3092,11 +3106,11 @@ public class DefaultTrackSelector extends MappingTrackSelector * * @param parameters The selection parameters with audio offload mode preferences. * @param formatSupport The {@link Capabilities} for the selected track. - * @param selection The track selection. + * @param format The format of the track selection. * @return Whether the renderer supports tunneling for the {@link ExoTrackSelection}. */ private static boolean rendererSupportsOffload( - Parameters parameters, @Capabilities int formatSupport, ExoTrackSelection selection) { + Parameters parameters, @Capabilities int formatSupport, Format format) { if (RendererCapabilities.getAudioOffloadSupport(formatSupport) == AUDIO_OFFLOAD_NOT_SUPPORTED) { return false; } @@ -3108,9 +3122,7 @@ public class DefaultTrackSelector extends MappingTrackSelector } // TODO(b/235883373): Add check for OPUS where gapless info is in initialization data if (parameters.isGaplessSupportRequired) { - boolean isGapless = - selection.getSelectedFormat().encoderDelay != 0 - || selection.getSelectedFormat().encoderPadding != 0; + boolean isGapless = format.encoderDelay != 0 || format.encoderPadding != 0; boolean isGaplessSupported = (RendererCapabilities.getAudioOffloadSupport(formatSupport) & AUDIO_OFFLOAD_GAPLESS_SUPPORTED) @@ -3643,7 +3655,8 @@ public class DefaultTrackSelector extends MappingTrackSelector usesHardwareAcceleration = RendererCapabilities.getHardwareAccelerationSupport(formatSupport) == RendererCapabilities.HARDWARE_ACCELERATION_SUPPORTED; - selectionEligibility = evaluateSelectionEligibility(formatSupport, hasMappedVideoTracks); + selectionEligibility = + evaluateSelectionEligibility(parameters, formatSupport, hasMappedVideoTracks); } @Override @@ -3717,19 +3730,24 @@ public class DefaultTrackSelector extends MappingTrackSelector } private @SelectionEligibility int evaluateSelectionEligibility( - @Capabilities int rendererSupport, boolean hasMappedVideoTracks) { + Parameters params, @Capabilities int rendererSupport, boolean hasMappedVideoTracks) { if (!isSupported(rendererSupport, parameters.exceedRendererCapabilitiesIfNecessary)) { return SELECTION_ELIGIBILITY_NO; } if (!isWithinConstraints && !parameters.exceedAudioConstraintsIfNecessary) { return SELECTION_ELIGIBILITY_NO; } + if (params.audioOffloadModePreference == AUDIO_OFFLOAD_MODE_PREFERENCE_REQUIRED + && !rendererSupportsOffload(params, rendererSupport, format)) { + return SELECTION_ELIGIBILITY_NO; + } return isSupported(rendererSupport, /* allowExceedsCapabilities= */ false) && isWithinConstraints && format.bitrate != Format.NO_VALUE && !parameters.forceHighestSupportedBitrate && !parameters.forceLowestBitrate && (parameters.allowMultipleAdaptiveSelections || !hasMappedVideoTracks) + && params.audioOffloadModePreference != AUDIO_OFFLOAD_MODE_PREFERENCE_REQUIRED ? SELECTION_ELIGIBILITY_ADAPTIVE : SELECTION_ELIGIBILITY_FIXED; } 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 453a1faedc..bd213ffda4 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 @@ -20,7 +20,9 @@ import static androidx.media3.common.C.FORMAT_HANDLED; import static androidx.media3.common.C.FORMAT_UNSUPPORTED_SUBTYPE; import static androidx.media3.common.C.FORMAT_UNSUPPORTED_TYPE; import static androidx.media3.common.TrackSelectionParameters.AUDIO_OFFLOAD_MODE_PREFERENCE_ENABLED; +import static androidx.media3.common.TrackSelectionParameters.AUDIO_OFFLOAD_MODE_PREFERENCE_REQUIRED; import static androidx.media3.exoplayer.RendererCapabilities.ADAPTIVE_NOT_SEAMLESS; +import static androidx.media3.exoplayer.RendererCapabilities.AUDIO_OFFLOAD_GAPLESS_SUPPORTED; import static androidx.media3.exoplayer.RendererCapabilities.AUDIO_OFFLOAD_SUPPORTED; import static androidx.media3.exoplayer.RendererCapabilities.DECODER_SUPPORT_FALLBACK; import static androidx.media3.exoplayer.RendererCapabilities.DECODER_SUPPORT_FALLBACK_MIMETYPE; @@ -30,6 +32,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 androidx.media3.exoplayer.audio.AudioSink.OFFLOAD_MODE_DISABLED; +import static androidx.media3.exoplayer.audio.AudioSink.OFFLOAD_MODE_ENABLED_GAPLESS_NOT_REQUIRED; import static androidx.media3.exoplayer.audio.AudioSink.OFFLOAD_MODE_ENABLED_GAPLESS_REQUIRED; import static com.google.common.truth.Truth.assertThat; import static org.mockito.Mockito.mock; @@ -2318,7 +2321,7 @@ public final class DefaultTrackSelectorTest { @Test public void - selectTracks_withSingleTrackAndOffloadPreferenceEnabled_returnsRendererConfigOffloadEnabled() + selectTracks_withSingleTrackAndOffloadPreferenceEnabled_returnsRendererConfigOffloadModeEnabledGaplessRequired() throws Exception { Format formatAAC = new Format.Builder().setId("0").setSampleMimeType(MimeTypes.AUDIO_AAC).build(); @@ -2347,8 +2350,6 @@ public final class DefaultTrackSelectorTest { periodId, TIMELINE); - assertThat(trackSelector.getParameters().audioOffloadModePreference) - .isEqualTo(AUDIO_OFFLOAD_MODE_PREFERENCE_ENABLED); assertFixedSelection(result.selections[0], trackGroups, formatAAC); assertThat(result.rendererConfigurations[0].offloadModePreferred) .isEqualTo(OFFLOAD_MODE_ENABLED_GAPLESS_REQUIRED); @@ -2356,7 +2357,7 @@ public final class DefaultTrackSelectorTest { @Test public void - selectTracks_withMultipleAudioTracksAndOffloadPreferenceEnabled_returnsRendererConfigOffloadDisabled() + selectTracks_withMultipleAudioTracksAndOffloadPreferenceEnabled_returnsRendererConfigOffloadModeDisabled() throws Exception { Format.Builder formatBuilder = AUDIO_FORMAT.buildUpon(); TrackGroupArray trackGroups = @@ -2385,8 +2386,6 @@ public final class DefaultTrackSelectorTest { periodId, TIMELINE); - assertThat(trackSelector.getParameters().audioOffloadModePreference) - .isEqualTo(AUDIO_OFFLOAD_MODE_PREFERENCE_ENABLED); assertThat(result.length).isEqualTo(1); assertAdaptiveSelection(result.selections[0], trackGroups.get(0), 0, 1); assertThat(result.rendererConfigurations[0].offloadModePreferred) @@ -2395,20 +2394,16 @@ public final class DefaultTrackSelectorTest { @Test public void - selectTracks_gaplessTrackWithOffloadPreferenceGaplessRequired_returnsConfigOffloadDisabled() + selectTracks_withMultipleAudioTracksAndOffloadPreferenceRequired_returnsRendererConfigOffloadModeEnabledGaplessRequired() throws Exception { - Format formatAAC = - new Format.Builder() - .setId("0") - .setSampleMimeType(MimeTypes.AUDIO_AAC) - .setEncoderDelay(100) - .build(); - TrackGroupArray trackGroups = new TrackGroupArray(new TrackGroup(formatAAC)); + Format format1 = AUDIO_FORMAT.buildUpon().setId("0").build(); + Format format2 = AUDIO_FORMAT.buildUpon().setId("0").build(); + TrackGroupArray trackGroups = singleTrackGroup(format1, format2); trackSelector.setParameters( trackSelector .buildUponParameters() .setAudioOffloadPreference( - AUDIO_OFFLOAD_MODE_PREFERENCE_ENABLED, + AUDIO_OFFLOAD_MODE_PREFERENCE_REQUIRED, /* isGaplessSupportRequired= */ true, /* isSpeedChangeSupportRequired= */ false) .build()); @@ -2428,13 +2423,207 @@ public final class DefaultTrackSelectorTest { periodId, TIMELINE); - assertThat(trackSelector.getParameters().audioOffloadModePreference) - .isEqualTo(AUDIO_OFFLOAD_MODE_PREFERENCE_ENABLED); - assertFixedSelection(result.selections[0], trackGroups, formatAAC); + assertThat(result.length).isEqualTo(1); + assertFixedSelection(result.selections[0], trackGroups, format1); + assertThat(result.rendererConfigurations[0].offloadModePreferred) + .isEqualTo(OFFLOAD_MODE_ENABLED_GAPLESS_REQUIRED); + } + + @Test + public void + selectTracks_withAudioAndVideoAndOffloadPreferenceEnabled_returnsRendererConfigOffloadModeDisabled() + throws Exception { + TrackGroupArray trackGroups = + new TrackGroupArray(new TrackGroup(VIDEO_FORMAT), new TrackGroup(AUDIO_FORMAT)); + trackSelector.setParameters( + trackSelector + .buildUponParameters() + .setAudioOffloadPreference( + AUDIO_OFFLOAD_MODE_PREFERENCE_ENABLED, + /* isGaplessSupportRequired= */ false, + /* isSpeedChangeSupportRequired= */ false) + .build()); + RendererCapabilities audioCapabilities = + new FakeRendererCapabilities( + C.TRACK_TYPE_AUDIO, + RendererCapabilities.create( + FORMAT_HANDLED, + ADAPTIVE_NOT_SEAMLESS, + TUNNELING_NOT_SUPPORTED, + AUDIO_OFFLOAD_SUPPORTED)); + + TrackSelectorResult result = + trackSelector.selectTracks( + new RendererCapabilities[] {VIDEO_CAPABILITIES, audioCapabilities}, + trackGroups, + periodId, + TIMELINE); + + assertThat(result.length).isEqualTo(2); + assertFixedSelection(result.selections[0], trackGroups, VIDEO_FORMAT); + assertFixedSelection(result.selections[1], trackGroups, AUDIO_FORMAT); + assertThat(result.rendererConfigurations[1].offloadModePreferred) + .isEqualTo(OFFLOAD_MODE_DISABLED); + } + + @Test + public void + selectTracks_withAudioAndVideoAndOffloadPreferenceRequired_returnsRendererConfigOffloadModeEnabledGaplessNotRequired() + throws Exception { + TrackGroupArray trackGroups = + new TrackGroupArray(new TrackGroup(VIDEO_FORMAT), new TrackGroup(AUDIO_FORMAT)); + trackSelector.setParameters( + trackSelector + .buildUponParameters() + .setAudioOffloadPreference( + AUDIO_OFFLOAD_MODE_PREFERENCE_REQUIRED, + /* isGaplessSupportRequired= */ false, + /* isSpeedChangeSupportRequired= */ false) + .build()); + RendererCapabilities audioCapabilities = + new FakeRendererCapabilities( + C.TRACK_TYPE_AUDIO, + RendererCapabilities.create( + FORMAT_HANDLED, + ADAPTIVE_NOT_SEAMLESS, + TUNNELING_NOT_SUPPORTED, + AUDIO_OFFLOAD_SUPPORTED)); + + TrackSelectorResult result = + trackSelector.selectTracks( + new RendererCapabilities[] {VIDEO_CAPABILITIES, audioCapabilities}, + trackGroups, + periodId, + TIMELINE); + + assertThat(result.length).isEqualTo(2); + assertThat(result.selections[0]).isNull(); + assertFixedSelection(result.selections[1], trackGroups, AUDIO_FORMAT); + assertThat(result.rendererConfigurations[1].offloadModePreferred) + .isEqualTo(OFFLOAD_MODE_ENABLED_GAPLESS_NOT_REQUIRED); + } + + @Test + public void + selectTracks_gaplessTrackWithOffloadPreferenceGaplessRequired_returnsConfigOffloadDisabled() + throws Exception { + Format formatAac = + new Format.Builder() + .setId("0") + .setSampleMimeType(MimeTypes.AUDIO_AAC) + .setEncoderDelay(100) + .build(); + TrackGroupArray trackGroups = new TrackGroupArray(new TrackGroup(formatAac)); + trackSelector.setParameters( + trackSelector + .buildUponParameters() + .setAudioOffloadPreference( + AUDIO_OFFLOAD_MODE_PREFERENCE_ENABLED, + /* isGaplessSupportRequired= */ true, + /* isSpeedChangeSupportRequired= */ false) + .build()); + // Offload playback without gapless transitions is supported + RendererCapabilities capabilitiesOffloadSupport = + new FakeRendererCapabilities( + C.TRACK_TYPE_AUDIO, + RendererCapabilities.create( + FORMAT_HANDLED, + ADAPTIVE_NOT_SEAMLESS, + TUNNELING_NOT_SUPPORTED, + AUDIO_OFFLOAD_SUPPORTED)); + + TrackSelectorResult result = + trackSelector.selectTracks( + new RendererCapabilities[] {capabilitiesOffloadSupport}, + trackGroups, + periodId, + TIMELINE); + + assertThat(result.length).isEqualTo(1); + assertFixedSelection(result.selections[0], trackGroups, formatAac); assertThat(result.rendererConfigurations[0].offloadModePreferred) .isEqualTo(OFFLOAD_MODE_DISABLED); } + @Test + public void + selectTracks_gaplessTrackWithOffloadPreferenceGaplessRequiredWithGaplessSupport_returnsConfigOffloadModeEnabledGaplessRequired() + throws Exception { + Format formatAac = + new Format.Builder() + .setId("0") + .setSampleMimeType(MimeTypes.AUDIO_AAC) + .setEncoderDelay(100) + .build(); + TrackGroupArray trackGroups = new TrackGroupArray(new TrackGroup(formatAac)); + trackSelector.setParameters( + trackSelector + .buildUponParameters() + .setAudioOffloadPreference( + AUDIO_OFFLOAD_MODE_PREFERENCE_ENABLED, + /* isGaplessSupportRequired= */ true, + /* isSpeedChangeSupportRequired= */ false) + .build()); + // Offload playback without gapless transitions is supported + RendererCapabilities capabilitiesOffloadSupport = + new FakeRendererCapabilities( + C.TRACK_TYPE_AUDIO, + RendererCapabilities.create( + FORMAT_HANDLED, + ADAPTIVE_NOT_SEAMLESS, + TUNNELING_NOT_SUPPORTED, + /* audioOffloadSupport= */ AUDIO_OFFLOAD_SUPPORTED + | AUDIO_OFFLOAD_GAPLESS_SUPPORTED)); + + TrackSelectorResult result = + trackSelector.selectTracks( + new RendererCapabilities[] {capabilitiesOffloadSupport}, + trackGroups, + periodId, + TIMELINE); + + assertThat(result.length).isEqualTo(1); + assertFixedSelection(result.selections[0], trackGroups, formatAac); + assertThat(result.rendererConfigurations[0].offloadModePreferred) + .isEqualTo(OFFLOAD_MODE_ENABLED_GAPLESS_REQUIRED); + } + + @Test + public void + selectTracks_gaplessTrackWithOffloadPreferenceRequiredGaplessRequired_returnsEmptySelection() + throws Exception { + Format formatAac = + new Format.Builder().setSampleMimeType(MimeTypes.AUDIO_AAC).setEncoderDelay(100).build(); + TrackGroupArray trackGroups = new TrackGroupArray(new TrackGroup(formatAac)); + trackSelector.setParameters( + trackSelector + .buildUponParameters() + .setAudioOffloadPreference( + AUDIO_OFFLOAD_MODE_PREFERENCE_REQUIRED, + /* isGaplessSupportRequired= */ true, + /* isSpeedChangeSupportRequired= */ false) + .build()); + // Offload playback without gapless transitions is supported + RendererCapabilities capabilitiesOffloadSupport = + new FakeRendererCapabilities( + C.TRACK_TYPE_AUDIO, + RendererCapabilities.create( + FORMAT_HANDLED, + ADAPTIVE_NOT_SEAMLESS, + TUNNELING_NOT_SUPPORTED, + AUDIO_OFFLOAD_SUPPORTED)); + + TrackSelectorResult result = + trackSelector.selectTracks( + new RendererCapabilities[] {capabilitiesOffloadSupport}, + trackGroups, + periodId, + TIMELINE); + + assertThat(result.selections).hasLength(1); + assertThat(result.selections[0]).isNull(); + } + /** * Tests that track selector will select the video track with the highest number of matching role * flags given by {@link Parameters}.