Android車載應用開發(fā)與分析(7)- 車載多媒體(二)- 多媒體應用架構與MediaSession框架

參考資料
媒體應用架構概覽 | Android 開發(fā)者 | Android Developers
MediaSession | Android Developers
MediaSession框架全解析_qzns木雨的博客-CSDN博客_mediasession

1. 多媒體應用架構

1.1 傳統(tǒng)應用架構

播放音頻或視頻的多媒體應用通常由兩部分組成:

  • 播放器:接收傳入的數(shù)據(jù)多媒體辉阶,并輸出音頻或視頻〈穸螅可以是MediaPlayer谆甜、ExoPlayer或其他Player。
  • 界面:用于顯示集绰、控制播放器狀態(tài)界面规辱。

image

眾所周知,如果需要在應用的后臺繼續(xù)播放音頻栽燕,我們就需要把Player放置在Service中罕袋,那么界面播放器之間通信就非常值得研究了。很長一段時間里碍岔,都是由Service提供一個Binder來實現(xiàn)與播放器之間的通信浴讯。但是往往下拉的狀態(tài)欄桌面的Widget都需要與Service之間進行通信,這時候Service就不得不通過實現(xiàn)一系列AIDL接口/廣播/ContentProvider完成與其它應用之間的通信付秕,而這些通信手段既增加了應用開發(fā)者之間的溝通成本兰珍,也增加了應用之間的耦合度。

為了解決上面的問題询吴,Android官方從Android5.0開始提供了MediaSession框架掠河。

1.2 MediaSession 框架

MediaSession框架規(guī)范了音視頻應用中界面播放器之間的通信接口,實現(xiàn)界面與播放器之間的完全解耦猛计∵肽。框架定義了兩個重要的類媒體會話媒體控制器,它們?yōu)闃嫿ǘ嗝襟w播放器應用提供了一個完善的結構奉瘤。

媒體會話媒體控制器通過以下方式相互通信:使用與標準播放器操作(播放勾拉、暫停煮甥、停止等)相對應的預定義回調(diào),以及用于定義應用獨有的特殊行為的可擴展自定義調(diào)用藕赞。

image

2. MediaSession 介紹

MediaSession框架屬于典型的C/S架構成肘,有四個常用的成員類,是整個MediaSession框架流程控制的核心斧蜕。

2.1 客戶端媒體瀏覽器 - MediaBrowser

媒體瀏覽器双霍,用來連接MediaBrowserService訂閱數(shù)據(jù),通過它的回調(diào)接口我們可以獲取與Service的連接狀態(tài)以及獲取在Service中的音樂庫數(shù)據(jù)批销。在客戶端(也就是上文我們提到的界面洒闸,或者說是控制端)中創(chuàng)建。
媒體瀏覽器不是線程安全的均芽。所有調(diào)用都應在構造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.1.1 MediaBrowser.ConnectionCallback

用于接收與MediaBrowserService連接事件的回調(diào),在創(chuàng)建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()
}

private val connectionCallback = object : MediaBrowser.ConnectionCallback() {

    override fun onConnected() {
        super.onConnected()
    }

    override fun onConnectionFailed() {
        super.onConnectionFailed()
    }

    override fun onConnectionSuspended() {
        super.onConnectionSuspended()
    }
}

2.1.2 MediaBrowser.ItemCallback

用于返回MediaBrowser.getItem()的結果深纲。

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.1.3 MediaBrowser.MediaItem

包含有關單個媒體項的信息,用于瀏覽/搜索媒體布朦。MediaItem依賴于服務端提供囤萤,因此框架本身無法保證它包含的值都是正確的。

2.1.4 MediaBrowser.SubscriptionCallback

用于訂與MediaBrowserServiceMediaBrowser.MediaItem列表變化的回調(diào)是趴。

private val connectionCallback = object : MediaBrowser.ConnectionCallback() {

    override fun onConnected() {
        super.onConnected()
        // ...
        if(mMediaBrowser.isConnected) {
            val mediaId = mMediaBrowser.root
            // 需要先取消訂閱
            mMediaBrowser.unsubscribe(mediaId)
            // 服務端會調(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.2 客戶端媒體控制器 - MediaController

媒體控制器,用來向服務端發(fā)送控制指令澄惊,例如:播放唆途、暫停等等,在客戶端中創(chuàng)建掸驱。媒體控制器是線程安全的肛搬。MediaController還有一個關聯(lián)的權限android.permission.MEDIA_CONTENT_CONTROL(不是必須加的權限)必須是系統(tǒng)級應用才可以獲取,幸運的是車載應用一般都是系統(tǒng)級應用毕贼。
MediaController必須在MediaBrowser連接成功后才可以創(chuàng)建温赔。

private val connectionCallback = object : MediaBrowser.ConnectionCallback() {

    override fun onConnected() {
        super.onConnected()
        // ...
        if(mMediaBrowser.isConnected) {
            val sessionToken = mMediaBrowser.sessionToken
            mMediaController = MediaController(applicationContext,sessionToken)
        }
    }
}

2.2.1 MediaController.Callback

用于從MediaSession接收回調(diào)。使用方式如下:

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)
    }

    override fun onExtrasChanged(extras: Bundle?) {
        super.onExtrasChanged(extras)
    }
    // ...
}

