本文首发地址 https://h89.cn/archives/476.html

一、音频焦点简介

在 Android 系统中,音频焦点(Audio Focus)是一种机制,用于管理多个应用程序同时播放音频时的冲突。当一个应用程序请求音频焦点并获得它时,其他应用程序在播放音频时需要做出相应的调整,以避免多个音频同时播放造成混乱。

二、使用场景

(一)音乐播放器

  1. 当用户正在使用音乐播放器收听歌曲时,若有来电,音乐播放器应暂停播放,以让电话铃声能够清晰地被听到。当电话结束后,音乐播放器可以根据情况恢复播放。
  2. 若用户在听音乐的过程中打开了另一个音乐类应用,此时正在播放音乐的应用应该暂停或降低音量,以避免两个音乐同时播放。

(二)语音导航应用

  1. 在用户使用语音导航的同时,如果有音乐播放,导航的语音提示应该能够优先播放,确保用户能够清楚地听到导航指令。
  2. 当导航语音提示结束后,音乐可以恢复正常播放。

(三)社交类应用的语音消息

  1. 当用户在收听社交类应用的语音消息时,若有其他音频正在播放,应该暂停或降低其他音频的音量,以便用户能够听清语音消息。
  2. 语音消息播放完毕后,其他音频可以恢复播放。

三、版本兼容性处理

(一)Android 8.0 及以上版本

在 Android 8.0(API 26)及以上版本中,音频焦点的请求方式发生了变化,推荐使用 AudioFocusRequest 来构建请求:

import android.content.Context;
import android.media.AudioAttributes;
import android.media.AudioFocusRequest;
import android.media.AudioManager;
import android.os.Build;

public class AudioFocusHelper {
    private AudioManager audioManager;
    private AudioFocusRequest focusRequest;
    private OnAudioFocusChangeListener focusChangeListener;

    public interface OnAudioFocusChangeListener {
        void onAudioFocusChange(int focusChange);
    }

    public AudioFocusHelper(Context context, OnAudioFocusChangeListener listener) {
        audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
        this.focusChangeListener = listener;
    }

    public boolean requestAudioFocus() {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            AudioAttributes playbackAttributes = new AudioAttributes.Builder()
                .setUsage(AudioAttributes.USAGE_MEDIA)
                .setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
                .build();

            focusRequest = new AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN)
                .setAudioAttributes(playbackAttributes)
                .setAcceptsDelayedFocusGain(true)
                .setOnAudioFocusChangeListener(focusChangeListener::onAudioFocusChange)
                .build();

            int result = audioManager.requestAudioFocus(focusRequest);
            return result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED;
        } else {
            int result = audioManager.requestAudioFocus(focusChangeListener::onAudioFocusChange,
                AudioManager.STREAM_MUSIC,
                AudioManager.AUDIOFOCUS_GAIN);
            return result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED;
        }
    }

    public void abandonAudioFocus() {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            if (focusRequest != null) {
                audioManager.abandonAudioFocusRequest(focusRequest);
            }
        } else {
            audioManager.abandonAudioFocus(focusChangeListener::onAudioFocusChange);
        }
    }
}

四、音频焦点与 MediaSession 结合使用

在现代 Android 应用中,推荐将音频焦点与 MediaSession 结合使用,以提供更好的媒体控制体验。以下是使用 AndroidX 库的更新版本:

import android.content.Context;
import android.media.AudioManager;
import androidx.media.session.MediaSessionCompat;
import androidx.media.app.NotificationCompat;
import androidx.media.session.PlaybackStateCompat;

public class MediaPlaybackManager {
    private MediaSessionCompat mediaSession;
    private AudioFocusHelper audioFocusHelper;
    private boolean playOnAudioFocus = false;
    private Context context;

    public MediaPlaybackManager(Context context) {
        this.context = context;
        audioFocusHelper = new AudioFocusHelper(context, this::onAudioFocusChange);
        mediaSession = new MediaSessionCompat(context, "MediaPlaybackManager");
        setupMediaSession();
    }

    private void setupMediaSession() {
        mediaSession.setCallback(new MediaSessionCompat.Callback() {
            @Override
            public void onPlay() {
                if (audioFocusHelper.requestAudioFocus()) {
                    mediaSession.setActive(true);
                    // 开始播放
                    startPlayback();
                } else {
                    playOnAudioFocus = true;
                }
            }

            @Override
            public void onPause() {
                // 暂停播放
                pausePlayback();
            }

            @Override
            public void onStop() {
                audioFocusHelper.abandonAudioFocus();
                mediaSession.setActive(false);
                // 停止播放
                stopPlayback();
            }
        });

        // 设置播放状态
        PlaybackStateCompat state = new PlaybackStateCompat.Builder()
            .setActions(PlaybackStateCompat.ACTION_PLAY | 
                       PlaybackStateCompat.ACTION_PAUSE | 
                       PlaybackStateCompat.ACTION_PLAY_PAUSE |
                       PlaybackStateCompat.ACTION_STOP)
            .setState(PlaybackStateCompat.STATE_NONE, 0, 1.0f)
            .build();
        mediaSession.setPlaybackState(state);
    }

    private void onAudioFocusChange(int focusChange) {
        switch (focusChange) {
            case AudioManager.AUDIOFOCUS_GAIN:
                if (playOnAudioFocus) {
                    playOnAudioFocus = false;
                    startPlayback();
                }
                // 恢复音量
                break;
            case AudioManager.AUDIOFOCUS_LOSS:
                playOnAudioFocus = false;
                stopPlayback();
                break;
            case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT:
                playOnAudioFocus = true;
                pausePlayback();
                break;
            case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:
                // 降低音量
                break;
        }
    }

    private void startPlayback() {
        // 实现播放逻辑
        PlaybackStateCompat state = new PlaybackStateCompat.Builder()
            .setState(PlaybackStateCompat.STATE_PLAYING, 0, 1.0f)
            .build();
        mediaSession.setPlaybackState(state);
    }

    private void pausePlayback() {
        // 实现暂停逻辑
        PlaybackStateCompat state = new PlaybackStateCompat.Builder()
            .setState(PlaybackStateCompat.STATE_PAUSED, 0, 1.0f)
            .build();
        mediaSession.setPlaybackState(state);
    }

    private void stopPlayback() {
        // 实现停止逻辑
        PlaybackStateCompat state = new PlaybackStateCompat.Builder()
            .setState(PlaybackStateCompat.STATE_STOPPED, 0, 1.0f)
            .build();
        mediaSession.setPlaybackState(state);
    }
}

五、音频焦点在前台服务中的使用

对于需要在后台持续播放音频的应用,应该在前台服务中处理音频焦点。以下是使用 AndroidX 和通知渠道的更新版本:

import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.os.Build;
import android.os.IBinder;
import androidx.core.app.NotificationCompat;
import androidx.media.app.NotificationCompat.MediaStyle;

public class AudioPlaybackService extends Service {
    private static final int NOTIFICATION_ID = 1;
    private static final String CHANNEL_ID = "audio_playback_channel";
    private AudioFocusHelper audioFocusHelper;
    private MediaPlaybackManager mediaPlaybackManager;
    private NotificationManager notificationManager;

    @Override
    public void onCreate() {
        super.onCreate();
        audioFocusHelper = new AudioFocusHelper(this, this::onAudioFocusChange);
        mediaPlaybackManager = new MediaPlaybackManager(this);
        notificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
        createNotificationChannel();
    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        if (audioFocusHelper.requestAudioFocus()) {
            startForeground(NOTIFICATION_ID, createNotification());
            // 开始播放
            return START_STICKY;
        }
        return START_NOT_STICKY;
    }

