diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 69c02c5536..f0eb0f5fb0 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -17,6 +17,24 @@ * Ogg: Fix bug when seeking in files with a long duration ([#391](https://github.com/androidx/media/issues/391)). * Audio: +* Audio Offload: + * Add `AudioSink.getFormatOffloadSupport(Format)` that retrieves level of + offload support the sink can provide for the format through a + `DefaultAudioOffloadSupportProvider`. It returns the new + `AudioOffloadSupport` that contains `isFormatSupported`, + `isGaplessSupported`, and `isSpeedChangeSupported`. + * Add `AudioSink.setOffloadMode()` through which the offload configuration + on the audio sink is configured. Default is + `AudioSink.OFFLOAD_MODE_DISABLED`. + * Offload can be enabled through `setAudioOffloadPreference` in + `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. + * Remove parameter `enableOffload` from + `DefaultRenderersFactory.buildAudioSink` method signature. + * Remove method `DefaultAudioSink.Builder.setOffloadMode`. + * Remove intdef value + `DefaultAudioSink.OffloadMode.OFFLOAD_MODE_ENABLED_GAPLESS_DISABLED`. * Video: * Make `MediaCodecVideoRenderer` report a `VideoSize` with a width and height of 0 when the renderer is disabled. diff --git a/api.txt b/api.txt index d6dc7db144..06cb12e068 100644 --- a/api.txt +++ b/api.txt @@ -1077,10 +1077,13 @@ package androidx.media3.common { method public static androidx.media3.common.TrackSelectionParameters fromBundle(android.os.Bundle); method public static androidx.media3.common.TrackSelectionParameters getDefaults(android.content.Context); method public android.os.Bundle toBundle(); + field public final int audioOffloadModePreference; field public final com.google.common.collect.ImmutableSet disabledTrackTypes; field public final boolean forceHighestSupportedBitrate; field public final boolean forceLowestBitrate; field @androidx.media3.common.C.SelectionFlags public final int ignoredTextSelectionFlags; + field public final boolean isGaplessSupportRequired; + field public final boolean isSpeedChangeSupportRequired; field public final int maxAudioBitrate; field public final int maxAudioChannelCount; field public final int maxVideoBitrate; @@ -1114,6 +1117,7 @@ package androidx.media3.common { method public androidx.media3.common.TrackSelectionParameters.Builder clearOverridesOfType(@androidx.media3.common.C.TrackType int); method public androidx.media3.common.TrackSelectionParameters.Builder clearVideoSizeConstraints(); method public androidx.media3.common.TrackSelectionParameters.Builder clearViewportSizeConstraints(); + method public androidx.media3.common.TrackSelectionParameters.Builder setAudioOffloadPreference(int, boolean, boolean); method public androidx.media3.common.TrackSelectionParameters.Builder setForceHighestSupportedBitrate(boolean); method public androidx.media3.common.TrackSelectionParameters.Builder setForceLowestBitrate(boolean); method public androidx.media3.common.TrackSelectionParameters.Builder setIgnoredTextSelectionFlags(@androidx.media3.common.C.SelectionFlags int); 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 b65bc9400a..e96ee4084b 100644 --- a/libraries/common/src/main/java/androidx/media3/common/TrackSelectionParameters.java +++ b/libraries/common/src/main/java/androidx/media3/common/TrackSelectionParameters.java @@ -18,12 +18,14 @@ package androidx.media3.common; import static androidx.media3.common.util.Assertions.checkNotNull; import static androidx.media3.common.util.BundleableUtil.toBundleArrayList; import static com.google.common.base.MoreObjects.firstNonNull; +import static java.lang.annotation.ElementType.TYPE_USE; import android.content.Context; import android.graphics.Point; import android.os.Bundle; import android.os.Looper; import android.view.accessibility.CaptioningManager; +import androidx.annotation.IntDef; import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; import androidx.media3.common.util.BundleableUtil; @@ -34,6 +36,10 @@ import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import com.google.common.primitives.Ints; import com.google.errorprone.annotations.CanIgnoreReturnValue; +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; @@ -65,6 +71,31 @@ 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}. + */ + @UnstableApi + @Documented + @Retention(RetentionPolicy.SOURCE) + @Target(TYPE_USE) + @IntDef({ + AUDIO_OFFLOAD_MODE_PREFERENCE_ENABLED, + AUDIO_OFFLOAD_MODE_PREFERENCE_DISABLED, + }) + public @interface AudioOffloadModePreference {} + + /** + * The track selector will enable audio offload if the selected tracks and renderer capabilities + * are compatible. + */ + @UnstableApi public static final int AUDIO_OFFLOAD_MODE_PREFERENCE_ENABLED = 1; + /** + * The track selector will disable audio offload on the audio sink. Track selection will not take + * into consideration whether or not a track is offload compatible. + */ + @UnstableApi public static final int AUDIO_OFFLOAD_MODE_PREFERENCE_DISABLED = 0; + /** * A builder for {@link TrackSelectionParameters}. See the {@link TrackSelectionParameters} * documentation for explanations of the parameters that can be configured using this builder. @@ -90,6 +121,9 @@ public class TrackSelectionParameters implements Bundleable { private int maxAudioChannelCount; private int maxAudioBitrate; private ImmutableList preferredAudioMimeTypes; + private @AudioOffloadModePreference int audioOffloadModePreference; + private boolean isGaplessSupportRequired; + private boolean isSpeedChangeSupportRequired; // Text private ImmutableList preferredTextLanguages; private @C.RoleFlags int preferredTextRoleFlags; @@ -124,6 +158,9 @@ public class TrackSelectionParameters implements Bundleable { maxAudioChannelCount = Integer.MAX_VALUE; maxAudioBitrate = Integer.MAX_VALUE; preferredAudioMimeTypes = ImmutableList.of(); + audioOffloadModePreference = AUDIO_OFFLOAD_MODE_PREFERENCE_DISABLED; + isGaplessSupportRequired = false; + isSpeedChangeSupportRequired = false; // Text preferredTextLanguages = ImmutableList.of(); preferredTextRoleFlags = 0; @@ -214,6 +251,17 @@ public class TrackSelectionParameters implements Bundleable { bundle.getBoolean( FIELD_SELECT_UNDETERMINED_TEXT_LANGUAGE, DEFAULT_WITHOUT_CONTEXT.selectUndeterminedTextLanguage); + audioOffloadModePreference = + bundle.getInt( + FIELD_AUDIO_OFFLOAD_MODE_PREFERENCE, + DEFAULT_WITHOUT_CONTEXT.audioOffloadModePreference); + isGaplessSupportRequired = + bundle.getBoolean( + FIELD_IS_GAPLESS_SUPPORT_REQUIRED, DEFAULT_WITHOUT_CONTEXT.isGaplessSupportRequired); + isSpeedChangeSupportRequired = + bundle.getBoolean( + FIELD_IS_SPEED_CHANGE_SUPPORT_REQUIRED, + DEFAULT_WITHOUT_CONTEXT.isSpeedChangeSupportRequired); // General forceLowestBitrate = bundle.getBoolean(FIELD_FORCE_LOWEST_BITRATE, DEFAULT_WITHOUT_CONTEXT.forceLowestBitrate); @@ -270,6 +318,9 @@ public class TrackSelectionParameters implements Bundleable { maxAudioChannelCount = parameters.maxAudioChannelCount; maxAudioBitrate = parameters.maxAudioBitrate; preferredAudioMimeTypes = parameters.preferredAudioMimeTypes; + audioOffloadModePreference = parameters.audioOffloadModePreference; + isGaplessSupportRequired = parameters.isGaplessSupportRequired; + isSpeedChangeSupportRequired = parameters.isSpeedChangeSupportRequired; // Text preferredTextLanguages = parameters.preferredTextLanguages; preferredTextRoleFlags = parameters.preferredTextRoleFlags; @@ -560,6 +611,28 @@ public class TrackSelectionParameters implements Bundleable { return this; } + /** + * Sets the audio offload mode preferences. This includes whether to enable/disable offload as + * well as to set requirements like if the device must support gapless transitions or speed + * change during offload. + * + *

If {@code isGaplessSupportRequired}, then audio offload will be enabled only if the device + * supports gapless transitions during offload or the selected audio is not gapless. + * + *

If {@code isSpeedChangeSupportRequired}, then audio offload will be enabled only if the + * device supports changing playback speed during offload. + */ + @CanIgnoreReturnValue + public Builder setAudioOffloadPreference( + @AudioOffloadModePreference int audioOffloadModePreference, + boolean isGaplessSupportRequired, + boolean isSpeedChangeSupportRequired) { + this.audioOffloadModePreference = audioOffloadModePreference; + this.isGaplessSupportRequired = isGaplessSupportRequired; + this.isSpeedChangeSupportRequired = isSpeedChangeSupportRequired; + return this; + } + // Text /** @@ -912,6 +985,25 @@ public class TrackSelectionParameters implements Bundleable { * no preference. The default is an empty list. */ 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}. + */ + public final @AudioOffloadModePreference int audioOffloadModePreference; + /** + * A constraint on enabling offload. If {@code isGaplessSupportRequired}, then audio offload will + * be enabled only if the device supports gapless transitions during offload or the selected audio + * is not gapless. + */ + public final boolean isGaplessSupportRequired; + /** + * A constraint on enabling offload. If {@code isSpeedChangeSupportRequired}, then audio offload + * will be enabled only if the device supports changing playback speed during offload. + */ + public final boolean isSpeedChangeSupportRequired; + // Text /** * The preferred languages for text tracks as IETF BCP 47 conformant tags in order of preference. @@ -982,6 +1074,9 @@ public class TrackSelectionParameters implements Bundleable { this.maxAudioChannelCount = builder.maxAudioChannelCount; this.maxAudioBitrate = builder.maxAudioBitrate; this.preferredAudioMimeTypes = builder.preferredAudioMimeTypes; + this.audioOffloadModePreference = builder.audioOffloadModePreference; + this.isGaplessSupportRequired = builder.isGaplessSupportRequired; + this.isSpeedChangeSupportRequired = builder.isSpeedChangeSupportRequired; // Text this.preferredTextLanguages = builder.preferredTextLanguages; this.preferredTextRoleFlags = builder.preferredTextRoleFlags; @@ -1029,6 +1124,9 @@ public class TrackSelectionParameters implements Bundleable { && maxAudioChannelCount == other.maxAudioChannelCount && maxAudioBitrate == other.maxAudioBitrate && preferredAudioMimeTypes.equals(other.preferredAudioMimeTypes) + && audioOffloadModePreference == other.audioOffloadModePreference + && isGaplessSupportRequired == other.isGaplessSupportRequired + && isSpeedChangeSupportRequired == other.isSpeedChangeSupportRequired // Text && preferredTextLanguages.equals(other.preferredTextLanguages) && preferredTextRoleFlags == other.preferredTextRoleFlags @@ -1064,6 +1162,9 @@ public class TrackSelectionParameters implements Bundleable { result = 31 * result + maxAudioChannelCount; result = 31 * result + maxAudioBitrate; result = 31 * result + preferredAudioMimeTypes.hashCode(); + result = 31 * result + audioOffloadModePreference; + result = 31 * result + (isGaplessSupportRequired ? 1 : 0); + result = 31 * result + (isSpeedChangeSupportRequired ? 1 : 0); // Text result = 31 * result + preferredTextLanguages.hashCode(); result = 31 * result + preferredTextRoleFlags; @@ -1105,6 +1206,9 @@ public class TrackSelectionParameters implements Bundleable { private static final String FIELD_DISABLED_TRACK_TYPE = Util.intToStringMaxRadix(24); private static final String FIELD_PREFERRED_VIDEO_ROLE_FLAGS = Util.intToStringMaxRadix(25); private static final String FIELD_IGNORED_TEXT_SELECTION_FLAGS = Util.intToStringMaxRadix(26); + private static final String FIELD_AUDIO_OFFLOAD_MODE_PREFERENCE = Util.intToStringMaxRadix(27); + private static final String FIELD_IS_GAPLESS_SUPPORT_REQUIRED = Util.intToStringMaxRadix(28); + private static final String FIELD_IS_SPEED_CHANGE_SUPPORT_REQUIRED = Util.intToStringMaxRadix(29); /** * Defines a minimum field ID value for subclasses to use when implementing {@link #toBundle()} @@ -1149,6 +1253,9 @@ public class TrackSelectionParameters implements Bundleable { bundle.putInt(FIELD_PREFERRED_TEXT_ROLE_FLAGS, preferredTextRoleFlags); bundle.putInt(FIELD_IGNORED_TEXT_SELECTION_FLAGS, ignoredTextSelectionFlags); bundle.putBoolean(FIELD_SELECT_UNDETERMINED_TEXT_LANGUAGE, selectUndeterminedTextLanguage); + bundle.putInt(FIELD_AUDIO_OFFLOAD_MODE_PREFERENCE, audioOffloadModePreference); + bundle.putBoolean(FIELD_IS_GAPLESS_SUPPORT_REQUIRED, isGaplessSupportRequired); + bundle.putBoolean(FIELD_IS_SPEED_CHANGE_SUPPORT_REQUIRED, isSpeedChangeSupportRequired); // General bundle.putBoolean(FIELD_FORCE_LOWEST_BITRATE, forceLowestBitrate); bundle.putBoolean(FIELD_FORCE_HIGHEST_SUPPORTED_BITRATE, forceHighestSupportedBitrate); diff --git a/libraries/common/src/main/java/androidx/media3/common/util/Util.java b/libraries/common/src/main/java/androidx/media3/common/util/Util.java index 24f48712d0..101455c4df 100644 --- a/libraries/common/src/main/java/androidx/media3/common/util/Util.java +++ b/libraries/common/src/main/java/androidx/media3/common/util/Util.java @@ -1892,6 +1892,17 @@ public final class Util { } } + /** Creates {@link AudioFormat} with given sampleRate, channelConfig, and encoding. */ + @UnstableApi + @RequiresApi(21) + public static AudioFormat getAudioFormat(int sampleRate, int channelConfig, int encoding) { + return new AudioFormat.Builder() + .setSampleRate(sampleRate) + .setChannelMask(channelConfig) + .setEncoding(encoding) + .build(); + } + /** * Returns the frame size for audio with {@code channelCount} channels in the specified encoding. * diff --git a/libraries/common/src/test/java/androidx/media3/common/TrackSelectionParametersTest.java b/libraries/common/src/test/java/androidx/media3/common/TrackSelectionParametersTest.java index c3cbe2019d..35d9dc164c 100644 --- a/libraries/common/src/test/java/androidx/media3/common/TrackSelectionParametersTest.java +++ b/libraries/common/src/test/java/androidx/media3/common/TrackSelectionParametersTest.java @@ -52,6 +52,10 @@ public final class TrackSelectionParametersTest { assertThat(parameters.preferredAudioRoleFlags).isEqualTo(0); assertThat(parameters.maxAudioChannelCount).isEqualTo(Integer.MAX_VALUE); assertThat(parameters.maxAudioBitrate).isEqualTo(Integer.MAX_VALUE); + assertThat(parameters.audioOffloadModePreference) + .isEqualTo(TrackSelectionParameters.AUDIO_OFFLOAD_MODE_PREFERENCE_DISABLED); + assertThat(parameters.isGaplessSupportRequired).isFalse(); + assertThat(parameters.isSpeedChangeSupportRequired).isFalse(); // Text assertThat(parameters.preferredAudioMimeTypes).isEmpty(); assertThat(parameters.preferredTextLanguages).isEmpty(); @@ -96,6 +100,10 @@ public final class TrackSelectionParametersTest { .setMaxAudioChannelCount(10) .setMaxAudioBitrate(11) .setPreferredAudioMimeTypes(MimeTypes.AUDIO_AC3, MimeTypes.AUDIO_E_AC3) + .setAudioOffloadPreference( + TrackSelectionParameters.AUDIO_OFFLOAD_MODE_PREFERENCE_ENABLED, + /* isGaplessSupportRequired= */ false, + /* isSpeedChangeSupportRequired= */ true) // Text .setPreferredTextLanguages("de", "en") .setPreferredTextRoleFlags(C.ROLE_FLAG_CAPTION) @@ -140,6 +148,10 @@ public final class TrackSelectionParametersTest { assertThat(parameters.preferredAudioMimeTypes) .containsExactly(MimeTypes.AUDIO_AC3, MimeTypes.AUDIO_E_AC3) .inOrder(); + assertThat(parameters.audioOffloadModePreference) + .isEqualTo(TrackSelectionParameters.AUDIO_OFFLOAD_MODE_PREFERENCE_ENABLED); + assertThat(parameters.isGaplessSupportRequired).isFalse(); + assertThat(parameters.isSpeedChangeSupportRequired).isTrue(); // Text assertThat(parameters.preferredTextLanguages).containsExactly("de", "en").inOrder(); assertThat(parameters.preferredTextRoleFlags).isEqualTo(C.ROLE_FLAG_CAPTION); diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/DefaultRenderersFactory.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/DefaultRenderersFactory.java index 2b38a57d21..c8420f1725 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/DefaultRenderersFactory.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/DefaultRenderersFactory.java @@ -101,7 +101,6 @@ public class DefaultRenderersFactory implements RenderersFactory { private MediaCodecSelector mediaCodecSelector; private boolean enableFloatOutput; private boolean enableAudioTrackPlaybackParams; - private boolean enableOffload; /** * @param context A {@link Context}. @@ -220,29 +219,6 @@ public class DefaultRenderersFactory implements RenderersFactory { return this; } - /** - * Sets whether audio should be played using the offload path. - * - *

Audio offload disables ExoPlayer audio processing, but significantly reduces the energy - * consumption of the playback when {@link - * ExoPlayer#experimentalSetOffloadSchedulingEnabled(boolean) offload scheduling} is enabled. - * - *

Most Android devices can only support one offload {@link android.media.AudioTrack} at a time - * and can invalidate it at any time. Thus an app can never be guaranteed that it will be able to - * play in offload. - * - *

The default value is {@code false}. - * - * @param enableOffload Whether to enable use of audio offload for supported formats, if - * available. - * @return This factory, for convenience. - */ - @CanIgnoreReturnValue - public DefaultRenderersFactory setEnableAudioOffload(boolean enableOffload) { - this.enableOffload = enableOffload; - return this; - } - /** * Sets whether to enable setting playback speed using {@link * android.media.AudioTrack#setPlaybackParams(PlaybackParams)}, which is supported from API level @@ -303,7 +279,7 @@ public class DefaultRenderersFactory implements RenderersFactory { renderersList); @Nullable AudioSink audioSink = - buildAudioSink(context, enableFloatOutput, enableAudioTrackPlaybackParams, enableOffload); + buildAudioSink(context, enableFloatOutput, enableAudioTrackPlaybackParams); if (audioSink != null) { buildAudioRenderers( context, @@ -636,25 +612,16 @@ public class DefaultRenderersFactory implements RenderersFactory { * @param enableFloatOutput Whether to enable use of floating point audio output, if available. * @param enableAudioTrackPlaybackParams Whether to enable setting playback speed using {@link * android.media.AudioTrack#setPlaybackParams(PlaybackParams)}, if supported. - * @param enableOffload Whether to enable use of audio offload for supported formats, if - * available. * @return The {@link AudioSink} to which the audio renderers will output. May be {@code null} if * no audio renderers are required. If {@code null} is returned then {@link * #buildAudioRenderers} will not be called. */ @Nullable protected AudioSink buildAudioSink( - Context context, - boolean enableFloatOutput, - boolean enableAudioTrackPlaybackParams, - boolean enableOffload) { + Context context, boolean enableFloatOutput, boolean enableAudioTrackPlaybackParams) { return new DefaultAudioSink.Builder(context) .setEnableFloatOutput(enableFloatOutput) .setEnableAudioTrackPlaybackParams(enableAudioTrackPlaybackParams) - .setOffloadMode( - enableOffload - ? DefaultAudioSink.OFFLOAD_MODE_ENABLED_GAPLESS_REQUIRED - : DefaultAudioSink.OFFLOAD_MODE_DISABLED) .build(); } diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayer.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayer.java index b7cab5f4e8..5db2e757d8 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayer.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayer.java @@ -43,6 +43,7 @@ import androidx.media3.common.MediaItem; import androidx.media3.common.Player; import androidx.media3.common.PriorityTaskManager; import androidx.media3.common.Timeline; +import androidx.media3.common.TrackSelectionParameters; import androidx.media3.common.Tracks; import androidx.media3.common.VideoFrameProcessor; import androidx.media3.common.VideoSize; @@ -55,7 +56,6 @@ import androidx.media3.exoplayer.analytics.AnalyticsCollector; import androidx.media3.exoplayer.analytics.AnalyticsListener; import androidx.media3.exoplayer.analytics.DefaultAnalyticsCollector; import androidx.media3.exoplayer.audio.AudioSink; -import androidx.media3.exoplayer.audio.DefaultAudioSink; import androidx.media3.exoplayer.audio.MediaCodecAudioRenderer; import androidx.media3.exoplayer.metadata.MetadataRenderer; import androidx.media3.exoplayer.source.DefaultMediaSourceFactory; @@ -1807,9 +1807,8 @@ public interface ExoPlayer extends Player { * the following: * *

    - *
  • Audio offload rendering is enabled in {@link - * DefaultRenderersFactory#setEnableAudioOffload} or the equivalent option passed to {@link - * DefaultAudioSink.Builder#setOffloadMode}. + *
  • Audio offload rendering is enabled through {@link + * TrackSelectionParameters.Builder#setAudioOffloadPreference}. *
  • An audio track is playing in a format that the device supports offloading (for example, * MP3 or AAC). *
  • The {@link AudioSink} is playing with an offload {@link AudioTrack}. 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 ddac15318d..3839753266 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/RendererCapabilities.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/RendererCapabilities.java @@ -144,8 +144,8 @@ public interface RendererCapabilities { int HARDWARE_ACCELERATION_NOT_SUPPORTED = 0; /** - * Level of decoder support. One of {@link #DECODER_SUPPORT_FALLBACK_MIMETYPE}, {@link - * #DECODER_SUPPORT_FALLBACK}, and {@link #DECODER_SUPPORT_PRIMARY}. + * Level of decoder support. One of {@link #DECODER_SUPPORT_PRIMARY}, {@link + * #DECODER_SUPPORT_FALLBACK}, and {@link #DECODER_SUPPORT_FALLBACK_MIMETYPE}}. * *

    For video renderers, the level of support is indicated for non-tunneled output. */ @@ -155,7 +155,7 @@ public interface RendererCapabilities { @IntDef({DECODER_SUPPORT_FALLBACK_MIMETYPE, DECODER_SUPPORT_PRIMARY, DECODER_SUPPORT_FALLBACK}) @interface DecoderSupport {} /** A mask to apply to {@link Capabilities} to obtain {@link DecoderSupport} only. */ - int MODE_SUPPORT_MASK = 0b11 << 7; + int DECODER_SUPPORT_MASK = 0b11 << 7; /** * The format's MIME type is unsupported and the renderer may use a decoder for a fallback MIME * type. @@ -166,15 +166,49 @@ public interface RendererCapabilities { /** The format exceeds the primary decoder's capabilities but is supported by fallback decoder */ int DECODER_SUPPORT_FALLBACK = 0; + /** + * Level of renderer support for audio offload. + * + *

    Speed change and gapless transition support with audio offload is represented by the bit + * mask flags {@link #AUDIO_OFFLOAD_SPEED_CHANGE_SUPPORTED} and {@link + * #AUDIO_OFFLOAD_GAPLESS_SUPPORTED} respectively. If neither feature is supported then the value + * will be either {@link #AUDIO_OFFLOAD_SUPPORTED} or {@link #AUDIO_OFFLOAD_NOT_SUPPORTED}. + * + *

    For non-audio renderers, the level of support is always {@link + * #AUDIO_OFFLOAD_NOT_SUPPORTED}. + */ + @Documented + @Retention(RetentionPolicy.SOURCE) + @Target(TYPE_USE) + @IntDef({ + AUDIO_OFFLOAD_SPEED_CHANGE_SUPPORTED, + AUDIO_OFFLOAD_GAPLESS_SUPPORTED, + AUDIO_OFFLOAD_SUPPORTED, + AUDIO_OFFLOAD_NOT_SUPPORTED + }) + @interface AudioOffloadSupport {} + /** A mask to apply to {@link Capabilities} to obtain {@link AudioOffloadSupport} only. */ + int AUDIO_OFFLOAD_SUPPORT_MASK = 0b111 << 9; + + /** The renderer supports audio offload and speed changes with this format. */ + int AUDIO_OFFLOAD_SPEED_CHANGE_SUPPORTED = 0b100 << 9; + /** The renderer supports audio offload and gapless transitions with this format. */ + int AUDIO_OFFLOAD_GAPLESS_SUPPORTED = 0b10 << 9; + /** The renderer supports audio offload with this format. */ + int AUDIO_OFFLOAD_SUPPORTED = 0b1 << 9; + /** Audio offload is not supported with this format. */ + int AUDIO_OFFLOAD_NOT_SUPPORTED = 0; + /** * Combined renderer capabilities. * *

    This is a bitwise OR of {@link C.FormatSupport}, {@link AdaptiveSupport}, {@link - * TunnelingSupport}, {@link HardwareAccelerationSupport} and {@link DecoderSupport}. Use {@link - * #getFormatSupport}, {@link #getAdaptiveSupport}, {@link #getTunnelingSupport}, {@link - * #getHardwareAccelerationSupport} and {@link #getDecoderSupport} to obtain individual - * components. Use {@link #create(int)}, {@link #create(int, int, int)} or {@link #create(int, - * int, int, int, int)} to create combined capabilities from individual components. + * TunnelingSupport}, {@link HardwareAccelerationSupport}, {@link DecoderSupport} and {@link + * AudioOffloadSupport}. Use {@link #getFormatSupport}, {@link #getAdaptiveSupport}, {@link + * #getTunnelingSupport}, {@link #getHardwareAccelerationSupport}, {@link #getDecoderSupport} and + * {@link AudioOffloadSupport} to obtain individual components. Use {@link #create(int)}, {@link + * #create(int, int, int)}, {@link #create(int, int, int, int)}, or {@link #create(int, int, int, + * int, int, int)} to create combined capabilities from individual components. * *

    Possible values: * @@ -196,7 +230,14 @@ public interface RendererCapabilities { * of {@link #HARDWARE_ACCELERATION_SUPPORTED} and {@link * #HARDWARE_ACCELERATION_NOT_SUPPORTED}. *

  • {@link DecoderSupport}: The level of decoder support. One of {@link - * #DECODER_SUPPORT_PRIMARY} and {@link #DECODER_SUPPORT_FALLBACK}. + * #DECODER_SUPPORT_PRIMARY}, {@link #DECODER_SUPPORT_FALLBACK}, or {@link + * #DECODER_SUPPORT_FALLBACK_MIMETYPE}. + *
  • {@link AudioOffloadSupport}: The level of offload support. Value will have the flag + * {@link #AUDIO_OFFLOAD_SUPPORTED} or be {@link #AUDIO_OFFLOAD_NOT_SUPPORTED}. In addition, + * if it is {@link #AUDIO_OFFLOAD_SUPPORTED}, then one can check for {@link + * #AUDIO_OFFLOAD_SPEED_CHANGE_SUPPORTED} and {@link #AUDIO_OFFLOAD_GAPLESS_SUPPORTED}. + * These represent speed change and gapless transition support with audio offload + * respectively. *
*/ @Documented @@ -211,23 +252,30 @@ public interface RendererCapabilities { * *

{@link AdaptiveSupport} is set to {@link #ADAPTIVE_NOT_SUPPORTED}, {@link TunnelingSupport} * is set to {@link #TUNNELING_NOT_SUPPORTED}, {@link HardwareAccelerationSupport} is set to - * {@link #HARDWARE_ACCELERATION_NOT_SUPPORTED} and {@link DecoderSupport} is set to {@link - * #DECODER_SUPPORT_PRIMARY}. + * {@link #HARDWARE_ACCELERATION_NOT_SUPPORTED}, {@link DecoderSupport} is set to {@link + * #DECODER_SUPPORT_PRIMARY} and {@link AudioOffloadSupport} is set to {@link + * #AUDIO_OFFLOAD_NOT_SUPPORTED}. * * @param formatSupport The {@link C.FormatSupport}. * @return The combined {@link Capabilities} of the given {@link C.FormatSupport}, {@link - * #ADAPTIVE_NOT_SUPPORTED} and {@link #TUNNELING_NOT_SUPPORTED}. + * #ADAPTIVE_NOT_SUPPORTED}, {@link #TUNNELING_NOT_SUPPORTED} and {@link + * #AUDIO_OFFLOAD_NOT_SUPPORTED}. */ static @Capabilities int create(@C.FormatSupport int formatSupport) { - return create(formatSupport, ADAPTIVE_NOT_SUPPORTED, TUNNELING_NOT_SUPPORTED); + return create( + formatSupport, + ADAPTIVE_NOT_SUPPORTED, + TUNNELING_NOT_SUPPORTED, + AUDIO_OFFLOAD_NOT_SUPPORTED); } /** * Returns {@link Capabilities} combining the given {@link C.FormatSupport}, {@link * AdaptiveSupport} and {@link TunnelingSupport}. * - *

{@link HardwareAccelerationSupport} is set to {@link #HARDWARE_ACCELERATION_NOT_SUPPORTED} - * and {@link DecoderSupport} is set to {@link #DECODER_SUPPORT_PRIMARY}. + *

{@link HardwareAccelerationSupport} is set to {@link #HARDWARE_ACCELERATION_NOT_SUPPORTED}, + * {@link DecoderSupport} is set to {@link #DECODER_SUPPORT_PRIMARY}, and {@link + * AudioOffloadSupport} is set to {@link #AUDIO_OFFLOAD_NOT_SUPPORTED}. * * @param formatSupport The {@link C.FormatSupport}. * @param adaptiveSupport The {@link AdaptiveSupport}. @@ -243,14 +291,44 @@ public interface RendererCapabilities { adaptiveSupport, tunnelingSupport, HARDWARE_ACCELERATION_NOT_SUPPORTED, - DECODER_SUPPORT_PRIMARY); + DECODER_SUPPORT_PRIMARY, + AUDIO_OFFLOAD_NOT_SUPPORTED); } /** * Returns {@link Capabilities} combining the given {@link C.FormatSupport}, {@link - * AdaptiveSupport}, {@link TunnelingSupport}, {@link HardwareAccelerationSupport} and {@link + * AdaptiveSupport}, {@link TunnelingSupport}, and {@link AudioOffloadSupport}. + * + *

{@link HardwareAccelerationSupport} is set to {@link #HARDWARE_ACCELERATION_NOT_SUPPORTED} + * and {@link DecoderSupport} is set to {@link #DECODER_SUPPORT_PRIMARY}. + * + * @param formatSupport The {@link C.FormatSupport}. + * @param adaptiveSupport The {@link AdaptiveSupport}. + * @param tunnelingSupport The {@link TunnelingSupport}. + * @param audioOffloadSupport The {@link AudioOffloadSupport}. + * @return The combined {@link Capabilities}. + */ + static @Capabilities int create( + @C.FormatSupport int formatSupport, + @AdaptiveSupport int adaptiveSupport, + @TunnelingSupport int tunnelingSupport, + @AudioOffloadSupport int audioOffloadSupport) { + return create( + formatSupport, + adaptiveSupport, + tunnelingSupport, + HARDWARE_ACCELERATION_NOT_SUPPORTED, + DECODER_SUPPORT_PRIMARY, + audioOffloadSupport); + } + + /** + * Returns {@link Capabilities} combining the given {@link C.FormatSupport}, {@link + * AdaptiveSupport}, {@link TunnelingSupport}, {@link HardwareAccelerationSupport}, and {@link * DecoderSupport}. * + *

{@link AudioOffloadSupport} is set to {@link #AUDIO_OFFLOAD_NOT_SUPPORTED}. + * * @param formatSupport The {@link C.FormatSupport}. * @param adaptiveSupport The {@link AdaptiveSupport}. * @param tunnelingSupport The {@link TunnelingSupport}. @@ -258,6 +336,34 @@ public interface RendererCapabilities { * @param decoderSupport The {@link DecoderSupport}. * @return The combined {@link Capabilities}. */ + static @Capabilities int create( + @C.FormatSupport int formatSupport, + @AdaptiveSupport int adaptiveSupport, + @TunnelingSupport int tunnelingSupport, + @HardwareAccelerationSupport int hardwareAccelerationSupport, + @DecoderSupport int decoderSupport) { + return create( + formatSupport, + adaptiveSupport, + tunnelingSupport, + hardwareAccelerationSupport, + decoderSupport, + AUDIO_OFFLOAD_NOT_SUPPORTED); + } + + /** + * Returns {@link Capabilities} combining the given {@link C.FormatSupport}, {@link + * AdaptiveSupport}, {@link TunnelingSupport}, {@link HardwareAccelerationSupport}, {@link + * DecoderSupport} and {@link AudioOffloadSupport}. + * + * @param formatSupport The {@link C.FormatSupport}. + * @param adaptiveSupport The {@link AdaptiveSupport}. + * @param tunnelingSupport The {@link TunnelingSupport}. + * @param hardwareAccelerationSupport The {@link HardwareAccelerationSupport}. + * @param decoderSupport The {@link DecoderSupport}. + * @param audioOffloadSupport The {@link AudioOffloadSupport} + * @return The combined {@link Capabilities}. + */ // Suppression needed for IntDef casting. @SuppressLint("WrongConstant") static @Capabilities int create( @@ -265,12 +371,14 @@ public interface RendererCapabilities { @AdaptiveSupport int adaptiveSupport, @TunnelingSupport int tunnelingSupport, @HardwareAccelerationSupport int hardwareAccelerationSupport, - @DecoderSupport int decoderSupport) { + @DecoderSupport int decoderSupport, + @AudioOffloadSupport int audioOffloadSupport) { return formatSupport | adaptiveSupport | tunnelingSupport | hardwareAccelerationSupport - | decoderSupport; + | decoderSupport + | audioOffloadSupport; } /** @@ -331,7 +439,19 @@ public interface RendererCapabilities { // Suppression needed for IntDef casting. @SuppressLint("WrongConstant") static @DecoderSupport int getDecoderSupport(@Capabilities int supportFlags) { - return supportFlags & MODE_SUPPORT_MASK; + return supportFlags & DECODER_SUPPORT_MASK; + } + + /** + * Returns the {@link AudioOffloadSupport} from the combined {@link Capabilities}. + * + * @param supportFlags The combined {@link Capabilities}. + * @return The {@link AudioOffloadSupport} only. + */ + // Suppression needed for IntDef casting. + @SuppressLint("WrongConstant") + static @AudioOffloadSupport int getAudioOffloadSupport(@Capabilities int supportFlags) { + return supportFlags & AUDIO_OFFLOAD_SUPPORT_MASK; } /** Returns the name of the {@link Renderer}. */ diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/RendererConfiguration.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/RendererConfiguration.java index 452ec4fcaf..b5fa8c3501 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/RendererConfiguration.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/RendererConfiguration.java @@ -15,8 +15,11 @@ */ package androidx.media3.exoplayer; +import static androidx.media3.exoplayer.audio.AudioSink.OFFLOAD_MODE_DISABLED; + import androidx.annotation.Nullable; import androidx.media3.common.util.UnstableApi; +import androidx.media3.exoplayer.audio.AudioSink; /** The configuration of a {@link Renderer}. */ @UnstableApi @@ -24,15 +27,34 @@ public final class RendererConfiguration { /** The default configuration. */ public static final RendererConfiguration DEFAULT = - new RendererConfiguration(/* tunneling= */ false); + new RendererConfiguration( + /* offloadModePreferred= */ OFFLOAD_MODE_DISABLED, /* tunneling= */ false); + + /** The offload mode preference with which to configure the renderer. */ + public final @AudioSink.OffloadMode int offloadModePreferred; /** Whether to enable tunneling. */ public final boolean tunneling; /** + * Creates an instance with {@code tunneling} and sets {@link #offloadModePreferred} to {@link + * AudioSink#OFFLOAD_MODE_DISABLED}. + * * @param tunneling Whether to enable tunneling. */ public RendererConfiguration(boolean tunneling) { + this.offloadModePreferred = OFFLOAD_MODE_DISABLED; + this.tunneling = tunneling; + } + + /** + * Creates an instance. + * + * @param offloadModePreferred The offload mode to use. + * @param tunneling Whether to enable tunneling. + */ + public RendererConfiguration(@AudioSink.OffloadMode int offloadModePreferred, boolean tunneling) { + this.offloadModePreferred = offloadModePreferred; this.tunneling = tunneling; } @@ -45,11 +67,13 @@ public final class RendererConfiguration { return false; } RendererConfiguration other = (RendererConfiguration) obj; - return tunneling == other.tunneling; + return offloadModePreferred == other.offloadModePreferred && tunneling == other.tunneling; } @Override public int hashCode() { - return tunneling ? 0 : 1; + int hashCode = offloadModePreferred << 1; + hashCode += (tunneling ? 1 : 0); + return hashCode; } } diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/AudioOffloadSupport.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/AudioOffloadSupport.java new file mode 100644 index 0000000000..5df205778f --- /dev/null +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/AudioOffloadSupport.java @@ -0,0 +1,134 @@ +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package androidx.media3.exoplayer.audio; + +import androidx.annotation.Nullable; +import androidx.media3.common.util.UnstableApi; +import com.google.errorprone.annotations.CanIgnoreReturnValue; + +/** Represents the support capabilities for audio offload playback. */ +@UnstableApi +public final class AudioOffloadSupport { + + /** The default configuration. */ + public static final AudioOffloadSupport DEFAULT_UNSUPPORTED = + new AudioOffloadSupport.Builder().build(); + + /** A builder to create {@link AudioOffloadSupport} instances. */ + public static final class Builder { + /** Whether the format is supported with offload playback. */ + private boolean isFormatSupported; + /** Whether playback of the format is supported with gapless transitions. */ + private boolean isGaplessSupported; + /** Whether playback of the format is supported with speed changes. */ + private boolean isSpeedChangeSupported; + + public Builder() {} + + public Builder(AudioOffloadSupport audioOffloadSupport) { + isFormatSupported = audioOffloadSupport.isFormatSupported; + isGaplessSupported = audioOffloadSupport.isGaplessSupported; + isSpeedChangeSupported = audioOffloadSupport.isSpeedChangeSupported; + } + + /** + * Sets if media format is supported in offload playback. + * + *

Default is {@code false}. + */ + @CanIgnoreReturnValue + public Builder setIsFormatSupported(boolean isFormatSupported) { + this.isFormatSupported = isFormatSupported; + return this; + } + + /** + * Sets whether playback of the format is supported with gapless transitions. + * + *

Default is {@code false}. + */ + @CanIgnoreReturnValue + public Builder setIsGaplessSupported(boolean isGaplessSupported) { + this.isGaplessSupported = isGaplessSupported; + return this; + } + + /** + * Sets whether playback of the format is supported with speed changes. + * + *

Default is {@code false}. + */ + @CanIgnoreReturnValue + public Builder setIsSpeedChangeSupported(boolean isSpeedChangeSupported) { + this.isSpeedChangeSupported = isSpeedChangeSupported; + return this; + } + + /** + * Builds the {@link AudioOffloadSupport}. + * + * @throws IllegalStateException If either {@link #isGaplessSupported} or {@link + * #isSpeedChangeSupported} are true when {@link #isFormatSupported} is false. + */ + public AudioOffloadSupport build() { + if (!isFormatSupported && (isGaplessSupported || isSpeedChangeSupported)) { + throw new IllegalStateException( + "Secondary offload attribute fields are true but primary isFormatSupported is false"); + } + return new AudioOffloadSupport(this); + } + } + + /** Whether the format is supported with offload playback. */ + public final boolean isFormatSupported; + /** Whether playback of the format is supported with gapless transitions. */ + public final boolean isGaplessSupported; + /** Whether playback of the format is supported with speed changes. */ + public final boolean isSpeedChangeSupported; + + private AudioOffloadSupport(AudioOffloadSupport.Builder builder) { + this.isFormatSupported = builder.isFormatSupported; + this.isGaplessSupported = builder.isGaplessSupported; + this.isSpeedChangeSupported = builder.isSpeedChangeSupported; + } + + /** Creates a new {@link Builder}, copying the initial values from this instance. */ + public Builder buildUpon() { + return new Builder(this); + } + + @Override + public boolean equals(@Nullable Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + AudioOffloadSupport other = (AudioOffloadSupport) obj; + return isFormatSupported == other.isFormatSupported + && isGaplessSupported == other.isGaplessSupported + && isSpeedChangeSupported == other.isSpeedChangeSupported; + } + + @Override + public int hashCode() { + int hashCode = (isFormatSupported ? 1 : 0) << 2; + hashCode += (isGaplessSupported ? 1 : 0) << 1; + hashCode += (isSpeedChangeSupported ? 1 : 0); + return hashCode; + } +} 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 a056432dca..00a441507e 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 @@ -285,6 +285,39 @@ public interface AudioSink { /** Returned by {@link #getCurrentPositionUs(boolean)} when the position is not set. */ long CURRENT_POSITION_NOT_SET = Long.MIN_VALUE; + /** + * Audio offload mode configuration. One of {@link #OFFLOAD_MODE_DISABLED}, {@link + * #OFFLOAD_MODE_ENABLED_GAPLESS_REQUIRED} or {@link #OFFLOAD_MODE_ENABLED_GAPLESS_NOT_REQUIRED}. + */ + @Documented + @Retention(RetentionPolicy.SOURCE) + @Target(TYPE_USE) + @IntDef({ + OFFLOAD_MODE_DISABLED, + OFFLOAD_MODE_ENABLED_GAPLESS_REQUIRED, + OFFLOAD_MODE_ENABLED_GAPLESS_NOT_REQUIRED + }) + @interface OffloadMode {} + + /** The audio sink will never play in offload mode. */ + int OFFLOAD_MODE_DISABLED = 0; + /** + * The audio sink will prefer offload playback except in the case where both the track is gapless + * and the device does support gapless offload playback. + * + *

Use this option to prioritize uninterrupted playback of consecutive audio tracks over power + * savings. + */ + int OFFLOAD_MODE_ENABLED_GAPLESS_REQUIRED = 1; + /** + * The audio sink will prefer offload playback even if this might result in silence gaps between + * tracks. + * + *

Use this option to prioritize battery saving at the cost of a possible non seamless + * transitions between tracks of the same album. + */ + int OFFLOAD_MODE_ENABLED_GAPLESS_NOT_REQUIRED = 2; + /** * Sets the listener for sink events, which should be the audio renderer. * @@ -316,6 +349,16 @@ public interface AudioSink { @SinkFormatSupport int getFormatSupport(Format format); + /** + * Returns the level of offload support that the sink can provide for a given {@link Format}. + * + * @param format The format. + * @return The level of support provided. + */ + default AudioOffloadSupport getFormatOffloadSupport(Format format) { + return AudioOffloadSupport.DEFAULT_UNSUPPORTED; + } + /** * Returns the playback position in the stream starting at zero, in microseconds, or {@link * #CURRENT_POSITION_NOT_SET} if it is not yet available. @@ -459,6 +502,14 @@ public interface AudioSink { */ void disableTunneling(); + /** + * Sets audio offload mode, if possible. Enabling offload is only possible if the sink is based on + * a platform {@link AudioTrack}, and requires platform API version 29 onwards. + * + * @throws IllegalStateException Thrown if enabling offload on platform API version < 29. + */ + default void setOffloadMode(@OffloadMode int offloadMode) {} + /** * Sets the playback volume. * diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/DefaultAudioOffloadSupportProvider.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/DefaultAudioOffloadSupportProvider.java new file mode 100644 index 0000000000..64d405db7f --- /dev/null +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/DefaultAudioOffloadSupportProvider.java @@ -0,0 +1,172 @@ +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package androidx.media3.exoplayer.audio; + +import static androidx.media3.common.util.Assertions.checkNotNull; + +import android.content.Context; +import android.media.AudioFormat; +import android.media.AudioManager; +import androidx.annotation.DoNotInline; +import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; +import androidx.media3.common.AudioAttributes; +import androidx.media3.common.C; +import androidx.media3.common.Format; +import androidx.media3.common.MimeTypes; +import androidx.media3.common.util.UnstableApi; +import androidx.media3.common.util.Util; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; + +/** + * Provides the {@link AudioOffloadSupport} capabilities for a {@link Format} and {@link + * AudioAttributes}. + */ +@UnstableApi +public final class DefaultAudioOffloadSupportProvider + implements DefaultAudioSink.AudioOffloadSupportProvider { + + /** AudioManager parameters key for retrieving support of variable speeds during offload. */ + private static final String OFFLOAD_VARIABLE_RATE_SUPPORTED_KEY = "offloadVariableRateSupported"; + + @Nullable private final Context context; + + /** + * Whether variable speeds are supported during offload. If {@code null} then it has not been + * attempted to retrieve value from {@link AudioManager}. + */ + private @MonotonicNonNull Boolean isOffloadVariableRateSupported; + + /** Creates an instance. */ + public DefaultAudioOffloadSupportProvider() { + this(/* context= */ null); + } + + /** + * Creates an instance. + * + * @param context The context used to retrieve the {@link AudioManager} parameters for checking + * offload variable rate support. + */ + public DefaultAudioOffloadSupportProvider(@Nullable Context context) { + this.context = context; + } + + @Override + public AudioOffloadSupport getAudioOffloadSupport( + Format format, AudioAttributes audioAttributes) { + checkNotNull(format); + checkNotNull(audioAttributes); + + if (Util.SDK_INT < 29) { + return AudioOffloadSupport.DEFAULT_UNSUPPORTED; + } + + // isOffloadVariableRateSupported is lazily-loaded instead of being initialized in + // the constructor so that the platform will be queried from the playback thread. + boolean isOffloadVariableRateSupported = isOffloadVariableRateSupported(context); + + @C.Encoding + int encoding = MimeTypes.getEncoding(checkNotNull(format.sampleMimeType), format.codecs); + if (encoding == C.ENCODING_INVALID) { + return AudioOffloadSupport.DEFAULT_UNSUPPORTED; + } + int channelConfig = Util.getAudioTrackChannelConfig(format.channelCount); + if (channelConfig == AudioFormat.CHANNEL_INVALID) { + return AudioOffloadSupport.DEFAULT_UNSUPPORTED; + } + + AudioFormat audioFormat = Util.getAudioFormat(format.sampleRate, channelConfig, encoding); + if (Util.SDK_INT >= 31) { + return Api31.getOffloadedPlaybackSupport( + audioFormat, + audioAttributes.getAudioAttributesV21().audioAttributes, + isOffloadVariableRateSupported); + } + return Api29.getOffloadedPlaybackSupport( + audioFormat, + audioAttributes.getAudioAttributesV21().audioAttributes, + isOffloadVariableRateSupported); + } + + private boolean isOffloadVariableRateSupported(@Nullable Context context) { + if (isOffloadVariableRateSupported != null) { + return isOffloadVariableRateSupported; + } + + if (context != null) { + AudioManager audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); + if (audioManager != null) { + String offloadVariableRateSupportedKeyValue = + audioManager.getParameters(/* keys= */ OFFLOAD_VARIABLE_RATE_SUPPORTED_KEY); + isOffloadVariableRateSupported = + offloadVariableRateSupportedKeyValue != null + && offloadVariableRateSupportedKeyValue.equals( + OFFLOAD_VARIABLE_RATE_SUPPORTED_KEY + "=1"); + } else { + isOffloadVariableRateSupported = false; + } + } else { + isOffloadVariableRateSupported = false; + } + return isOffloadVariableRateSupported; + } + + @RequiresApi(29) + private static final class Api29 { + private Api29() {} + + @DoNotInline + public static AudioOffloadSupport getOffloadedPlaybackSupport( + AudioFormat audioFormat, + android.media.AudioAttributes audioAttributes, + boolean isOffloadVariableRateSupported) { + if (!AudioManager.isOffloadedPlaybackSupported(audioFormat, audioAttributes)) { + return AudioOffloadSupport.DEFAULT_UNSUPPORTED; + } + return new AudioOffloadSupport.Builder() + .setIsFormatSupported(true) + .setIsSpeedChangeSupported(isOffloadVariableRateSupported) + // Manual testing has shown that Pixels on Android 11 support gapless offload. + .setIsGaplessSupported(Util.SDK_INT == 30 && Util.MODEL.startsWith("Pixel")) + .build(); + } + } + + @RequiresApi(31) + private static final class Api31 { + private Api31() {} + + @DoNotInline + public static AudioOffloadSupport getOffloadedPlaybackSupport( + AudioFormat audioFormat, + android.media.AudioAttributes audioAttributes, + boolean isOffloadVariableRateSupported) { + int playbackOffloadSupport = + AudioManager.getPlaybackOffloadSupport(audioFormat, audioAttributes); + if (playbackOffloadSupport == AudioManager.PLAYBACK_OFFLOAD_NOT_SUPPORTED) { + return AudioOffloadSupport.DEFAULT_UNSUPPORTED; + } + AudioOffloadSupport.Builder audioOffloadSupport = new AudioOffloadSupport.Builder(); + return audioOffloadSupport + .setIsFormatSupported(true) + .setIsGaplessSupported( + playbackOffloadSupport == AudioManager.PLAYBACK_OFFLOAD_GAPLESS_SUPPORTED) + .setIsSpeedChangeSupported(isOffloadVariableRateSupported) + .build(); + } + } +} 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 a1907c6ad7..9e57a88c0b 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 @@ -25,11 +25,9 @@ import static java.lang.Math.max; import static java.lang.Math.min; import static java.lang.annotation.ElementType.TYPE_USE; -import android.annotation.SuppressLint; import android.content.Context; import android.media.AudioDeviceInfo; import android.media.AudioFormat; -import android.media.AudioManager; import android.media.AudioTrack; import android.media.PlaybackParams; import android.media.metrics.LogSessionId; @@ -227,6 +225,23 @@ public final class DefaultAudioSink implements AudioSink { double maxAudioTrackPlaybackSpeed); } + /** + * Provides the {@link AudioOffloadSupport} to convey the level of offload support the sink can + * provide. + */ + public interface AudioOffloadSupportProvider { + /** + * Returns the {@link AudioOffloadSupport} the audio sink can provide for the media based on its + * {@link Format} and {@link AudioAttributes} + * + * @param format The {@link Format}. + * @param audioAttributes The {@link AudioAttributes}. + * @return The {@link AudioOffloadSupport} the sink can provide for the media based on its + * {@link Format} and {@link AudioAttributes}. + */ + AudioOffloadSupport getAudioOffloadSupport(Format format, AudioAttributes audioAttributes); + } + /** A builder to create {@link DefaultAudioSink} instances. */ public static final class Builder { @@ -235,9 +250,11 @@ public final class DefaultAudioSink implements AudioSink { @Nullable private androidx.media3.common.audio.AudioProcessorChain audioProcessorChain; private boolean enableFloatOutput; private boolean enableAudioTrackPlaybackParams; - private int offloadMode; - AudioTrackBufferSizeProvider audioTrackBufferSizeProvider; - @Nullable AudioOffloadListener audioOffloadListener; + + private boolean buildCalled; + private AudioTrackBufferSizeProvider audioTrackBufferSizeProvider; + private @MonotonicNonNull AudioOffloadSupportProvider audioOffloadSupportProvider; + @Nullable private AudioOffloadListener audioOffloadListener; /** * @deprecated Use {@link #Builder(Context)} instead. @@ -246,7 +263,6 @@ public final class DefaultAudioSink implements AudioSink { public Builder() { this.context = null; audioCapabilities = DEFAULT_AUDIO_CAPABILITIES; - offloadMode = OFFLOAD_MODE_DISABLED; audioTrackBufferSizeProvider = AudioTrackBufferSizeProvider.DEFAULT; } @@ -258,7 +274,6 @@ public final class DefaultAudioSink implements AudioSink { public Builder(Context context) { this.context = context; audioCapabilities = DEFAULT_AUDIO_CAPABILITIES; - offloadMode = OFFLOAD_MODE_DISABLED; audioTrackBufferSizeProvider = AudioTrackBufferSizeProvider.DEFAULT; } @@ -332,22 +347,6 @@ public final class DefaultAudioSink implements AudioSink { return this; } - /** - * Sets the offload mode. If an audio format can be both played with offload and encoded audio - * passthrough, it will be played in offload. Audio offload is supported from API level 29. Most - * Android devices can only support one offload {@link AudioTrack} at a time and can invalidate - * it at any time. Thus an app can never be guaranteed that it will be able to play in offload. - * Audio processing (for example, speed adjustment) will not be available when offload is in - * use. - * - *

The default value is {@link #OFFLOAD_MODE_DISABLED}. - */ - @CanIgnoreReturnValue - public Builder setOffloadMode(@OffloadMode int offloadMode) { - this.offloadMode = offloadMode; - return this; - } - /** * Sets an {@link AudioTrackBufferSizeProvider} to compute the buffer size when {@link * #configure} is called with {@code specifiedBufferSize == 0}. @@ -361,6 +360,21 @@ public final class DefaultAudioSink implements AudioSink { return this; } + /** + * Sets an {@link AudioOffloadSupportProvider} to provide the sink's offload support + * capabilities for a given {@link Format} and {@link AudioAttributes} for calls to {@link + * #getFormatOffloadSupport(Format)}. + * + *

If this setter is not called, then the {@link DefaultAudioSink} uses an instance of {@link + * DefaultAudioOffloadSupportProvider}. + */ + @CanIgnoreReturnValue + public Builder setAudioOffloadSupportProvider( + AudioOffloadSupportProvider audioOffloadSupportProvider) { + this.audioOffloadSupportProvider = audioOffloadSupportProvider; + return this; + } + /** * Sets an optional {@link AudioOffloadListener} to receive events relevant to offloaded * playback. @@ -376,9 +390,14 @@ public final class DefaultAudioSink implements AudioSink { /** Builds the {@link DefaultAudioSink}. Must only be called once per Builder instance. */ public DefaultAudioSink build() { + checkState(!buildCalled); + buildCalled = true; if (audioProcessorChain == null) { audioProcessorChain = new DefaultAudioProcessorChain(); } + if (audioOffloadSupportProvider == null) { + audioOffloadSupportProvider = new DefaultAudioOffloadSupportProvider(context); + } return new DefaultAudioSink(this); } } @@ -397,44 +416,6 @@ public final class DefaultAudioSink implements AudioSink { /** The default skip silence flag. */ private static final boolean DEFAULT_SKIP_SILENCE = false; - /** Audio offload mode configuration. */ - @Documented - @Retention(RetentionPolicy.SOURCE) - @Target(TYPE_USE) - @IntDef({ - OFFLOAD_MODE_DISABLED, - OFFLOAD_MODE_ENABLED_GAPLESS_REQUIRED, - OFFLOAD_MODE_ENABLED_GAPLESS_NOT_REQUIRED, - OFFLOAD_MODE_ENABLED_GAPLESS_DISABLED - }) - public @interface OffloadMode {} - - /** The audio sink will never play in offload mode. */ - public static final int OFFLOAD_MODE_DISABLED = 0; - /** - * The audio sink will prefer offload playback except if the track is gapless and the device does - * not advertise support for gapless playback in offload. - * - *

Use this option to prioritize seamless transitions between tracks of the same album to power - * savings. - */ - public static final int OFFLOAD_MODE_ENABLED_GAPLESS_REQUIRED = 1; - /** - * The audio sink will prefer offload playback even if this might result in silence gaps between - * tracks. - * - *

Use this option to prioritize battery saving at the cost of a possible non seamless - * transitions between tracks of the same album. - */ - public static final int OFFLOAD_MODE_ENABLED_GAPLESS_NOT_REQUIRED = 2; - /** - * The audio sink will prefer offload playback, disabling gapless offload support. - * - *

Use this option if gapless has undesirable side effects. For example if it introduces - * hardware issues. - */ - public static final int OFFLOAD_MODE_ENABLED_GAPLESS_DISABLED = 3; - /** Output mode of the audio sink. */ @Documented @Retention(RetentionPolicy.SOURCE) @@ -498,12 +479,13 @@ public final class DefaultAudioSink implements AudioSink { private final AudioTrackPositionTracker audioTrackPositionTracker; private final ArrayDeque mediaPositionParametersCheckpoints; private final boolean preferAudioTrackPlaybackParams; - private final @OffloadMode int offloadMode; + private @OffloadMode int offloadMode; private @MonotonicNonNull StreamEventCallbackV29 offloadStreamEventCallbackV29; private final PendingExceptionHolder initializationExceptionPendingExceptionHolder; private final PendingExceptionHolder writeExceptionPendingExceptionHolder; private final AudioTrackBufferSizeProvider audioTrackBufferSizeProvider; + private final AudioOffloadSupportProvider audioOffloadSupportProvider; @Nullable private final AudioOffloadListener audioOffloadListener; @Nullable private PlayerId playerId; @@ -561,8 +543,9 @@ public final class DefaultAudioSink implements AudioSink { audioProcessorChain = builder.audioProcessorChain; enableFloatOutput = Util.SDK_INT >= 21 && builder.enableFloatOutput; preferAudioTrackPlaybackParams = Util.SDK_INT >= 23 && builder.enableAudioTrackPlaybackParams; - offloadMode = Util.SDK_INT >= 29 ? builder.offloadMode : OFFLOAD_MODE_DISABLED; + offloadMode = OFFLOAD_MODE_DISABLED; audioTrackBufferSizeProvider = builder.audioTrackBufferSizeProvider; + audioOffloadSupportProvider = checkNotNull(builder.audioOffloadSupportProvider); releasingConditionVariable = new ConditionVariable(Clock.DEFAULT); releasingConditionVariable.open(); audioTrackPositionTracker = new AudioTrackPositionTracker(new PositionTrackerListener()); @@ -621,15 +604,20 @@ public final class DefaultAudioSink implements AudioSink { // guaranteed to support. return SINK_FORMAT_SUPPORTED_WITH_TRANSCODING; } - if (!offloadDisabledUntilNextConfiguration && useOffloadedPlayback(format, audioAttributes)) { - return SINK_FORMAT_SUPPORTED_DIRECTLY; - } if (getAudioCapabilities().isPassthroughPlaybackSupported(format)) { return SINK_FORMAT_SUPPORTED_DIRECTLY; } return SINK_FORMAT_UNSUPPORTED; } + @Override + public AudioOffloadSupport getFormatOffloadSupport(Format format) { + if (offloadDisabledUntilNextConfiguration) { + return AudioOffloadSupport.DEFAULT_UNSUPPORTED; + } + return audioOffloadSupportProvider.getAudioOffloadSupport(format, audioAttributes); + } + @Override public long getCurrentPositionUs(boolean sourceEnded) { if (!isAudioTrackInitialized() || startMediaTimeUsNeedsInit) { @@ -651,6 +639,7 @@ public final class DefaultAudioSink implements AudioSink { int outputChannelConfig; int outputPcmFrameSize; boolean enableAudioTrackPlaybackParams; + boolean enableOffloadGapless = false; if (MimeTypes.AUDIO_RAW.equals(inputFormat.sampleMimeType)) { Assertions.checkArgument(Util.isEncodingLinearPcm(inputFormat.pcmEncoding)); @@ -706,13 +695,18 @@ public final class DefaultAudioSink implements AudioSink { inputPcmFrameSize = C.LENGTH_UNSET; outputSampleRate = inputFormat.sampleRate; outputPcmFrameSize = C.LENGTH_UNSET; - if (useOffloadedPlayback(inputFormat, audioAttributes)) { + AudioOffloadSupport audioOffloadSupport = + offloadMode != OFFLOAD_MODE_DISABLED + ? getFormatOffloadSupport(inputFormat) + : AudioOffloadSupport.DEFAULT_UNSUPPORTED; + if (offloadMode != OFFLOAD_MODE_DISABLED && audioOffloadSupport.isFormatSupported) { outputMode = OUTPUT_MODE_OFFLOAD; outputEncoding = MimeTypes.getEncoding(checkNotNull(inputFormat.sampleMimeType), inputFormat.codecs); outputChannelConfig = Util.getAudioTrackChannelConfig(inputFormat.channelCount); // Offload requires AudioTrack playback parameters to apply speed changes quickly. enableAudioTrackPlaybackParams = true; + enableOffloadGapless = audioOffloadSupport.isGaplessSupported; } else { outputMode = OUTPUT_MODE_PASSTHROUGH; @Nullable @@ -750,7 +744,6 @@ public final class DefaultAudioSink implements AudioSink { outputSampleRate, inputFormat.bitrate, enableAudioTrackPlaybackParams ? MAX_PLAYBACK_SPEED : DEFAULT_PLAYBACK_SPEED); - offloadDisabledUntilNextConfiguration = false; Configuration pendingConfiguration = new Configuration( @@ -763,7 +756,8 @@ public final class DefaultAudioSink implements AudioSink { outputEncoding, bufferSize, audioProcessingPipeline, - enableAudioTrackPlaybackParams); + enableAudioTrackPlaybackParams, + enableOffloadGapless); if (isAudioTrackInitialized()) { this.pendingConfiguration = pendingConfiguration; } else { @@ -789,7 +783,7 @@ public final class DefaultAudioSink implements AudioSink { audioTrack = buildAudioTrackWithRetry(); if (isOffloadedPlayback(audioTrack)) { registerStreamEventCallbackV29(audioTrack); - if (offloadMode != OFFLOAD_MODE_ENABLED_GAPLESS_DISABLED) { + if (configuration.enableOffloadGapless) { audioTrack.setOffloadDelayPadding( configuration.inputFormat.encoderDelay, configuration.inputFormat.encoderPadding); } @@ -854,8 +848,9 @@ public final class DefaultAudioSink implements AudioSink { // The current audio track can be reused for the new configuration. configuration = pendingConfiguration; pendingConfiguration = null; - if (isOffloadedPlayback(audioTrack) - && offloadMode != OFFLOAD_MODE_ENABLED_GAPLESS_DISABLED) { + if (audioTrack != null + && isOffloadedPlayback(audioTrack) + && configuration.enableOffloadGapless) { // If the first track is very short (typically <1s), the offload AudioTrack might // not have started yet. Do not call setOffloadEndOfStream as it would throw. if (audioTrack.getPlayState() == AudioTrack.PLAYSTATE_PLAYING) { @@ -1350,6 +1345,12 @@ public final class DefaultAudioSink implements AudioSink { } } + @Override + public void setOffloadMode(@OffloadMode int offloadMode) { + Assertions.checkState(Util.SDK_INT >= 29); + this.offloadMode = offloadMode; + } + @Override public void setVolume(float volume) { if (this.volume != volume) { @@ -1660,36 +1661,6 @@ public final class DefaultAudioSink implements AudioSink { : writtenEncodedFrames; } - private boolean useOffloadedPlayback(Format format, AudioAttributes audioAttributes) { - if (Util.SDK_INT < 29 || offloadMode == OFFLOAD_MODE_DISABLED) { - return false; - } - @C.Encoding - int encoding = MimeTypes.getEncoding(checkNotNull(format.sampleMimeType), format.codecs); - if (encoding == C.ENCODING_INVALID) { - return false; - } - int channelConfig = Util.getAudioTrackChannelConfig(format.channelCount); - if (channelConfig == AudioFormat.CHANNEL_INVALID) { - return false; - } - AudioFormat audioFormat = getAudioFormat(format.sampleRate, channelConfig, encoding); - - switch (getOffloadedPlaybackSupport( - audioFormat, audioAttributes.getAudioAttributesV21().audioAttributes)) { - case AudioManager.PLAYBACK_OFFLOAD_NOT_SUPPORTED: - return false; - case AudioManager.PLAYBACK_OFFLOAD_SUPPORTED: - boolean isGapless = format.encoderDelay != 0 || format.encoderPadding != 0; - boolean gaplessSupportRequired = offloadMode == OFFLOAD_MODE_ENABLED_GAPLESS_REQUIRED; - return !isGapless || !gaplessSupportRequired; - case AudioManager.PLAYBACK_OFFLOAD_GAPLESS_SUPPORTED: - return true; - default: - throw new IllegalStateException(); - } - } - private AudioCapabilities getAudioCapabilities() { if (audioCapabilitiesReceiver == null && context != null) { // Must be lazily initialized to receive audio capabilities receiver listener event on the @@ -1702,23 +1673,6 @@ public final class DefaultAudioSink implements AudioSink { return audioCapabilities; } - @RequiresApi(29) - @SuppressLint("InlinedApi") - private int getOffloadedPlaybackSupport( - AudioFormat audioFormat, android.media.AudioAttributes audioAttributes) { - if (Util.SDK_INT >= 31) { - return AudioManager.getPlaybackOffloadSupport(audioFormat, audioAttributes); - } - if (!AudioManager.isOffloadedPlaybackSupported(audioFormat, audioAttributes)) { - return AudioManager.PLAYBACK_OFFLOAD_NOT_SUPPORTED; - } - // Manual testing has shown that Pixels on Android 11 support gapless offload. - if (Util.SDK_INT == 30 && Util.MODEL.startsWith("Pixel")) { - return AudioManager.PLAYBACK_OFFLOAD_GAPLESS_SUPPORTED; - } - return AudioManager.PLAYBACK_OFFLOAD_SUPPORTED; - } - private static boolean isOffloadedPlayback(AudioTrack audioTrack) { return Util.SDK_INT >= 29 && audioTrack.isOffloadedPlayback(); } @@ -1935,15 +1889,6 @@ public final class DefaultAudioSink implements AudioSink { } } - @RequiresApi(21) - private static AudioFormat getAudioFormat(int sampleRate, int channelConfig, int encoding) { - return new AudioFormat.Builder() - .setSampleRate(sampleRate) - .setChannelMask(channelConfig) - .setEncoding(encoding) - .build(); - } - private static int getAudioTrackMinBufferSize( int sampleRateInHz, int channelConfig, int encoding) { int minBufferSize = AudioTrack.getMinBufferSize(sampleRateInHz, channelConfig, encoding); @@ -2037,6 +1982,7 @@ public final class DefaultAudioSink implements AudioSink { public final int bufferSize; public final AudioProcessingPipeline audioProcessingPipeline; public final boolean enableAudioTrackPlaybackParams; + public final boolean enableOffloadGapless; public Configuration( Format inputFormat, @@ -2048,7 +1994,8 @@ public final class DefaultAudioSink implements AudioSink { int outputEncoding, int bufferSize, AudioProcessingPipeline audioProcessingPipeline, - boolean enableAudioTrackPlaybackParams) { + boolean enableAudioTrackPlaybackParams, + boolean enableOffloadGapless) { this.inputFormat = inputFormat; this.inputPcmFrameSize = inputPcmFrameSize; this.outputMode = outputMode; @@ -2059,6 +2006,7 @@ public final class DefaultAudioSink implements AudioSink { this.bufferSize = bufferSize; this.audioProcessingPipeline = audioProcessingPipeline; this.enableAudioTrackPlaybackParams = enableAudioTrackPlaybackParams; + this.enableOffloadGapless = enableOffloadGapless; } public Configuration copyWithBufferSize(int bufferSize) { @@ -2072,7 +2020,8 @@ public final class DefaultAudioSink implements AudioSink { outputEncoding, bufferSize, audioProcessingPipeline, - enableAudioTrackPlaybackParams); + enableAudioTrackPlaybackParams, + enableOffloadGapless); } /** Returns if the configurations are sufficiently compatible to reuse the audio track. */ @@ -2082,7 +2031,8 @@ public final class DefaultAudioSink implements AudioSink { && newConfiguration.outputSampleRate == outputSampleRate && newConfiguration.outputChannelConfig == outputChannelConfig && newConfiguration.outputPcmFrameSize == outputPcmFrameSize - && newConfiguration.enableAudioTrackPlaybackParams == enableAudioTrackPlaybackParams; + && newConfiguration.enableAudioTrackPlaybackParams == enableAudioTrackPlaybackParams + && newConfiguration.enableOffloadGapless == enableOffloadGapless; } public long inputFramesToDurationUs(long frameCount) { @@ -2145,7 +2095,7 @@ public final class DefaultAudioSink implements AudioSink { private AudioTrack createAudioTrackV29( boolean tunneling, AudioAttributes audioAttributes, int audioSessionId) { AudioFormat audioFormat = - getAudioFormat(outputSampleRate, outputChannelConfig, outputEncoding); + Util.getAudioFormat(outputSampleRate, outputChannelConfig, outputEncoding); android.media.AudioAttributes audioTrackAttributes = getAudioTrackAttributesV21(audioAttributes, tunneling); return new AudioTrack.Builder() @@ -2163,7 +2113,7 @@ public final class DefaultAudioSink implements AudioSink { boolean tunneling, AudioAttributes audioAttributes, int audioSessionId) { return new AudioTrack( getAudioTrackAttributesV21(audioAttributes, tunneling), - getAudioFormat(outputSampleRate, outputChannelConfig, outputEncoding), + Util.getAudioFormat(outputSampleRate, outputChannelConfig, outputEncoding), bufferSize, AudioTrack.MODE_STREAM, audioSessionId); diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/ForwardingAudioSink.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/ForwardingAudioSink.java index ffd42b41b6..ae9c2342a5 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/ForwardingAudioSink.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/ForwardingAudioSink.java @@ -56,6 +56,11 @@ public class ForwardingAudioSink implements AudioSink { return sink.getFormatSupport(format); } + @Override + public AudioOffloadSupport getFormatOffloadSupport(Format format) { + return sink.getFormatOffloadSupport(format); + } + @Override public long getCurrentPositionUs(boolean sourceEnded) { return sink.getCurrentPositionUs(sourceEnded); @@ -161,6 +166,11 @@ public class ForwardingAudioSink implements AudioSink { sink.disableTunneling(); } + @Override + public void setOffloadMode(@OffloadMode int offloadMode) { + sink.setOffloadMode(offloadMode); + } + @Override public void setVolume(float volume) { sink.setVolume(volume); 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 db4a232e78..12a9835f14 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 @@ -293,12 +293,17 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media int tunnelingSupport = Util.SDK_INT >= 21 ? TUNNELING_SUPPORTED : TUNNELING_NOT_SUPPORTED; boolean formatHasDrm = format.cryptoType != C.CRYPTO_TYPE_NONE; boolean supportsFormatDrm = supportsFormatDrm(format); + + @AudioOffloadSupport int audioOffloadSupport = AUDIO_OFFLOAD_NOT_SUPPORTED; // In direct mode, if the format has DRM then we need to use a decoder that only decrypts. - // Else we don't don't need a decoder at all. + // Else we don't need a decoder at all. if (supportsFormatDrm - && audioSink.supportsFormat(format) && (!formatHasDrm || MediaCodecUtil.getDecryptOnlyDecoderInfo() != null)) { - return RendererCapabilities.create(C.FORMAT_HANDLED, ADAPTIVE_NOT_SEAMLESS, tunnelingSupport); + audioOffloadSupport = getAudioOffloadSupport(format); + if (audioSink.supportsFormat(format)) { + return RendererCapabilities.create( + C.FORMAT_HANDLED, ADAPTIVE_NOT_SEAMLESS, tunnelingSupport, audioOffloadSupport); + } } // If the input is PCM then it will be passed directly to the sink. Hence the sink must support // the input format directly. @@ -354,7 +359,24 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media adaptiveSupport, tunnelingSupport, hardwareAccelerationSupport, - decoderSupport); + decoderSupport, + audioOffloadSupport); + } + + private @AudioOffloadSupport int getAudioOffloadSupport(Format format) { + androidx.media3.exoplayer.audio.AudioOffloadSupport audioSinkOffloadSupport = + audioSink.getFormatOffloadSupport(format); + if (!audioSinkOffloadSupport.isFormatSupported) { + return AUDIO_OFFLOAD_NOT_SUPPORTED; + } + @AudioOffloadSupport int audioOffloadSupport = AUDIO_OFFLOAD_SUPPORTED; + if (audioSinkOffloadSupport.isGaplessSupported) { + audioOffloadSupport |= AUDIO_OFFLOAD_GAPLESS_SUPPORTED; + } + if (audioSinkOffloadSupport.isSpeedChangeSupported) { + audioOffloadSupport |= AUDIO_OFFLOAD_SPEED_CHANGE_SUPPORTED; + } + return audioOffloadSupport; } @Override @@ -404,6 +426,16 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media @Override protected boolean shouldUseBypass(Format format) { + if (getConfiguration().offloadModePreferred != AudioSink.OFFLOAD_MODE_DISABLED) { + @AudioOffloadSupport int audioOffloadSupport = getAudioOffloadSupport(format); + if ((audioOffloadSupport & RendererCapabilities.AUDIO_OFFLOAD_SUPPORTED) != 0 + && (getConfiguration().offloadModePreferred + == AudioSink.OFFLOAD_MODE_ENABLED_GAPLESS_NOT_REQUIRED + || (audioOffloadSupport & RendererCapabilities.AUDIO_OFFLOAD_GAPLESS_SUPPORTED) != 0 + || (format.encoderDelay == 0 && format.encoderPadding == 0))) { + return true; + } + } return audioSink.supportsFormat(format); } @@ -542,6 +574,16 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media } } try { + if (Util.SDK_INT >= 29) { + if (isBypassEnabled() + && getConfiguration().offloadModePreferred != AudioSink.OFFLOAD_MODE_DISABLED) { + // TODO(b/280050553): Investigate potential issue where bypass is enabled for passthrough + // but offload is not supported + audioSink.setOffloadMode(getConfiguration().offloadModePreferred); + } else { + audioSink.setOffloadMode(AudioSink.OFFLOAD_MODE_DISABLED); + } + } audioSink.configure(audioSinkInputFormat, /* specifiedBufferSize= */ 0, channelMap); } catch (AudioSink.ConfigurationException e) { throw createRendererException( @@ -669,6 +711,15 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media } } + @CallSuper + @Override + protected void onProcessedOutputBuffer(long presentationTimeUs) { + super.onProcessedOutputBuffer(presentationTimeUs); + // An output buffer has been successfully processed. If this value is not set to false then + // onQueueInputBuffer on transition from offload to codec-based playback may occur early. + allowFirstBufferPositionDiscontinuity = false; + } + @Override protected void onProcessedStreamChange() { super.onProcessedStreamChange(); 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 8c0c136694..f457fb4ea4 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 @@ -592,6 +592,11 @@ public abstract class MediaCodecRenderer extends BaseRenderer { return false; } + /** Returns whether bypass is enabled by the renderer. */ + protected final boolean isBypassEnabled() { + return bypassEnabled; + } + /** * Sets an exception to be re-thrown by render. * @@ -2313,7 +2318,6 @@ public abstract class MediaCodecRenderer extends BaseRenderer { && inputFormat.sampleMimeType.equals(MimeTypes.AUDIO_OPUS)) { oggOpusAudioPacketizer.packetize(bypassSampleBuffer); } - if (!bypassBatchBuffer.append(bypassSampleBuffer)) { bypassSampleBufferPending = true; return; 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 0782dd976a..276544f405 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 @@ -15,8 +15,12 @@ */ package androidx.media3.exoplayer.trackselection; +import static androidx.media3.common.TrackSelectionParameters.AUDIO_OFFLOAD_MODE_PREFERENCE_DISABLED; 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; +import static androidx.media3.exoplayer.RendererCapabilities.AUDIO_OFFLOAD_NOT_SUPPORTED; +import static androidx.media3.exoplayer.RendererCapabilities.AUDIO_OFFLOAD_SPEED_CHANGE_SUPPORTED; import static java.lang.annotation.ElementType.TYPE_USE; import static java.util.Collections.max; @@ -59,6 +63,7 @@ import androidx.media3.exoplayer.RendererCapabilities; import androidx.media3.exoplayer.RendererCapabilities.AdaptiveSupport; import androidx.media3.exoplayer.RendererCapabilities.Capabilities; import androidx.media3.exoplayer.RendererConfiguration; +import androidx.media3.exoplayer.audio.AudioSink; import androidx.media3.exoplayer.source.MediaSource.MediaPeriodId; import androidx.media3.exoplayer.source.TrackGroupArray; import com.google.common.base.Predicate; @@ -455,6 +460,17 @@ public class DefaultTrackSelector extends MappingTrackSelector return this; } + @CanIgnoreReturnValue + @Override + public ParametersBuilder setAudioOffloadPreference( + @TrackSelectionParameters.AudioOffloadModePreference int audioOffloadModePreference, + boolean isGaplessSupportRequired, + boolean isSpeedChangeSupportRequired) { + delegate.setAudioOffloadPreference( + audioOffloadModePreference, isGaplessSupportRequired, isSpeedChangeSupportRequired); + return this; + } + // Text @CanIgnoreReturnValue @@ -2475,6 +2491,16 @@ public class DefaultTrackSelector extends MappingTrackSelector mappedTrackInfo, rendererFormatSupports, rendererConfigurations, rendererTrackSelections); } + // Configure audio renderer to use offload if appropriate. + if (parameters.audioOffloadModePreference != AUDIO_OFFLOAD_MODE_PREFERENCE_DISABLED) { + maybeConfigureRendererForOffload( + parameters, + mappedTrackInfo, + rendererFormatSupports, + rendererConfigurations, + rendererTrackSelections); + } + return Pair.create(rendererConfigurations, rendererTrackSelections); } @@ -2925,7 +2951,7 @@ public class DefaultTrackSelector extends MappingTrackSelector * if so. * * @param mappedTrackInfo Mapped track information. - * @param renderererFormatSupports The {@link Capabilities} for each mapped track, indexed by + * @param rendererFormatSupports The {@link Capabilities} for each mapped track, indexed by * renderer, track group and track (in that order). * @param rendererConfigurations The renderer configurations. Configurations may be replaced with * ones that enable tunneling as a result of this call. @@ -2933,7 +2959,7 @@ public class DefaultTrackSelector extends MappingTrackSelector */ private static void maybeConfigureRenderersForTunneling( MappedTrackInfo mappedTrackInfo, - @Capabilities int[][][] renderererFormatSupports, + @Capabilities int[][][] rendererFormatSupports, @NullableType RendererConfiguration[] rendererConfigurations, @NullableType ExoTrackSelection[] trackSelections) { // Check whether we can enable tunneling. To enable tunneling we require exactly one audio and @@ -2947,7 +2973,7 @@ public class DefaultTrackSelector extends MappingTrackSelector if ((rendererType == C.TRACK_TYPE_AUDIO || rendererType == C.TRACK_TYPE_VIDEO) && trackSelection != null) { if (rendererSupportsTunneling( - renderererFormatSupports[i], mappedTrackInfo.getTrackGroups(i), trackSelection)) { + rendererFormatSupports[i], mappedTrackInfo.getTrackGroups(i), trackSelection)) { if (rendererType == C.TRACK_TYPE_AUDIO) { if (tunnelingAudioRendererIndex != -1) { enableTunneling = false; @@ -2969,7 +2995,7 @@ public class DefaultTrackSelector extends MappingTrackSelector enableTunneling &= tunnelingAudioRendererIndex != -1 && tunnelingVideoRendererIndex != -1; if (enableTunneling) { RendererConfiguration tunnelingRendererConfiguration = - new RendererConfiguration(/* tunneling= */ true); + new RendererConfiguration(AudioSink.OFFLOAD_MODE_DISABLED, /* tunneling= */ true); rendererConfigurations[tunnelingAudioRendererIndex] = tunnelingRendererConfiguration; rendererConfigurations[tunnelingVideoRendererIndex] = tunnelingRendererConfiguration; } @@ -3003,6 +3029,97 @@ public class DefaultTrackSelector extends MappingTrackSelector return true; } + /** + * Determines whether audio offload can be enabled, replacing a {@link RendererConfiguration} in + * {@code rendererConfigurations} with one that enables audio offload on the appropriate renderer. + * + * @param parameters The selection parameters with audio offload mode preferences. + * @param mappedTrackInfo Mapped track information. + * @param rendererFormatSupports The {@link Capabilities} for each mapped track, indexed by + * renderer, track group and track (in that order). + * @param rendererConfigurations The renderer configurations. A configuration may be replaced with + * one that enables audio offload as a result of this call. + * @param trackSelections The renderer track selections. + */ + private static void maybeConfigureRendererForOffload( + Parameters parameters, + MappedTrackInfo mappedTrackInfo, + @Capabilities int[][][] rendererFormatSupports, + @NullableType RendererConfiguration[] rendererConfigurations, + @NullableType ExoTrackSelection[] trackSelections) { + // Check whether we can enable offload. To enable offload we require exactly one audio track + // and a renderer with support matching the requirements set by + // setAudioOffloadPreference. There also cannot be other non-audio renderers with their own + // selected tracks. + int audioRendererIndex = C.INDEX_UNSET; + int audioRenderersSupportingOffload = 0; + boolean hasNonAudioRendererWithSelectedTracks = false; + for (int i = 0; i < mappedTrackInfo.getRendererCount(); i++) { + int rendererType = mappedTrackInfo.getRendererType(i); + ExoTrackSelection trackSelection = trackSelections[i]; + if (rendererType != C.TRACK_TYPE_AUDIO && trackSelection != null) { + hasNonAudioRendererWithSelectedTracks = true; + break; + } + if (rendererType == C.TRACK_TYPE_AUDIO + && trackSelection != null + && trackSelection.length() == 1) { + int trackGroupIndex = + mappedTrackInfo.getTrackGroups(i).indexOf(trackSelection.getTrackGroup()); + @Capabilities + int trackFormatSupport = + rendererFormatSupports[i][trackGroupIndex][trackSelection.getIndexInTrackGroup(0)]; + if (rendererSupportsOffload(parameters, trackFormatSupport, trackSelection)) { + audioRendererIndex = i; + audioRenderersSupportingOffload++; + } + } + } + if (!hasNonAudioRendererWithSelectedTracks && audioRenderersSupportingOffload == 1) { + RendererConfiguration offloadRendererConfiguration = + new RendererConfiguration( + parameters.isGaplessSupportRequired + ? AudioSink.OFFLOAD_MODE_ENABLED_GAPLESS_REQUIRED + : AudioSink.OFFLOAD_MODE_ENABLED_GAPLESS_NOT_REQUIRED, + /* tunneling= */ rendererConfigurations[audioRendererIndex] != null + && rendererConfigurations[audioRendererIndex].tunneling); + rendererConfigurations[audioRendererIndex] = offloadRendererConfiguration; + } + } + + /** + * Returns whether a renderer supports offload for a {@link ExoTrackSelection}. + * + * @param parameters The selection parameters with audio offload mode preferences. + * @param formatSupport The {@link Capabilities} for the selected track. + * @param selection The track selection. + * @return Whether the renderer supports tunneling for the {@link ExoTrackSelection}. + */ + private static boolean rendererSupportsOffload( + Parameters parameters, @Capabilities int formatSupport, ExoTrackSelection selection) { + if (RendererCapabilities.getAudioOffloadSupport(formatSupport) == AUDIO_OFFLOAD_NOT_SUPPORTED) { + return false; + } + if (parameters.isSpeedChangeSupportRequired + && (RendererCapabilities.getAudioOffloadSupport(formatSupport) + & AUDIO_OFFLOAD_SPEED_CHANGE_SUPPORTED) + == 0) { + return false; + } + // 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 isGaplessSupported = + (RendererCapabilities.getAudioOffloadSupport(formatSupport) + & AUDIO_OFFLOAD_GAPLESS_SUPPORTED) + != 0; + return !isGapless || isGaplessSupported; + } + return true; + } + /** * Returns true if the {@link FormatSupport} in the given {@link Capabilities} is {@link * C#FORMAT_HANDLED} or if {@code allowExceedsCapabilities} is set and the format support is diff --git a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/audio/AudioOffloadSupportTest.java b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/audio/AudioOffloadSupportTest.java new file mode 100644 index 0000000000..9e344f3218 --- /dev/null +++ b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/audio/AudioOffloadSupportTest.java @@ -0,0 +1,74 @@ +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package androidx.media3.exoplayer.audio; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertThrows; + +import androidx.test.ext.junit.runners.AndroidJUnit4; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** Unit tests for {@link AudioOffloadSupport}. */ +@RunWith(AndroidJUnit4.class) +public final class AudioOffloadSupportTest { + + @Test + public void checkDefaultUnsupported_allFieldsAreFalse() { + AudioOffloadSupport audioOffloadSupport = AudioOffloadSupport.DEFAULT_UNSUPPORTED; + + assertThat(audioOffloadSupport.isFormatSupported).isFalse(); + assertThat(audioOffloadSupport.isGaplessSupported).isFalse(); + assertThat(audioOffloadSupport.isSpeedChangeSupported).isFalse(); + } + + @Test + public void hashCode_withAllFlagsTrue_reportedExpectedValue() { + AudioOffloadSupport audioOffloadSupport = + new AudioOffloadSupport.Builder() + .setIsFormatSupported(true) + .setIsGaplessSupported(true) + .setIsSpeedChangeSupported(true) + .build(); + + assertThat(audioOffloadSupport.hashCode()).isEqualTo(7); + } + + @Test + public void build_withoutFormatSupportedWithGaplessSupported_throwsIllegalStateException() { + AudioOffloadSupport.Builder audioOffloadSupport = + new AudioOffloadSupport.Builder() + .setIsFormatSupported(false) + .setIsGaplessSupported(true) + .setIsSpeedChangeSupported(false); + + assertThrows(IllegalStateException.class, audioOffloadSupport::build); + } + + @Test + public void buildUpon_individualSetters_equalsToOriginal() { + AudioOffloadSupport audioOffloadSupport = + new AudioOffloadSupport.Builder() + .setIsFormatSupported(true) + .setIsGaplessSupported(true) + .setIsSpeedChangeSupported(false) + .build(); + + AudioOffloadSupport copy = audioOffloadSupport.buildUpon().build(); + + assertThat(copy).isEqualTo(audioOffloadSupport); + } +} diff --git a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/audio/DefaultAudioSinkTest.java b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/audio/DefaultAudioSinkTest.java index 753434c7aa..bccb71ab02 100644 --- a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/audio/DefaultAudioSinkTest.java +++ b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/audio/DefaultAudioSinkTest.java @@ -19,12 +19,14 @@ import static androidx.media3.exoplayer.audio.AudioSink.CURRENT_POSITION_NOT_SET import static androidx.media3.exoplayer.audio.AudioSink.SINK_FORMAT_SUPPORTED_DIRECTLY; import static androidx.media3.exoplayer.audio.AudioSink.SINK_FORMAT_SUPPORTED_WITH_TRANSCODING; import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertThrows; import androidx.media3.common.C; import androidx.media3.common.Format; import androidx.media3.common.MimeTypes; import androidx.media3.common.PlaybackParameters; import androidx.media3.exoplayer.audio.DefaultAudioSink.DefaultAudioProcessorChain; +import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; import java.nio.ByteBuffer; import java.nio.ByteOrder; @@ -65,7 +67,6 @@ public final class DefaultAudioSinkTest { defaultAudioSink = new DefaultAudioSink.Builder() .setAudioProcessorChain(new DefaultAudioProcessorChain(teeAudioProcessor)) - .setOffloadMode(DefaultAudioSink.OFFLOAD_MODE_DISABLED) .build(); } @@ -380,6 +381,15 @@ public final class DefaultAudioSinkTest { assertThat(defaultAudioSink.getPlaybackParameters().speed).isEqualTo(1); } + @Test + public void build_calledTwice_throwsIllegalStateException() throws Exception { + DefaultAudioSink.Builder defaultAudioSinkBuilder = + new DefaultAudioSink.Builder(ApplicationProvider.getApplicationContext()); + defaultAudioSinkBuilder.build(); + + assertThrows(IllegalStateException.class, defaultAudioSinkBuilder::build); + } + private void configureDefaultAudioSink(int channelCount) throws AudioSink.ConfigurationException { configureDefaultAudioSink(channelCount, /* trimStartFrames= */ 0, /* trimEndFrames= */ 0); } 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 2ba87a3522..683af28ab3 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 @@ -77,6 +77,15 @@ public class MediaCodecAudioRendererTest { .setEncoderDelay(100) .setEncoderPadding(150) .build(); + private static final AudioOffloadSupport AUDIO_OFFLOAD_SUPPORTED_GAPLESS_NOT_SUPPORTED = + new AudioOffloadSupport.Builder() + .setIsFormatSupported(true) + .setIsGaplessSupported(false) + .build(); + private static final RendererConfiguration + RENDERER_CONFIGURATION_OFFLOAD_ENABLED_GAPLESS_REQUIRED = + new RendererConfiguration( + AudioSink.OFFLOAD_MODE_ENABLED_GAPLESS_REQUIRED, /* tunneling= */ false); private MediaCodecAudioRenderer mediaCodecAudioRenderer; private MediaCodecSelector mediaCodecSelector; @@ -98,6 +107,8 @@ public class MediaCodecAudioRendererTest { return MimeTypes.AUDIO_RAW.equals(format.sampleMimeType) && format.pcmEncoding == C.ENCODING_PCM_16BIT; }); + when(audioSink.getFormatOffloadSupport(any())) + .thenReturn(AudioOffloadSupport.DEFAULT_UNSUPPORTED); mediaCodecSelector = (mimeType, requiresSecureDecoder, requiresTunnelingDecoder) -> @@ -434,6 +445,199 @@ public class MediaCodecAudioRendererTest { assertThat(RendererCapabilities.getFormatSupport(capabilities)).isEqualTo(C.FORMAT_HANDLED); } + @Test + public void render_withOffloadConfiguredWithoutOffloadSupport_setsOffloadModeDisabled() + throws Exception { + when(audioSink.getFormatOffloadSupport(AUDIO_AAC)) + .thenReturn(AudioOffloadSupport.DEFAULT_UNSUPPORTED); + Format format = AUDIO_AAC.buildUpon().setEncoderDelay(0).setEncoderPadding(0).build(); + FakeSampleStream fakeSampleStream = + new FakeSampleStream( + new DefaultAllocator(/* trimOnReset= */ true, /* individualAllocationSize= */ 1024), + /* mediaSourceEventDispatcher= */ null, + DrmSessionManager.DRM_UNSUPPORTED, + new DrmSessionEventListener.EventDispatcher(), + format, + ImmutableList.of( + oneByteSample(/* timeUs= */ 0, C.BUFFER_FLAG_KEY_FRAME), + oneByteSample(/* timeUs= */ 1_000), + END_OF_STREAM_ITEM)); + fakeSampleStream.writeData(/* startPositionUs= */ 0); + + mediaCodecAudioRenderer.enable( + RENDERER_CONFIGURATION_OFFLOAD_ENABLED_GAPLESS_REQUIRED, + new Format[] {format}, + fakeSampleStream, + /* positionUs= */ 0, + /* joining= */ false, + /* mayRenderStartOfStream= */ true, + /* startPositionUs= */ 0, + /* offsetUs= */ 0); + mediaCodecAudioRenderer.start(); + mediaCodecAudioRenderer.setCurrentStreamFinal(); + + while (!mediaCodecAudioRenderer.isEnded()) { + mediaCodecAudioRenderer.render(/* positionUs= */ 0, /* elapsedRealtimeUs= */ 0); + } + + verify(audioSink).setOffloadMode(AudioSink.OFFLOAD_MODE_DISABLED); + } + + @Test + public void render_withOffloadConfiguredWithoutGaplessOffloadSupport_setsOffloadModeDisabled() + throws Exception { + when(audioSink.getFormatOffloadSupport(AUDIO_AAC)) + .thenReturn(AUDIO_OFFLOAD_SUPPORTED_GAPLESS_NOT_SUPPORTED); + Format format = AUDIO_AAC.buildUpon().setEncoderDelay(100).setEncoderPadding(100).build(); + FakeSampleStream fakeSampleStream = + new FakeSampleStream( + new DefaultAllocator(/* trimOnReset= */ true, /* individualAllocationSize= */ 1024), + /* mediaSourceEventDispatcher= */ null, + DrmSessionManager.DRM_UNSUPPORTED, + new DrmSessionEventListener.EventDispatcher(), + format, + ImmutableList.of( + oneByteSample(/* timeUs= */ 0, C.BUFFER_FLAG_KEY_FRAME), + oneByteSample(/* timeUs= */ 1_000), + END_OF_STREAM_ITEM)); + fakeSampleStream.writeData(/* startPositionUs= */ 0); + + mediaCodecAudioRenderer.enable( + RENDERER_CONFIGURATION_OFFLOAD_ENABLED_GAPLESS_REQUIRED, + new Format[] {format}, + fakeSampleStream, + /* positionUs= */ 0, + /* joining= */ false, + /* mayRenderStartOfStream= */ true, + /* startPositionUs= */ 0, + /* offsetUs= */ 0); + mediaCodecAudioRenderer.start(); + mediaCodecAudioRenderer.setCurrentStreamFinal(); + + while (!mediaCodecAudioRenderer.isEnded()) { + mediaCodecAudioRenderer.render(/* positionUs= */ 0, /* elapsedRealtimeUs= */ 0); + } + + verify(audioSink).setOffloadMode(AudioSink.OFFLOAD_MODE_DISABLED); + } + + @Test + public void render_replaceStreamWithOffloadSupport_setsOffloadModeEnabled() throws Exception { + when(audioSink.getFormatOffloadSupport(any())) + .thenReturn(AUDIO_OFFLOAD_SUPPORTED_GAPLESS_NOT_SUPPORTED); + Format format1 = AUDIO_AAC.buildUpon().setEncoderDelay(100).setEncoderPadding(100).build(); + Format format2 = AUDIO_AAC.buildUpon().setEncoderDelay(0).setEncoderPadding(0).build(); + FakeSampleStream fakeSampleStream1 = + new FakeSampleStream( + new DefaultAllocator(/* trimOnReset= */ true, /* individualAllocationSize= */ 1024), + /* mediaSourceEventDispatcher= */ null, + DrmSessionManager.DRM_UNSUPPORTED, + new DrmSessionEventListener.EventDispatcher(), + format1, + ImmutableList.of( + oneByteSample(/* timeUs= */ 0, C.BUFFER_FLAG_KEY_FRAME), + oneByteSample(/* timeUs= */ 1_000), + END_OF_STREAM_ITEM)); + fakeSampleStream1.writeData(/* startPositionUs= */ 0); + FakeSampleStream fakeSampleStream2 = + new FakeSampleStream( + new DefaultAllocator(/* trimOnReset= */ true, /* individualAllocationSize= */ 1024), + /* mediaSourceEventDispatcher= */ null, + DrmSessionManager.DRM_UNSUPPORTED, + new DrmSessionEventListener.EventDispatcher(), + format2, + ImmutableList.of( + oneByteSample(/* timeUs= */ 1_000_000, C.BUFFER_FLAG_KEY_FRAME), + oneByteSample(/* timeUs= */ 1_001_000), + END_OF_STREAM_ITEM)); + fakeSampleStream2.writeData(/* startPositionUs= */ 0); + mediaCodecAudioRenderer.enable( + RENDERER_CONFIGURATION_OFFLOAD_ENABLED_GAPLESS_REQUIRED, + new Format[] {format1}, + fakeSampleStream1, + /* positionUs= */ 0, + /* joining= */ false, + /* mayRenderStartOfStream= */ true, + /* startPositionUs= */ 0, + /* offsetUs= */ 0); + mediaCodecAudioRenderer.start(); + while (!mediaCodecAudioRenderer.hasReadStreamToEnd()) { + mediaCodecAudioRenderer.render(/* positionUs= */ 0, /* elapsedRealtimeUs= */ 0); + } + + mediaCodecAudioRenderer.replaceStream( + new Format[] {format2}, + fakeSampleStream2, + /* startPositionUs= */ 1_000_000, + /* offsetUs= */ 1_000_000); + mediaCodecAudioRenderer.setCurrentStreamFinal(); + while (!mediaCodecAudioRenderer.isEnded()) { + mediaCodecAudioRenderer.render(/* positionUs= */ 0, /* elapsedRealtimeUs= */ 0); + } + + InOrder inOrder = inOrder(audioSink); + inOrder.verify(audioSink).setOffloadMode(AudioSink.OFFLOAD_MODE_DISABLED); + inOrder.verify(audioSink).setOffloadMode(AudioSink.OFFLOAD_MODE_ENABLED_GAPLESS_REQUIRED); + } + + @Test + public void render_replaceStreamWithoutGaplessSupport_setsOffloadModeDisabled() throws Exception { + when(audioSink.getFormatOffloadSupport(any())) + .thenReturn(AUDIO_OFFLOAD_SUPPORTED_GAPLESS_NOT_SUPPORTED); + Format format1 = AUDIO_AAC.buildUpon().setEncoderDelay(0).setEncoderPadding(0).build(); + Format format2 = AUDIO_AAC.buildUpon().setEncoderDelay(100).setEncoderPadding(100).build(); + FakeSampleStream fakeSampleStream1 = + new FakeSampleStream( + new DefaultAllocator(/* trimOnReset= */ true, /* individualAllocationSize= */ 1024), + /* mediaSourceEventDispatcher= */ null, + DrmSessionManager.DRM_UNSUPPORTED, + new DrmSessionEventListener.EventDispatcher(), + format1, + ImmutableList.of( + oneByteSample(/* timeUs= */ 0, C.BUFFER_FLAG_KEY_FRAME), + oneByteSample(/* timeUs= */ 1_000), + END_OF_STREAM_ITEM)); + fakeSampleStream1.writeData(/* startPositionUs= */ 0); + FakeSampleStream fakeSampleStream2 = + new FakeSampleStream( + new DefaultAllocator(/* trimOnReset= */ true, /* individualAllocationSize= */ 1024), + /* mediaSourceEventDispatcher= */ null, + DrmSessionManager.DRM_UNSUPPORTED, + new DrmSessionEventListener.EventDispatcher(), + format2, + ImmutableList.of( + oneByteSample(/* timeUs= */ 1_000_000, C.BUFFER_FLAG_KEY_FRAME), + oneByteSample(/* timeUs= */ 1_001_000), + END_OF_STREAM_ITEM)); + fakeSampleStream2.writeData(/* startPositionUs= */ 0); + mediaCodecAudioRenderer.enable( + RENDERER_CONFIGURATION_OFFLOAD_ENABLED_GAPLESS_REQUIRED, + new Format[] {format1}, + fakeSampleStream1, + /* positionUs= */ 0, + /* joining= */ false, + /* mayRenderStartOfStream= */ true, + /* startPositionUs= */ 0, + /* offsetUs= */ 0); + mediaCodecAudioRenderer.start(); + while (!mediaCodecAudioRenderer.hasReadStreamToEnd()) { + mediaCodecAudioRenderer.render(/* positionUs= */ 0, /* elapsedRealtimeUs= */ 0); + } + verify(audioSink).setOffloadMode(AudioSink.OFFLOAD_MODE_ENABLED_GAPLESS_REQUIRED); + + mediaCodecAudioRenderer.replaceStream( + new Format[] {format2}, + fakeSampleStream2, + /* startPositionUs= */ 1_000_000, + /* offsetUs= */ 1_000_000); + mediaCodecAudioRenderer.setCurrentStreamFinal(); + while (!mediaCodecAudioRenderer.isEnded()) { + mediaCodecAudioRenderer.render(/* positionUs= */ 0, /* elapsedRealtimeUs= */ 0); + } + + verify(audioSink).setOffloadMode(AudioSink.OFFLOAD_MODE_DISABLED); + } + private static Format getAudioSinkFormat(Format inputFormat) { return new Format.Builder() .setSampleMimeType(MimeTypes.AUDIO_RAW) diff --git a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/e2etest/OggOpusPlaybackTest.java b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/e2etest/OggOpusPlaybackTest.java index 60b0a445ce..718e6653a8 100644 --- a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/e2etest/OggOpusPlaybackTest.java +++ b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/e2etest/OggOpusPlaybackTest.java @@ -15,6 +15,8 @@ */ package androidx.media3.exoplayer.e2etest; +import static androidx.media3.common.TrackSelectionParameters.AUDIO_OFFLOAD_MODE_PREFERENCE_ENABLED; + import android.content.Context; import androidx.annotation.Nullable; import androidx.media3.common.Format; @@ -22,10 +24,11 @@ import androidx.media3.common.MediaItem; import androidx.media3.common.Player; import androidx.media3.exoplayer.DefaultRenderersFactory; import androidx.media3.exoplayer.ExoPlayer; -import androidx.media3.exoplayer.audio.AudioCapabilities; +import androidx.media3.exoplayer.audio.AudioOffloadSupport; import androidx.media3.exoplayer.audio.AudioSink; import androidx.media3.exoplayer.audio.DefaultAudioSink; import androidx.media3.exoplayer.audio.ForwardingAudioSink; +import androidx.media3.exoplayer.trackselection.DefaultTrackSelector; import androidx.media3.test.utils.DumpFileAsserts; import androidx.media3.test.utils.Dumper; import androidx.media3.test.utils.FakeClock; @@ -54,9 +57,19 @@ public class OggOpusPlaybackTest { Context applicationContext = ApplicationProvider.getApplicationContext(); OffloadRenderersFactory offloadRenderersFactory = new OffloadRenderersFactory(applicationContext); + DefaultTrackSelector trackSelector = new DefaultTrackSelector(applicationContext); + trackSelector.setParameters( + trackSelector + .buildUponParameters() + .setAudioOffloadPreference( + AUDIO_OFFLOAD_MODE_PREFERENCE_ENABLED, + /* isGaplessSupportRequired= */ false, + /* isSpeedChangeSupportRequired= */ false) + .build()); ExoPlayer player = new ExoPlayer.Builder(applicationContext, offloadRenderersFactory) .setClock(new FakeClock(/* isAutoAdvancing= */ true)) + .setTrackSelector(trackSelector) .build(); player.setMediaItem(MediaItem.fromUri("asset:///media/ogg/" + INPUT_FILE)); player.prepare(); @@ -81,22 +94,16 @@ public class OggOpusPlaybackTest { */ public OffloadRenderersFactory(Context context) { super(context); - setEnableAudioOffload(true); } @Override protected AudioSink buildAudioSink( - Context context, - boolean enableFloatOutput, - boolean enableAudioTrackPlaybackParams, - boolean enableOffload) { + Context context, boolean enableFloatOutput, boolean enableAudioTrackPlaybackParams) { dumpingAudioSink = new DumpingAudioSink( - new DefaultAudioSink.Builder() - .setAudioCapabilities(AudioCapabilities.getCapabilities(context)) + new DefaultAudioSink.Builder(context) .setEnableFloatOutput(enableFloatOutput) .setEnableAudioTrackPlaybackParams(enableAudioTrackPlaybackParams) - .setOffloadMode(DefaultAudioSink.OFFLOAD_MODE_ENABLED_GAPLESS_REQUIRED) .build()); return dumpingAudioSink; } @@ -128,6 +135,15 @@ public class OggOpusPlaybackTest { return true; } + @Override + public AudioOffloadSupport getFormatOffloadSupport(Format format) { + return new AudioOffloadSupport.Builder() + .setIsFormatSupported(true) + .setIsGaplessSupported(false) + .setIsSpeedChangeSupported(false) + .build(); + } + @Override public boolean handleBuffer( ByteBuffer buffer, long presentationTimeUs, int encodedAccessUnitCount) 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 12b0a1a851..453a1faedc 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 @@ -19,7 +19,9 @@ import static androidx.media3.common.C.FORMAT_EXCEEDS_CAPABILITIES; 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.exoplayer.RendererCapabilities.ADAPTIVE_NOT_SEAMLESS; +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; import static androidx.media3.exoplayer.RendererCapabilities.DECODER_SUPPORT_PRIMARY; @@ -27,6 +29,8 @@ import static androidx.media3.exoplayer.RendererCapabilities.HARDWARE_ACCELERATI import static androidx.media3.exoplayer.RendererCapabilities.HARDWARE_ACCELERATION_SUPPORTED; 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_REQUIRED; import static com.google.common.truth.Truth.assertThat; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; @@ -2312,6 +2316,125 @@ public final class DefaultTrackSelectorTest { assertFixedSelection(result.selections[0], trackGroups, formatDV); } + @Test + public void + selectTracks_withSingleTrackAndOffloadPreferenceEnabled_returnsRendererConfigOffloadEnabled() + throws Exception { + Format formatAAC = + new Format.Builder().setId("0").setSampleMimeType(MimeTypes.AUDIO_AAC).build(); + TrackGroupArray trackGroups = new TrackGroupArray(new TrackGroup(formatAAC)); + trackSelector.setParameters( + trackSelector + .buildUponParameters() + .setAudioOffloadPreference( + AUDIO_OFFLOAD_MODE_PREFERENCE_ENABLED, + /* isGaplessSupportRequired= */ true, + /* isSpeedChangeSupportRequired= */ false) + .build()); + 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(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); + } + + @Test + public void + selectTracks_withMultipleAudioTracksAndOffloadPreferenceEnabled_returnsRendererConfigOffloadDisabled() + throws Exception { + Format.Builder formatBuilder = AUDIO_FORMAT.buildUpon(); + TrackGroupArray trackGroups = + singleTrackGroup(formatBuilder.setId("0").build(), formatBuilder.setId("1").build()); + trackSelector.setParameters( + trackSelector + .buildUponParameters() + .setAudioOffloadPreference( + AUDIO_OFFLOAD_MODE_PREFERENCE_ENABLED, + /* isGaplessSupportRequired= */ true, + /* isSpeedChangeSupportRequired= */ false) + .build()); + 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(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) + .isEqualTo(OFFLOAD_MODE_DISABLED); + } + + @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()); + 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(trackSelector.getParameters().audioOffloadModePreference) + .isEqualTo(AUDIO_OFFLOAD_MODE_PREFERENCE_ENABLED); + assertFixedSelection(result.selections[0], trackGroups, formatAAC); + assertThat(result.rendererConfigurations[0].offloadModePreferred) + .isEqualTo(OFFLOAD_MODE_DISABLED); + } + /** * Tests that track selector will select the video track with the highest number of matching role * flags given by {@link Parameters}.