2.2.2 MediaController.PlaybackInfo

保存有關當前播放以及如何處理此會話的音頻的信息鬼癣。使用方式如下:

// 獲取當前回話播放的音頻信息
val playbackInfo = mMediaController.playbackInfo

2.2.3 MediaController.TransportControls

用于控制會話中媒體播放的接口陶贼。這允許客戶端向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.3 服務端媒體瀏覽服務 - MediaBrowserService

媒體瀏覽器服務待秃,繼承自Service拜秧,MediaBrowserService屬于服務端,也是承載播放器(如MediaPlayer章郁、ExoPlayer等)和MediaSession的容器枉氮。
實現(xiàn)MediaBrowserService時會要求復寫onGetRootonLoadChildren兩個方法。
onGetRoot通過的返回值決定是否允許客戶端的MediaBrowser連接到MediaBrowserService
當客戶端調(diào)用MediaBrowser.subscribe時會觸發(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ā)楼肪,可以通過返回null拒絕客戶端的連接。
        return BrowserRoot(ROOT_ID, null)
    }

    override fun onLoadChildren(
        parentId: String,
        result: Result<MutableList<MediaBrowser.MediaItem>>
    ) {
    // 由MediaBrowser.subscribe觸發(fā)
        when (parentId) {
            ROOT_ID -> {
                // 查詢本地媒體庫
                // ...
                // 將此消息與當前線程分離惹悄,并允許稍后進行sendResult調(diào)用
                result.detach()
                // 設定到 result 中
                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.3.1 MediaBrowserService.BrowserRoot

包含瀏覽器服務首次連接時需要返回給客戶端的信息。

MediaBrowserService.BrowserRoot API 列表

方法名 備注
Bundle getExtras() 獲取有關瀏覽器服務的附加信息俘侠。
String getRootId() 獲取用于瀏覽的根 ID象缀。

2.3.2 MediaBrowserService.Result<T>

包含瀏覽器服務返回給客戶端的結果集。通過調(diào)用sendResult()將結果返回給調(diào)用方爷速,但是在此之前需要調(diào)用detach()央星。

MediaBrowserService.Result API 列表

方法名 備注
void detach() 將此消息與當前線程分離,并允許稍后進行調(diào)用sendResult(T)
void sendResult(T result) 將結果發(fā)送回調(diào)用方惫东。

2.4 服務端媒體會話 - MediaSession

媒體會話莉给,即受控端。通過設定MediaSession.Callback回調(diào)來接收媒體控制器MediaController發(fā)送的指令廉沮。
創(chuàng)建MediaSession后還需要調(diào)用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中的大部分方法都是一一對應的
    // 在該方法中實現(xiàn)對 播放器 的控制,
    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>>
    ) {
        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")
    }
}

2.4.1 MediaSession.Callback

接收來自控制器和系統(tǒng)的媒體按鈕、傳輸控件和命令坪稽。與MediaController.transportControls中的大部分方法都是一一對應的曼玩。使用方式如下:

override fun onCreate() {
    super.onCreate()
    mediaSession = MediaSession(this, "TAG")
    mediaSession.setCallback(callback)
    sessionToken = mediaSession.sessionToken
}

// 與MediaController.transportControls中的方法是一一對應的。
// 在該方法中實現(xiàn)對 播放器 的控制窒百,
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()
        // 此時MediaController.Callback.onPlaybackStateChanged會回調(diào)
        mediaSession.setPlaybackState(state)
    }

    override fun onPause() {
        super.onPause()
    }

    override fun onStop() {
        super.onStop()
    }

}

2.4.2 MediaSession.QueueItem

作為播放隊列一部分的單個項目篙梢。它包含隊列中項目及其 ID 的說明顷帖。
MediaSession.QueueItem API 列表

方法名 備注
MediaDescription getDescription() 返回介質(zhì)的說明。包含媒體的基礎信息如:標題渤滞、封面等等贬墩。
long getQueueId() 獲取此項目的隊列 ID。

2.4.3 MediaSession.Token

表示正在進行的會話蔼水。這可以通過會話所有者傳遞給客戶端震糖,以允許客戶端與服務端之間建立通信。

2.5 播放器狀態(tài) - PlaybackState

