MediaPlayer的生命週期和緩衝策略 (源碼篇)

概述

最近打算對公司的播放器進行優化.那麼作爲一個Android開發人員,Android自帶的MediaPlayer本身具有很好的借鑑意義。MediaPlayer其實只是播放器在java層包的一層殼,具體的實現由評分機制決定,而在Android 7 之後Google官方移除了AwesomePlayer,故NuPlayer成爲了大多數場景下播放器的底層實現。本文主要針對NuPlayer的生命週期、buffering策略以及拉取、消費多媒體數據進行講解、學習。

必備的知識體系

在講述原理之前,需要確保具有以下知識點的儲備:

  1. MediaCodec、ACodec ,NuPlayer的編解碼通過MediaCodec實現,而MediaCodec其實是Android在OpenMax基礎上又封裝了一層的ACodec實現的。ACodec是MediaCodec與OpenMax溝通的橋樑,MediaCodec通過ACodec發送命令給OpenMax,而ACodec負責接收事件回調並通知MediaCodec.
  2. MediaPlayer,相對於NuPlayer在c++層的實現,MediaPlayer在java層供用戶使用。換句話說,我們可以通過MediaPlayer的狀態轉換去了解NuPlayer在對應階段的行爲和過程,因爲java的native方法跟c++方法是一一映射的
  3. ALooper、AMessage, Google在c++層封裝了一套類似Android Looper的架構,用於維持消息隊列和派發消息。

MediaPlayer的api

  1. setDataSource ,設置數據源
  2. prepare,在調用MediaPlayer::start之前必須調用此函數,使得MediaPlayer進入prepared狀態
  3. start,開始播放

MediaPlayer的setDataSource(path: String)流程

  1. 協議判斷 : java層的MediaPlayer會判斷path數據源是否屬於file協議,如果不是則創建MediaHTTPService的binder,隨後作爲參數調用c++層的setDataSource函數。
  2. 播放器創建: 在MediaPlayer::setDataSource(c++層)的調用中,會先通過評分機制創建播放器,此處通常返回NU_PLAYER類型。即創建NuPlayerDriver.
  3. GenericSource: GenericSource作爲在c++層對數據源的包裝。當數據源爲文件時,GenericSource會持有文件描述符fd,而當數據源通過http或者https協議拉取時,GenericSource會持有MediaHTTPService的BpBinder
    在這裏插入圖片描述

MediaPlayer::prepare的調用流程

在這裏插入圖片描述
在MediaPlayer::prepare的調用流程中,NuPlayer在背後給我們完成了以下幾件事:

  1. 創建MediaHTTPConnection: 當設置數據源對應http協議時,會創建java層的實例。該類主要通過HttpURLConnection來獲取InputStream,從而讀取數據.
  2. 創建NuCachedSource2: 該類成員mLooper是ALooper類的實例,通過不斷髮送AMessage來驅使NuCachedSource2進行多媒體資源的數據讀取.(如果是http協議,則是通過調用MediaHTTPConnection的函數完成讀取數據操作),同時通過調整mFetching標誌來限制讀取行爲等。
  3. 創建MediaExtractor: 舉例來說,mp4文件其實是封裝後的文件數據,本身具備一定的數據格式,所以協議通過MediaExtractor來進行demux。同時,GenericSource在調用initDataSource的過程中,會根據mime類型創建對應的MediaExtractor子類實例,比如mp4文件對應的就是MPEG4Extractor.隨後再通過extractor獲取媒體得時長、track數量等。

NuCachedSource2的成員變量:

  1. mCacheOffset: 用來標誌當前媒體數據的緩衝起始位置。
  2. mCache->totalSize: 已緩衝的大小
  3. mFetching: 用於標誌當前是否在進行緩衝
  4. mLastAccessPos: 最近訪問的位置

NuCachedSource2的緩衝行爲:
  1. 可以導致NuCachedSource2進行buffering的行爲大致分爲兩種:a).解碼器需要讀取某個媒體位置的數據,但是緩衝無法cover.隨後,NuCachedSource2設置mFetching = true,開始進行新的緩衝 b).NuCachedSource2的自主行爲。在MediaPlayer::prepare階段,GenericSource通過調用NuCachedSource2::Create創建該類實例,隨後NuCachedSource發送kWhatFetchMore消息至自身的ALooper成員,驅使讀取行爲。
  2. buffering的上下限:GenericSource本身會根據媒體數據的比特率以及緩衝數據大小算出目前的緩衝時長。隨後,跟kLowWaterMarkUs(2s)和kHighWaterMarkUs(5s)比較,當緩衝時長少於2s時,會繼續進行buffering行爲,當緩衝時長高於5s後會自動停止。

