Android车载多媒体开发MediaSession框架示例详解

目录
  • 一、多媒体应用架构
    • 1.1 音视频传统应用架构
    • 1.2 MediaSession 框架
      • 媒体会话
      • 媒体控制器
  • 二、MediaSession
    • 2.1 概述
    • 2.2 MediaBrowser
      • 2.2.1 MediaBrowser.ConnectionCallback
      • 2.2.2 MediaBrowser.ItemCallback
      • 2.2.3 MediaBrowser.MediaItem
      • 2.2.4 MediaBrowser.SubscriptionCallback
    • 2.3 MediaController
      • 2.3.1 MediaController.Callback
      • 2.3.2 MediaController.PlaybackInfo
      • 2.3.3 MediaController.TransportControls
    • 2.4 MediaBrowserService
      • 2.4.1 MediaBrowserService.BrowserRoot
    • 2.5 MediaSession.Callback
      • 2.5.1 MediaSession.QueueItem
    • 2.6 PlaybackState
      • 2.6.1 PlaybackState.Builder
      • 2.6.2 PlaybackState.CustomAction
  • 文末

一、多媒体应用架构

1.1 音视频传统应用架构

通常,传统的播放音频或视频的多媒体应用由两部分组成:

播放器:用于吸收数字媒体并将其呈现为视频和/或音频;

界面:带有用于运行播放器并显示播放器状态(可选)的传输控件;

在 Android 应用开发中,从零开始构建自己的播放器还可以考虑以下选项:

  • MediaPlayer :提供准系统播放器的基本功能,支持最常见的音频/视频格式和数据源。
  • ExoPlayer :一个提供低层级 Android 音频 API 的开放源代码库。ExoPlayer 支持 DASH 和 HLS 流等高性能功能,这些功能在 MediaPlayer 中未提供。 众所周知,如果要在应用的后台继续播放音频,最常见的方式就是把 Player 放置在 Service 中,Service 提供一个 Binder 来实现界面播放器之间的通信。但是,如果遇到锁屏时,如果要与 Service 之间进行通信就不得不用到 AIDL 接口/广播/ContentProvider 来完成与其它应用之间的通信,而这些通信手段既增加了应用开发者之间的沟通成本,也增加了应用之间的耦合度。为了解决上面的问题,Android 官方从 Android5.0 开始提供了 MediaSession 框架。

1.2 MediaSession 框架

MediaSession 框架规范了音视频应用中界面与播放器之间的通信接口,实现界面与播放器之间的完全解耦。MediaSession 框架定义了媒体会话和媒体控制器两个重要的类,它们为构建多媒体播放器应用提供了一个完善的技术架构。

媒体会话和媒体控制器通过以下方式相互通信:使用与标准播放器操作(播放、暂停、停止等)相对应的预定义回调,以及用于定义应用独有的特殊行为的可扩展自定义调用。

媒体会话

媒体会话负责与播放器的所有通信。它会对应用的其他部分隐藏播放器的 API。系统只能从控制播放器的媒体会话中调用播放器。

会话会维护播放器状态(播放/暂停)的表示形式以及播放内容的相关信息。会话可以接收来自一个或多个媒体控制器的 回调 。这样,应用的界面以及运行 Wear OS 和 Android Auto 的配套设备便可以控制您的播放器。响应回调的逻辑必须保持一致。无论哪个客户端应用发起了回调,对 MediaSession 回调的响应都是相同的。

媒体控制器

媒体控制器的作用是隔离界面,界面的代码只与媒体控制器(而非播放器本身)通信,媒体控制器会将传输控制操作转换为对媒体会话的回调。每当会话状态发生变化时,它也会接收来自媒体会话的回调,这为自动更新关联界面提供了一种机制,媒体控制器一次只能连接到一个媒体会话。

当您使用媒体控制器和媒体会话时,就可以在运行时部署不同的接口和/或播放器。这样一来,您可以根据运行应用的设备的功能单独更改该应用的外观和/或性能。

二、MediaSession

2.1 概述