用于承載播放狀態(tài)的類趴腋。如當前播放位置和當前控制功能吊说。
MediaSession.Callback更改狀態(tài)后需要調(diào)用MediaSession.setPlaybackState把狀態(tài)同步給客戶端论咏。使用方式如下:

private val callback = object : MediaSession.Callback() {

    override fun onPlay() {
        super.onPlay()
        // ...
        // 更新狀態(tài)
        val state = PlaybackState.Builder()
            .setState(
                PlaybackState.STATE_PLAYING,1,1f
            )
            .build()
        mediaSession.setPlaybackState(state)
    }
}

2.5.1 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.5.2 PlaybackState.CustomAction

CustomActions可用于通過將特定于應用程序的操作發(fā)送給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();

PlaybackState.CustomAction API 說明

方法名 備注
String getAction() 返回CustomAction的action。
Bundle getExtras() 返回附加項雅宾,這些附加項提供有關操作的其他特定于應用程序的信息养涮,如果沒有,則返回 null眉抬。
int getIcon() 返回package中圖標的資源 ID贯吓。
CharSequence getName() 返回此操作的顯示名稱。

2.6 元數(shù)據(jù)類 - MediaMetadata

包含有關項目的基礎數(shù)據(jù)蜀变,例如標題悄谐、藝術家等。一般需要服務端從本地數(shù)據(jù)庫或遠端查詢出原始數(shù)據(jù)在封裝成MediaMetadata再通過MediaSession.setMetadata(metadata)返回到客戶端MediaController.Callback.onMetadataChanged中库北。

MediaMetadata API 說明

方法名 備注
boolean containsKey(String key) 如果給定的key包含在元數(shù)據(jù)中爬舰,則返回 true
int describeContents() 描述此可打包實例的封送處理表示中包含的特殊對象的種類。
Bitmap getBitmap(String key) 返回給定的key的Bitmap;如果給定key不存在位圖寒瓦,則返回 null情屹。
int getBitmapDimensionLimit() 獲取創(chuàng)建此元數(shù)據(jù)時位圖的寬度/高度限制(以像素為單位)。
MediaDescription getDescription() 獲取此元數(shù)據(jù)的簡單說明以進行顯示杂腰。
long getLong(String key) 返回與給定key關聯(lián)的值垃你,如果給定key不再存在,則返回 0L颈墅。
Rating getRating(String key) 對于給定的key返回Rating;如果給定key不存在Rating蜡镶,則返回 null。
String getString(String key) 以 String 格式返回與給定key關聯(lián)的文本值恤筛,如果給定key不存在所需類型的映射,或者null值顯式與該key關聯(lián)芹橡,則返回 null毒坛。
CharSequence getText(String key) 返回與給定鍵關聯(lián)的值,如果給定鍵不存在所需類型的映射林说,或者與該鍵顯式關聯(lián) null 值煎殷,則返回 null。
Set<String> keySet() 返回一個 Set腿箩,其中包含在此元數(shù)據(jù)中用作key的字符串豪直。
int size() 返回此元數(shù)據(jù)中的字段數(shù)。

MediaMetadata 常用Key

方法名 備注
METADATA_KEY_ALBUM 媒體的唱片集標題珠移。
METADATA_KEY_ALBUM_ART 媒體原始來源的相冊的插圖弓乙,Bitmap格式
METADATA_KEY_ALBUM_ARTIST 媒體原始來源的專輯的藝術家末融。
METADATA_KEY_ALBUM_ART_URI 媒體原始源的相冊的圖稿,Uri格式(推薦使用)
METADATA_KEY_ART 媒體封面暇韧,Bitmap格式
METADATA_KEY_ART_URI 媒體的封面勾习,Uri格式。
METADATA_KEY_ARTIST 媒體的藝術家懈玻。
METADATA_KEY_AUTHOR 媒體的作者巧婶。
METADATA_KEY_BT_FOLDER_TYPE 藍牙 AVRCP 1.5 的 6.10.2.2 節(jié)中指定的媒體的藍牙文件夾類型。
METADATA_KEY_COMPILATION 媒體的編譯狀態(tài)涂乌。
METADATA_KEY_COMPOSER 媒體的作曲家艺栈。
METADATA_KEY_DATE 媒體的創(chuàng)建或發(fā)布日期。
METADATA_KEY_DISC_NUMBER 介質(zhì)原始來源的光盤編號湾盒。
METADATA_KEY_DISPLAY_DESCRIPTION 適合向用戶顯示的說明湿右。
METADATA_KEY_DISPLAY_ICON 適合向用戶顯示的圖標或縮略圖。
METADATA_KEY_DISPLAY_ICON_URI 適合向用戶顯示的圖標或縮略圖历涝, Uri格式诅需。
METADATA_KEY_DISPLAY_SUBTITLE 適合向用戶顯示的副標題。
METADATA_KEY_DISPLAY_TITLE 適合向用戶顯示的標題荧库。
METADATA_KEY_DURATION 媒體的持續(xù)時間(以毫秒為單位)堰塌。
METADATA_KEY_GENRE 媒體的流派。
METADATA_KEY_MEDIA_ID 用于標識內(nèi)容的字符串Key分衫。
METADATA_KEY_MEDIA_URI 媒體內(nèi)容场刑,Uri格式。
METADATA_KEY_NUM_TRACKS 媒體原始源中的曲目數(shù)蚪战。
METADATA_KEY_RATING 媒體的總體評分牵现。
METADATA_KEY_TITLE 媒體的標題。
METADATA_KEY_TRACK_NUMBER 媒體的磁道編號邀桑。
METADATA_KEY_USER_RATING 用戶對媒體的分級瞎疼。
METADATA_KEY_WRITER 媒體作家。
String METADATA_KEY_YEAR 媒體創(chuàng)建或發(fā)布為長的年份壁畸。