以下爲GenericSource緩衝行爲的源碼實現(基於Android7.0.0):

 void NuPlayer::GenericSource::BufferingMonitor::onPollBuffering_l() {
    status_t finalStatus = UNKNOWN_ERROR;
    int64_t cachedDurationUs = -1ll;
    ssize_t cachedDataRemaining = -1;
    
    //僅適合WVME格式
    if (mWVMExtractor != NULL) {
      ...
    } else if (mCachedSource != NULL) { //mCachedSource -> NuCachedSource2
        //cachedDataRemaining -> mCacheOffset + mCache->totalSize() - lastBytePosCached
        cachedDataRemaining =
                mCachedSource->approxDataRemaining(&finalStatus);

        if (finalStatus == OK) {
            off64_t size;
            int64_t bitrate = 0ll;
            if (mDurationUs > 0 && mCachedSource->getSize(&size) == OK) {
                // |bitrate| uses bits/second unit, while size is number of bytes.
                bitrate = size * 8000000ll / mDurationUs;
            } else if (mBitrate > 0) {
                bitrate = mBitrate;
            }
            //如果比特率有效,計算緩衝時長
            if (bitrate > 0) {
                cachedDurationUs = cachedDataRemaining * 8000000ll / bitrate;
            }
        }
    }
	//獲取緩存字節狀態異常
    if (finalStatus != OK) {
        if (finalStatus == ERROR_END_OF_STREAM) {//EOS
            notifyBufferingUpdate_l(100);
        }
        stopBufferingIfNecessary_l();
        return;
    } else if (cachedDurationUs >= 0ll) {
        if (mDurationUs > 0ll) {
            int64_t cachedPosUs = getLastReadPosition_l() + cachedDurationUs;
            int percentage = 100.0 * cachedPosUs / mDurationUs;
            if (percentage > 100) {
                percentage = 100;
            }
			//通知緩衝進度
            notifyBufferingUpdate_l(percentage);
        }

        //kLowWaterMarkUs-> 2s,當cachedDurationUs少於2s時會繼續拉取媒體資源數據
        if (cachedDurationUs < kLowWaterMarkUs) {
            // Take into account the data cached in downstream components to try to avoid
            // unnecessary pause.
            if (mOffloadAudio && mFirstDequeuedBufferRealUs >= 0) {
                int64_t downStreamCacheUs = mlastDequeuedBufferMediaUs - mFirstDequeuedBufferMediaUs
                        - (ALooper::GetNowUs() - mFirstDequeuedBufferRealUs);
                if (downStreamCacheUs > 0) {
                    cachedDurationUs += downStreamCacheUs;
                }
            }

            if (cachedDurationUs < kLowWaterMarkUs) {
                startBufferingIfNecessary_l();
            }
        } else {
            //如果當前在buffering, mPrepareBuffering == true
            //kHighWaterMarkUs -> 5s, kHighWaterMarkRebufferUs -> 15s
            int64_t highWaterMark = mPrepareBuffering ? kHighWaterMarkUs : kHighWaterMarkRebufferUs;
            if (cachedDurationUs > highWaterMark) {
                //如果緩衝時長夠了,則停止buffering行爲(爲流量考慮吧?)
                stopBufferingIfNecessary_l();
            }
        }
    } else if (cachedDataRemaining >= 0) {
        if (cachedDataRemaining < kLowWaterMarkBytes) {
            startBufferingIfNecessary_l();
        } else if (cachedDataRemaining > kHighWaterMarkBytes) {
            stopBufferingIfNecessary_l();
        }
    }

    //該消息主要用於重入該函數
    schedulePollBuffering_l();
}

MediaPlayer::start的調用流程

在這裏插入圖片描述

NuPlayer所做的主要工作:

  1. readBuffer: 通過GenericSource讀取媒體資源的數據,讀取後的數據通過調用MediaExtractor解析生成MediaBuffer。同時如果緩衝字節低於lowLimit的話開啓NuCachedSource2的緩衝行爲。由此可見,數據拉取由NuCachedSource2進行,之後這些在特定封裝格式下的數據由MediaExtractor進行解析(此處並不是解碼,比如mp4文件本身由moov、mdat等結構,這些必須通過extractor抽離出數據)。
  2. onPollingBuffer_l: NuCachedSource2緩衝行爲的主體,在該函數中會判斷是否繼續buffering,同時會通過schedulePollBuffering_l發送消息使得ALooper經過延遲再反覆循環調用onPollingBuffer_l。
  3. instantiateDecoder: 初始化NuPlayerDecoder以及創建、配置、啓動MediaCodec
  4. MediaCodec: 解碼器,比如H264視頻數據可以通過它解出yuv等原始數據。當NuPlayer創建並配置好MediaCodec實例之後,會由MediaCodec通過ACodec創建OpenMax,並把實際的解碼工作交由OpenMax實現。
  5. NuPlayerRender: 用於控制解碼後的媒體數據的渲染過程、音視頻同步(NuPlayer以MediaClock時間爲基準),具體的渲染工作比如視頻可分爲軟件繪製和硬件繪製,交由ACodec負責。