MediaSession 框架主要是用来解决音乐界面和服务之间的通信问题,属于典型的 C/S 架构,有四个常用的成员类,分别是 MediaBrowser、MediaBrowserService、MediaController 和 MediaSession,是整个 MediaSession 框架流程控制的核心。

  • MediaBrowser:媒体浏览器,用来连接媒体服务 MediaBrowserService 和订阅数据,在注册的回调接口中可以获取到 Service 的连接状态、获取音乐数据,一般在客户端中创建。
  • MediaBrowserService:媒体服务,它有两个关键的回调函数,onGetRoot(控制客户端媒体浏览器的连接请求,返回值中决定是否允许连接),onLoadChildren(媒体浏览器向服务器发送数据订阅请求时会被调用,一般在这里执行异步获取数据的操作,然后在将数据发送回媒体浏览器注册的接口中)。
  • MediaController:媒体控制器,在客户端中工作,通过控制器向媒体服务器发送指令,然后通过 MediaControllerCompat.Callback 设置回调函数来接受服务端的状态。MediaController 创建时需要受控端的配对令牌,因此需要在浏览器连接成功后才进行 MediaController 的创建。
  • MediaSession:媒体会话,受控端,通过设置 MediaSessionCompat.Callback 回调来接收 MediaController 发送的指令,收到指令后会触发 Callback 中的回调方法,比如播放暂停等。Session 一般在 Service.onCreate 方法中创建,最后需调用 setSessionToken 方法设置用于和控制器配对的令牌并通知浏览器连接服务成功。 其中,MediaBrowser 和 MediaController 是客户端使用的,MediaBrowserService 和 MediaSession 是服务端使用的。由于客户端和服务端是异步通信,所以采用的大量的回调,因此有大量的回调类,框架示意图如下。

2.2 MediaBrowser

MediaBrowser 是媒体浏览器,用来连接 MediaBrowserService 和订阅数据,通过它的回调接口我们可以获取与 Service的连接状态以及获取在 Service中的音乐库数据。

客户端(也就是前面提到的界面,或者说是控制端)中创建。媒体浏览器不是线程安全的,所有调用都应在构造 MediaBrowser 的线程上进行。

@RequiresApi(Build.VERSION_CODES.M)
override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)
    val component = ComponentName(this, MediaService::class.java)
    mMediaBrowser = MediaBrowser(this, component, connectionCallback, null);
    mMediaBrowser.connect()
}

2.2.1 MediaBrowser.ConnectionCallback

连接状态回调,当 MediaBrowser 向 service 发起连接请求后,请求结果将在这个 callback 中返回,获取到的 meidaId 对应服务端在 onGetRoot 函数中设置的 mediaId,如果连接成功那么就可以做创建媒体控制器之类的操作了。

@RequiresApi(Build.VERSION_CODES.M)
override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)
    val component = ComponentName(this, MediaService::class.java)
    mMediaBrowser = MediaBrowser(this, component, connectionCallback, null);
    mMediaBrowser.connect()
}
private val connectionCallback = object : MediaBrowser.ConnectionCallback() {
    override fun onConnected() {
        super.onConnected()
        ...  //连接成功后我们才可以创建媒体控制器
    }
    override fun onConnectionFailed() {
        super.onConnectionFailed()
    }
    override fun onConnectionSuspended() {
        super.onConnectionSuspended()
    }
}

2.2.2 MediaBrowser.ItemCallback

媒体控制器是负责向 service 发送例如播放暂停之类的指令的,这些指令的执行结果将在这个回调中返回,可重写的函数有很多,比如播放状态的改变,音乐信息的改变等。

private val connectionCallback = object : MediaBrowser.ConnectionCallback() {
    override fun onConnected() {
        super.onConnected()
         ... //返回执行结果
        if(mMediaBrowser.isConnected) {
            val mediaId = mMediaBrowser.root
            mMediaBrowser.getItem(mediaId, itemCallback)
        }
    }
}
@RequiresApi(Build.VERSION_CODES.M)
private val itemCallback = object : MediaBrowser.ItemCallback(){
    override fun onItemLoaded(item: MediaBrowser.MediaItem?) {
        super.onItemLoaded(item)
    }
    override fun onError(mediaId: String) {
        super.onError(mediaId)
    }
}

2.2.3 MediaBrowser.MediaItem

包含有关单个媒体项的信息,用于浏览/搜索媒体。MediaItem依赖于服务端提供,因此框架本身无法保证它包含的值都是正确的。

2.2.4 MediaBrowser.SubscriptionCallback

连接成功后,首先需要的是订阅服务,同样还需要注册订阅回调,订阅成功的话服务端可以返回一个音乐信息的序列,可以在客户端展示获取的音乐列表数据。例如,下面是订阅 MediaBrowserService 中 MediaBrowser.MediaItem 列表变化的回调。

private val connectionCallback = object : MediaBrowser.ConnectionCallback() {
    override fun onConnected() {
        super.onConnected()
        // ...
        if(mMediaBrowser.isConnected) {
            val mediaId = mMediaBrowser.root
            //需要先取消订阅
            mMediaBrowser.unsubscribe(mediaId)
            //服务端会调用 onLoadChildren
            mMediaBrowser.subscribe(mediaId, subscribeCallback)
        }
    }
}
private val subscribeCallback = object : MediaBrowser.SubscriptionCallback(){
    override fun onChildrenLoaded(
        parentId: String,
        children: MutableList<MediaBrowser.MediaItem>
    ) {
        super.onChildrenLoaded(parentId, children)
    }
    override fun onChildrenLoaded(
        parentId: String,
        children: MutableList<MediaBrowser.MediaItem>,
        options: Bundle
    ) {
        super.onChildrenLoaded(parentId, children, options)
    }
    override fun onError(parentId: String) {
        super.onError(parentId)
    }
    override fun onError(parentId: String, options: Bundle) {
        super.onError(parentId, options)
    }
}

