Set offload mode preference through TrackSelectionParameters
Added piping to present offload support from the audio sink to the renderer and track selection. Applications can set offload mode preference and with both sink support and compatible track selection, renderer will be configured for offload. PiperOrigin-RevId: 534450534
This commit is contained in:
parent
c73955a4cd
commit
4f0a256cc9
@ -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.
|
||||
|
4
api.txt
4
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<java.lang.Integer> 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);
|
||||
|
@ -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<String> preferredAudioMimeTypes;
|
||||
private @AudioOffloadModePreference int audioOffloadModePreference;
|
||||
private boolean isGaplessSupportRequired;
|
||||
private boolean isSpeedChangeSupportRequired;
|
||||
// Text
|
||||
private ImmutableList<String> 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.
|
||||
*
|
||||
* <p>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.
|
||||
*
|
||||
* <p>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<String> 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);
|
||||
|
@ -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.
|
||||
*
|
||||
|
@ -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);
|
||||
|
@ -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.
|
||||
*
|
||||
* <p>Audio offload disables ExoPlayer audio processing, but significantly reduces the energy
|
||||
* consumption of the playback when {@link
|
||||
* ExoPlayer#experimentalSetOffloadSchedulingEnabled(boolean) offload scheduling} is enabled.
|
||||
*
|
||||
* <p>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.
|
||||
*
|
||||
* <p>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();
|
||||
}
|
||||
|
||||
|
@ -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:
|
||||
*
|
||||
* <ul>
|
||||
* <li>Audio offload rendering is enabled in {@link
|
||||
* DefaultRenderersFactory#setEnableAudioOffload} or the equivalent option passed to {@link
|
||||
* DefaultAudioSink.Builder#setOffloadMode}.
|
||||
* <li>Audio offload rendering is enabled through {@link
|
||||
* TrackSelectionParameters.Builder#setAudioOffloadPreference}.
|
||||
* <li>An audio track is playing in a format that the device supports offloading (for example,
|
||||
* MP3 or AAC).
|
||||
* <li>The {@link AudioSink} is playing with an offload {@link AudioTrack}.
|
||||
|
@ -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}}.
|
||||
*
|
||||
* <p>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.
|
||||
*
|
||||
* <p>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}.
|
||||
*
|
||||
* <p>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.
|
||||
*
|
||||
* <p>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.
|
||||
*
|
||||
* <p>Possible values:
|
||||
*
|
||||
@ -196,7 +230,14 @@ public interface RendererCapabilities {
|
||||
* of {@link #HARDWARE_ACCELERATION_SUPPORTED} and {@link
|
||||
* #HARDWARE_ACCELERATION_NOT_SUPPORTED}.
|
||||
* <li>{@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}.
|
||||
* <li>{@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.
|
||||
* </ul>
|
||||
*/
|
||||
@Documented
|
||||
@ -211,23 +252,30 @@ public interface RendererCapabilities {
|
||||
*
|
||||
* <p>{@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}.
|
||||
*
|
||||
* <p>{@link HardwareAccelerationSupport} is set to {@link #HARDWARE_ACCELERATION_NOT_SUPPORTED}
|
||||
* and {@link DecoderSupport} is set to {@link #DECODER_SUPPORT_PRIMARY}.
|
||||
* <p>{@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}.
|
||||
*
|
||||
* <p>{@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}.
|
||||
*
|
||||
* <p>{@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}. */
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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.
|
||||
*
|
||||
* <p>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.
|
||||
*
|
||||
* <p>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.
|
||||
*
|
||||
* <p>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;
|
||||
}
|
||||
}
|
@ -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.
|
||||
*
|
||||
* <p>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.
|
||||
*
|
||||
* <p>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.
|
||||
*
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
@ -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.
|
||||
*
|
||||
* <p>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)}.
|
||||
*
|
||||
* <p>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.
|
||||
*
|
||||
* <p>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.
|
||||
*
|
||||
* <p>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.
|
||||
*
|
||||
* <p>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<MediaPositionParameters> mediaPositionParametersCheckpoints;
|
||||
private final boolean preferAudioTrackPlaybackParams;
|
||||
private final @OffloadMode int offloadMode;
|
||||
private @OffloadMode int offloadMode;
|
||||
private @MonotonicNonNull StreamEventCallbackV29 offloadStreamEventCallbackV29;
|
||||
private final PendingExceptionHolder<InitializationException>
|
||||
initializationExceptionPendingExceptionHolder;
|
||||
private final PendingExceptionHolder<WriteException> 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);
|
||||
|
@ -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);
|
||||
|
@ -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();
|
||||
|
@ -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;
|
||||
|
@ -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
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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}.
|
||||
|
Loading…
x
Reference in New Issue
Block a user