NuPlayerRender的音視頻同步:

  1. MediaClock: 外部時鐘,持有mAnchorTimeMediaUs(此刻音頻軌正在播放的幀的pts)、mAnchorTimeRealUs(mAnchorTimeMediaUs換算成系統時間的值)、nowUs(當前系統時間)三個成員變量。
  2. delayUs: 時延,每次render獲取到新的解碼數據後會進行音頻軌和視頻軌的渲染。拿音頻來說,解碼後的音頻數據會通過AudioOutput寫到音頻設備進行輸出。如果一次寫入行爲無法cover所有的解碼音頻軌數據,則需要進行長度爲delayUs的時延。時延過後繼續解碼完成的音頻數據的寫入。

delayUs = (nextMediaRealTime - nowUs) / 2, nextMediaRealTime -> 下一個解碼數據pts換算成系統時間的值,nowUs -> 當前系統時間

  1. drainVideoQueue: 視頻的同步則相對簡單些,在drainVideoQueue的函數調用中,會檢查當前渲染的視頻幀的pts,會將pts和nowUs(當前系統時間)進行比較。如果pts晚於nowUs,則進行(nowUs - pts)的延時

MediaCodec的生命週期

  • MediaCodec的生命週期由三個init、configure、start三個階段組成,分別對應INITIALIZED、CONFIGURED、STARTED三個狀態。
  • ACodec: MediaCodec並不會直接與OpenMax進行交流,主要的原因是因爲CodecBase的實現類同時有ACodec和MediaFilter,從而將與OpenMax交接的職責劃分給了CodecBase接口去實現,而MediaCodec與ACodec則通過AMessage進行溝通。
  • OMX: ACodec並不直接持有OMX實例,而是通過OMXClient與MediaPlayerService端持有的omx實例發送參數,屬於cs架構(總之,跟binder傳參是一致的~)。隨後,再由omx實例完成OpenMax架構的創建、配置。

在這裏插入圖片描述

在MediaCodec的生命週期中,主要做了以下三件事:

  • 通過ACodec創建OMXNodeInstance實例,並由ACodec的CodecObserver監聽OMX的事件回調
  • 配置ACodec
  • 通過ACodec分配解碼的buffer以及端口,並且在ACodec完成OMX的啓動之後,會先向MediaCodec提交一次OMX的解碼數據,之後循壞調用postFillThisBuffer來通知MediaCodec不斷填充待解碼的媒體數據

MediaCodec如何與GenericSource發生聯繫:

  1. 實例引用: 在NuPlayer::start的函數調用中,會完成MediaCodec實例的初始化、配置等生命週期
  2. demux數據的生產者: GenericSource通過MediaExtractor解析出不含文件格式的多媒體數據後,存放在Track::source::mPackets的數據結構中,供MediaCodec解碼
  3. demux數據的消費者: MediaCodec的職責是解碼,當其完成初始化之後,會等待ACodec發送kWhatFillThisBuffer的消息,隨後將Track::source::mPackets填充至buffer中,再通過ACodec發送命令給OpenMax進行解碼

ACodec的事件驅動:

  • allocateNode: ACodec通過調用IOMX::allocateNode完成OMXNodeInstance的創建,同時在傳參時傳入了實現CodecObserver的監聽函數,從而可以根據OMX的解碼狀態實時的驅動自身行爲,並通知MediaCodec響應。
  • 狀態:ACodec具有ExecutingToIdleStateIdleToLoadedStateLoadedState等狀態,而這些狀態都會監聽OMX的事件回調,當一次OMX底層執行命令成功後,往往ACodec會在回調中進行狀態切換
  • emptyBuffer: ACodec要求OMX對buffer進行一次消費行爲。當OMX解碼完成後,會通過消息回調通知ACodec,隨後ACodec得以再次調用postFillThisBuffer請求MediaCodec完成待解碼數據的填充。當MediaCodec啓動之後,這個待解碼數據的填充、消費也是最主要的循環過程,而這個過程由OMX的事件進行驅動。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章