Allow SimpleExoPlayer to handle audio focus

Add automatic audio focus handling to SimpleExoPlayer. Audio focus
handling is an opt-in feature that can be requested by passing
the system's AudioManager and an AudioFocusConfiguration to
SimpleExoPlayer.setAudioFocusConfiguration.

When audio focus is being managed by SimpleExoPlayer, the player
will transparently handle pausing playback during
AUDIOFOCUS_LOSS_TRANSIENT, as well as lowering playback volume
during AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK.

-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=208045732
This commit is contained in:
borrelli 2018-08-09 07:33:04 -07:00 committed by Oliver Woodman
parent c1998da44e
commit 9db0dbc0e9
9 changed files with 620 additions and 75 deletions

View File

@ -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)).

View File

@ -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.
*/

View File

@ -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")

View File

@ -66,6 +66,7 @@ import java.util.concurrent.CopyOnWriteArraySet;
private final ArrayDeque<PlaybackInfoUpdate> 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,

View File

@ -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,

View File

@ -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.
*
* <p>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.
*
* <p>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.
*
* <p>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)}.
*
* <p>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();

View File

@ -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<com.google.android.exoplayer2.video.VideoListener>
@ -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,9 +412,12 @@ public class SimpleExoPlayer
@Override
public void setAudioAttributes(AudioAttributes audioAttributes) {
if (Util.areEqual(this.audioAttributes, audioAttributes)) {
return;
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) {
@ -435,6 +433,13 @@ public class SimpleExoPlayer
}
}
@AudioFocusManager.PlayerCommand
int playerCommand =
audioFocusManager.setAudioAttributes(
handleAudioFocus ? audioAttributes : null, getPlayWhenReady(), getPlaybackState());
updatePlayWhenReady(getPlayWhenReady(), playerCommand);
}
@Override
public AudioAttributes getAudioAttributes() {
return audioAttributes;
@ -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);
}
}
}

View File

@ -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)

View File

@ -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.
*
* <p>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);
}
}
}
}