AAOS CarMediaService problem analysis

Article directory

      • Problem Description
      • Car Bluetooth music process
      • Music monitoring focus change process
      • BT request focus process
      • MediaSession server process
      • The interaction between BT and music

Problem description

  • question

    When the AAOS interface is connected to Bluetooth, the music played by the Music app is paused.

  • analyze
    Pause is the behavior of the application. The Music application will listen for changes in focus, and will call pause when it detects the loss of focus. However, the Music application will also request focus when it first starts playing. The focus is on bt for the first time. bt lost focus, but immediately requested focus again. BT requests focus, causing the Music application to lose focus and pause.

Before you understand the problem, you must first understand it

  1. Where does Bluetooth music call the audio framework?
  2. audio focus
  3. carMediaService

Car Bluetooth music process

  • A2DP side

    Code location:
    system\bt\btif\src\btif_avrcp_audio_track.cc
    system\bt\stack\a2dp\a2dp_aac.cc
    In the above BtifAvrcpAudioTrackCreate() function. An AAudioStreamBuilder will be created here, and AAudio will process and write data through audiotrack in legacy mode.

  • AAudio side

    For Bluetooth music, the car terminal is a sink terminal, used as a player. The corresponding process in btif_avrcp_audio_track is to implement playback by calling the AAudio interface. Among them, AAudio does not implement mmap and adopts legacy mode.
    The code is in frameworks\av\media\libaaudio\src\legacy\AudioStreamTrack.cpp
    This is achieved by creating an audiotrack, then setting parameters and writing data into it. When audiotrack starts, the device will also be obtained by getoutputAttr. At this time, the routing information has been provided by AAOS according to car_audio_policy.xml
    To register.

  • Overall process:

    The data sent from the source (that is, the mobile phone through Bluetooth) is aac or ldac encoded data. The data continues to be decoded in a2dp and does not enter audiotrack. a2dp has the function equivalent to the decoder in the player, and the decoded data is called audiotrak for playback.

Music monitoring focus change process

  • Implement AudioFocusListener and then register to AudioMananger
 private OnAudioFocusChangeListener mAudioFocusListener = new OnAudioFocusChangeListener() {
        public void onAudioFocusChange(int focusChange) {
            mMediaplayerHandler.obtainMessage(FOCUSCHANGE, focusChange, 0).sendToTarget();
        }
    };
    mAudioManager.requestAudioFocus(
                mAudioFocusListener, AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN);

  • When the focus of the frame layer changes, call back to the externally registered listener.

  • Implementation of MediaPlayBackService in Music

Messages are sent through the looper for processing, and if focus is lost, the operation is pause.

case AudioManager.AUDIOFOCUS_LOSS:
Log.v(LOGTAG, "AudioFocus: received AUDIOFOCUS_LOSS");
    if (isPlaying()) {
        mPausedByTransientLossOfFocus = false;
    }
    pause();
break;
case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT:
    Log.v(LOGTAG, "AudioFocus: received AUDIOFOCUS_LOSS_TRANSIENT");
    if (isPlaying()) {
        mPausedByTransientLossOfFocus = true;
    }
    pause();
break;

The process of BT requesting focus

  • Listen to the onPrepare event of mediassion

requestFocus will be called after the Prepare event occurs. Its usage is USAGE_MEDIA. This will call back to AAOS’s CarAudioFocus
Processing is performed based on the interaction matrix. The music application is currently held. After BT requests focus, a message will be sent to notify music that the focus has been lost.
In the above process, we know that after losing focus, the player’s pause will be called to perform the pause operation.

packages\apps\Bluetooth\src\com\android\bluetooth\avrcpcontroller\AvrcpControllerStateMachine.java

BluetoothMediaBrowserService.addressedPlayerChanged(mSessionCallbacks);
sBluetoothMediaBrowserService.mSession.setCallback(callback);

MediaSessionCompat.Callback mSessionCallbacks = new MediaSessionCompat.Callback() {
        @Override
        public void onPrepare() {
            logD("onPrepare");
            A2dpSinkService a2dpSinkService = A2dpSinkService.getA2dpSinkService();
            if (a2dpSinkService != null) {
                a2dpSinkService.requestAudioFocus(mDevice, true);
            }
        }
}


    private synchronized int requestAudioFocus() {
        if (DBG) Log.d(TAG, "requestAudioFocus()");
        // Bluetooth A2DP may carry Music, Audio Books, Navigation, or other sounds so mark content
        // type unknown.
        AudioAttributes streamAttributes =
                new AudioAttributes.Builder().setUsage(AudioAttributes.USAGE_MEDIA)
                        .setContentType(AudioAttributes.CONTENT_TYPE_UNKNOWN)
                        .build();
        // Bluetooth ducking is handled at the native layer at the request of AudioManager.
        AudioFocusRequest focusRequest =
                new AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN).setAudioAttributes(
                        streamAttributes)
                        .setOnAudioFocusChangeListener(mAudioFocusListener, this)
                        .build();
        int focusRequestStatus = mAudioManager.requestAudioFocus(focusRequest);
        // If the request is granted begin streaming immediately and schedule an upgrade.
        if (focusRequestStatus == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
            startFluorideStreaming();
            mAudioFocus = AudioManager.AUDIOFOCUS_GAIN;
        }
        return focusRequestStatus;
    }


  • The question now is Where is onPrepare triggered?