3. MediaSession 簡單實踐

MediaSession 框架核心類通信過程如下圖所示贼急。


image

客戶端源碼如下所示:

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,會觸發(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列表,會觸發(fā)MediaBrowserService.onLoadItem方法
                mMediaBrowser.getItem(rootMediaId,itemCallback)
                mMediaBrowser.unsubscribe(rootMediaId)
                // 訂閱服務端 media item的改變令杈,會觸發(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)
        }
    }
}

服務端源碼如下所示:


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")
        // 根據(jù)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中的方法是一一對應的掉丽。
    // 在該方法中實現(xiàn)對 播放器 的控制跌榔,
    private val callback = object : MediaSession.Callback() {

        override fun onPlay() {
            super.onPlay()
            // 處理 播放器 的播放邏輯。
            // 車載應用的話机打,別忘了處理音頻焦點
            Log.e("TAG", "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()
        }

        // 還有其它方法需要復寫
    }
}

上述的代碼只是幫助理解MediaSession框架的通信過程矫户,本身的功能非常的簡陋。上一篇Android車載應用開發(fā)與分析(6)- 車載多媒體(一)- 音視頻基礎知識與MediaPlayer中介紹了音視頻的基礎知識和MediaPlayer的生命周期残邀,再通過本篇了解了MediaSession框架的基礎使用皆辽,下一篇我們就可以開始解析車載Android中的原生LocalMedia應用了。

4. MediaSession API 列表

4.1 MediaBrowser 相關組件 API 列表

4.1.1 MediaBrowser

方法名 備注
void connect() 連接到媒體瀏覽器服務芥挣。
void disconnect() 斷開與媒體瀏覽器服務的連接驱闷。
Bundle getExtras() 獲取介質(zhì)服務的任何附加信息。
void getItem(String mediaId, MediaBrowser.ItemCallback cb) 從連接的服務中檢索特定的MediaItem
String getRoot() 獲取根ID空免。
ComponentName getServiceComponent() 獲取媒體瀏覽器連接到的服務組件空另。
MediaSession.Token getSessionToken() 獲取與媒體瀏覽器關聯(lián)的媒體會話Token。
boolean isConnected() 返回瀏覽器是否連接到服務蹋砚。
void subscribe(String parentId,Bundle options, MediaBrowser.SubscriptionCallback callback) 使用特定于服務的參數(shù)進行查詢扼菠,以獲取有關指定 ID 中包含的媒體項的信息,并訂閱以在更新更改時接收更新。
void subscribe(String parentId, MediaBrowser.SubscriptionCallback callback) 詢有關包含在指定 ID 中的媒體項的信息,并訂閱以在更改時接收更新煤禽。
void unsubscribe(String parentId) 取消訂閱指定媒體 ID 。
void unsubscribe(String parentId, MediaBrowser.SubscriptionCallback callback) 通過回調(diào)取消訂閱對指定媒體 ID秧饮。

4.1.2 MediaBrowser.ConnectionCallback

方法 備注
onConnected() 與MediaBrowserService連接成功。在調(diào)用MediaBrowser.connect()后才會有回調(diào)泽篮。
onConnectionFailed() 與MediaBrowserService連接失敗盗尸。
onConnectionSuspended() 與MediaBrowserService連接斷開。

4.1.3 MediaBrowser. ItemCallback

方法名 備注
onError(String mediaId) 檢索時出錯帽撑,或者連接的服務不支持時回調(diào)泼各。
onItemLoaded(MediaBrowser.MediaItem item) 返回Item時調(diào)用。

4.1.4 MediaBrowser. MediaItem

方法名 備注
int describeContents() 描述此可打包實例的封送處理表示中包含的特殊對象的種類亏拉。
MediaDescription getDescription() 獲取介質(zhì)的說明历恐。包含媒體的基礎信息如:標題、封面等等专筷。
int getFlags() 獲取項的標志。FLAG_BROWSABLE:表示Item具有自己的子項蒸苇。FLAG_PLAYABLE:表示Item可播放
String getMediaId() 返回此項的媒體 ID磷蛹。
boolean isBrowsable() 返回此項目是否可瀏覽。
boolean isPlayable() 返回此項是否可播放溪烤。

4.1.5 MediaBrowser.SubscriptionCallback

方法名 備注
onChildrenLoaded(String parentId, List<MediaBrowser.MediaItem> children) 在加載或更新子項列表時回調(diào)味咳。
onChildrenLoaded(String parentId, List<MediaBrowser.MediaItem> children,Bundle options) 在加載或更新子項列表時回調(diào)庇勃。
onError(String parentId) 當 ID 不存在或訂閱時出現(xiàn)其他錯誤時回調(diào)。
onError(String parentId, Bundle options) 當 ID 不存在或訂閱時出現(xiàn)其他錯誤時回調(diào)槽驶。

4.2 MediaController 相關組件 API 列表

4.2.1 MediaController

方法名 備注
void adjustVolume (int direction, int flags) 調(diào)整此會話正在播放的輸出的音量责嚷。
boolean dispatchMediaButtonEvent (KeyEvent keyEvent) 將指定的媒體按鈕事件發(fā)送到會話。
Bundle getExtras() 獲取此會話的附加內(nèi)容掂铐。
long getFlags() 獲取此會話的標志罕拂。
MediaMetadata getMetadata() 獲取此會話的當前Metadata。
String getPackageName() 獲取會話所有者的程序包名稱全陨。
MediaController.PlaybackInfo getPlaybackInfo() 獲取此會話的當前播放信息爆班。
PlaybackState getPlaybackState() 獲取此會話的當前播放狀態(tài)。
List<MediaSession.QueueItem> getQueue() 獲取此會話的當前播放隊列(如果已設置)辱姨。
CharSequence getQueueTitle() 獲取此會話的隊列標題柿菩。
int getRatingType() 獲取會話支持的評級類型。
PendingIntent getSessionActivity() 獲取啟動與此會話關聯(lián)的 UI 的意圖(如果存在)雨涛。
Bundle getSessionInfo() 獲取創(chuàng)建會話時設置的其他會話信息枢舶。
MediaSession.Token getSessionToken() 獲取連接到的會話的令牌。
String getTag() 獲取會話的標記以進行調(diào)試替久。
MediaController.TransportControls getTransportControls() 獲取TransportControls實例以將控制操作發(fā)送到關聯(lián)的會話凉泄。
void registerCallback (MediaController.Callback callback, Handler handler) 注冊回調(diào)以從會話接收更新。
void registerCallback (MediaController.Callback callback) 注冊回調(diào)以從會話接收更新侣肄。
void sendCommand (String command, Bundle args, ResultReceiver cb) 向會話發(fā)送通用命令旧困。
void setVolumeTo (int value, int flags) 設置此會話正在播放的輸出的音量。
void unregisterCallback (MediaController.Callback callback) 注銷指定的回調(diào)稼锅。

4.2.2 MediaController.Callback

方法名 備注
void onAudioInfoChanged (MediaController.PlaybackInfo info) 當前音頻信息發(fā)生改變吼具。
void onExtrasChanged (Bundle extras) 當前附加內(nèi)容發(fā)生改變。
void onMetadataChanged (MediaMetadata metadata) 當前Metadata發(fā)生改變矩距。
void onPlaybackStateChanged(PlaybackState state) 當前播放狀態(tài)發(fā)生改變拗盒。客戶端通過該回調(diào)來顯示界面上音視頻的播放狀態(tài)锥债。
void onQueueChanged (List<MediaSession.QueueItem> queue) 當前隊列中項目發(fā)生改變陡蝇。
void onQueueTitleChanged (CharSequence title) 當前隊列標題發(fā)生改變。
void onSessionDestroyed() 會話銷毀哮肚。
void onSessionEvent (String event, Bundle extras) MediaSession所有者發(fā)送的自定義事件登夫。

4.2.3 MediaController. PlaybackInfo

方法名 備注
AudioAttributes getAudioAttributes() 獲取此會話的音頻屬性。
int getCurrentVolume() 獲取此會話的當前音量允趟。
int getMaxVolume() 獲取可為此會話設置的最大音量恼策。
int getPlaybackType() 獲取影響音量處理的播放類型。
int getVolumeControl() 獲取可以使用的音量控件的類型潮剪。
String getVolumeControlId() 獲取此會話的音量控制 ID涣楷。

4.2.4 MediaController. TransportControls

