音頻焦點問題


當我們在settings中試聽鈴聲,這時候突然來了一個電話,那么會出現試聽鈴聲和來電鈴聲同時播放的情況。當然,此情況同樣適用於鬧鍾鈴聲,媒體音樂播放等。那么怎么解決這個問題呢?這就需要當音頻焦點。---》

因為系統中可能會有多個應用程序會播放音頻,所以需要考慮他們之間該如何交互,為了避免多個應用程序同時播放音樂,Android 系統使用音頻焦點來進行統一管理,即只有獲得了音頻焦點的應用程序才可以播放音樂。 您的應用程序在開始播放音頻文件前,首先應該請求獲得音頻焦點,並且應該同時注冊監聽音頻焦點的丟失通知,即如果音頻焦點被系統或其他的應用程序搶占時,您的應用程序可以做出合適的響應。

首先,我要獲取一個音頻焦點並管理它。

      private boolean requestFocus() {
        // Request audio focus for playback
        int result = mAudioManager.requestAudioFocus(afChangeListener,
                AudioManager.STREAM_RING,
                AudioManager.AUDIOFOCUS_GAIN_TRANSIENT);
        return result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED;
    }

  OnAudioFocusChangeListener afChangeListener = new OnAudioFocusChangeListener() {
        public void onAudioFocusChange(int focusChange) {
            if (focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT
                    || focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK) {
                // Pause playback
                if (mLocalPlayer !=null && mLocalPlayer.isPlaying()){
                    mLocalPlayer.pause();
                }
            } else if (focusChange == AudioManager.AUDIOFOCUS_GAIN) {
                // Resume playback
                  startLocalPlayer();
            } else if (focusChange == AudioManager.AUDIOFOCUS_LOSS) {
                mAudioManager.abandonAudioFocus(afChangeListener);
                // Stop playback
                if (mLocalPlayer !=null && mLocalPlayer.isPlaying()){
                    mLocalPlayer.stop();
                }
            }
        }
    };

可以很清晰的看見,上面的第一個方法是獲取音頻焦點,通過requestAudioFocus()來實現。而第二個方法就是對音頻焦點進行監聽並管理。在這里,要先知道以上幾個值的含義:

  •      AUDIOFOCUS_GAIN_TRANSIENT:只是短暫獲得,一會就釋放焦點,比如你只是想發個notification時用下一秒不到的鈴聲。
  •      AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK:只是背景獲得,之前的音頻焦點使用者無需釋放焦點給我,我將與其共同使用。
  •      AUDIOFOCUS_GAIN:我要求完全獲得焦點,其他人需要釋放焦點。比如我要播放音樂了,這時就要搶占整個音頻焦點。
  •      AUDIOFOCUS_LOSS:你會長時間的失去焦點,所以不要指望在短時間內能獲得。請結束自己的相關音頻工作並做好收尾工作。
  •     AUDIOFOCUS_LOSS_TRANSIENT:你會短暫的失去音頻焦點,你可以暫停音樂,但不要釋放資源,因為你一會就可以奪回焦點並繼續使用。

知道了以上幾個字段的含義,在對應的狀態,我們就能做相應的處理。比如AUDIOFOCUS_LOSS_TRANSIENT短暫失去焦點,我們就暫停我們的音樂。AUDIOFOCUS_LOSS長期失去焦點,就直接停掉音樂。AUDIOFOCUS_GAIN我獲取了焦點,那么我就要開始播放音樂了(由於我完全獲取了焦點,其他音樂就無法播放了,自然當前就只有一個音樂進行播放)。

獲取音頻焦點,就要釋放音頻焦點:(在哪里釋放,就看當時的代碼吧)

    private void destroyLocalPlayer() {
        if (mLocalPlayer != null) {
            mLocalPlayer.reset();
            mLocalPlayer.release();
            mLocalPlayer = null;
            synchronized (sActiveRingtones) {
                sActiveRingtones.remove(this);
            }
        }
        mAudioManager.abandonAudioFocus(afChangeListener);
    }

在解決這個問題的時候,我選擇在每次播放試聽鈴聲時,獲取音頻焦點(何時獲取,也要看當時代碼情況):

    private void startLocalPlayer() {
        if (mLocalPlayer == null) {
            return;
        }
        synchronized (sActiveRingtones) {
            sActiveRingtones.add(this);
        }
        mLocalPlayer.setOnCompletionListener(mCompletionListener);
        if(requestFocus()){
            mLocalPlayer.start();
        }
    }

成功獲取到焦點,才可以播放當前的試聽鈴聲哦!

================================================================================================

更新!更新!這樣的改法果然引入了一個嚴重的bug,就是來電鈴聲不能播放!