You can see that the Callback of MediaSession is implemented. Understand the concept of MediaSession
MediaSession has client and server. The client corresponds to the UI side, and the server corresponds to the player.

The implementation of the UI side is in packages/app/Car/Media.
Mainly the encapsulation of several classes:
MediaControl: used to control MediaSession, the callback of MessaionCompat implemented in AVrcpControl above
play prepare is called by MediaControl.

packages\apps\Car\Media\src\com\android\car\media\service\MediaConnectorService.java
packages\apps\Car\libs\car-media-common\src\com\android\car\media\common\playback\PlaybackViewModel.java

 MediaControllerCompat.TransportControls controls = controller.getTransportControls();
controls.prepare();

The prepare here will be called to onPrepare of AvrcpControllerStateMachine.

  • How to monitor changes in playstate?

packages\apps\Music\src\com\android\music\MediaPlaybackService.java
It can be confirmed that the play state sent by music causes the MediaControl on the carMusicApp side to call prepare.
The RemoteControlClient below is actually a wrapper of MediaSession

 private void notifyChange(String what) {
        Intent i = new Intent(what);
        i.putExtra("id", Long.valueOf(getAudioId()));
        i.putExtra("artist", getArtistName());
        i.putExtra("album", getAlbumName());
        i.putExtra("track", getTrackName());
        i.putExtra("playing", isPlaying());
        sendStickyBroadcast(i);
        if (what.equals(PLAYSTATE_CHANGED)) {
            mRemoteControlClient.setPlaybackState(isPlaying()
                            ?RemoteControlClient.PLAYSTATE_PLAYING
                            : RemoteControlClient.PLAYSTATE_PAUSED);
        }


Code location:
packages\services\Car\service\src\com\android\car\CarMediaService.java

This is achieved by rewriting MediaController.Callback. The state changes set by MediaSession will be called to MediaCotroller through callback.

 private class MediaControllerCallback extends MediaController.Callback {
    
    public void onPlaybackStateChanged(@Nullable PlaybackState state) {
        
        setPrimaryMediaSource(mediaSource, MEDIA_SOURCE_MODE_PLAYBACK);
    
   }
}

    private void startMediaConnectorService(boolean startPlayback, UserHandle currentUser) {
        Intent serviceStart = new Intent(MEDIA_CONNECTION_ACTION);
        Log.d(CarLog.TAG_MEDIA, Log.getStackTraceString(new Throwable()));
        serviceStart.setPackage(mContext.getResources().getString(R.string.serviceMediaConnection));
        serviceStart.putExtra(EXTRA_AUTOPLAY, startPlayback);
        mContext.startForegroundServiceAsUser(serviceStart, currentUser);
    }


packages\apps\Car\Media\src\com\android\car\media\service\MediaConnectorService.java
In the above startMediaConnectorService, a service will be started. This service calls onStartCommand in MediaConnectorSerive.
Mediacontrol will be used in startcommad to perform prepare operations.

 public int onStartCommand(Intent intent, int flags, int startId) {
        playbackViewModel.getPlaybackStateWrapper().observe(this,
                playbackStateWrapper -> {
                    if (playbackStateWrapper != null) {
                        // If the source to play was specified in the intent ignore others.
                        ComponentName intentComp = mCurrentTask.mMediaComp;
                        ComponentName stateComp = playbackStateWrapper.getMediaSource().getBrowseServiceComponentName();
                        if (!Objects.equals(stateComp, intentComp)) {
                            return;
                        }
                        if (playbackStateWrapper.isPlaying()) {
                            stopTask();
                            return;
                        }
                        if ((playbackStateWrapper.getSupportedActions()
                                 & amp; PlaybackStateCompat.ACTION_PREPARE) != 0) {
                            playbackViewModel.getPlaybackController().getValue().prepare();
                            if (!autoplay) {
                                stopTask();
                            }
                        }
                        if (autoplay & amp; & amp; (playbackStateWrapper.getSupportedActions()
                                 & amp; PlaybackStateCompat.ACTION_PLAY) != 0) {
                            playbackViewModel.getPlaybackController().getValue().play();
                            stopTask();
                        }
                    }
                });

src\com\android\bluetooth\a2dpsink\A2dpSinkService.java

Summary: Just look at MediaSession and MediaControl. MediaControl is a class that controls the Service side on the UI side. In AAOS, all app playback control client implementations are implemented by MediaControl in carMediaApp (including pausing playback in the Bluetooth audio localplayer interface, next song, previous song, etc.). MediaSession is the server. This server includes (Bluetooth’s src\com\android\bluetooth
, and /apps/Car/LocalMediaPlayer). This implements the callback of Mediassion to respond to the control of the client UI. The status change after the response can be implemented on the client side by inheriting the callback of MediaControl.

The Music application will send a status change message to the session, and the client carMediaApp will respond to this message. The result of responding to this message is the prepare player. This prepare calls to Bluetooth’s MediaSeesion. MediaSeeion should normally have a one-to-one correspondence between a client and a server.

  • problem solving

    For session changes without MediaSource, MediaConnectService is not started.

MediaSession server process

First implement MediaSessionCompat.Callback(), and then set this callback to the sessionion of MediaBrowserService.

BluetoothMediaBrowserService.addressedPlayerChanged(mSessionCallbacks);
sBluetoothMediaBrowserService.mSession.setCallback(callback);

Transfer of tokens in session

  • Why can the session here receive messages sent by music?
    Because the MediaSession change message has been registered in CarMediaService. When the music application starts, a new MediaSession will be created.
    Here, changes in active MediaSession are monitored, all current Mediacontrols are passed, and callbacks are registered for these mediaControls.
    Listen to the onPlaybackStateChanged event in this callback. The management of mediacontrol here is achieved through tokens.
    Token is the data used to directly establish a connection between MediaSession and MdiaControl. You can use getHashCode to print its hash value for confirmation.
private void initUser(@UserIdInt int userId) {
    updateMediaSessionCallbackForCurrentUser();
    
        if (mSessionsListener != null) {
            mMediaSessionManager.removeOnActiveSessionsChangedListener(mSessionsListener);
        }
       mSessionsListener = new SessionChangedListener(ActivityManager.getCurrentUser());
        UserHandle currentUserHandle = new UserHandle(ActivityManager.getCurrentUser());
        mMediaSessionManager.addOnActiveSessionsChangedListener(null, currentUserHandle,
                new HandlerExecutor(mHandler), mSessionsListener);

}


    private class SessionChangedListener implements OnActiveSessionsChangedListener {
        private final int mCurrentUser;
        SessionChangedListener(int currentUser) {
            mCurrentUser = currentUser;
        }
        @Override
        public void onActiveSessionsChanged(List<MediaController> controllers) {
            if (ActivityManager.getCurrentUser() != mCurrentUser) {
                Slog.e(CarLog.TAG_MEDIA, "Active session callback for old user: " + mCurrentUser);
                return;
            }
            Log.d(CarLog.TAG_MEDIA, Log.getStackTraceString(new Throwable()));
            Log.d(CarLog.TAG_MEDIA, "controllers szie " + controllers.size());
            mMediaSessionUpdater.registerCallbacks(controllers);
        }
    }
    
    
 private void registerCallbacks(List<MediaController> newControllers) {
            List<MediaController> additions = new ArrayList<>(newControllers.size());
            Map<MediaSession.Token, MediaControllerCallback> updatedCallbacks =
                    new HashMap<>(newControllers.size());
            for (MediaController controller : newControllers) {
                MediaSession.Token token = controller.getSessionToken();
                String newPackageName = controller.getPackageName();
                Log.d(CarLog.TAG_MEDIA, Log.getStackTraceString(new Throwable()));
                MediaControllerCallback callback = mCallbacks.get(token);
                if (callback == null) {
                    callback = new MediaControllerCallback(controller);
                    callback.register();
                    additions.add(controller);
                }
                updatedCallbacks.put(token, callback);
            }
            
            
            
 private MediaControllerCallback(MediaController mediaController) {

    public void onPlaybackStateChanged(@Nullable PlaybackState state) {

}

Interaction between BT and music

  • Local music is playing, and the mobile phone plays Bluetooth music. Both sounds are played at the same time.

This is a problem of focus management. It stands to reason that when playing Bluetooth music, you should request focus. It will be requested only if the attribute is set, and will not be requested by default.
When playing, the previous process is ignored. Finally, SRC_STR_START will be called. Without requesting focus, the music application will continue to play. Modification method:

Solution: configure shouldRequestFocus to force the BT application to request focus every time it is played. After the forced request, the music will lose focus and pause.

packages\apps\Bluetooth\src\com\android\bluetooth\a2dpsink\A2dpSinkStreamHandler.java
        switch (message.what) {
            case SRC_STR_START:
                mStreamAvailable = true;
                if (isTvDevice() || shouldRequestFocus()) {
                    requestAudioFocusIfNone();
                }
                break;

  • When Bluetooth music is playing, local music playback will cause Bluetooth to stop directly.

This stop is called in Bluetooth’s MediaSessions callback onstop.
And this callback is triggered in CarMediaService. It is also stopped in the playbackstate callback of the music application in the above process.

Solution: Instead of calling stop, call pause to pause.

public void onPlaybackStateChanged(@Nullable PlaybackState state)


  private void setPlaybackMediaSource(ComponentName playbackMediaSource) {
        stopAndUnregisterCallback();