方法名 備注
void fastForward() 開始快進分唾。
void pause() 請求播放器暫停播放并保持在當前位置。
void play() 請求播放器在其當前位置開始播放狮斗。
void playFromMediaId (String mediaId, Bundle extras) 請求播放器開始播放特定媒體 ID绽乔。
void playFromSearch (String query, Bundle extras) 請求播放器開始播放特定的搜索查詢。
void playFromUri (Uri uri, Bundle extras) 請求播放器開始播放特定Uri碳褒。
void prepare() 請求播放器準備播放折砸。
void prepareFromMediaId (String mediaId, Bundle extras) 請求播放器為特定媒體 ID 準備播放。
void prepareFromSearch (String query, Bundle extras) 請求播放器為特定搜索查詢準備播放骤视。
void prepareFromUri (Uri uri, Bundle extras) 請求播放器為特定Uri鞍爱。
void rewind() 開始倒帶。
void seekTo(long pos) 移動到媒體流中的新位置专酗。
void sendCustomAction (PlaybackState.CustomAction customAction, Bundle args) 發(fā)送自定義操作以供MediaSession執(zhí)行睹逃。
void sendCustomAction (String action,Bundle args) 將自定義操作中的 id 和 args 發(fā)送回去,以便MediaSession執(zhí)行祷肯。
void setPlaybackSpeed (float speed) 設置播放速度沉填。
void setRating(Rating rating) 對當前內(nèi)容進行評級。
void skipToNext() 跳到下一項佑笋。
void skipToPrevious() 跳到上一項翼闹。
void skipToQueueItem(long id) 在播放隊列中播放具有特定 ID 的項目。
void stop() 請求播放器停止播放;它可以以任何適當?shù)姆绞角宄錉顟B(tài)蒋纬。

4.3 MediaBrowserService 相關組件 API 列表

4.3.1 MediaBrowserService

方法名 備注
final Bundle getBrowserRootHints() 獲取從當前連接 MediaBrowser的發(fā)送的根提示猎荠。
final MediaSessionManager.RemoteUserInfo getCurrentBrowserInfo() 獲取發(fā)送當前請求的瀏覽器信息。
MediaSession.Token getSessionToken() 獲取會話令牌蜀备,如果尚未創(chuàng)建會話令牌或已銷毀會話令牌关摇,則獲取 null。
void notifyChildrenChanged(String parentId) 通知所有連接的媒體瀏覽器指定父 ID 的子級已經(jīng)更改碾阁。
void notifyChildrenChanged(String parentId, Bundle options) 通知所有連接的媒體瀏覽器指定父 ID 的子級已經(jīng)更改输虱。
abstract MediaBrowserService.BrowserRoot onGetRoot(String clientPackageName,int clientUid, Bundle rootHints) 獲取供特定客戶端瀏覽的根信息。由MediaBrowser.connect觸發(fā)脂凶,可以通過返回null拒絕客戶端的連接宪睹。
abstract void onLoadChildren(String parentId, Result<List<MediaBrowser.MediaItem>> result) 獲取有關媒體項的子項的信息。由MediaBrowser.subscribe觸發(fā)蚕钦。
void onLoadChildren(String parentId, Result<List<MediaBrowser.MediaItem>> result,Bundle options) 獲取有關媒體項的子項的信息亭病。由MediaBrowser.subscribe觸發(fā)。
void onLoadItem(String itemId, Result<MediaBrowser.MediaItem> result) 獲取有關特定媒體項的信息嘶居。由MediaBrowser.getItem觸發(fā)命贴。
void setSessionToken(MediaSession.Token token) 設置媒體會話。

4.3.2 MediaBrowserService.BrowserRoot

方法名 備注
Bundle getExtras() 獲取有關瀏覽器服務的附加信息。
String getRootId() 獲取用于瀏覽的根 ID胸蛛。

4.3.3 MediaBrowserService.Result

方法名 備注
void detach() 將此消息與當前線程分離,并允許稍后進行調(diào)用sendResult(T)
void sendResult(T result) 將結果發(fā)送回調(diào)用方樱报。

4.4 MediaSession 相關組件 API 列表

4.4.1 MediaSession