2.3 MediaController

媒体控制器,用来向服务端发送控制指令,例如:播放、暂停等等,在客户端中创建。媒体控制器是线程安全的,MediaController 还有一个关联的权限 android.permission.MEDIA_CONTENT_CONTROL(不是必须加的权限)必须是系统级应用才可以获取,幸运的是车载应用一般都是系统级应用。

同时,MediaController必须在 MediaBrowser 连接成功后才可以创建。 所以,创建 MediaController 的代码如下:

private val connectionCallback = object : MediaBrowser.ConnectionCallback() {
    override fun onConnected() {
        super.onConnected()
        // ...
        if(mMediaBrowser.isConnected) {
            val sessionToken = mMediaBrowser.sessionToken
            mMediaController = MediaController(applicationContext,sessionToken)
        }
    }
}

2.3.1 MediaController.Callback

用于从 MediaSession 接收回调,所以使用的时候需要将 MediaController.Callback 注册到 MediaSession 中,如下:

private val connectionCallback = object : MediaBrowser.ConnectionCallback() {
    override fun onConnected() {
        super.onConnected()
        // ...
        if(mMediaBrowser.isConnected) {
            val sessionToken = mMediaBrowser.sessionToken
            mMediaController = MediaController(applicationContext,sessionToken)
            mMediaController.registerCallback(controllerCallback)
        }
    }
}
private val controllerCallback = object : MediaController.Callback() {
     override fun onAudioInfoChanged(info: MediaController.PlaybackInfo?) {
        super.onAudioInfoChanged(info)
       ... //回调方法接收受控端的状态,从而根据相应的状态刷新界面 UI
    }
    override fun onExtrasChanged(extras: Bundle?) {
        super.onExtrasChanged(extras)
    }
    // ...
}

2.3.2 MediaController.PlaybackInfo

获取当前播放的音频信息,包含播放的进度、时长等。

2.3.3 MediaController.TransportControls

用于控制会话中媒体播放的接口。客户端可以通过 Session 发送媒体控制命令,使用方式如下:

private val connectionCallback = object : MediaBrowser.ConnectionCallback() {
    override fun onConnected() {
        super.onConnected()
        // ...
        if(mMediaBrowser.isConnected) {
            val sessionToken = mMediaBrowser.sessionToken
            mMediaController = MediaController(applicationContext,sessionToken)
            // 播放媒体
            mMediaController.transportControls.play()
            // 暂停媒体
            mMediaController.transportControls.pause()
        }
    }
}

2.4 MediaBrowserService

媒体浏览器服务,继承自 Service,MediaBrowserService 属于服务端,也是承载播放器(如 MediaPlayer、ExoPlayer 等)和 MediaSession 的容器。 继承 MediaBrowserService 后,我们需要复写 onGetRoot和 onLoadChildren两个方法。onGetRoot 通过的返回值决定是否允许客户端的 MediaBrowser 连接到 MediaBrowserService。 当客户端调用 MediaBrowser.subscribe时会触发 onLoadChildren 方法。下面是使用事例:

const val FOLDERS_ID = "__FOLDERS__"
const val ARTISTS_ID = "__ARTISTS__"
const val ALBUMS_ID = "__ALBUMS__"
const val GENRES_ID = "__GENRES__"
const val ROOT_ID = "__ROOT__"
class MediaService : MediaBrowserService() {
    override fun onGetRoot(
        clientPackageName: String,
        clientUid: Int,
        rootHints: Bundle?
    ): BrowserRoot? {
        // 由 MediaBrowser.connect 触发,可以通过返回 null 拒绝客户端的连接。
        return BrowserRoot(ROOT_ID, null)
    }
    override fun onLoadChildren(
        parentId: String,
        result: Result<MutableList<MediaBrowser.MediaItem>>
    ) {
       //由 MediaBrowser.subscribe 触发
        when (parentId) {
            ROOT_ID -> {
                // 查询本地媒体库
                result.detach()
                result.sendResult()
            }
            FOLDERS_ID -> {
            }
            ALBUMS_ID -> {
            }
            ARTISTS_ID -> {
            }
            GENRES_ID -> {
            }
            else -> {
            }
        }
    }
}

最后,还需要在 manifest 中注册这个 Service。