    private void createNotificationChannel() {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            NotificationChannel channel = new NotificationChannel(
                CHANNEL_ID,
                "音频播放",
                NotificationManager.IMPORTANCE_LOW);
            channel.setDescription("显示音频播放控制");
            notificationManager.createNotificationChannel(channel);
        }
    }

    private Notification createNotification() {
        // 创建点击通知时的Intent
        Intent intent = new Intent(this, MainActivity.class);
        PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, intent, 0);

        // 创建媒体控制按钮的Intent
        Intent pauseIntent = new Intent(this, AudioPlaybackService.class);
        pauseIntent.setAction("ACTION_PAUSE");
        PendingIntent pausePendingIntent = PendingIntent.getService(this, 0, pauseIntent, 0);

        // 创建通知
        NotificationCompat.Builder builder = new NotificationCompat.Builder(this, CHANNEL_ID)
            .setContentTitle("正在播放音乐")
            .setContentText("歌曲名称 - 艺术家")
            .setSmallIcon(R.drawable.ic_music_note)
            .setContentIntent(pendingIntent)
            .addAction(R.drawable.ic_pause, "暂停", pausePendingIntent)
            .setStyle(new MediaStyle()
                .setShowActionsInCompactView(0)
                .setMediaSession(mediaPlaybackManager.getMediaSessionToken()))
            .setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
            .setOngoing(true);

        return builder.build();
    }

    @Override
    public void onDestroy() {
        audioFocusHelper.abandonAudioFocus();
        stopForeground(true);
        super.onDestroy();
    }

    @Override
    public IBinder onBind(Intent intent) {
        return null;
    }

    private void onAudioFocusChange(int focusChange) {
        // 处理音频焦点变化
    }
}

六、ExoPlayer 中的音频焦点处理

ExoPlayer 是 Android 推荐的现代媒体播放器库,它内置了对音频焦点的支持。以下是 ExoPlayer 与音频焦点结合使用的示例:

import android.content.Context;
import android.media.AudioManager;
import androidx.media3.common.AudioAttributes;
import androidx.media3.common.C;
import androidx.media3.common.Player;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.datasource.DefaultDataSource;
import androidx.media3.exoplayer.ExoPlayer;
import androidx.media3.exoplayer.trackselection.DefaultTrackSelector;
import androidx.media3.exoplayer.upstream.DefaultLoadControl;
import androidx.media3.ui.PlayerView;

@UnstableApi
public class ExoPlayerAudioFocusManager implements Player.Listener, AudioManager.OnAudioFocusChangeListener {
    private final ExoPlayer player;
    private final AudioManager audioManager;
    private final AudioFocusRequest audioFocusRequest;
    private boolean playOnFocusGain;
    private final Context context;

    public ExoPlayerAudioFocusManager(Context context, PlayerView playerView) {
        this.context = context;
        audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);

        // 创建ExoPlayer实例
        DefaultTrackSelector trackSelector = new DefaultTrackSelector(context);
        DefaultLoadControl loadControl = new DefaultLoadControl();
        DefaultDataSource.Factory dataSourceFactory = new DefaultDataSource.Factory(context);

        player = new ExoPlayer.Builder(context)
            .setTrackSelector(trackSelector)
            .setLoadControl(loadControl)
            .build();

        playerView.setPlayer(player);
        player.addListener(this);

        // 设置音频属性
        AudioAttributes audioAttributes = new AudioAttributes.Builder()
            .setUsage(C.USAGE_MEDIA)
            .setContentType(C.AUDIO_CONTENT_TYPE_MUSIC)
            .build();

        player.setAudioAttributes(audioAttributes, true);

        // 创建音频焦点请求
        if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
            android.media.AudioAttributes playbackAttributes = new android.media.AudioAttributes.Builder()
                .setUsage(android.media.AudioAttributes.USAGE_MEDIA)
                .setContentType(android.media.AudioAttributes.CONTENT_TYPE_MUSIC)
                .build();

