diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 09068c4cc9..5e7c988db2 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -2,6 +2,8 @@ ### dev-v2 (not yet released) ### +* Add a flag to opt-in to automatic audio focus handling via + `SimpleExoPlayer.setAudioAttributes`. * Distribute Cronet extension via jCenter. * Add `AudioListener` for listening to changes in audio configuration during playback ([#3994](https://github.com/google/ExoPlayer/issues/3994)). diff --git a/library/core/src/main/java/com/google/android/exoplayer2/C.java b/library/core/src/main/java/com/google/android/exoplayer2/C.java index de4ad65b44..29cbcd83e4 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/C.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/C.java @@ -283,24 +283,32 @@ public final class C { public static final int FLAG_AUDIBILITY_ENFORCED = android.media.AudioAttributes.FLAG_AUDIBILITY_ENFORCED; - /** - * Usage types for {@link com.google.android.exoplayer2.audio.AudioAttributes}. - */ + /** Usage types for {@link com.google.android.exoplayer2.audio.AudioAttributes}. */ @Retention(RetentionPolicy.SOURCE) - @IntDef({USAGE_ALARM, USAGE_ASSISTANCE_ACCESSIBILITY, USAGE_ASSISTANCE_NAVIGATION_GUIDANCE, - USAGE_ASSISTANCE_SONIFICATION, USAGE_GAME, USAGE_MEDIA, USAGE_NOTIFICATION, - USAGE_NOTIFICATION_COMMUNICATION_DELAYED, USAGE_NOTIFICATION_COMMUNICATION_INSTANT, - USAGE_NOTIFICATION_COMMUNICATION_REQUEST, USAGE_NOTIFICATION_EVENT, - USAGE_NOTIFICATION_RINGTONE, USAGE_UNKNOWN, USAGE_VOICE_COMMUNICATION, - USAGE_VOICE_COMMUNICATION_SIGNALLING}) + @IntDef({ + USAGE_ALARM, + USAGE_ASSISTANCE_ACCESSIBILITY, + USAGE_ASSISTANCE_NAVIGATION_GUIDANCE, + USAGE_ASSISTANCE_SONIFICATION, + USAGE_ASSISTANT, + USAGE_GAME, + USAGE_MEDIA, + USAGE_NOTIFICATION, + USAGE_NOTIFICATION_COMMUNICATION_DELAYED, + USAGE_NOTIFICATION_COMMUNICATION_INSTANT, + USAGE_NOTIFICATION_COMMUNICATION_REQUEST, + USAGE_NOTIFICATION_EVENT, + USAGE_NOTIFICATION_RINGTONE, + USAGE_UNKNOWN, + USAGE_VOICE_COMMUNICATION, + USAGE_VOICE_COMMUNICATION_SIGNALLING + }) public @interface AudioUsage {} /** * @see android.media.AudioAttributes#USAGE_ALARM */ public static final int USAGE_ALARM = android.media.AudioAttributes.USAGE_ALARM; - /** - * @see android.media.AudioAttributes#USAGE_ASSISTANCE_ACCESSIBILITY - */ + /** @see android.media.AudioAttributes#USAGE_ASSISTANCE_ACCESSIBILITY */ public static final int USAGE_ASSISTANCE_ACCESSIBILITY = android.media.AudioAttributes.USAGE_ASSISTANCE_ACCESSIBILITY; /** @@ -313,6 +321,8 @@ public final class C { */ public static final int USAGE_ASSISTANCE_SONIFICATION = android.media.AudioAttributes.USAGE_ASSISTANCE_SONIFICATION; + /** @see android.media.AudioAttributes#USAGE_ASSISTANT */ + public static final int USAGE_ASSISTANT = android.media.AudioAttributes.USAGE_ASSISTANT; /** * @see android.media.AudioAttributes#USAGE_GAME */ @@ -365,6 +375,29 @@ public final class C { public static final int USAGE_VOICE_COMMUNICATION_SIGNALLING = android.media.AudioAttributes.USAGE_VOICE_COMMUNICATION_SIGNALLING; + /** Audio focus types for {@link AudioFocusConfiguration}. */ + @Retention(RetentionPolicy.SOURCE) + @IntDef({ + AUDIOFOCUS_NONE, + AUDIOFOCUS_GAIN, + AUDIOFOCUS_GAIN_TRANSIENT, + AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK, + AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE + }) + public @interface AudioFocusGain {} + /** @see AudioManager#AUDIOFOCUS_NONE */ + public static final int AUDIOFOCUS_NONE = AudioManager.AUDIOFOCUS_NONE; + /** @see AudioManager#AUDIOFOCUS_GAIN */ + public static final int AUDIOFOCUS_GAIN = AudioManager.AUDIOFOCUS_GAIN; + /** @see AudioManager#AUDIOFOCUS_GAIN_TRANSIENT */ + public static final int AUDIOFOCUS_GAIN_TRANSIENT = AudioManager.AUDIOFOCUS_GAIN_TRANSIENT; + /** @see AudioManager#AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK */ + public static final int AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK = + AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK; + /** @see AudioManager#AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE */ + public static final int AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE = + AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE; + /** * Flags which can apply to a buffer containing a media sample. */ diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerFactory.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerFactory.java index a00d11c831..b00a485843 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerFactory.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerFactory.java @@ -19,6 +19,7 @@ import android.content.Context; import android.os.Looper; import android.support.annotation.Nullable; import com.google.android.exoplayer2.analytics.AnalyticsCollector; +import com.google.android.exoplayer2.audio.AudioAttributes; import com.google.android.exoplayer2.drm.DrmSessionManager; import com.google.android.exoplayer2.drm.FrameworkMediaCrypto; import com.google.android.exoplayer2.trackselection.TrackSelector; @@ -145,7 +146,9 @@ public final class ExoPlayerFactory { * * @param renderersFactory A factory for creating {@link Renderer}s to be used by the instance. * @param trackSelector The {@link TrackSelector} that will be used by the instance. - * @deprecated Use {@link #newSimpleInstance(Context, RenderersFactory, TrackSelector)}. + * @deprecated Use {@link #newSimpleInstance(Context, RenderersFactory, TrackSelector)}. The use + * of {@link SimpleExoPlayer#setAudioAttributes(AudioAttributes, boolean)} to manage audio + * focus will be unavailable for the {@link SimpleExoPlayer} returned by this method. */ @Deprecated @SuppressWarnings("nullness:argument.type.incompatible") diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java index dcc88fc453..648168816f 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java @@ -66,6 +66,7 @@ import java.util.concurrent.CopyOnWriteArraySet; private final ArrayDeque pendingPlaybackInfoUpdates; private boolean playWhenReady; + private boolean internalPlayWhenReady; private @RepeatMode int repeatMode; private boolean shuffleModeEnabled; private int pendingOperationAcks; @@ -219,9 +220,17 @@ import java.util.concurrent.CopyOnWriteArraySet; @Override public void setPlayWhenReady(boolean playWhenReady) { + setPlayWhenReady(playWhenReady, /* suppressPlayback= */ false); + } + + public void setPlayWhenReady(boolean playWhenReady, boolean suppressPlayback) { + boolean internalPlayWhenReady = playWhenReady && !suppressPlayback; + if (this.internalPlayWhenReady != internalPlayWhenReady) { + this.internalPlayWhenReady = internalPlayWhenReady; + internalPlayer.setPlayWhenReady(internalPlayWhenReady); + } if (this.playWhenReady != playWhenReady) { this.playWhenReady = playWhenReady; - internalPlayer.setPlayWhenReady(playWhenReady); updatePlaybackInfo( playbackInfo, /* positionDiscontinuity= */ false, diff --git a/library/core/src/main/java/com/google/android/exoplayer2/PlaybackInfo.java b/library/core/src/main/java/com/google/android/exoplayer2/PlaybackInfo.java index b556db6be9..b338de15b4 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/PlaybackInfo.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/PlaybackInfo.java @@ -91,7 +91,7 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult; /* manifest= */ null, DUMMY_MEDIA_PERIOD_ID, startPositionUs, - /* contentPositionUs =*/ C.TIME_UNSET, + /* contentPositionUs= */ C.TIME_UNSET, Player.STATE_IDLE, /* isLoading= */ false, TrackGroupArray.EMPTY, diff --git a/library/core/src/main/java/com/google/android/exoplayer2/Player.java b/library/core/src/main/java/com/google/android/exoplayer2/Player.java index b2313e6085..748e31d77f 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/Player.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/Player.java @@ -85,9 +85,34 @@ public interface Player { * equivalent stream type using {@link Util#getStreamTypeForAudioUsage(int)}. * * @param audioAttributes The attributes to use for audio playback. + * @deprecated Use {@link AudioComponent#setAudioAttributes(AudioAttributes, boolean)}. */ + @Deprecated void setAudioAttributes(AudioAttributes audioAttributes); + /** + * Sets the attributes for audio playback, used by the underlying audio track. If not set, the + * default audio attributes will be used. They are suitable for general media playback. + * + *

