当前位置: 首页 > article >正文

学习Android Audio 焦点记录

Android Audio 焦点管理详解

1.概述

  • 音频焦点:Android 系统中用于管理多个应用同时播放音频的机制。
  • 核心目标:确保用户在不同应用之间切换时,音频播放行为合理且有序。

2.音频焦点的基本概念

2.1 什么是音频焦点?

  • 定义:音频焦点是系统为应用分配的一种资源,用于控制音频播放的优先级。
  • 作用:避免多个应用同时播放音频,导致声音混乱。

2.2音频焦点的类型

焦点类型使用场景
AUDIOFOCUS_NONE无焦点状态,通常用于初始化或未请求焦点时。
AUDIOFOCUS_GAIN长时间占用音频焦点,如音乐播放器、视频播放器等。
AUDIOFOCUS_GAIN_TRANSIENT短时获取焦点,失去焦点时暂停播放,如语音提示、通知音等。
AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK短时获取焦点,失去焦点时降低音量,如导航提示音等。
AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE短时获取焦点,失去焦点时完全停止播放,如电话铃声、紧急提示音等。

3.获取与释放音频焦点

3.1获取音频焦点

  • 方法:使用 AudioManager.requestAudioFocus
  • 示例代码
    AudioManager audioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
    int result = audioManager.requestAudioFocus(
        focusChangeListener,
        AudioManager.STREAM_MUSIC,
        AudioManager.AUDIOFOCUS_GAIN
    );
    if (result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
        // 成功获取焦点
    }
    

3.2释放音频焦点

  • 方法:使用 AudioManager.abandonAudioFocus
  • 示例代码
  audioManager.abandonAudioFocus(focusChangeListener);

4.处理音频焦点变化

4.1焦点变化事件

焦点类型使用场景
AUDIOFOCUS_LOSS永久失去焦点,停止播放并释放资源。
AUDIOFOCUS_LOSS_TRANSIENT暂时失去焦点,暂停播放。
AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK暂时失去焦点,降低音量。

4.2实现 OnAudioFocusChangeListener

  • 示例代码
  AudioManager.OnAudioFocusChangeListener focusChangeListener = new AudioManager.OnAudioFocusChangeListener() {
    @Override
    public void onAudioFocusChange(int focusChange) {
        switch (focusChange) {
            case AudioManager.AUDIOFOCUS_GAIN:
                // 恢复播放
                break;
            case AudioManager.AUDIOFOCUS_LOSS:
                // 停止播放
                break;
            case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT:
                // 暂停播放
                break;
            case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:
                // 降低音量
                break;
        }
    }
};

5.代码分析

5.1requestAudioFocus分析

AudioManager.java
AudioServer.java
MediaFocusControl.java
FocusRequester.java

在AudioManager中做了如下工作:

  • 检查传入的 AudioManager.OnAudioFocusChangeListener 是否为空。
  • 检查音频流类型(如 AudioManager.STREAM_MUSIC)是否有效。
  • 检查焦点请求类型(如 AUDIOFOCUS_GAIN)是否合法。
  • 将传参打包成AudioFocusRequest对象,并且生成FocusRequestInfo存储到mAudioFocusIdListenerMap中便于后续管理和触发焦点变化事件。
  • 通过binder调用AudioService.java接口requestAudioFocus
    在AudioService.java中做了如下工作:
  • 初始化MediaMetrics,这说明这个方法有日志记录的功能,帮助开发者跟踪焦点请求的情况。
  • 完成权限和参数校验
    protected int requestAudioFocus(@NonNull AudioAttributes aa, int focusChangeHint, IBinder cb,
            IAudioFocusDispatcher fd, @NonNull String clientId, @NonNull String callingPackageName,
            int flags, int sdk, boolean forceDuck, int testUid) {

        // we need a valid binder callback for clients
        if (!cb.pingBinder()) {
            Log.e(TAG, " AudioFocus DOA client for requestAudioFocus(), aborting.");
            return AudioManager.AUDIOFOCUS_REQUEST_FAILED;
        }
        synchronized(mAudioFocusLock) {
            if (mFocusStack.size() > MAX_STACK_SIZE) {
                Log.e(TAG, "Max AudioFocus stack size reached, failing requestAudioFocus()");
                return AudioManager.AUDIOFOCUS_REQUEST_FAILED;
            }

            boolean enteringRingOrCall = !mRingOrCallActive
                    & (AudioSystem.IN_VOICE_COMM_FOCUS_ID.compareTo(clientId) == 0);
            if (enteringRingOrCall) { mRingOrCallActive = true; }

            final AudioFocusInfo afiForExtPolicy;
            if (mFocusPolicy != null) {
                // construct AudioFocusInfo as it will be communicated to audio focus policy
                afiForExtPolicy = new AudioFocusInfo(aa, uid,
                        clientId, callingPackageName, focusChangeHint, 0 /*lossReceived*/,
                        flags, sdk);
            } else {
                afiForExtPolicy = null;
            }
            // handle delayed focus
            boolean focusGrantDelayed = false;
            if (!canReassignAudioFocus()) {
                if ((flags & AudioManager.AUDIOFOCUS_FLAG_DELAY_OK) == 0) {
                    return AudioManager.AUDIOFOCUS_REQUEST_FAILED;
                } else {
                    // request has AUDIOFOCUS_FLAG_DELAY_OK: focus can't be
                    // granted right now, so the requester will be inserted in the focus stack
                    // to receive focus later
                    focusGrantDelayed = true;
                }
            }
            // external focus policy?
            if (mFocusPolicy != null) {
                if (notifyExtFocusPolicyFocusRequest_syncAf(afiForExtPolicy, fd, cb)) {
                    // stop handling focus request here as it is handled by external audio
                    // focus policy (return code will be handled in AudioManager)
                    return AudioManager.AUDIOFOCUS_REQUEST_WAITING_FOR_EXT_POLICY;
                } else {
                    // an error occured, client already dead, bail early
                    return AudioManager.AUDIOFOCUS_REQUEST_FAILED;
                }
            }
            AudioFocusDeathHandler afdh = new AudioFocusDeathHandler(cb);
            try {
                cb.linkToDeath(afdh, 0);
            } catch (RemoteException e) {
                // client has already died!
                Log.w(TAG, "AudioFocus  requestAudioFocus() could not link to "+cb+" binder death");
                return AudioManager.AUDIOFOCUS_REQUEST_FAILED;
            }
            if (!mFocusStack.empty() && mFocusStack.peek().hasSameClient(clientId)) {
                // if focus is already owned by this client and the reason for acquiring the focus
                // hasn't changed, don't do anything
                final FocusRequester fr = mFocusStack.peek();
                if (fr.getGainRequest() == focusChangeHint && fr.getGrantFlags() == flags) {
                    // unlink death handler so it can be gc'ed.
                    // linkToDeath() creates a JNI global reference preventing collection.
                    cb.unlinkToDeath(afdh, 0);
                    notifyExtPolicyFocusGrant_syncAf(fr.toAudioFocusInfo(),
                            AudioManager.AUDIOFOCUS_REQUEST_GRANTED);
                    return AudioManager.AUDIOFOCUS_REQUEST_GRANTED;
                }
                // the reason for the audio focus request has changed: remove the current top of
                // stack and respond as if we had a new focus owner
                if (!focusGrantDelayed) {
                    mFocusStack.pop();
                    // the entry that was "popped" is the same that was "peeked" above
                    fr.release();
                }
            }

            // focus requester might already be somewhere below in the stack, remove it
            removeFocusStackEntry(clientId, false /* signal */, false /*notifyFocusFollowers*/);

            final FocusRequester nfr = new FocusRequester(aa, focusChangeHint, flags, fd, cb,
                    clientId, afdh, callingPackageName, uid, this, sdk);

            if (mMultiAudioFocusEnabled
                    && (focusChangeHint == AudioManager.AUDIOFOCUS_GAIN)) {
                if (enteringRingOrCall) {
                    if (!mMultiAudioFocusList.isEmpty()) {
                        for (FocusRequester multifr : mMultiAudioFocusList) {
                            multifr.handleFocusLossFromGain(focusChangeHint, nfr, forceDuck);
                        }
                    }
                } else {
                    boolean needAdd = true;
                    if (!mMultiAudioFocusList.isEmpty()) {
                        for (FocusRequester multifr : mMultiAudioFocusList) {
                            if (multifr.getClientUid() == Binder.getCallingUid()) {
                                needAdd = false;
                                break;
                            }
                        }
                    }
                    if (needAdd) {
                        mMultiAudioFocusList.add(nfr);
                    }
                    nfr.handleFocusGainFromRequest(AudioManager.AUDIOFOCUS_REQUEST_GRANTED);
                    notifyExtPolicyFocusGrant_syncAf(nfr.toAudioFocusInfo(),
                            AudioManager.AUDIOFOCUS_REQUEST_GRANTED);
                    return AudioManager.AUDIOFOCUS_REQUEST_GRANTED;
                }
            }
            if (focusGrantDelayed) {
                //找到最后一个被锁定的焦点所有者(exclusive focus owner)的位置,然后将nfr插入到该位置之下
                final int requestResult = pushBelowLockedFocusOwners(nfr);
                if (requestResult != AudioManager.AUDIOFOCUS_REQUEST_FAILED) {
                    notifyExtPolicyFocusGrant_syncAf(nfr.toAudioFocusInfo(), requestResult);
                }
                return requestResult;
            } else {
                // propagate the focus change through the stack
                propagateFocusLossFromGain_syncAf(focusChangeHint, nfr, forceDuck);
                // push focus requester at the top of the audio focus stack
                mFocusStack.push(nfr);
                nfr.handleFocusGainFromRequest(AudioManager.AUDIOFOCUS_REQUEST_GRANTED);
            }
            notifyExtPolicyFocusGrant_syncAf(nfr.toAudioFocusInfo(),
                    AudioManager.AUDIOFOCUS_REQUEST_GRANTED);

            if (ENFORCE_MUTING_FOR_RING_OR_CALL & enteringRingOrCall) {
                runAudioCheckerForRingOrCallAsync(true/*enteringRingOrCall*/);
            }
        }//synchronized(mAudioFocusLock)

        return AudioManager.AUDIOFOCUS_REQUEST_GRANTED;
    }