方法名 備注
MediaController getController() 獲取此會話的控制器葬项。
MediaSessionManager.RemoteUserInfo getCurrentControllerInfo() 獲取發(fā)送當前請求的控制器信息。
MediaSession.Token getSessionToken() 獲取此會話令牌對象迹蛤。
boolean isActive() 獲取此會話的當前活動狀態(tài)民珍。
void release() 當應用完成播放時,必須調(diào)用此項盗飒。
void sendSessionEvent (String event, Bundle extras) 將專有事件發(fā)送給監(jiān)聽此會話的所有MediaController嚷量。會觸發(fā)MediaController.Callback.onSessionEvent。
void setActive(boolean active) 設置此會話當前是否處于活動狀態(tài)并準備好接收命令逆趣。
void setCallback (MediaSession.Callback callback) 設置回調(diào)以接收媒體會話的更新蝶溶。
void setCallback (MediaSession.Callback callback,Handler handler) 設置回調(diào)以接收媒體會話的更新。
void setExtras(Bundle extras) 設置一些可與MediaSession關聯(lián)的附加功能宣渗。
void setFlags(int flags) 為會話設置標志抖所。
void setMediaButtonBroadcastReceiver(ComponentName broadcastReceiver) 設置應接收媒體按鈕的清單聲明類的組件名稱。
void setMediaButtonReceiver(PendingIntent mbr) 此方法在 API 級別 31 中已棄用痕囱。改用setMediaButtonBroadcastReceiver(android.content.ComponentName)田轧。
void setMetadata(MediaMetadata metadata) 更新當前MediaMetadata。
void setPlaybackState(PlaybackState state) 更新當前播放狀態(tài)鞍恢。
void setPlaybackToLocal(AudioAttributes attributes) 設置此會話音頻的屬性傻粘。
void setPlaybackToRemote(VolumeProvider volumeProvider) 將此會話配置為使用遠程音量處理。
void setQueue(List<MediaSession.QueueItem> queue) 更新播放隊列中的項目列表帮掉。
void setQueueTitle(CharSequence title) 設置播放隊列的標題弦悉。
void setRatingType(int type) 設置此會話使用的評級樣式。
void setSessionActivity(PendingIntent pi) 設置啟動此會話的Activity的Intent旭寿。

4.4.2 MediaSession.Callback

方法名 備注
void onCommand(String command,Bundle args,ResultReceiver cb) 當控制器已向此會話發(fā)送命令時調(diào)用警绩。
void onCustomAction(String action, Bundle extras) 當要執(zhí)行MediaControllerPlaybackState.CustomAction時調(diào)用。
void onFastForward() 處理快進請求盅称。
boolean onMediaButtonEvent(Intent mediaButtonIntent) 當按下媒體按鈕并且此會話具有最高優(yōu)先級或控制器向會話發(fā)送媒體按鈕事件時調(diào)用肩祥。
void onPause() 處理暫停播放的請求。
void onPlay() 處理開始播放的請求缩膝。
void onPlayFromMediaId(String mediaId, Bundle extras) 處理播放應用提供的特定mediaId的播放請求混狠。
void onPlayFromSearch(String query, Bundle extras) 處理從搜索查詢開始播放的請求。
void onPlayFromUri(Uri uri, Bundle extras) 處理播放由URI表示的特定媒體項的請求疾层。
void onPrepare() 處理準備播放的請求将饺。
void onPrepareFromMediaId(String mediaId, Bundle extras) 處理應用提供的特定mediaId的準備播放請求
void onPrepareFromSearch(String query, Bundle extras) 處理準備從搜索查詢播放的請求。
void onPrepareFromUri(Uri uri, Bundle extras) 處理由URI表示的特定媒體項的準備請求。
void onRewind() 處理倒帶請求予弧。
void onSeekTo(long pos) 處理跳轉到特定位置的請求刮吧。
void onSetPlaybackSpeed(float speed) 處理修改播放速度的請求。
void onSetRating(Rating rating) 處理設定評級的請求掖蛤。
void onSkipToNext() 處理要跳到下一個媒體項的請求杀捻。
void onSkipToPrevious() 處理要跳到上一個媒體項的請求。
void onSkipToQueueItem(long id) 處理跳轉到播放隊列中具有給定 ID 的項目的請求蚓庭。
void onStop() 處理停止播放的請求致讥。

4.4.3 MediaSession.QueueItem

方法名 備注
MediaDescription getDescription() 返回介質(zhì)的說明。包含媒體的基礎信息如:標題器赞、封面等等垢袱。
long getQueueId() 獲取此項目的隊列 ID。

4.5 PlaybackState 相關組件 API 列表

4.5.1 PlaybackState

方法名 備注
long getActions() 獲取此會話上可用的當前操作港柜。
long getActiveQueueItemId() 獲取隊列中當前活動項的 ID请契。
long getBufferedPosition() 獲取當前緩沖位置(以毫秒為單位)。
List<PlaybackState.CustomAction> getCustomActions() 獲取自定義操作的列表潘懊。
CharSequence getErrorMessage() 獲取用戶可讀的錯誤消息姚糊。
Bundle getExtras() 獲取在此播放狀態(tài)下設置的任何自定義附加內(nèi)容。
long getLastPositionUpdateTime() 獲取上次更新位置的經(jīng)過的實時時間授舟。
float getPlaybackSpeed() 獲取當前播放速度作為正常播放的倍數(shù)救恨。
long getPosition() 獲取當前播放位置(以毫秒為單位)。
int getState() 獲取當前播放狀態(tài)释树。
boolean isActive() 返回是否將其視為活動播放狀態(tài)肠槽。