<service
    android:name=".MediaService"
    android:label="@string/service_name">
    <intent-filter>
        <action android:name="android.media.browse.MediaBrowserService" />
    </intent-filter>
</service>

2.4.1 MediaBrowserService.BrowserRoot

返回包含浏览器服务首次连接时需要发送给客户端的信息。构造函数如下:

MediaBrowserService.BrowserRoot(String rootId, Bundle extras)

除此之外,还有两个方法:

  • getExtras():获取有关浏览器服务的附加信息
  • getRootId():获取用于浏览的根 ID 2.4.2 MediaBrowserService.Result

包含浏览器服务返回给客户端的结果集。通过调用 sendResult()将结果返回给调用方,但是在此之前需要调用 detach()。

  • detach():将此消息与当前线程分离,并允许稍后进行调用 sendResult(T)
  • sendResult():将结果发送回调用方。 2.5 MediaSession

媒体会话,即**受控端。**通过设定 MediaSession.Callback回调来接收媒体控制器 MediaController发送的指令,如控制音乐的【上一曲】、【下一曲】等。

创建 MediaSession后还需要调用 setSessionToken()方法设置用于和**控制器配对的令牌,使用方式如下:

const val FOLDERS_ID = "__FOLDERS__"
const val ARTISTS_ID = "__ARTISTS__"
const val ALBUMS_ID = "__ALBUMS__"
const val GENRES_ID = "__GENRES__"
const val ROOT_ID = "__ROOT__"
class MediaService : MediaBrowserService() {
    private lateinit var mediaSession: MediaSession;
    override fun onCreate() {
        super.onCreate()
        mediaSession = MediaSession(this, "TAG")
        mediaSession.setCallback(callback)
        sessionToken = mediaSession.sessionToken
    }
    // 与 MediaController.transportControls 中的大部分方法都是一一对应的
    // 在该方法中实现对 播放器 的控制,
    private val callback = object : MediaSession.Callback() {
        override fun onPlay() {
            super.onPlay()
            // 处理 播放器 的播放逻辑。
            // 车载应用的话,别忘了处理音频焦点
        }
        override fun onPause() {
            super.onPause()
        }
    }
        override fun onGetRoot(
        clientPackageName: String,
        clientUid: Int,
        rootHints: Bundle?
    ): BrowserRoot? {
        Log.e("TAG", "onGetRoot: $rootHints")
        return BrowserRoot(ROOT_ID, null)
    }
    override fun onLoadChildren(
        parentId: String,
        result: Result<MutableList<MediaBrowser.MediaItem>>
    ) {
        result.detach()
        when (parentId) {
            ROOT_ID -> {
                result.sendResult(null)
            }
            FOLDERS_ID -> {
            }
            ALBUMS_ID -> {
            }
            ARTISTS_ID -> {
            }
            GENRES_ID -> {
            }
            else -> {
            }
        }
    }
    override fun onLoadItem(itemId: String?, result: Result<MediaBrowser.MediaItem>?) {
        super.onLoadItem(itemId, result)
        Log.e("TAG", "onLoadItem: $itemId")
    }
}

2.5 MediaSession.Callback

接收来自客户端或系统的媒体按钮、传输控件和命令,入【上一曲】、【下一曲】。与 MediaController.transportControls 中的大部分方法都是一一对应的。

private val callback = object : MediaSession.Callback() {
     override fun onPlay() {
        super.onPlay()
        if (!mediaSession.isActive) {
            mediaSession.isActive = true
        }
        //更新播放状态.
        val state = PlaybackState.Builder()
            .setState(
                PlaybackState.STATE_PLAYING,1,1f
            )
            .build()
        mediaSession.setPlaybackState(state)
    }
    override fun onPause() {
        super.onPause()
    }
    override fun onStop() {
        super.onStop()
    }
}

2.5.1 MediaSession.QueueItem

播放队列一部分的单个项目,相比 MediaMetadata,多了一个 ID 属性。常用的方法有:

  • getDescription():返回介质的说明,包含媒体的基础信息,如标题、封面等。
  • getQueueId():获取此项目的队列 ID。

 MediaSession.Token

表示正在进行的会话,可以通过会话所有者传递给客户端,以允许客户端与服务端之间建立通信。

2.6 PlaybackState

用于承载播放状态的类。如当前播放位置和当前控制功能。在 MediaSession.Callback更改状态后需要调用 MediaSession.setPlaybackState把状态同步给客户端。

private val callback = object : MediaSession.Callback() {
    override fun onPlay() {
        super.onPlay()
        // 更新状态
        val state = PlaybackState.Builder()
            .setState(
                PlaybackState.STATE_PLAYING,1,1f
            )
            .build()
        mediaSession.setPlaybackState(state)
    }
}

