Android車載多媒體開發——MediaSession框架

語言: CN / TW / HK

一、多媒體應用架構

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.1 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.2 MediaSession.QueueItem

播放佇列一部分的單個專案,相比 MediaMetadata,多了一個 ID 屬性。常用的方法有:

  • getDescription():返回介質的說明,包含媒體的基礎資訊,如標題、封面等。
  • getQueueId():獲取此專案的佇列 ID。 2.5.3 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市場下滑的厲害,淘汰人員逐漸增多。一定要眼看未來,才沒有近憂。