Setting the audio attributes during playback may introduce a short gap in audio output as + * the audio track is recreated. A new audio session id will also be generated. + * + *

If tunneling is enabled by the track selector, the specified audio attributes will be + * ignored, but they will take effect if audio is later played without tunneling. + * + *

If the device is running a build before platform API version 21, audio attributes cannot + * be set directly on the underlying audio track. In this case, the usage will be mapped onto an + * equivalent stream type using {@link Util#getStreamTypeForAudioUsage(int)}. + * + *

If audio focus should be handled, the {@link AudioAttributes#usage} must be {@link + * C#USAGE_MEDIA} or {@link C#USAGE_GAME}. Other usages will throw an {@link + * IllegalArgumentException}. + * + * @param audioAttributes The attributes to use for audio playback. + * @param handleAudioFocus True if the player should handle audio focus, false otherwise. + */ + void setAudioAttributes(AudioAttributes audioAttributes, boolean handleAudioFocus); + /** Returns the attributes for audio playback. */ AudioAttributes getAudioAttributes(); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java b/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java index 70dbcaf730..ed47c85d46 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java @@ -19,7 +19,6 @@ import android.annotation.TargetApi; import android.content.Context; import android.graphics.Rect; import android.graphics.SurfaceTexture; -import android.media.AudioManager; import android.media.MediaCodec; import android.media.PlaybackParams; import android.os.Handler; @@ -33,6 +32,7 @@ import android.view.TextureView; import com.google.android.exoplayer2.analytics.AnalyticsCollector; import com.google.android.exoplayer2.analytics.AnalyticsListener; import com.google.android.exoplayer2.audio.AudioAttributes; +import com.google.android.exoplayer2.audio.AudioFocusManager; import com.google.android.exoplayer2.audio.AudioListener; import com.google.android.exoplayer2.audio.AudioRendererEventListener; import com.google.android.exoplayer2.decoder.DecoderCounters; @@ -72,7 +72,7 @@ public class SimpleExoPlayer protected final Renderer[] renderers; - private final ExoPlayer player; + private final ExoPlayerImpl player; private final Handler eventHandler; private final ComponentListener componentListener; private final CopyOnWriteArraySet @@ -85,8 +85,7 @@ public class SimpleExoPlayer private final BandwidthMeter bandwidthMeter; private final AnalyticsCollector analyticsCollector; - @SuppressWarnings({"unused", "FieldCanBeLocal"}) - private final @Nullable AudioManager audioManager; + private final AudioFocusManager audioFocusManager; private Format videoFormat; private Format audioFormat; @@ -116,7 +115,9 @@ public class SimpleExoPlayer * @param looper The {@link Looper} which must be used for all calls to the player and which is * used to call listeners on. * @deprecated Use {@link #SimpleExoPlayer(Context, RenderersFactory, TrackSelector, LoadControl, - * BandwidthMeter, DrmSessionManager, Looper)}. + * BandwidthMeter, DrmSessionManager, Looper)}. The use of {@link + * SimpleExoPlayer#setAudioAttributes(AudioAttributes, boolean)} to manage audio focus will be + * unavailable for a player created with this constructor. */ @Deprecated protected SimpleExoPlayer( @@ -253,7 +254,7 @@ public class SimpleExoPlayer // Build the player and associated objects. player = - createExoPlayerImpl(renderers, trackSelector, loadControl, bandwidthMeter, clock, looper); + new ExoPlayerImpl(renderers, trackSelector, loadControl, bandwidthMeter, clock, looper); analyticsCollector = analyticsCollectorFactory.createAnalyticsCollector(player, clock); addListener(analyticsCollector); videoDebugListeners.add(analyticsCollector); @@ -265,13 +266,7 @@ public class SimpleExoPlayer if (drmSessionManager instanceof DefaultDrmSessionManager) { ((DefaultDrmSessionManager) drmSessionManager).addListener(eventHandler, analyticsCollector); } - // TODO: Remove null check once the deprecated factory method and constructor that don't take - // Contexts have been removed. - audioManager = - context == null - ? null - : (AudioManager) - context.getApplicationContext().getSystemService(Context.AUDIO_SERVICE); + audioFocusManager = new AudioFocusManager(context, componentListener); } @Override @@ -417,22 +412,32 @@ public class SimpleExoPlayer @Override public void setAudioAttributes(AudioAttributes audioAttributes) { - if (Util.areEqual(this.audioAttributes, audioAttributes)) { - return; - } - this.audioAttributes = audioAttributes; - for (Renderer renderer : renderers) { - if (renderer.getTrackType() == C.TRACK_TYPE_AUDIO) { - player - .createMessage(renderer) - .setType(C.MSG_SET_AUDIO_ATTRIBUTES) - .setPayload(audioAttributes) - .send(); + setAudioAttributes(audioAttributes, /* handleAudioFocus= */ false); + } + + @Override + public void setAudioAttributes(AudioAttributes audioAttributes, boolean handleAudioFocus) { + if (!Util.areEqual(this.audioAttributes, audioAttributes)) { + this.audioAttributes = audioAttributes; + for (Renderer renderer : renderers) { + if (renderer.getTrackType() == C.TRACK_TYPE_AUDIO) { + player + .createMessage(renderer) + .setType(C.MSG_SET_AUDIO_ATTRIBUTES) + .setPayload(audioAttributes) + .send(); + } + } + for (AudioListener audioListener : audioListeners) { + audioListener.onAudioAttributesChanged(audioAttributes); } } - for (AudioListener audioListener : audioListeners) { - audioListener.onAudioAttributesChanged(audioAttributes); - } + + @AudioFocusManager.PlayerCommand + int playerCommand = + audioFocusManager.setAudioAttributes( + handleAudioFocus ? audioAttributes : null, getPlayWhenReady(), getPlaybackState()); + updatePlayWhenReady(getPlayWhenReady(), playerCommand); } @Override @@ -452,11 +457,7 @@ public class SimpleExoPlayer return; } this.audioVolume = audioVolume; - for (Renderer renderer : renderers) { - if (renderer.getTrackType() == C.TRACK_TYPE_AUDIO) { - player.createMessage(renderer).setType(C.MSG_SET_VOLUME).setPayload(audioVolume).send(); - } - } + sendVolumeToRenderers(); for (AudioListener audioListener : audioListeners) { audioListener.onVolumeChanged(audioVolume); } @@ -792,12 +793,17 @@ public class SimpleExoPlayer mediaSource.addEventListener(eventHandler, analyticsCollector); this.mediaSource = mediaSource; } + @AudioFocusManager.PlayerCommand + int playerCommand = audioFocusManager.handlePrepare(getPlayWhenReady()); + updatePlayWhenReady(getPlayWhenReady(), playerCommand); player.prepare(mediaSource, resetPosition, resetState); } @Override public void setPlayWhenReady(boolean playWhenReady) { - player.setPlayWhenReady(playWhenReady); + @AudioFocusManager.PlayerCommand + int playerCommand = audioFocusManager.handleSetPlayWhenReady(playWhenReady, getPlaybackState()); + updatePlayWhenReady(playWhenReady, playerCommand); } @Override @@ -892,11 +898,13 @@ public class SimpleExoPlayer mediaSource = null; analyticsCollector.resetForNewMediaSource(); } + audioFocusManager.handleStop(); currentCues = Collections.emptyList(); } @Override public void release() { + audioFocusManager.handleStop(); player.release(); removeSurfaceCallbacks(); if (surface != null) { @@ -1039,28 +1047,6 @@ public class SimpleExoPlayer // Internal methods. - /** - * Creates the {@link ExoPlayer} implementation used by this instance. - * - * @param renderers The {@link Renderer}s that will be used by the instance. - * @param trackSelector The {@link TrackSelector} that will be used by the instance. - * @param loadControl The {@link LoadControl} that will be used by the instance. - * @param bandwidthMeter The {@link BandwidthMeter} that will be used by the instance. - * @param clock The {@link Clock} that will be used by this instance. - * @param looper The {@link Looper} which must be used for all calls to the player and which is - * used to call listeners on. - * @return A new {@link ExoPlayer} instance. - */ - protected ExoPlayer createExoPlayerImpl( - Renderer[] renderers, - TrackSelector trackSelector, - LoadControl loadControl, - BandwidthMeter bandwidthMeter, - Clock clock, - Looper looper) { - return new ExoPlayerImpl(renderers, trackSelector, loadControl, bandwidthMeter, clock, looper); - } - private void removeSurfaceCallbacks() { if (textureView != null) { if (textureView.getSurfaceTextureListener() != componentListener) { @@ -1114,9 +1100,31 @@ public class SimpleExoPlayer } } - private final class ComponentListener implements VideoRendererEventListener, - AudioRendererEventListener, TextOutput, MetadataOutput, SurfaceHolder.Callback, - TextureView.SurfaceTextureListener { + private void sendVolumeToRenderers() { + float scaledVolume = audioVolume * audioFocusManager.getVolumeMultiplier(); + for (Renderer renderer : renderers) { + if (renderer.getTrackType() == C.TRACK_TYPE_AUDIO) { + player.createMessage(renderer).setType(C.MSG_SET_VOLUME).setPayload(scaledVolume).send(); + } + } + } + + private void updatePlayWhenReady( + boolean playWhenReady, @AudioFocusManager.PlayerCommand int playerCommand) { + + player.setPlayWhenReady( + playWhenReady && playerCommand != AudioFocusManager.PLAYER_COMMAND_DO_NOT_PLAY, + playerCommand != AudioFocusManager.PLAYER_COMMAND_PLAY_WHEN_READY); + } + + private final class ComponentListener + implements VideoRendererEventListener, + AudioRendererEventListener, + TextOutput, + MetadataOutput, + SurfaceHolder.Callback, + TextureView.SurfaceTextureListener, + AudioFocusManager.PlayerControl { // VideoRendererEventListener implementation @@ -1314,6 +1322,17 @@ public class SimpleExoPlayer public void onSurfaceTextureUpdated(SurfaceTexture surfaceTexture) { // Do nothing. } - } + // AudioFocusManager.PlayerControl implementation + + @Override + public void setVolumeMultiplier(float volumeMultiplier) { + sendVolumeToRenderers(); + } + + @Override + public void executePlayerCommand(@AudioFocusManager.PlayerCommand int playerCommand) { + updatePlayWhenReady(getPlayWhenReady(), playerCommand); + } + } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioAttributes.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioAttributes.java index f726805c4b..848b3ee10c 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioAttributes.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioAttributes.java @@ -102,7 +102,7 @@ public final class AudioAttributes { } @TargetApi(21) - /* package */ android.media.AudioAttributes getAudioAttributesV21() { + public android.media.AudioAttributes getAudioAttributesV21() { if (audioAttributesV21 == null) { audioAttributesV21 = new android.media.AudioAttributes.Builder() .setContentType(contentType) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioFocusManager.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioFocusManager.java new file mode 100644 index 0000000000..d078cddcc1 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioFocusManager.java @@ -0,0 +1,454 @@ +/* + * Copyright (C) 2018 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 com.google.android.exoplayer2.audio; + +import android.content.Context; +import android.media.AudioFocusRequest; +import android.media.AudioManager; +import android.support.annotation.IntDef; +import android.support.annotation.Nullable; +import android.support.annotation.RequiresApi; +import android.util.Log; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.ExoPlayer; +import com.google.android.exoplayer2.Player; +import com.google.android.exoplayer2.source.MediaSource; +import com.google.android.exoplayer2.util.Assertions; +import com.google.android.exoplayer2.util.Util; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; + +/** Manages requesting and responding to changes in audio focus. */ +public final class AudioFocusManager { + + /** Interface to allow AudioFocusManager to give commands to a player. */ + public interface PlayerControl { + /** + * Called when the volume multiplier on the player should be changed. + * + * @param volumeMultiplier The new volume multiplier. + */ + void setVolumeMultiplier(float volumeMultiplier); + + /** + * Called when a command must be executed on the player. + * + * @param playerCommand The command that must be executed. + */ + void executePlayerCommand(@PlayerCommand int playerCommand); + } + + /** Player commands. */ + @Retention(RetentionPolicy.SOURCE) + @IntDef({ + PLAYER_COMMAND_DO_NOT_PLAY, + PLAYER_COMMAND_WAIT_FOR_CALLBACK, + PLAYER_COMMAND_PLAY_WHEN_READY, + }) + public @interface PlayerCommand {} + /** Do not play. */ + public static final int PLAYER_COMMAND_DO_NOT_PLAY = -1; + /** Do not play now. Wait for callback to play. */ + public static final int PLAYER_COMMAND_WAIT_FOR_CALLBACK = 0; + /** Play freely. */ + public static final int PLAYER_COMMAND_PLAY_WHEN_READY = 1; + + /** Audio focus state. */ + @Retention(RetentionPolicy.SOURCE) + @IntDef({ + AUDIO_FOCUS_STATE_LOST_FOCUS, + AUDIO_FOCUS_STATE_NO_FOCUS, + AUDIO_FOCUS_STATE_HAVE_FOCUS, + AUDIO_FOCUS_STATE_LOSS_TRANSIENT, + AUDIO_FOCUS_STATE_LOSS_TRANSIENT_DUCK + }) + private @interface AudioFocusState {} + /** No audio focus was held, but has been lost by another app taking it permanently. */ + private static final int AUDIO_FOCUS_STATE_LOST_FOCUS = -1; + /** No audio focus is currently being held. */ + private static final int AUDIO_FOCUS_STATE_NO_FOCUS = 0; + /** The requested audio focus is currently held. */ + private static final int AUDIO_FOCUS_STATE_HAVE_FOCUS = 1; + /** Audio focus has been temporarily lost. */ + private static final int AUDIO_FOCUS_STATE_LOSS_TRANSIENT = 2; + /** Audio focus has been temporarily lost, but playback may continue with reduced volume. */ + private static final int AUDIO_FOCUS_STATE_LOSS_TRANSIENT_DUCK = 3; + + private static final String TAG = "AudioFocusManager"; + + private static final float VOLUME_MULTIPLIER_DUCK = 0.2f; + private static final float VOLUME_MULTIPLIER_DEFAULT = 1.0f; + + private final @Nullable AudioManager audioManager; + private final AudioFocusListener focusListener; + private final PlayerControl playerControl; + private @Nullable AudioAttributes audioAttributes; + + private @AudioFocusState int audioFocusState; + private int focusGain; + private float volumeMultiplier = 1.0f; + + private @MonotonicNonNull AudioFocusRequest audioFocusRequest; + private boolean rebuildAudioFocusRequest; + + /** + * Constructs an AudioFocusManager to automatically handle audio focus for a player. + * + * @param context The current context. + * @param playerControl A {@link PlayerControl} to handle commands from this instance. + */ + public AudioFocusManager(@Nullable Context context, PlayerControl playerControl) { + this.audioManager = + context == null + ? null + : (AudioManager) + context.getApplicationContext().getSystemService(Context.AUDIO_SERVICE); + this.playerControl = playerControl; + this.focusListener = new AudioFocusListener(); + this.audioFocusState = AUDIO_FOCUS_STATE_NO_FOCUS; + } + + /** Gets the current player volume multiplier. */ + public float getVolumeMultiplier() { + return volumeMultiplier; + } + + /** + * Sets audio attributes that should be used to manage audio focus. + * + * @param audioAttributes The audio attributes or {@code null} if audio focus should not be + * managed automatically. + * @param playWhenReady The current state of {@link ExoPlayer#getPlayWhenReady()}. + * @param playerState The current player state; {@link ExoPlayer#getPlaybackState()}. + * @return A command to execute on the player. One of {@link #PLAYER_COMMAND_DO_NOT_PLAY}, {@link + * #PLAYER_COMMAND_WAIT_FOR_CALLBACK}, and {@link #PLAYER_COMMAND_PLAY_WHEN_READY}. + */ + public @PlayerCommand int setAudioAttributes( + @Nullable AudioAttributes audioAttributes, boolean playWhenReady, int playerState) { + if (audioAttributes == null) { + return PLAYER_COMMAND_PLAY_WHEN_READY; + } + + Assertions.checkNotNull( + audioManager, "SimpleExoPlayer must be created with a context to handle audio focus."); + if (!Util.areEqual(this.audioAttributes, audioAttributes)) { + this.audioAttributes = audioAttributes; + focusGain = convertAudioAttributesToFocusGain(audioAttributes); + + Assertions.checkArgument( + focusGain == C.AUDIOFOCUS_GAIN || focusGain == C.AUDIOFOCUS_NONE, + "Automatic handling of audio focus is only available for USAGE_MEDIA and USAGE_GAME."); + if (playWhenReady + && (playerState == Player.STATE_BUFFERING || playerState == Player.STATE_READY)) { + return requestAudioFocus(); + } + } + + if (playerState == Player.STATE_IDLE) { + return PLAYER_COMMAND_WAIT_FOR_CALLBACK; + } else { + return handlePrepare(playWhenReady); + } + } + + /** + * Called by a player as part of {@link ExoPlayer#prepare(MediaSource, boolean, boolean)}. + * + * @param playWhenReady The current state of {@link ExoPlayer#getPlayWhenReady()}. + * @return A command to execute on the player. One of {@link #PLAYER_COMMAND_DO_NOT_PLAY}, {@link + * #PLAYER_COMMAND_WAIT_FOR_CALLBACK}, and {@link #PLAYER_COMMAND_PLAY_WHEN_READY}. + */ + public @PlayerCommand int handlePrepare(boolean playWhenReady) { + if (audioManager == null) { + return PLAYER_COMMAND_PLAY_WHEN_READY; + } + + return playWhenReady ? requestAudioFocus() : PLAYER_COMMAND_DO_NOT_PLAY; + } + + /** + * Called by the player as part of {@link ExoPlayer#setPlayWhenReady(boolean)}. + * + * @param playWhenReady The desired value of playWhenReady. + * @param playerState The current state of the player. + * @return A command to execute on the player. One of {@link #PLAYER_COMMAND_DO_NOT_PLAY}, {@link + * #PLAYER_COMMAND_WAIT_FOR_CALLBACK}, and {@link #PLAYER_COMMAND_PLAY_WHEN_READY}. + */ + public @PlayerCommand int handleSetPlayWhenReady(boolean playWhenReady, int playerState) { + if (audioManager == null) { + return PLAYER_COMMAND_PLAY_WHEN_READY; + } + + if (!playWhenReady) { + abandonAudioFocus(); + return PLAYER_COMMAND_DO_NOT_PLAY; + } else if (playerState != Player.STATE_IDLE) { + return requestAudioFocus(); + } + return focusGain != C.AUDIOFOCUS_NONE + ? PLAYER_COMMAND_WAIT_FOR_CALLBACK + : PLAYER_COMMAND_PLAY_WHEN_READY; + } + + /** Called by the player as part of {@link ExoPlayer#stop(boolean)}. */ + public void handleStop() { + if (audioManager == null) { + return; + } + + abandonAudioFocus(/* forceAbandon= */ true); + } + + // Internal methods. + + private @PlayerCommand int requestAudioFocus() { + int focusRequestResult; + + if (focusGain == C.AUDIOFOCUS_NONE) { + if (audioFocusState != AUDIO_FOCUS_STATE_NO_FOCUS) { + abandonAudioFocus(/* forceAbandon= */ true); + } + return PLAYER_COMMAND_PLAY_WHEN_READY; + } + + if (audioFocusState == AUDIO_FOCUS_STATE_NO_FOCUS) { + if (Util.SDK_INT >= 26) { + focusRequestResult = requestAudioFocusV26(); + } else { + focusRequestResult = requestAudioFocusDefault(); + } + audioFocusState = + focusRequestResult == AudioManager.AUDIOFOCUS_REQUEST_GRANTED + ? AUDIO_FOCUS_STATE_HAVE_FOCUS + : AUDIO_FOCUS_STATE_NO_FOCUS; + } + + if (audioFocusState == AUDIO_FOCUS_STATE_NO_FOCUS) { + return PLAYER_COMMAND_DO_NOT_PLAY; + } + + return audioFocusState == AUDIO_FOCUS_STATE_LOSS_TRANSIENT + ? PLAYER_COMMAND_WAIT_FOR_CALLBACK + : PLAYER_COMMAND_PLAY_WHEN_READY; + } + + private void abandonAudioFocus() { + abandonAudioFocus(/* forceAbandon= */ false); + } + + private void abandonAudioFocus(boolean forceAbandon) { + if (focusGain == C.AUDIOFOCUS_NONE && audioFocusState == AUDIO_FOCUS_STATE_NO_FOCUS) { + return; + } + + if (focusGain != C.AUDIOFOCUS_GAIN + || audioFocusState == AUDIO_FOCUS_STATE_LOST_FOCUS + || forceAbandon) { + if (Util.SDK_INT >= 26) { + abandonAudioFocusV26(); + } else { + abandonAudioFocusDefault(); + } + audioFocusState = AUDIO_FOCUS_STATE_NO_FOCUS; + } + } + + private int requestAudioFocusDefault() { + AudioManager audioManager = Assertions.checkNotNull(this.audioManager); + return audioManager.requestAudioFocus( + focusListener, + Util.getStreamTypeForAudioUsage(Assertions.checkNotNull(audioAttributes).usage), + focusGain); + } + + @RequiresApi(26) + private int requestAudioFocusV26() { + if (audioFocusRequest == null || rebuildAudioFocusRequest) { + AudioFocusRequest.Builder builder = + audioFocusRequest == null + ? new AudioFocusRequest.Builder(focusGain) + : new AudioFocusRequest.Builder(audioFocusRequest); + + boolean willPauseWhenDucked = willPauseWhenDucked(); + audioFocusRequest = + builder + .setAudioAttributes(Assertions.checkNotNull(audioAttributes).getAudioAttributesV21()) + .setWillPauseWhenDucked(willPauseWhenDucked) + .setOnAudioFocusChangeListener(focusListener) + .build(); + + rebuildAudioFocusRequest = false; + } + return Assertions.checkNotNull(audioManager).requestAudioFocus(audioFocusRequest); + } + + private void abandonAudioFocusDefault() { + Assertions.checkNotNull(audioManager).abandonAudioFocus(focusListener); + } + + @RequiresApi(26) + private void abandonAudioFocusV26() { + if (audioFocusRequest != null) { + Assertions.checkNotNull(audioManager).abandonAudioFocusRequest(audioFocusRequest); + } + } + + private boolean willPauseWhenDucked() { + return audioAttributes != null && audioAttributes.contentType == C.CONTENT_TYPE_SPEECH; + } + + /** + * Converts {@link AudioAttributes} to one of the audio focus request. + * + *

This follows the class Javadoc of {@link AudioFocusRequest}. + * + * @param audioAttributes The audio attributes associated with this focus request. + * @return The type of audio focus gain that should be requested. + */ + private static int convertAudioAttributesToFocusGain(@Nullable AudioAttributes audioAttributes) { + + if (audioAttributes == null) { + // Don't handle audio focus. It may be either video only contents or developers + // want to have more finer grained control. (e.g. adding audio focus listener) + return C.AUDIOFOCUS_NONE; + } + + switch (audioAttributes.usage) { + // USAGE_VOICE_COMMUNICATION_SIGNALLING is for DTMF that may happen multiple times + // during the phone call when AUDIOFOCUS_GAIN_TRANSIENT is requested for that. + // Don't request audio focus here. + case C.USAGE_VOICE_COMMUNICATION_SIGNALLING: + return C.AUDIOFOCUS_NONE; + + // Javadoc says 'AUDIOFOCUS_GAIN: Examples of uses of this focus gain are for music + // playback, for a game or a video player' + case C.USAGE_GAME: + case C.USAGE_MEDIA: + return C.AUDIOFOCUS_GAIN; + + // Special usages: USAGE_UNKNOWN shouldn't be used. Request audio focus to prevent + // multiple media playback happen at the same time. + case C.USAGE_UNKNOWN: + Log.w( + TAG, + "Specify a proper usage in the audio attributes for audio focus" + + " handling. Using AUDIOFOCUS_GAIN by default."); + return C.AUDIOFOCUS_GAIN; + + // Javadoc says 'AUDIOFOCUS_GAIN_TRANSIENT: An example is for playing an alarm, or + // during a VoIP call' + case C.USAGE_ALARM: + case C.USAGE_VOICE_COMMUNICATION: + return C.AUDIOFOCUS_GAIN_TRANSIENT; + + // Javadoc says 'AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK: Examples are when playing + // driving directions or notifications' + case C.USAGE_ASSISTANCE_NAVIGATION_GUIDANCE: + case C.USAGE_ASSISTANCE_SONIFICATION: + case C.USAGE_NOTIFICATION: + case C.USAGE_NOTIFICATION_COMMUNICATION_DELAYED: + case C.USAGE_NOTIFICATION_COMMUNICATION_INSTANT: + case C.USAGE_NOTIFICATION_COMMUNICATION_REQUEST: + case C.USAGE_NOTIFICATION_EVENT: + case C.USAGE_NOTIFICATION_RINGTONE: + return C.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK; + + // Javadoc says 'AUDIOFOCUS_GAIN_EXCLUSIVE: This is typically used if you are doing + // audio recording or speech recognition'. + // Assistant is considered as both recording and notifying developer + case C.USAGE_ASSISTANT: + if (Util.SDK_INT >= 19) { + return C.AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE; + } else { + return C.AUDIOFOCUS_GAIN_TRANSIENT; + } + + // Special usages: + case C.USAGE_ASSISTANCE_ACCESSIBILITY: + if (audioAttributes.contentType == C.CONTENT_TYPE_SPEECH) { + // Voice shouldn't be interrupted by other playback. + return C.AUDIOFOCUS_GAIN_TRANSIENT; + } + return C.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK; + default: + Log.w(TAG, "Unidentified audio usage: " + audioAttributes.usage); + return C.AUDIOFOCUS_NONE; + } + } + + // Internal audio focus listener. + + private class AudioFocusListener implements AudioManager.OnAudioFocusChangeListener { + @Override + public void onAudioFocusChange(int focusChange) { + // Convert the platform focus change to internal state. + switch (focusChange) { + case AudioManager.AUDIOFOCUS_LOSS: + audioFocusState = AUDIO_FOCUS_STATE_LOST_FOCUS; + break; + case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT: + audioFocusState = AUDIO_FOCUS_STATE_LOSS_TRANSIENT; + break; + case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK: + if (willPauseWhenDucked()) { + audioFocusState = AUDIO_FOCUS_STATE_LOSS_TRANSIENT; + } else { + audioFocusState = AUDIO_FOCUS_STATE_LOSS_TRANSIENT_DUCK; + } + break; + case AudioManager.AUDIOFOCUS_GAIN: + audioFocusState = AUDIO_FOCUS_STATE_HAVE_FOCUS; + break; + default: + Log.w(TAG, "Unknown focus change type: " + focusChange); + // Early return. + return; + } + + // Handle the internal state (change). + switch (audioFocusState) { + case AUDIO_FOCUS_STATE_NO_FOCUS: + // Focus was not requested; nothing to do. + break; + case AUDIO_FOCUS_STATE_LOST_FOCUS: + playerControl.executePlayerCommand(PLAYER_COMMAND_DO_NOT_PLAY); + abandonAudioFocus(/* forceAbandon= */ true); + break; + case AUDIO_FOCUS_STATE_LOSS_TRANSIENT: + playerControl.executePlayerCommand(PLAYER_COMMAND_WAIT_FOR_CALLBACK); + break; + case AUDIO_FOCUS_STATE_LOSS_TRANSIENT_DUCK: + // Volume will be adjusted by the code below. + break; + case AUDIO_FOCUS_STATE_HAVE_FOCUS: + playerControl.executePlayerCommand(PLAYER_COMMAND_PLAY_WHEN_READY); + break; + default: + throw new IllegalStateException("Unknown audio focus state: " + audioFocusState); + } + + float volumeMultiplier = + (audioFocusState == AUDIO_FOCUS_STATE_LOSS_TRANSIENT_DUCK) + ? AudioFocusManager.VOLUME_MULTIPLIER_DUCK + : AudioFocusManager.VOLUME_MULTIPLIER_DEFAULT; + if (AudioFocusManager.this.volumeMultiplier != volumeMultiplier) { + AudioFocusManager.this.volumeMultiplier = volumeMultiplier; + playerControl.setVolumeMultiplier(volumeMultiplier); + } + } + } +}