• AAOS CarMediaService 问题分析


    问题描述

    • 问题

      AAOS界面连接蓝牙的情况下,Music应用播放音乐会暂停。

    • 分析
      暂停是应用的行为,Music应用会监听focus的变化,监听到焦点失去的情况会调用暂停。但是Music 应用刚启动播放的时候 也会请求焦点,焦点第一次是在bt这边的。bt失去焦点,但立马又重新请求了焦点。 BT请求焦点就导致Music应用失去焦点而暂停。

    了解问题之前首先要理解

    1. 蓝牙音乐在哪里调用到音频框架?
    2. 音频焦点
    3. carMediaService

    车载蓝牙音乐流程

    • A2DP端

      代码位置:
      system\bt\btif\src\btif_avrcp_audio_track.cc
      system\bt\stack\a2dp\a2dp_aac.cc
      在上述的BtifAvrcpAudioTrackCreate()函数中。在这里面会创建一个AAudioStreamBuilder, AAudio 通过legacy的模式 的audiotrack 来进行处理 写数据。

    • AAudio端

      蓝牙音乐, 车载端是一个sink 端,作为播放来使用。 对应的流程在btif_avrcp_audio_track 是通过调用AAudio的接口来实现播放。其中AAudio 没有实现mmap的方式 走的是legacy模式
      代码在frameworks\av\media\libaaudio\src\legacy\AudioStreamTrack.cpp中
      也就是通过创建audiotrack,然后设置参数、往里面写数据实现的。audiotrack start 的时候 同样会getoutputAttr获取到设备, 这个时候的路由信息已经由AAOS 根据car_audio_policy.xml
      进行注册。

    • 整体流程:

      从source端(也就是手机通过蓝牙)发送过来的是aac或者ldac编码的数据, 数据在a2dp中继续解码 并不是进入到audiotrack。 a2dp中有相当于播放器中解码器的功能,解码后的数据才调用audiotrak进行播放。

    Music 监听焦点变化流程

    • 实现AudioFocusListener 然后注册到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);
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 在框架层焦点发生变化的时候 回调到外部注册进去的listener

    • Music 中MediaPlayBackService的实现

    通过looper发送消息进行处理,而如果是focus丢失的时候,所做的操作是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;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    BT请求焦点的流程

    • 监听mediassion 的onPrepare事件

    有Prepare事件发生后会调用requestFocus。其usage是USAGE_MEDIA。这个会回调到AAOS的CarAudioFocus
    进行处理, 其就是根据交互矩阵进行处理的。当前持有的是music应用,在BT 请求焦点后,会发送消息通知music 焦点失去了。
    在上面的流程知道 失去焦点后 会调用player的 pause 进行暂停操作。

    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;
        }
    
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 那现在的问题是 哪里触发了onPrepare

    可以看到是实现了MediaSession的Callback。 理解MediaSession的概念
    MediaSession 有客户端和服务端。 客户端对应的是UI 这一端,服务端对应的是player。

    UI这一端的实现是在packages/app/Car/Media 中。
    主要是几个类 的封装分别是
    MediaControl:用来控制MediaSession, 在上面的AVrcpControl中 实现的MessaionCompat的callback回调的
    play prepare都是由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();
    
    • 1
    • 2

    这里的prepare 会调用到AvrcpControllerStateMachine的onPrepare中。

    • 如何监听到playstate的变化的?

    packages\apps\Music\src\com\android\music\MediaPlaybackService.java
    可以确认是music 发送的play state 导致 carMusicApp这边MediaControl调用prepare的.
    下面的RemoteControlClient实际是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);
            }
    
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    代码位置:
    packages\services\Car\service\src\com\android\car\CarMediaService.java

    是通过重新编写MediaController.Callback来实现的。MediaSession设置的状态变化会通过callback调用到MediaCotroller当中

      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);
        }
    
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    packages\apps\Car\Media\src\com\android\car\media\service\MediaConnectorService.java
    在上述的startMediaConnectorService会启动一个service 这个service调用到MediaConnectorSerive中onStartCommand。
    在startcommad 中会使用mediacontrol 进行prepare操作。

        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()
                                    & PlaybackStateCompat.ACTION_PREPARE) != 0) {
                                playbackViewModel.getPlaybackController().getValue().prepare();
                                if (!autoplay) {
                                    stopTask();
                                }
                            }
                            if (autoplay && (playbackStateWrapper.getSupportedActions()
                                    & PlaybackStateCompat.ACTION_PLAY) != 0) {
                                playbackViewModel.getPlaybackController().getValue().play();
                                stopTask();
                            }
                        }
                    });
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28

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

    总结: 单单看MediaSession 和 MediaControl。 MediaControl是UI端控制Service端的类,在AAOS中所有的app播放控制客户端的实现都是carMediaApp中MediaControl的实现的(包括蓝牙audio localplayer界面中暂停播放,下一首 上一首等等)。 MediaSession是服务端, 这个服务端包括(蓝牙的src\com\android\bluetooth
    ,和/apps/Car/LocalMediaPlayer)。这这里面实现了Mediassion 的callback 用来响应client 端UI的控制。 而响应之后的状态改变可以通过继承MediaControl的callback 在客户端实现。

    而Music应用中会向session发送状态改变的消息,客户端carMediaApp会响应这个消息,响应这个消息的结果就是prepare播放器。这个prepare调用到蓝牙的MediaSeesion。MediaSeeion正常应该 一个客户端和服务器一一对应的。

    • 问题的解决

      对于没有MediaSource的session变化 不启动MediaConnectService。

    MediaSession 服务端的流程

    首先实现MediaSessionCompat.Callback(),然后将这个callback 设置到MediaBrowserService的sesseion中。

    BluetoothMediaBrowserService.addressedPlayerChanged(mSessionCallbacks);
    sBluetoothMediaBrowserService.mSession.setCallback(callback);
    
    • 1
    • 2

    session 中token的传递

    • 为什么music 发送的消息 这边的session 可以接收到。
      因为在CarMediaService 中已经注册了MediaSession变化的消息。 在音乐应用启动的时候 会新建MediaSession,
      而在这里就会监听了active MediaSession的变化,同时传递当前所有的Mediacontrol, 然后对这些mediaControl注册callback。
      在这个callback 中监听onPlaybackStateChanged事件。 而在这边对mediacontrol的管理是通过token实现的。
      token 是在MediaSession 和 MdiaControl 直接建立连接的数据。可以通过getHashCode来打印其hash值确认。
    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 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 newControllers) {
                List additions = new ArrayList<>(newControllers.size());
                Map 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) {
    
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57

    BT和music 之间的相互影响

    • 本地音乐在播放,手机播放蓝牙音乐 两个声音同时播放

    这个是焦点管理的问题, 按理理解蓝牙音乐播放的时候 应该去请求焦点。需要设置属性才会请求,默认不会请求。
    播放的时候 前面流程不用管,最后会调用到SRC_STR_START中,没有请求焦点,music应用就会一直播。修改方法:

    解决方法:通过配置shouldRequestFocus 让BT应用在每次播放的时候都强制请求焦点解决。强制请求之后,music就会失去焦点暂停。

    packages\apps\Bluetooth\src\com\android\bluetooth\a2dpsink\A2dpSinkStreamHandler.java
            switch (message.what) {
                case SRC_STR_START:
                    mStreamAvailable = true;
                    if (isTvDevice() || shouldRequestFocus()) {
                        requestAudioFocusIfNone();
                    }
                    break;
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 蓝牙音乐在播放的时候,本地音乐播放会导致蓝牙直接stop掉。

    这个stop 是在蓝牙的MediaSessions callback onstop中调用的。
    而这个回调是在CarMediaService中被触发的。也是在上面的流程中music应用的playbackstate 回调中stop的。

    解决方法:不调用stop, 调用pause进行暂停。

    public void onPlaybackStateChanged(@Nullable PlaybackState state)
    
    
      private void setPlaybackMediaSource(ComponentName playbackMediaSource) {
            stopAndUnregisterCallback();
    
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
  • 相关阅读:
    CentOS7如何安装图形界面
    探索图像分割技术:使用 OpenCV 的分水岭算法
    【强化学习】Sarsa算法求解悬崖行走问题 + Python代码实战
    快速检测 GlassFish 任意文件读取漏洞的 Python 脚本
    纽约时报起诉OpenAI和微软将决定未来LLM的发展
    电子学:第014课——实验 15:防入侵报警器(第一部分)
    九、ELK安装ElastAlert 2插件钉钉机器人告警
    【MySQL 数据宝典】【磁盘结构】- 005 Undo log 撤销日志
    linux安装Jdk
    基于阻塞队列的生产消费模型
  • 原文地址:https://blog.csdn.net/H2008066215019910120/article/details/134043806