Android車(chē)載多媒體開(kāi)發(fā)MediaSession框架示例詳解
一、多媒體應(yīng)用架構(gòu)
1.1 音視頻傳統(tǒng)應(yīng)用架構(gòu)
通常,傳統(tǒng)的播放音頻或視頻的多媒體應(yīng)用由兩部分組成:
播放器:用于吸收數(shù)字媒體并將其呈現(xiàn)為視頻和/或音頻;
界面:帶有用于運(yùn)行播放器并顯示播放器狀態(tài)(可選)的傳輸控件;
在 Android 應(yīng)用開(kāi)發(fā)中,從零開(kāi)始構(gòu)建自己的播放器還可以考慮以下選項(xiàng):
- MediaPlayer :提供準(zhǔn)系統(tǒng)播放器的基本功能,支持最常見(jiàn)的音頻/視頻格式和數(shù)據(jù)源。
- ExoPlayer :一個(gè)提供低層級(jí) Android 音頻 API 的開(kāi)放源代碼庫(kù)。ExoPlayer 支持 DASH 和 HLS 流等高性能功能,這些功能在 MediaPlayer 中未提供。 眾所周知,如果要在應(yīng)用的后臺(tái)繼續(xù)播放音頻,最常見(jiàn)的方式就是把 Player 放置在 Service 中,Service 提供一個(gè) Binder 來(lái)實(shí)現(xiàn)界面與播放器之間的通信。但是,如果遇到鎖屏?xí)r,如果要與 Service 之間進(jìn)行通信就不得不用到 AIDL 接口/廣播/ContentProvider 來(lái)完成與其它應(yīng)用之間的通信,而這些通信手段既增加了應(yīng)用開(kāi)發(fā)者之間的溝通成本,也增加了應(yīng)用之間的耦合度。為了解決上面的問(wèn)題,Android 官方從 Android5.0 開(kāi)始提供了 MediaSession 框架。
1.2 MediaSession 框架
MediaSession 框架規(guī)范了音視頻應(yīng)用中界面與播放器之間的通信接口,實(shí)現(xiàn)界面與播放器之間的完全解耦。MediaSession 框架定義了媒體會(huì)話(huà)和媒體控制器兩個(gè)重要的類(lèi),它們?yōu)闃?gòu)建多媒體播放器應(yīng)用提供了一個(gè)完善的技術(shù)架構(gòu)。
媒體會(huì)話(huà)和媒體控制器通過(guò)以下方式相互通信:使用與標(biāo)準(zhǔn)播放器操作(播放、暫停、停止等)相對(duì)應(yīng)的預(yù)定義回調(diào),以及用于定義應(yīng)用獨(dú)有的特殊行為的可擴(kuò)展自定義調(diào)用。
媒體會(huì)話(huà)
媒體會(huì)話(huà)負(fù)責(zé)與播放器的所有通信。它會(huì)對(duì)應(yīng)用的其他部分隱藏播放器的 API。系統(tǒng)只能從控制播放器的媒體會(huì)話(huà)中調(diào)用播放器。
會(huì)話(huà)會(huì)維護(hù)播放器狀態(tài)(播放/暫停)的表示形式以及播放內(nèi)容的相關(guān)信息。會(huì)話(huà)可以接收來(lái)自一個(gè)或多個(gè)媒體控制器的 回調(diào) 。這樣,應(yīng)用的界面以及運(yùn)行 Wear OS 和 Android Auto 的配套設(shè)備便可以控制您的播放器。響應(yīng)回調(diào)的邏輯必須保持一致。無(wú)論哪個(gè)客戶(hù)端應(yīng)用發(fā)起了回調(diào),對(duì) MediaSession 回調(diào)的響應(yīng)都是相同的。
媒體控制器
媒體控制器的作用是隔離界面,界面的代碼只與媒體控制器(而非播放器本身)通信,媒體控制器會(huì)將傳輸控制操作轉(zhuǎn)換為對(duì)媒體會(huì)話(huà)的回調(diào)。每當(dāng)會(huì)話(huà)狀態(tài)發(fā)生變化時(shí),它也會(huì)接收來(lái)自媒體會(huì)話(huà)的回調(diào),這為自動(dòng)更新關(guān)聯(lián)界面提供了一種機(jī)制,媒體控制器一次只能連接到一個(gè)媒體會(huì)話(huà)。
當(dāng)您使用媒體控制器和媒體會(huì)話(huà)時(shí),就可以在運(yùn)行時(shí)部署不同的接口和/或播放器。這樣一來(lái),您可以根據(jù)運(yùn)行應(yīng)用的設(shè)備的功能單獨(dú)更改該應(yīng)用的外觀(guān)和/或性能。
二、MediaSession
2.1 概述
MediaSession 框架主要是用來(lái)解決音樂(lè)界面和服務(wù)之間的通信問(wèn)題,屬于典型的 C/S 架構(gòu),有四個(gè)常用的成員類(lèi),分別是 MediaBrowser、MediaBrowserService、MediaController 和 MediaSession,是整個(gè) MediaSession 框架流程控制的核心。
- MediaBrowser:媒體瀏覽器,用來(lái)連接媒體服務(wù) MediaBrowserService 和訂閱數(shù)據(jù),在注冊(cè)的回調(diào)接口中可以獲取到 Service 的連接狀態(tài)、獲取音樂(lè)數(shù)據(jù),一般在客戶(hù)端中創(chuàng)建。
- MediaBrowserService:媒體服務(wù),它有兩個(gè)關(guān)鍵的回調(diào)函數(shù),onGetRoot(控制客戶(hù)端媒體瀏覽器的連接請(qǐng)求,返回值中決定是否允許連接),onLoadChildren(媒體瀏覽器向服務(wù)器發(fā)送數(shù)據(jù)訂閱請(qǐng)求時(shí)會(huì)被調(diào)用,一般在這里執(zhí)行異步獲取數(shù)據(jù)的操作,然后在將數(shù)據(jù)發(fā)送回媒體瀏覽器注冊(cè)的接口中)。
- MediaController:媒體控制器,在客戶(hù)端中工作,通過(guò)控制器向媒體服務(wù)器發(fā)送指令,然后通過(guò) MediaControllerCompat.Callback 設(shè)置回調(diào)函數(shù)來(lái)接受服務(wù)端的狀態(tài)。MediaController 創(chuàng)建時(shí)需要受控端的配對(duì)令牌,因此需要在瀏覽器連接成功后才進(jìn)行 MediaController 的創(chuàng)建。
- MediaSession:媒體會(huì)話(huà),受控端,通過(guò)設(shè)置 MediaSessionCompat.Callback 回調(diào)來(lái)接收 MediaController 發(fā)送的指令,收到指令后會(huì)觸發(fā) Callback 中的回調(diào)方法,比如播放暫停等。Session 一般在 Service.onCreate 方法中創(chuàng)建,最后需調(diào)用 setSessionToken 方法設(shè)置用于和控制器配對(duì)的令牌并通知瀏覽器連接服務(wù)成功。 其中,MediaBrowser 和 MediaController 是客戶(hù)端使用的,MediaBrowserService 和 MediaSession 是服務(wù)端使用的。由于客戶(hù)端和服務(wù)端是異步通信,所以采用的大量的回調(diào),因此有大量的回調(diào)類(lèi),框架示意圖如下。
2.2 MediaBrowser
MediaBrowser 是媒體瀏覽器,用來(lái)連接 MediaBrowserService 和訂閱數(shù)據(jù),通過(guò)它的回調(diào)接口我們可以獲取與 Service的連接狀態(tài)以及獲取在 Service中的音樂(lè)庫(kù)數(shù)據(jù)。
在客戶(hù)端(也就是前面提到的界面,或者說(shuō)是控制端)中創(chuàng)建。媒體瀏覽器不是線(xiàn)程安全的,所有調(diào)用都應(yīng)在構(gòu)造 MediaBrowser 的線(xiàn)程上進(jìn)行。
@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
連接狀態(tài)回調(diào),當(dāng) MediaBrowser 向 service 發(fā)起連接請(qǐng)求后,請(qǐng)求結(jié)果將在這個(gè) callback 中返回,獲取到的 meidaId 對(duì)應(yīng)服務(wù)端在 onGetRoot 函數(shù)中設(shè)置的 mediaId,如果連接成功那么就可以做創(chuàng)建媒體控制器之類(lèi)的操作了。
@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() ... //連接成功后我們才可以創(chuàng)建媒體控制器 } override fun onConnectionFailed() { super.onConnectionFailed() } override fun onConnectionSuspended() { super.onConnectionSuspended() } }
2.2.2 MediaBrowser.ItemCallback
媒體控制器是負(fù)責(zé)向 service 發(fā)送例如播放暫停之類(lèi)的指令的,這些指令的執(zhí)行結(jié)果將在這個(gè)回調(diào)中返回,可重寫(xiě)的函數(shù)有很多,比如播放狀態(tài)的改變,音樂(lè)信息的改變等。
private val connectionCallback = object : MediaBrowser.ConnectionCallback() { override fun onConnected() { super.onConnected() ... //返回執(zhí)行結(jié)果 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
包含有關(guān)單個(gè)媒體項(xiàng)的信息,用于瀏覽/搜索媒體。MediaItem依賴(lài)于服務(wù)端提供,因此框架本身無(wú)法保證它包含的值都是正確的。
2.2.4 MediaBrowser.SubscriptionCallback
連接成功后,首先需要的是訂閱服務(wù),同樣還需要注冊(cè)訂閱回調(diào),訂閱成功的話(huà)服務(wù)端可以返回一個(gè)音樂(lè)信息的序列,可以在客戶(hù)端展示獲取的音樂(lè)列表數(shù)據(jù)。例如,下面是訂閱 MediaBrowserService 中 MediaBrowser.MediaItem 列表變化的回調(diào)。
private val connectionCallback = object : MediaBrowser.ConnectionCallback() { override fun onConnected() { super.onConnected() // ... if(mMediaBrowser.isConnected) { val mediaId = mMediaBrowser.root //需要先取消訂閱 mMediaBrowser.unsubscribe(mediaId) //服務(wù)端會(huì)調(diào)用 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
媒體控制器,用來(lái)向服務(wù)端發(fā)送控制指令,例如:播放、暫停等等,在客戶(hù)端中創(chuàng)建。媒體控制器是線(xiàn)程安全的,MediaController 還有一個(gè)關(guān)聯(lián)的權(quán)限 android.permission.MEDIA_CONTENT_CONTROL(不是必須加的權(quán)限)必須是系統(tǒng)級(jí)應(yīng)用才可以獲取,幸運(yùn)的是車(chē)載應(yīng)用一般都是系統(tǒng)級(jí)應(yīng)用。
同時(shí),MediaController必須在 MediaBrowser 連接成功后才可以創(chuàng)建。 所以,創(chuàng)建 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 接收回調(diào),所以使用的時(shí)候需要將 MediaController.Callback 注冊(cè)到 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) ... //回調(diào)方法接收受控端的狀態(tài),從而根據(jù)相應(yīng)的狀態(tài)刷新界面 UI } override fun onExtrasChanged(extras: Bundle?) { super.onExtrasChanged(extras) } // ... }
2.3.2 MediaController.PlaybackInfo
獲取當(dāng)前播放的音頻信息,包含播放的進(jìn)度、時(shí)長(zhǎng)等。
2.3.3 MediaController.TransportControls
用于控制會(huì)話(huà)中媒體播放的接口??蛻?hù)端可以通過(guò) Session 發(fā)送媒體控制命令,使用方式如下:
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
媒體瀏覽器服務(wù),繼承自 Service,MediaBrowserService 屬于服務(wù)端,也是承載播放器(如 MediaPlayer、ExoPlayer 等)和 MediaSession 的容器。 繼承 MediaBrowserService 后,我們需要復(fù)寫(xiě) onGetRoot和 onLoadChildren兩個(gè)方法。onGetRoot 通過(guò)的返回值決定是否允許客戶(hù)端的 MediaBrowser 連接到 MediaBrowserService。 當(dāng)客戶(hù)端調(diào)用 MediaBrowser.subscribe時(shí)會(huì)觸發(fā) 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 觸發(fā),可以通過(guò)返回 null 拒絕客戶(hù)端的連接。 return BrowserRoot(ROOT_ID, null) } override fun onLoadChildren( parentId: String, result: Result<MutableList<MediaBrowser.MediaItem>> ) { //由 MediaBrowser.subscribe 觸發(fā) when (parentId) { ROOT_ID -> { // 查詢(xún)本地媒體庫(kù) result.detach() result.sendResult() } FOLDERS_ID -> { } ALBUMS_ID -> { } ARTISTS_ID -> { } GENRES_ID -> { } else -> { } } } }
最后,還需要在 manifest 中注冊(cè)這個(gè) 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
返回包含瀏覽器服務(wù)首次連接時(shí)需要發(fā)送給客戶(hù)端的信息。構(gòu)造函數(shù)如下:
MediaBrowserService.BrowserRoot(String rootId, Bundle extras)
除此之外,還有兩個(gè)方法:
- getExtras():獲取有關(guān)瀏覽器服務(wù)的附加信息
- getRootId():獲取用于瀏覽的根 ID 2.4.2 MediaBrowserService.Result
包含瀏覽器服務(wù)返回給客戶(hù)端的結(jié)果集。通過(guò)調(diào)用 sendResult()將結(jié)果返回給調(diào)用方,但是在此之前需要調(diào)用 detach()。
- detach():將此消息與當(dāng)前線(xiàn)程分離,并允許稍后進(jìn)行調(diào)用 sendResult(T)
- sendResult():將結(jié)果發(fā)送回調(diào)用方。 2.5 MediaSession
媒體會(huì)話(huà),即**受控端。**通過(guò)設(shè)定 MediaSession.Callback回調(diào)來(lái)接收媒體控制器 MediaController發(fā)送的指令,如控制音樂(lè)的【上一曲】、【下一曲】等。
創(chuàng)建 MediaSession后還需要調(diào)用 setSessionToken()方法設(shè)置用于和**控制器配對(duì)的令牌,使用方式如下:
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 中的大部分方法都是一一對(duì)應(yīng)的 // 在該方法中實(shí)現(xiàn)對(duì) 播放器 的控制, private val callback = object : MediaSession.Callback() { override fun onPlay() { super.onPlay() // 處理 播放器 的播放邏輯。 // 車(chē)載應(yīng)用的話(huà),別忘了處理音頻焦點(diǎn) } 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
接收來(lái)自客戶(hù)端或系統(tǒng)的媒體按鈕、傳輸控件和命令,入【上一曲】、【下一曲】。與 MediaController.transportControls 中的大部分方法都是一一對(duì)應(yīng)的。
private val callback = object : MediaSession.Callback() { override fun onPlay() { super.onPlay() if (!mediaSession.isActive) { mediaSession.isActive = true } //更新播放狀態(tài). 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
播放隊(duì)列一部分的單個(gè)項(xiàng)目,相比 MediaMetadata,多了一個(gè) ID 屬性。常用的方法有:
- getDescription():返回介質(zhì)的說(shuō)明,包含媒體的基礎(chǔ)信息,如標(biāo)題、封面等。
- getQueueId():獲取此項(xiàng)目的隊(duì)列 ID。
MediaSession.Token
表示正在進(jìn)行的會(huì)話(huà),可以通過(guò)會(huì)話(huà)所有者傳遞給客戶(hù)端,以允許客戶(hù)端與服務(wù)端之間建立通信。
2.6 PlaybackState
用于承載播放狀態(tài)的類(lèi)。如當(dāng)前播放位置和當(dāng)前控制功能。在 MediaSession.Callback更改狀態(tài)后需要調(diào)用 MediaSession.setPlaybackState把狀態(tài)同步給客戶(hù)端。
private val callback = object : MediaSession.Callback() { override fun onPlay() { super.onPlay() // 更新?tīng)顟B(tài) val state = PlaybackState.Builder() .setState( PlaybackState.STATE_PLAYING,1,1f ) .build() mediaSession.setPlaybackState(state) } }
2.6.1 PlaybackState.Builder
PlaybackState.Builder 主要用來(lái)創(chuàng)建 PlaybackState 對(duì)象,創(chuàng)建它使用的是建造者模式,如下。
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可用于通過(guò)將特定于應(yīng)用程序的操作發(fā)送給 MediaControllers,這樣就可以擴(kuò)展標(biāo)準(zhǔn)傳輸控件的功能。
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();
常見(jiàn)的 API,有如下一些:
- getAction():返回 CustomAction 的 action
- getExtras():返回附加項(xiàng),這些附加項(xiàng)提供有關(guān)操作的其他特定于應(yīng)用程序的信息
- getIcon():返回 package 中圖標(biāo)的資源 ID
- getName():返回此操作的顯示名稱(chēng) 2.7 MediaMetadata
包含有關(guān)項(xiàng)目的基礎(chǔ)數(shù)據(jù),例如標(biāo)題、藝術(shù)家等。一般需要服務(wù)端從本地?cái)?shù)據(jù)庫(kù)或遠(yuǎn)端查詢(xún)出原始數(shù)據(jù)在封裝成 MediaMetadata 再通過(guò) MediaSession.setMetadata(metadata)返回到客戶(hù)端的 MediaController.Callback.onMetadataChanged中。
常見(jiàn)的 API 有如下:
- containsKey():如果給定的 key 包含在元數(shù)據(jù)中,則返回 true。
- describeContents():描述此可打包實(shí)例的封送處理表示中包含的特殊對(duì)象的種類(lèi)。
- getBitmap():返回給定的 key 的 Bitmap,如果給定 key 不存在位圖,則返回 null。
- getBitmapDimensionLimit():獲取創(chuàng)建此元數(shù)據(jù)時(shí)位圖的寬度/高度限制
- getDescription():獲取此元數(shù)據(jù)的簡(jiǎn)單說(shuō)明以進(jìn)行顯示。
- keySet():返回一個(gè) Set,其中包含在此元數(shù)據(jù)中用作 key 的字符串。 三、示例
下圖是 MediaSession 框架核心類(lèi)的通信過(guò)程。
可以看到,在 MediaSession 框架中,首先客戶(hù)端通過(guò) MediaBrowserService 連接到 MediaBrowserService,MediaBrowserService 接受到請(qǐng)求之后處理相關(guān)的請(qǐng)求,MediaSession 控制播放狀態(tài),并將狀態(tài)同步給客戶(hù)端,客戶(hù)端最后 MediaController 進(jìn)行相應(yīng)的操作。
客戶(hù)端示例代碼:
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,會(huì)觸發(fā) 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 列表,會(huì)觸發(fā) MediaBrowserService.onLoadItem 方法 mMediaBrowser.getItem(rootMediaId,itemCallback) mMediaBrowser.unsubscribe(rootMediaId) // 訂閱服務(wù)端 media item 的改變,會(huì)觸發(fā) 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 } // 還有其它狀態(tài)需要處理 } } //音頻信息,音量 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) } } }
下面是服務(wù)端的示例源碼,主要用于處理客戶(hù)端的請(qǐng)求,并將結(jié)果返回給客戶(hù)端。
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() { // 控制是否允許客戶(hù)端連接,并返回 root media id 給客戶(hù)端 override fun onGetRoot( clientPackageName: String, clientUid: Int, rootHints: Bundle? ): BrowserRoot? { Log.e("TAG", "onGetRoot: $rootHints") return BrowserRoot(ROOT_ID, null) } // 處理客戶(hù)端的訂閱信息 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") // 根據(jù) itemId,返回對(duì)用 MediaItem result?.detach() result?.sendResult(null) } private lateinit var mediaSession: MediaSession; override fun onCreate() { super.onCreate() mediaSession = MediaSession(this, "TAG") mediaSession.setCallback(callback) // 設(shè)置 token sessionToken = mediaSession.sessionToken } // 與 MediaController.transportControls 中的方法是一一對(duì)應(yīng)的。 // 在該方法中實(shí)現(xiàn)對(duì) 播放器 的控制, private val callback = object : MediaSession.Callback() { override fun onPlay() { super.onPlay() // 處理 播放器 的播放邏輯。 // 車(chē)載應(yīng)用的話(huà),別忘了處理音頻焦點(diǎn) Log.e("TAG", "onPlay:") if (!mediaSession.isActive) { mediaSession.isActive = true } // 更新?tīng)顟B(tài) val state = PlaybackState.Builder() .setState( PlaybackState.STATE_PLAYING, 1, 1f ) .build() mediaSession.setPlaybackState(state) } override fun onPause() { super.onPause() } override fun onStop() { super.onStop() } //省略其他方法 } }
上文主要介紹車(chē)載自媒體開(kāi)發(fā)與MediaSession框架解析;這只是其中一小部分;更多的Android車(chē)載技術(shù)可以前往[私信]領(lǐng)取,里面內(nèi)容如下腦圖所示:(需要可以參考,當(dāng)做輔導(dǎo)資料)
文末
車(chē)載系統(tǒng)開(kāi)發(fā),在這幾年崗位逐漸增多。Android轉(zhuǎn)崗車(chē)載開(kāi)發(fā)是個(gè)很好的發(fā)展方向。汽車(chē)的普及同比10年增長(zhǎng)300%以上;近幾年的新能源汽車(chē)逐漸普及,車(chē)載開(kāi)發(fā)人員更是需求很大;普遍缺少人才。
感覺(jué)Android市場(chǎng)下滑的厲害,淘汰人員逐漸增多。一定要眼看未來(lái),才沒(méi)有近憂(yōu)。
以上就是Android車(chē)載多媒體開(kāi)發(fā)MediaSession框架示例詳解的詳細(xì)內(nèi)容,更多關(guān)于Android車(chē)載多媒體MediaSession的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
基于Manifest.xml中不要出現(xiàn)重復(fù)的uses permission的說(shuō)明
本篇文章對(duì)Manifest.xml中不要出現(xiàn)重復(fù)的uses permission進(jìn)行了介紹。需要的朋友參考下2013-05-05Android studio 項(xiàng)目手動(dòng)在本地磁盤(pán)中刪除module后,殘留文件夾無(wú)法刪除的問(wèn)題解決方法
這篇文章主要介紹了Android studio 項(xiàng)目手動(dòng)在本地磁盤(pán)中刪除module后,殘留文件夾無(wú)法刪除問(wèn)題,本文給出了解決方法,對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-03-03Android實(shí)現(xiàn)APP歡迎頁(yè)面簡(jiǎn)單制作思路
這篇文章主要為大家詳細(xì)介紹了Android實(shí)現(xiàn)APP歡迎頁(yè)面簡(jiǎn)單制作思路,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2016-08-08Android開(kāi)發(fā)實(shí)現(xiàn)保存圖片到手機(jī)相冊(cè)功能
這篇文章主要介紹了Android開(kāi)發(fā)實(shí)現(xiàn)保存圖片到手機(jī)相冊(cè)功能,涉及Android圖形及文件相關(guān)操作技巧,需要的朋友可以參考下2019-03-03Android開(kāi)發(fā)實(shí)現(xiàn)的幾何圖形工具類(lèi)GeometryUtil完整實(shí)例
這篇文章主要介紹了Android開(kāi)發(fā)實(shí)現(xiàn)的幾何圖形工具類(lèi)GeometryUtil,涉及Android坐標(biāo)圖形數(shù)值運(yùn)算相關(guān)操作技巧,需要的朋友可以參考下2017-11-11Android HttpURLConnection.getResponseCode()錯(cuò)誤解決方法
在使用HttpURLConnection.getResponseCode()的時(shí)候直接報(bào)錯(cuò)是IOException錯(cuò)誤,一直想不明白,同一個(gè)程序我調(diào)用了兩次,結(jié)果有一個(gè)鏈接一直O(jiān)K,另一個(gè)卻一直報(bào)這個(gè)錯(cuò)誤2013-06-06淺析Android中g(shù)etWidth()和getMeasuredWidth()的區(qū)別
這篇文章主要介紹了淺析Android中g(shù)etWidth()和getMeasuredWidth()的區(qū)別 ,getMeasuredWidth()獲取的是view原始的大小,getWidth()獲取的是這個(gè)view最終顯示的大小,具體區(qū)別介紹大家參考下本文2018-04-04Android實(shí)現(xiàn)手勢(shì)滑動(dòng)(左滑和右滑)
這篇文章主要為大家詳細(xì)介紹了Android實(shí)現(xiàn)手勢(shì)滑動(dòng),左滑和右滑效果,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2020-07-07