4.5.2 PlaybackState.Builder

方法名 備注
PlaybackState.Builder addCustomAction(String action, String name, int icon) 將自定義操作添加到播放狀態(tài)。
PlaybackState.Builder addCustomAction (PlaybackState.CustomAction customAction) 將自定義操作添加到播放狀態(tài)奢啥。
PlaybackState.Builder setActions(long actions) 設置此會話上可用的當前操作秸仙。
PlaybackState.Builder setActiveQueueItemId(long id) 通過指定活動項目的 id 來設置播放隊列中的活動項目。
PlaybackState.Builder setBufferedPosition(long bufferedPosition) 設置當前緩沖位置(以毫秒為單位)桩盲。
PlaybackState.Builder setErrorMessage(CharSequence error) 設置用戶可讀的錯誤消息寂纪。
PlaybackState.Builder setExtras(Bundle extras) 設置要包含在播放狀態(tài)中的任何自定義附加內(nèi)容。
PlaybackState.Builder setState(int state, long position, float playbackSpeed) 設置當前播放狀態(tài)赌结。
PlaybackState.Builder setState(int state, long position, float playbackSpeed, long updateTime) 設置當前播放狀態(tài)捞蛋。
PlaybackState build() 生成并返回具有這些值的PlaybackState實例。

4.5.3 PlaybackState.CustomAction

方法名 備注
String getAction() 返回CustomAction的action柬姚。
Bundle getExtras() 返回附加項拟杉,這些附加項提供有關操作的其他特定于應用程序的信息,如果沒有量承,則返回 null搬设。
int getIcon() 返回package中圖標的資源 ID穴店。
CharSequence getName() 返回此操作的顯示名稱。
最后編輯于
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末拿穴,一起剝皮案震驚了整個濱河市泣洞,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌贞言,老刑警劉巖斜棚,帶你破解...
    沈念sama閱讀 206,968評論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異该窗,居然都是意外死亡,警方通過查閱死者的電腦和手機蚤霞,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,601評論 2 382
  • 文/潘曉璐 我一進店門酗失,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人昧绣,你說我怎么就攤上這事规肴。” “怎么了夜畴?”我有些...
    開封第一講書人閱讀 153,220評論 0 344
  • 文/不壞的土叔 我叫張陵拖刃,是天一觀的道長。 經(jīng)常有香客問我贪绘,道長兑牡,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,416評論 1 279
  • 正文 為了忘掉前任税灌,我火速辦了婚禮均函,結果婚禮上,老公的妹妹穿的比我還像新娘菱涤。我一直安慰自己苞也,他們只是感情好,可當我...
    茶點故事閱讀 64,425評論 5 374
  • 文/花漫 我一把揭開白布粘秆。 她就那樣靜靜地躺著如迟,像睡著了一般。 火紅的嫁衣襯著肌膚如雪攻走。 梳的紋絲不亂的頭發(fā)上殷勘,一...
    開封第一講書人閱讀 49,144評論 1 285
  • 那天,我揣著相機與錄音陋气,去河邊找鬼劳吠。 笑死,一個胖子當著我的面吹牛巩趁,可吹牛的內(nèi)容都是我干的痒玩。 我是一名探鬼主播淳附,決...
    沈念sama閱讀 38,432評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼蠢古!你這毒婦竟也來了奴曙?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 37,088評論 0 261
  • 序言:老撾萬榮一對情侶失蹤草讶,失蹤者是張志新(化名)和其女友劉穎洽糟,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體堕战,經(jīng)...
    沈念sama閱讀 43,586評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡坤溃,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,028評論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了嘱丢。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片薪介。...
    茶點故事閱讀 38,137評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖越驻,靈堂內(nèi)的尸體忽然破棺而出汁政,到底是詐尸還是另有隱情,我是刑警寧澤缀旁,帶...
    沈念sama閱讀 33,783評論 4 324
  • 正文 年R本政府宣布记劈,位于F島的核電站,受9級特大地震影響并巍,放射性物質(zhì)發(fā)生泄漏目木。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,343評論 3 307
  • 文/蒙蒙 一履澳、第九天 我趴在偏房一處隱蔽的房頂上張望嘶窄。 院中可真熱鬧,春花似錦距贷、人聲如沸柄冲。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,333評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽现横。三九已至,卻和暖如春阁最,著一層夾襖步出監(jiān)牢的瞬間戒祠,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,559評論 1 262
  • 我被黑心中介騙來泰國打工速种, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留姜盈,地道東北人。 一個月前我還...
    沈念sama閱讀 45,595評論 2 355
  • 正文 我出身青樓配阵,卻偏偏與公主長得像馏颂,于是被迫代替她去往敵國和親示血。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 42,901評論 2 345

推薦閱讀更多精彩內(nèi)容