2.6.1 PlaybackState.Builder

PlaybackState.Builder 主要用来创建 PlaybackState 对象,创建它使用的是建造者模式,如下。

PlaybackState state = new PlaybackState.Builder()
        .setState(PlaybackState.STATE_PLAYING,
                mMediaPlayer.getCurrentPosition(), PLAYBACK_SPEED)
        .setActions(PLAYING_ACTIONS)
        .addCustomAction(mShuffle)
        .setActiveQueueItemId(mQueue.get(mCurrentQueueIdx).getQueueId())
        .build();

2.6.2 PlaybackState.CustomAction

CustomActions可用于通过将特定于应用程序的操作发送给 MediaControllers,这样就可以扩展标准传输控件的功能。

CustomAction action = new CustomAction
        .Builder("android.car.media.localmediaplayer.shuffle",
        mContext.getString(R.string.shuffle),
        R.drawable.shuffle)
        .build();
PlaybackState state = new PlaybackState.Builder()
        .setState(PlaybackState.STATE_PLAYING,
                mMediaPlayer.getCurrentPosition(), PLAYBACK_SPEED)
        .setActions(PLAYING_ACTIONS)
        .addCustomAction(action)
        .setActiveQueueItemId(mQueue.get(mCurrentQueueIdx).getQueueId())
        .build();

常见的 API,有如下一些:

  • getAction():返回 CustomAction 的 action
  • getExtras():返回附加项,这些附加项提供有关操作的其他特定于应用程序的信息
  • getIcon():返回 package 中图标的资源 ID
  • getName():返回此操作的显示名称 2.7 MediaMetadata

包含有关项目的基础数据,例如标题、艺术家等。一般需要服务端从本地数据库或远端查询出原始数据在封装成 MediaMetadata 再通过 MediaSession.setMetadata(metadata)返回到客户端的 MediaController.Callback.onMetadataChanged中。

常见的 API 有如下:

  • containsKey():如果给定的 key 包含在元数据中,则返回 true。
  • describeContents():描述此可打包实例的封送处理表示中包含的特殊对象的种类。
  • getBitmap():返回给定的 key 的 Bitmap,如果给定 key 不存在位图,则返回 null。
  • getBitmapDimensionLimit():获取创建此元数据时位图的宽度/高度限制
  • getDescription():获取此元数据的简单说明以进行显示。
  • keySet():返回一个 Set,其中包含在此元数据中用作 key 的字符串。 三、示例

下图是 MediaSession 框架核心类的通信过程。

可以看到,在 MediaSession 框架中,首先客户端通过 MediaBrowserService 连接到 MediaBrowserService,MediaBrowserService 接受到请求之后处理相关的请求,MediaSession 控制播放状态,并将状态同步给客户端,客户端最后 MediaController 进行相应的操作。

客户端示例代码:

class MainActivity : AppCompatActivity() {
    private lateinit var mMediaBrowser: MediaBrowser
    private lateinit var mMediaController: MediaController
    @RequiresApi(Build.VERSION_CODES.M)
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        val component = ComponentName(this, MediaService::class.java)
        mMediaBrowser = MediaBrowser(this, component, connectionCallback, null);
        // 连接到 MediaBrowserService,会触发 MediaBrowserService 的 onGetRoot 方法。
        mMediaBrowser.connect()
        findViewById<Button>(R.id.btn_play).setOnClickListener {
            mMediaController.transportControls.play()
        }
    }
    private val connectionCallback = object : MediaBrowser.ConnectionCallback() {
        override fun onConnected() {
            super.onConnected()
            if (mMediaBrowser.isConnected) {
                val sessionToken = mMediaBrowser.sessionToken
                mMediaController = MediaController(applicationContext, sessionToken)
                mMediaController.registerCallback(controllerCallback)
                // 获取根 mediaId
                val rootMediaId = mMediaBrowser.root
                // 获取根 mediaId 的 item 列表,会触发 MediaBrowserService.onLoadItem 方法
                mMediaBrowser.getItem(rootMediaId,itemCallback)
                mMediaBrowser.unsubscribe(rootMediaId)
                // 订阅服务端 media item 的改变,会触发 MediaBrowserService.onLoadChildren 方法
                mMediaBrowser.subscribe(rootMediaId, subscribeCallback)
            }
        }
    }
    private val controllerCallback = object : MediaController.Callback() {
        override fun onPlaybackStateChanged(state: PlaybackState?) {
            super.onPlaybackStateChanged(state)
            Log.d("TAG", "onPlaybackStateChanged: $state")
            when(state?.state){
                PlaybackState.STATE_PLAYING ->{
                    // 处理 UI
                }
                PlaybackState.STATE_PAUSED ->{
                    // 处理 UI
                }
                // 还有其它状态需要处理
            }
        }
        //音频信息,音量
        override fun onAudioInfoChanged(info: MediaController.PlaybackInfo?) {
            super.onAudioInfoChanged(info)
            val currentVolume = info?.currentVolume
            // 显示在 UI 上
        }
        override fun onMetadataChanged(metadata: MediaMetadata?) {
            super.onMetadataChanged(metadata)
            val artUri = metadata?.getString(MediaMetadata.METADATA_KEY_ALBUM_ART_URI)
            // 显示 UI 上
        }
        override fun onSessionEvent(event: String, extras: Bundle?) {
            super.onSessionEvent(event, extras)
            Log.d("TAG", "onSessionEvent: $event")
        }
        // ...
    }
    private val subscribeCallback = object : MediaBrowser.SubscriptionCallback() {
        override fun onChildrenLoaded(
            parentId: String,
            children: MutableList<MediaBrowser.MediaItem>
        ) {
            super.onChildrenLoaded(parentId, children)
        }
        override fun onChildrenLoaded(
            parentId: String,
            children: MutableList<MediaBrowser.MediaItem>,
            options: Bundle
        ) {
            super.onChildrenLoaded(parentId, children, options)
        }
        override fun onError(parentId: String) {
            super.onError(parentId)
        }
    }
    private val itemCallback = object : MediaBrowser.ItemCallback() {
        override fun onItemLoaded(item: MediaBrowser.MediaItem?) {
            super.onItemLoaded(item)
        }
        override fun onError(mediaId: String) {
            super.onError(mediaId)
        }
    }
}