            audioFocusRequest = new AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN)
                .setAudioAttributes(playbackAttributes)
                .setAcceptsDelayedFocusGain(true)
                .setOnAudioFocusChangeListener(this)
                .build();
        } else {
            audioFocusRequest = null;
        }
    }

    public void play(String mediaUrl) {
        // 准备媒体源
        MediaItem mediaItem = MediaItem.fromUri(mediaUrl);
        player.setMediaItem(mediaItem);
        player.prepare();

        // 请求音频焦点
        if (requestAudioFocus()) {
            player.play();
        } else {
            playOnFocusGain = true;
        }
    }

    public void pause() {
        player.pause();
    }

    public void release() {
        abandonAudioFocus();
        player.release();
    }

    private boolean requestAudioFocus() {
        if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O && audioFocusRequest != null) {
            int result = audioManager.requestAudioFocus(audioFocusRequest);
            return result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED;
        } else {
            int result = audioManager.requestAudioFocus(
                this, 
                AudioManager.STREAM_MUSIC, 
                AudioManager.AUDIOFOCUS_GAIN);
            return result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED;
        }
    }

    private void abandonAudioFocus() {
        if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O && audioFocusRequest != null) {
            audioManager.abandonAudioFocusRequest(audioFocusRequest);
        } else {
            audioManager.abandonAudioFocus(this);
        }
    }

    @Override
    public void onAudioFocusChange(int focusChange) {
        switch (focusChange) {
            case AudioManager.AUDIOFOCUS_GAIN:
                if (playOnFocusGain) {
                    playOnFocusGain = false;
                    player.setVolume(1.0f);
                    player.play();
                }
                break;

            case AudioManager.AUDIOFOCUS_LOSS:
                playOnFocusGain = false;
                player.pause();
                break;

            case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT:
                playOnFocusGain = true;
                player.pause();
                break;

            case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:
                // 降低音量而不是暂停
                player.setVolume(0.3f);
                break;
        }
    }

    @Override
    public void onPlayWhenReadyChanged(boolean playWhenReady, int reason) {
        if (playWhenReady) {
            requestAudioFocus();
        } else {
            abandonAudioFocus();
        }
    }

    @Override
    public void onIsPlayingChanged(boolean isPlaying) {
        // 处理播放状态变化
    }
}

七、音频焦点与蓝牙设备交互

当应用通过蓝牙设备播放音频时,需要特别处理音频焦点与蓝牙设备的交互:

import android.bluetooth.BluetoothA2dp;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothProfile;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.media.AudioManager;

public class BluetoothAudioManager {
    private final Context context;
    private final AudioManager audioManager;
    private final BluetoothAdapter bluetoothAdapter;
    private final AudioFocusHelper audioFocusHelper;
    private boolean isBluetoothConnected = false;
    private boolean wasPlayingBeforeDisconnect = false;

    public BluetoothAudioManager(Context context, AudioFocusHelper audioFocusHelper) {
        this.context = context;
        this.audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
        this.bluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
        this.audioFocusHelper = audioFocusHelper;
        registerBluetoothReceiver();
    }

    private void registerBluetoothReceiver() {
        IntentFilter filter = new IntentFilter();
        filter.addAction(BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED);
        filter.addAction(BluetoothDevice.ACTION_ACL_CONNECTED);
        filter.addAction(BluetoothDevice.ACTION_ACL_DISCONNECTED);
        context.registerReceiver(bluetoothReceiver, filter);
    }

    private final BroadcastReceiver bluetoothReceiver = new BroadcastReceiver() {
        @Override
        public void onReceive(Context context, Intent intent) {
            String action = intent.getAction();

            if (BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED.equals(action)) {
                int state = intent.getIntExtra(BluetoothA2dp.EXTRA_STATE, BluetoothA2dp.STATE_DISCONNECTED);

                switch (state) {
                    case BluetoothA2dp.STATE_CONNECTED:
                        onBluetoothConnected();
                        break;

                    case BluetoothA2dp.STATE_DISCONNECTED:
                        onBluetoothDisconnected();
                        break;
                }
            }
        }
    };

    private void onBluetoothConnected() {
        isBluetoothConnected = true;

        // 检查是否应该自动恢复播放
        if (wasPlayingBeforeDisconnect) {
            if (audioFocusHelper.requestAudioFocus()) {
                // 恢复播放
                startPlayback();
                wasPlayingBeforeDisconnect = false;
            }
        }
    }

    private void onBluetoothDisconnected() {
        isBluetoothConnected = false;

        // 如果正在播放,标记为应该在重新连接后恢复
        if (isCurrentlyPlaying()) {
            wasPlayingBeforeDisconnect = true;
            pausePlayback();
        }
    }

    public boolean isBluetoothAudioDevice() {
        if (bluetoothAdapter == null || !bluetoothAdapter.isEnabled()) {
            return false;
        }

        // 检查当前音频输出是否为蓝牙设备
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
            AudioDeviceInfo[] devices = audioManager.getDevices(AudioManager.GET_DEVICES_OUTPUTS);
            for (AudioDeviceInfo device : devices) {
                if (device.getType() == AudioDeviceInfo.TYPE_BLUETOOTH_SCO ||
                    device.getType() == AudioDeviceInfo.TYPE_BLUETOOTH_A2DP) {
                    return true;
                }
            }
        } else {
            // 兼容旧版本
            return audioManager.isBluetoothA2dpOn() || audioManager.isBluetoothScoOn();
        }