為什么呢?首先看一下來電鈴聲播放的代碼:

    private void handlePlay(SomeArgs args) {
        RingtoneFactory factory = (RingtoneFactory) args.arg1;
        Call incomingCall = (Call) args.arg2;
        args.recycle();
        // don't bother with any of this if there is an EVENT_STOP waiting.
        if (mHandler.hasMessages(EVENT_STOP)) {
            return;
        }

        // If the Ringtone Uri is EMPTY, then the "None" Ringtone has been selected. Do not play
        // anything.
        if(Uri.EMPTY.equals(incomingCall.getRingtone())) {
            mRingtone = null;
            return;
        }

        ThreadUtil.checkNotOnMainThread();

        if (mRingtone == null) {
            mRingtone = factory.getRingtone(incomingCall);
            if (mRingtone == null) {
                Uri ringtoneUri = incomingCall.getRingtone();
                String ringtoneUriString = (ringtoneUri == null) ? "null" :
                        ringtoneUri.toSafeString();
                Log.event(null, Log.Events.ERROR_LOG, "Failed to get ringtone from factory. " +
                        "Skipping ringing. Uri was: " + ringtoneUriString);
                return;
            }
        }

        handleRepeat();
    }

    private void handleRepeat() {
        if (mRingtone == null) {
            return;
        }
        if (mRingtone.isPlaying()) {
            Log.d(this, "Ringtone already playing.");
        } else {
            mRingtone.play();
        }

        // Repost event to restart ringer in {@link RESTART_RINGER_MILLIS}.
        synchronized(this) {
            if (!mHandler.hasMessages(EVENT_REPEAT)) {
                mHandler.sendEmptyMessageDelayed(EVENT_REPEAT, RESTART_RINGER_MILLIS);
            }
        }
    }

代碼路徑:packages\services\Telecomm\src\com\android\server\telecom\AsyncRingtonePlayer.java

來電鈴聲播放是mRingtone.play()這一句,也就是說,最終也是用到了Ringtone.java這個類,並且會調用到startLocalPlay()這個方法,而在這個方法里,剛才我們首先獲取了音頻焦點,並設定獲取成功才能播放。我通過追加log發現,來電鈴聲的時候,音頻焦點獲取失敗了,這讓我很費解,為什么試聽鈴聲就能夠獲取成功,來電鈴聲就不行呢?於是繼續向下分析:

mAudioManager.requestAudioFocus(afChangeListener,
                AudioManager.STREAM_RING,
                AudioManager.AUDIOFOCUS_GAIN_TRANSIENT);

首先進到AudioManager里看一看這個requestAudioFocus方法,最后一直追蹤到AudioService里,看一看這里的代碼:

    public int requestAudioFocus(AudioAttributes aa, int durationHint, IBinder cb,
            IAudioFocusDispatcher fd, String clientId, String callingPackageName, int flags,
            IAudioPolicyCallback pcb) {
        // permission checks
        if ((flags & AudioManager.AUDIOFOCUS_FLAG_LOCK) == AudioManager.AUDIOFOCUS_FLAG_LOCK) {
            if (AudioSystem.IN_VOICE_COMM_FOCUS_ID.equals(clientId)) {
                if (PackageManager.PERMISSION_GRANTED != mContext.checkCallingOrSelfPermission(
                            android.Manifest.permission.MODIFY_PHONE_STATE)) {
                    Log.e(TAG, "Invalid permission to (un)lock audio focus", new Exception());
                    return AudioManager.AUDIOFOCUS_REQUEST_FAILED;
                }
            } else {
                // only a registered audio policy can be used to lock focus
                synchronized (mAudioPolicies) {
                    if (!mAudioPolicies.containsKey(pcb.asBinder())) {
                        Log.e(TAG, "Invalid unregistered AudioPolicy to (un)lock audio focus");
                        return AudioManager.AUDIOFOCUS_REQUEST_FAILED;
                    }
                }
            }
        }

        return mMediaFocusControl.requestAudioFocus(aa, durationHint, cb, fd,
                clientId, callingPackageName, flags);
    }

代碼路徑:frameworks\base\services\core\java\com\android\server\audio\AudioService.java

上述方法中的AudioSystem.IN_VOICE_COMM_FOCUS_ID的注釋是:

 /**
     * Constant to identify a focus stack entry that is used to hold the focus while the phone
     * is ringing or during a call. Used by com.android.internal.telephony.CallManager when
     * entering and exiting calls.
     */
    public final static String IN_VOICE_COMM_FOCUS_ID = "AudioFocus_For_Phone_Ring_And_Calls";

這就顯而易見了,原來來電鈴聲注冊音頻焦點就會失敗。所以requestFocus的判斷加在這里並不合適。所以我將代碼改成:

    private void startLocalPlayer() {
        if (mLocalPlayer == null) {
            return;
        }
        synchronized (sActiveRingtones) {
            sActiveRingtones.add(this);
        }
        mLocalPlayer.setOnCompletionListener(mCompletionListener);
        requestFocus();
        mLocalPlayer.start();
    }

判斷是否成功什么的,去見鬼吧!

 
        

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM