FFmpeg開發筆記(三十三)分析ZLMediaKit對H.264流的插幀操作

aqi00發表於2024-06-29
《FFmpeg開發實戰:從零基礎到短影片上線》一書的“3.4.3 把原始的H264檔案封裝為MP4格式”介紹瞭如何把H.264裸流封裝為MP4檔案。那麼在網路上傳輸的H.264裸流是怎樣被接收端獲取影片格式的呢?前文指出H.264流必定以“SPS幀→PPS幀→IDR幀”開頭,接下來就來驗證是否確實如此。

這裡用到了雷霄驊雷神寫的H264分析器,在此向雷神致敬,雷神10年前寫的小程式至今仍然好用。開啟H264分析器,該軟體的初始介面如下圖所示:

單擊檔案路徑欄右邊的開啟按鈕,在彈出的檔案對話方塊中選擇某個H.264裸流檔案,再單擊介面右下角的開始按鈕,分析器便開始分析H264檔案的內容格式,分析後的結果介面如下圖所示:

從分析結果可見,H.264裸流的開頭三幀果然是“SPS幀→PPS幀→IDR幀”。單擊列表中的某個幀,介面右側會顯示該幀的詳細欄位資訊。

當然,分析器只能讀取H.264裸流檔案。倘若讓分析器讀取MP4檔案,就無法正常讀出各幀資訊。那麼流媒體伺服器又是怎麼把MP4檔案轉化為H.264裸流的呢?
以ZLMediaKit為例,它在向推流序列插入I幀時做了特殊處理,一旦出現I幀,就自動插入SPS與PPS等配置幀。具體程式碼在ZLMediaKit框架的ext-codec/H264.cpp,檢視該原始碼的H264Track::inputFrame_l函式,找到以下的程式碼片段,可見程式在判斷關鍵幀之後呼叫了insertConfigFrame函式。

// 判斷是否是I幀, 並且如果是,那判斷前面是否插入過config幀, 如果插入過就不插入了
if (frame->keyFrame() && !_latest_is_config_frame) {
    insertConfigFrame(frame); // 插入SPS幀和PPS幀
}
if(!frame->dropAble()){
    _latest_is_config_frame = false;
}
ret = VideoTrack::inputFrame(frame);

找到insertConfigFrame函式的定義程式碼如下,果然函式內容依次插入了SPS幀和PPS幀:

// 插入SPS幀和PPS幀
void H264Track::insertConfigFrame(const Frame::Ptr &frame) {
    if (!_sps.empty()) { // 插入SPS幀
        auto spsFrame = FrameImp::create<H264Frame>();
        spsFrame->_prefix_size = 4;
        spsFrame->_buffer.assign("\x00\x00\x00\x01", 4);
        spsFrame->_buffer.append(_sps);
        spsFrame->_dts = frame->dts();
        spsFrame->setIndex(frame->getIndex());
        VideoTrack::inputFrame(spsFrame);
    }
    if (!_pps.empty()) { // 插入PPS幀
        auto ppsFrame = FrameImp::create<H264Frame>();
        ppsFrame->_prefix_size = 4;
        ppsFrame->_buffer.assign("\x00\x00\x00\x01", 4);
        ppsFrame->_buffer.append(_pps);
        ppsFrame->_dts = frame->dts();
        ppsFrame->setIndex(frame->getIndex());
        VideoTrack::inputFrame(ppsFrame);
    }
}

由此可見,ZLMediaKit在每個關鍵幀前面都額外插入了SPS幀和PPS幀,確保H.264裸流維持著形如“SPS幀→PPS幀→IDR幀”的隊形。如果不新增SPS和PPS,客戶端在拉流時會報錯如下:

[NULL @ 0000022ed7782540] non-existing PPS 0 referenced

只有加上SPS與PPS,客戶端才能正常拉流解析資料,才能正常渲染影片畫面。
更多詳細的FFmpeg開發知識參見《FFmpeg開發實戰:從零基礎到短影片上線》一書。

相關文章