下面是服务端的示例源码,主要用于处理客户端的请求,并将结果返回给客户端。

const val FOLDERS_ID = "__FOLDERS__"
const val ARTISTS_ID = "__ARTISTS__"
const val ALBUMS_ID = "__ALBUMS__"
const val GENRES_ID = "__GENRES__"
const val ROOT_ID = "__ROOT__"
class MediaService : MediaBrowserService() {
    // 控制是否允许客户端连接,并返回 root media id 给客户端
    override fun onGetRoot(
        clientPackageName: String,
        clientUid: Int,
        rootHints: Bundle?
    ): BrowserRoot? {
        Log.e("TAG", "onGetRoot: $rootHints")
        return BrowserRoot(ROOT_ID, null)
    }
    // 处理客户端的订阅信息
    override fun onLoadChildren(
        parentId: String,
        result: Result<MutableList<MediaBrowser.MediaItem>>
    ) {
        Log.e("TAG", "onLoadChildren: $parentId")
        result.detach()
        when (parentId) {
            ROOT_ID -> {
                result.sendResult(null)
            }
            FOLDERS_ID -> {
            }
            ALBUMS_ID -> {
            }
            ARTISTS_ID -> {
            }
            GENRES_ID -> {
            }
            else -> {
            }
        }
    }
    override fun onLoadItem(itemId: String?, result: Result<MediaBrowser.MediaItem>?) {
        super.onLoadItem(itemId, result)
        Log.e("TAG", "onLoadItem: $itemId")
        // 根据 itemId,返回对用 MediaItem
        result?.detach()
        result?.sendResult(null)
    }
    private lateinit var mediaSession: MediaSession;
    override fun onCreate() {
        super.onCreate()
        mediaSession = MediaSession(this, "TAG")
        mediaSession.setCallback(callback)
        // 设置 token
        sessionToken = mediaSession.sessionToken
    }
    // 与 MediaController.transportControls 中的方法是一一对应的。
    // 在该方法中实现对 播放器 的控制,
    private val callback = object : MediaSession.Callback() {
        override fun onPlay() {
            super.onPlay()
            // 处理 播放器 的播放逻辑。
            // 车载应用的话,别忘了处理音频焦点
            Log.e("TAG", "onPlay:")
            if (!mediaSession.isActive) {
                mediaSession.isActive = true
            }
            // 更新状态
            val state = PlaybackState.Builder()
                .setState(
                    PlaybackState.STATE_PLAYING, 1, 1f
                )
                .build()
            mediaSession.setPlaybackState(state)
        }
        override fun onPause() {
            super.onPause()
        }
        override fun onStop() {
            super.onStop()
        }
        //省略其他方法
    }
}

上文主要介绍车载自媒体开发与MediaSession框架解析;这只是其中一小部分;更多的Android车载技术可以前往[私信]领取,里面内容如下脑图所示:(需要可以参考,当做辅导资料)

文末

车载系统开发,在这几年岗位逐渐增多。Android转岗车载开发是个很好的发展方向。汽车的普及同比10年增长300%以上;近几年的新能源汽车逐渐普及,车载开发人员更是需求很大;普遍缺少人才。