在MediaFocusControl.java中做了如下工作:

  • 检查Binder的存活状态来确保客户端有效,如果客户端已经死亡,则直接返回失败
  • 电话或铃声场景的特殊情况,设置相关标志位 enteringRingOrCall
  • 构建AudioFocusInfo
  • 当存在外部焦点策略(mFocusPolicy)时,方法将焦点请求转发给外部策略处理,并根据返回结果决定是否继续内部处理 比如CarAudio
  • 注册死亡通知,确保在客户端死亡时能正确释放焦点,避免资源泄漏。
  • 如果当前堆栈顶部的焦点请求者与当前客户端相同且参数未变,则通知外部焦点策略然后直接返回已授予状态,避免重复处理,
  • 移除旧的焦点请求从堆栈中
  • 生成FocusRequester对象
  • 根据是否启用多音频焦点(mMultiAudioFocusEnabled)来处理不同的场景
  • 对于延迟授予焦点的情况(AUDIOFOCUS_FLAG_DELAY_OK),方法将新请求插入堆栈中的适当位置,等待后续处理。否则,立即传播焦点变化,并更新焦点堆栈。
  • 最后,方法处理进入电话或铃声场景时的音频检查,确保系统音频状态正确更新,并返回焦点请求的结果。
开始
日志记录
权限检查
外部策略?
外部处理
从堆栈移除焦点
多焦点场景?
多焦点处理
延迟焦点场景?
找到合适的堆栈位置插入
发送通知
堆栈更新
恢复音量

5.2.propagateFocusLossFromGain_syncAf分析

这个函数的作用就是给注册的焦点发送焦点loss的消息。

handleFocusLossFromGain
dispatchAudioFocusChange
MediaFocusControl.java
FocusRequester.java
AudioManager.java