        return false;
    }

    public void handleBluetoothSco() {
        // 对于需要SCO连接的应用(如语音通话)
        if (bluetoothAdapter != null && bluetoothAdapter.isEnabled()) {
            if (!audioManager.isBluetoothScoOn()) {
                audioManager.startBluetoothSco();
                audioManager.setBluetoothScoOn(true);
            }
        }
    }

    public void releaseBluetoothSco() {
        if (audioManager.isBluetoothScoOn()) {
            audioManager.stopBluetoothSco();
            audioManager.setBluetoothScoOn(false);
        }
    }

    public void release() {
        context.unregisterReceiver(bluetoothReceiver);
        releaseBluetoothSco();
    }

    // 以下方法应由应用实现
    private boolean isCurrentlyPlaying() {
        // 返回当前播放状态
        return false;
    }

    private void startPlayback() {
        // 实现开始播放逻辑
    }

    private void pausePlayback() {
        // 实现暂停播放逻辑
    }
}

八、音频焦点请求失败的处理策略

在实际应用中,音频焦点请求可能会失败,我们需要有合适的处理策略:

  1. 立即重试策略:

    public boolean requestAudioFocusWithRetry() {
     int maxRetries = 3;
     int retryCount = 0;
    
     while (retryCount < maxRetries) {
         if (audioFocusHelper.requestAudioFocus()) {
             return true;
         }
         retryCount++;
         try {
             Thread.sleep(100); // 等待100毫秒后重试
         } catch (InterruptedException e) {
             Thread.currentThread().interrupt();
             return false;
         }
     }
     return false;
    }
  2. 延迟重试策略:

    private void scheduleAudioFocusRetry() {
     Handler handler = new Handler(Looper.getMainLooper());
     handler.postDelayed(() -> {
         if (audioFocusHelper.requestAudioFocus()) {
             if (playOnAudioFocus) {
                 startPlayback();
             }
         }
     }, 1000); // 1秒后重试
    }
  3. 用户触发重试:

    public void handleAudioFocusFailure() {
     // 显示提示给用户
     showPlaybackErrorDialog("无法获取音频焦点,是否重试?",
         () -> {
             if (audioFocusHelper.requestAudioFocus()) {
                 startPlayback();
             } else {
                 showToast("获取音频焦点失败");
             }
         });
    }

九、最佳实践与注意事项

  1. 始终释放音频焦点:当应用不再需要播放音频时,应及时释放音频焦点,以便其他应用可以使用。

  2. 处理各种焦点变化:正确处理 AUDIOFOCUS_GAIN、AUDIOFOCUS_LOSS、AUDIOFOCUS_LOSS_TRANSIENT 和 AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK 等各种情况。

  3. 使用 AndroidX 库:使用 AndroidX 中的 MediaSessionCompat 和其他兼容性类,确保在各个 Android 版本上的一致行为。

  4. 创建通知渠道:对于 Android 8.0 及以上版本,为前台服务创建适当的通知渠道。

  5. 考虑蓝牙设备:当应用通过蓝牙设备播放音频时,需要特别处理连接断开和重连的情况。

  6. 使用现代播放器:考虑使用 ExoPlayer 等现代媒体播放器,它们内置了对音频焦点的支持。

通过以上示例,我们可以看到音频焦点在 Android 应用中的重要性及其实现方式。合理使用音频焦点可以让应用提供更好的用户体验,与其他应用和谐共存。在实际开发中,建议根据具体场景选择合适的实现方式,并注意版本兼容性处理。


本文链接:Android 中音频焦点的使用场景及代码示例 - https://h89.cn/archives/476.html

版权声明:原创文章 遵循 CC 4.0 BY-SA 版权协议,转载请附上原文链接和本声明。

标签: MediaSession, ExoPlayer, 音频焦点, AudioFocusRequest, AudioManager, BluetoothAdapter

评论已关闭