感觉Android市场下滑的厉害,淘汰人员逐渐增多。一定要眼看未来,才没有近忧。

以上就是Android车载多媒体开发MediaSession框架示例详解的详细内容,更多关于Android车载多媒体MediaSession的资料请关注我们其它相关文章!

(0)

相关推荐

  • Android实现登录注册界面框架

    小项目框架 今天用QQ的时候想到了,不如用android studio 做一个类似于这样的登录软件.当然QQ的实现的功能特别复杂,UI界面也很多,不是单纯的一时新奇就可以做出来的.就是简单的实现了一些功能,做了三个界面:1.登录界面.2.注册界面.3.登陆后的界面. 功能描述 登录按钮------按钮实现跳转到下一个界面,并且判断输入的账号.密码是否符合规则(不为空),提示,登陆成功或失败 注册按钮------按钮实现跳转到注册界面 登录界面 main_activity.xml <LinearL

  • Android数据缓存框架内置ORM功能使用教程

    目录 使用教程如下 配置初始化 注解详解 CRUD操作 其他注意事项 使用教程如下 配置初始化 Orm.init(this, OrmConfig.Builder() .database("dcache_sample") .tables(Account::class.java) .version(1) .build()) 在自定义的Application类的入口加入一行配置,database为数据库名,version从1开始每次递增1,tables用来配置需要初始化的表,dcache中所

  • 低门槛开发iOS、Android、小程序应用的前端框架详解

    现如今跨平台开发技术已不是什么新鲜话题了,在市面上也有一些开源的框架可供选择,然而技术成熟.产品服务健全的平台并不多,其中也不乏推陈出新的框架值得关注. 比如最近使用的AVM,由APICloud迭代推出的多端开发框架,基于JavaScript,兼容多语法,如果是Vue.React的用户,可直接上手,没什么学习成本,具备虚拟DOM,可一次编写多端渲染:主要是APICloud上线已有7年,相对已经成熟,所以我把自己的一些认知和实践结合AVM官方文档的内容做了一下整理,希望能对需要使用跨平台开发技术的

  • Android开发框架MVC-MVP-MVVM-MVI的演变Demo

    目录 Android框架的历史演变 一. MVC框架 二. MVP框架 三. MVVM框架 3.1 半MVVM框架 3.2 带DataBinding的MVVM框架 四. MVI框架 Android框架的历史演变 记得最开始入门Android的时候,还未流行MVP,都是MVC一把梭,后面工作了就是使用了MVP,当时学习的时候好难理解它的回调. 到目前主流的MVVM,其实就是MVP的升级版,再到最新的MVI使用意图传输,隔离各层级的直接调用.我算是经历了Android框架变迁的全过程. 这里记录一下

  • Android开发Compose框架使用开篇

    目录 Compose的诞生 Compose好处 Compose 架构 @Composable的背后 智能重组真的那么智能吗 最后 Compose的诞生 在2019年的谷歌IO大会上,Compose作为Android新一代UI开发亮相,因为声明式开发越来越流行了,对标IOS开发SwiftUi,Compose的立项也为Android开发新加了声明式ui的开发选项,在2021年7月1.0正式版本的诞生,也意味着Compose即将进入生产环节,国际app巨头Twitter就首当其冲,在新页面上用上了Co

  • Android边播放边缓存视频框架AndroidVideoCache详解

    目录 一.背景 二.PlayerBase 三.AndroidVideoCache 3.1 基本原理 3.2 基本使用 3.3 源码分析 一.背景 现在的移动应用,视频是一个非常重要的组成部分,好像里面不搞一点视频就不是一个正常的移动App.在视频开发方面,可以分为视频录制和视频播放,视频录制的场景可能还比较少,这方面可以使用Google开源的 grafika.相比于视频录制,视频播放可以选择的方案就要多许多,比如Google的 ExoPlayer,B站的 ijkplayer,以及官方的Media

  • Android车载多媒体开发MediaSession框架示例详解

    目录 一.多媒体应用架构 1.1 音视频传统应用架构 1.2 MediaSession 框架 媒体会话 媒体控制器 二.MediaSession 2.1 概述 2.2 MediaBrowser 2.2.1 MediaBrowser.ConnectionCallback 2.2.2 MediaBrowser.ItemCallback 2.2.3 MediaBrowser.MediaItem 2.2.4 MediaBrowser.SubscriptionCallback 2.3 MediaContr

  • Spi机制在Android开发的应用示例详解

    目录 Spi机制介绍 举个例子 ServiceLoader.load 在Android中的应用 总结 Spi机制介绍 SPI 全称是 Service Provider Interface,是一种将服务接口与服务实现分离以达到解耦.可以提升程序可扩展性的机制.嘿嘿,看到这个概念很多人肯定是一头雾水了,没事,我们直接就可以简单理解为是一种反射机制,即我们不需要知道具体的实现方,只要定义好接口,我们就能够在运行时找到一个实现接口的类,我们具体看一下官方定义. 举个例子 加入我是一个库设计者,我希望把一

  • Android 动态加载 so实现示例详解

    目录 背景 so动态加载介绍 从一个例子出发 so库检索与删除 动态加载so 结束了吗? ELF文件 扩展 总结 背景 对于一个普通的android应用来说,so库的占比通常都是巨高不下的,因为我们无可避免的在开发中遇到各种各样需要用到native的需求,所以so库的动态化可以减少极大的包体积,自从2020腾讯的bugly团队发部关于动态化so的相关文章后,已经过去两年了,相关文章,经过两年的考验,实际上so动态加载也是非常成熟的一项技术了. 但是很遗憾,许多公司都还没有这方面的涉略又或者说不知

  • 基于gin的golang web开发:路由示例详解

    Gin是一个用Golang编写的HTTP网络框架.它的特点是类似于Martini的API,性能更好.在golang web开发领域是一个非常热门的web框架. 启动一个Gin web服务器 使用下面的命令安装Gin go get -u github.com/gin-gonic/gin 在代码里添加依赖 import "github.com/gin-gonic/gin" 快速启动一个Gin服务器的代码如下 package main import "github.com/gin-

  • Servlet开发JavaWeb工程示例详解

    一.什么是Servlet? Servlet是在服务器上运行的小程序,也就是一个Java类,但比较特殊,不需要new,自动就可以运行.也有创建.垃圾回收和销毁过程.Servlet是JavaWeb的三大组件之一(Servlet.Filter.Listener),它属于动态资源.Servlet的作用是处理请求,服务器会把接收到的请求交给Servlet来处理,在Servlet中通常需要: 接收请求数据: 处理请求: 完成响应. 例如客户端发出登录请求,或者输出注册请求,这些请求都应该由Servlet来完

  • Android Flutter实现3D动画效果示例详解

    目录 前言 AnimatedWidget 简介 3D 旋转动画的实现 总结 前言 上一篇我们介绍了 Animation 和 AnimationController 的使用,这是最基本的动画构建类.但是,如果我们想构建一个可复用的动画组件,通过外部参数来控制其动画效果的时候,上一篇的方法就不太合适了.在 Flutter 中提供了 AnimatedWidget 组件用于构建可复用的动画组件.本篇我们用 AnimatedWidget 来实现组件的3D 旋转效果,如下图所示. AnimatedWidge

  • Android画图实现MPAndroidchart折线图示例详解

    目录 效果图 依赖 activity.xml MainActivity MyMarkerView 自定义class maekertextview .xml 常用属性 效果图 用的是3.1.0的依赖 依赖 allprojects { repositories { jcenter() maven { url "https://jitpack.io" } } } //依赖 dependencies{ implementation 'com.github.PhilJay:MPAndroidCh

  • Android 应用程序的启动流程示例详解

    目录 应用进程的启动流程 1.ActivityStackSupervisor.startSpecificActivity 2.ATMS.startProcessAsync 3.LocalService.startProcess 4.startProcessLocked函数 5.ProcessList.startProcessLocked 6.ProcessList.startProcessLocked重载 7.ProcessList.startProcess 8.ZygoteState.star

  • Android App 与 U 盘通信示例详解

    前言 对于 U 盘的了解,相信大多数人应该只停留在跟 U 盘跟电脑通信的阶段,其实现在通过OTG 线就可以实现手机跟 U 盘之间的数据操作,不仅可以将 U 盘中的文件读取到手机中来,还能将手机中的文件导出到 U 盘中,从而实现手机与 U 盘之间的通信.本文将从 Android App 入手,通过相关的代码,带大家一步步了解手机与 U 盘之间的通信.代码我已经放上 Github 了,有需要的点击这里 . 一.自定义广播接收器接收 U 盘相关的信息 在 U 盘插入或插出的时候,系统都会发出一条相关的

  • Vue3搭建组件库开发环境的示例详解

    目录 1 packages 目录 1.1 foo 目录 1.2 yyg-demo-ui 目录 2 实现 foo 示例组件 2.1 初始化 package.json 2.2 初始化 foo 目录结构 2.3 定义 foo 组件的 props 2.4 实现 foo 组件 2.5 定义 foo 组件入口文件 3 实现 yyg-demo-ui 3.1 初始化 package.json 3.2 安装依赖 3.3 定义入口文件 前文已经初始化了 workspace-root,从本文开始就需要依次搭建组件库.

随机推荐