直接来看FocusRequester.java中的代码

    void handleFocusLossFromGain(int focusChange, FocusRequester requester, boolean forceDuck) {
        if (mAudioFocusDispatcher != null) {
            mAudioFocusDispatcher.dispatchAudioFocusChange(focusChange, requester);
        } else {
            dispatchAudioFocusChange(focusChange, requester);
        }
    }

在这里插入图片描述

  • 1、2、3分别是A、B和C申请焦点,分别在栈中的位置以及其丢失焦点类型
    3步骤C申请AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK时,会根据MediaFocusControl的ENFORCE_DUCKING 来决定谁负责降音,true由framework进行降音,false将会以AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK类型返回给B,由B自己负责降音
  • 4步骤是再次申请焦点时,
    1)如果申请者A再次申请,已经存在栈中但又不在栈顶,则先移除栈中的A,在从新push到栈顶
    2)如果申请者C再次申请,已经存在且在栈顶,并且申请的焦点类型与之前相同就直接返回给C获得焦点;反之申请焦点类型不同,则会移除栈顶C,然后在重新push到栈顶
  • 4步骤释放abandon焦点;何时释放焦点呢?
    1)焦点使用者播放完音频资源后主动释放焦点
    2)焦点使用者收到了LOSS_GAIN永久丢失焦点类型,主动释放焦点
    释放规则如上图,自行查看

5.3frameworkHandleFocusLoss

frameworkHandleFocusLoss来负责framework的降音逻辑,如果返回值是true那么代表framework已经处理过了不需要通知app,如果是false那么就是需要通知app处理
注意
焦点切换发生在同一个应用内时,framework不会处理,而是直接返回false,由应用自己处理
框架处理焦点丢失的主要场景包括:

  • 降低音量:当焦点丢失类型为 AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK 且满足特定条件时,由框架强制处理 ducking。
  • 淡出播放:当焦点丢失类型为 AUDIOFOCUS_LOSS 且满足特定条件时,由框架处理淡出逻辑。
    其他情况:通常由应用自己处理焦点丢失事件。

5.4 dispatchAudioFocusChange

在AudioManager.java中dispatchAudioFocusChange会从mAudioFocusIdListenerMap找到指定的app,然后把焦点改变的消息通知给app。app在自身做相应的处理。

6.总结

音频焦点框架主要就是对各个APP的焦点进行注册、通知、释放。焦点释放可能会在framework完成那么app也就收不到相应的焦点通知了。

参考链接

https://blog.csdn.net/jackzhouyu/article/details/130527253?ops_request_misc=%257B%2522request%255Fid%2522%253A%25226a043c2db23192475d075cb14bd2cd11%2522%252C%2522scm%2522%253A%252220140713.130102334.pc%255Fblog.%2522%257D&request_id=6a043c2db23192475d075cb14bd2cd11&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2blogfirst_rank_ecpm_v1~rank_v31_ecpm-1-130527253-null-null.nonecase&utm_term=%E7%84%A6%E7%82%B9&spm=1018.2226.3001.4450


http://www.kler.cn/a/583051.html

相关文章:

  • scoop退回软件版本的方法
  • 【AIGC】计算机视觉-YOLO系列家族
  • 【lf中的git实战】
  • Rust语言基础知识详解【九】
  • 【redis】hash基本命令和内部编码
  • 《MySQL数据库从零搭建到高效管理|库的基本操作》
  • leetcode hot100 图论
  • Hive-基础入门
  • 命令行重启Ubuntu软件
  • 使用谷歌地图google实现功能去选择定点位置,可以搜索位置。实现地图选择器组件:Vue + Google Maps API 实战
  • 如何使用Cursor的claude-3.7模型来开发高保真的原型设计图,学会写好的提示词人人都是设计师
  • Android Retrofit 请求执行模块执行原理深入源码分析(三)
  • c语言笔记 函数入门
  • 【uniapp】textarea maxlength字数计算不准确的问题
  • Go Context深度剖析
  • doris:Elasticsearch
  • 【K8s】使用Kubernetes的resources字段中的requests和limits字段控制Pod资源使用
  • uni-app如何发布项目为app_2025
  • Linux进程基础知识
  • 在线Doc/Docx转换为PDF格式 超快速转换的一款办公软件 文档快速转换 在线转换免费转换办公软件