解码H264视频出现花屏或马赛克的问题
常見的引起花屏或馬賽克問題的原因是因為丟包,這時候,開發者應該檢查自己的接收緩沖區是否太小,還有打印RTP的SeqNumber看有沒有不連續或亂序的問題,如果是用UDP傳輸,則RTP包容易發生亂序,需要開發者對包按順序進行重組再解碼。
我說的花屏問題的情況是假設網絡沒有數據丟包也沒有亂序的情況,假設輸入的網絡包是正常的。那問題出在哪里?是在程序去RTP頭、拿到Payload數據之后的處理流程有問題。
當我們從網絡中接收到RTP包,去了包頭,拿到Payload數據之后一般就會送去解碼,但是如果直接送去解碼器解碼,很可能會出現花屏。這個問題我很早就遇到過,當時查閱過資料,發現送給H264解碼器的必須是一個NALU單元,或者是完整的一幀數據(包含H264 StartCode),也就是說我們拿到Payload數據之后,還要將分片的數據組成一個NALU或完整的一幀之后才送給解碼器。怎么知道哪些RTP包屬于一個NALU呢?RTP協議對H264格式根據包的大小定義了幾種不同的封包規則:
三種打包方式:
1 .單一 NAL 單元模式
對于 NALU 的長度小于 MTU 大小的包, 一般采用單一 NAL 單元模式.
2 .組合封包模式
其次, 當 NALU 的長度特別小時, 可以把幾個 NALU 單元封在一個 RTP 包中.
3. FragmentationUnits (FUs).
而當 NALU 的長度超過 MTU 時, 就必須對 NALU 單元進行分片封包. 也稱為 Fragmentation Units (FUs)。這種封包方式有FU-A,FU-B。
關于如何對RTP H264解包的詳細過程,可參考我的一篇文章:《如何發送和接收RTP封包的H264,用FFmpeg解碼》
因此,關鍵是如何對RTP H264正確解包,還原NALU單元。簡單過程描述是:
首先,去RTP頭,然后定位到負載的?NALU_HEADER頭的位置,如下代碼所示:
?? ?NALU_HEADER * nalu_hdr = NULL;NALU_t ?nalu_data = { 0 };NALU_t * n = &nalu_data;FU_INDICATOR?? ?*fu_ind = NULL;FU_HEADER?? ??? ?*fu_hdr = NULL;nalu_hdr = (NALU_HEADER*)&payload[0]; ??接著,通過nalu_hdr->TYPE 變量就能知道是哪一種打包格式,
if (nalu_hdr->TYPE >0 && nalu_hdr->TYPE < 24) //單包{}else if (nalu_hdr->TYPE == 24) //STAP-A 單一時間的組合包{TRACE("當前包為STAP-A\n");}else if (nalu_hdr->TYPE == 25) //STAP-B 單一時間的組合包{TRACE("當前包為STAP-B\n");}else if (nalu_hdr->TYPE == 26) //MTAP16 多個時間的組合包{TRACE("當前包為MTAP16\n");}else if (nalu_hdr->TYPE == 27) //MTAP24 多個時間的組合包{TRACE("當前包為MTAP24\n");}else if (nalu_hdr->TYPE == 28) //FU-A分片包,解碼順序和傳輸順序相同{}else if (nalu_hdr->TYPE == 29) //FU-B分片包,解碼順序和傳輸順序相同{}else{}對于單包,我們很好處理,一個包就是一個NALU。而對于FU-A,FU-B的封包,我們需要定位到FU_HEADER的位置,通過FU_HEADER的某些成員能知道包是一個分片的開頭還是結尾,這樣就知道了NALU的起始和結束的邊界了。這個處理方法是在RTP解析層做的,另外還有一種方法--通過FFmpeg的拼幀函數,就是下面要介紹的這一種。
FFmpeg有專門的接口對多個不連續的數據塊組成一幀,這個強大的API就是:av_parser_parse2,讓我們看看如何使用它,下面是示例代碼:
首先要創建一個AVCodecParserContext結構:
m_avParserContext = av_parser_init(CODEC_ID_H264);然后,調用?av_parser_parse2函數對輸入的數據拼幀。
void CDecodeVideo:: OnDecodeVideo(PBYTE inbuf, long inLen, int nFrameType, __int64 llPts) {//TRACE("OnDecodeVideo size: %d \n", inLen);//if(m_vFormat == _VIDEO_H264)//{// int nalu_type = (inbuf[4] & 0x1F);// TRACE("nalu_type: %d, size: %d \n", nalu_type, inLen);//}if(!m_bDecoderOK)return;unsigned char *pOutBuf = NULL ;int nOutLen = 0 ;int nCurLen = inLen;int iRet = 0 ;while(nCurLen > 0){//拼幀,ffmpeg 需要一個完整的幀給 AVPacket才能正確的解碼,不然會花屏int nRet = av_parser_parse2(m_avParserContext,c, &pOutBuf,&nOutLen,inbuf,nCurLen/*nLen*/,AV_NOPTS_VALUE, AV_NOPTS_VALUE, AV_NOPTS_VALUE);inbuf += nRet;nCurLen -= nRet;if(nOutLen <= 0){continue;}int got_picture = 0;avpkt.size = nOutLen;avpkt.data = pOutBuf;ASSERT(c != NULL);int len = avcodec_decode_video2(c, picture, &got_picture, &avpkt);if (len < 0) {TRACE("Error while decoding frame Len %d\n", inLen);return;}if (got_picture){}}av_free(pOutBuf); }av_parser_parse2函數內部會對送進來的數據塊進行拼裝處理,組成完成的一幀,然后將數據拷貝到另外一個內存地址(可能分配了新的內存,需要調用者在外部釋放),新的內存地址通過參數返回給調用者。然后調用者就可以將返回的新內存地址里的數據(完整一幀)拿去解碼了。
使用完該接口對象,記得還要釋放對象:
av_parser_close(m_avParserContext);另外,我們還要注意:不要設置解碼器的CODEC_FLAG_TRUNCATED屬性,比如下面這樣設置是沒有必要的,并且會有惡劣影響。
if(codec->capabilities&CODEC_CAP_TRUNCATED)c->flags|= CODEC_FLAG_TRUNCATED; /* we do not send complete frames */設置這個屬性是告訴FFmpeg解碼器輸入的數據是碎片的或不完整的單元幀。而我們送去解碼器的已經是一個NALU或完整的一幀數據,所以不用設置這個屬性。
總結
以上是生活随笔為你收集整理的解码H264视频出现花屏或马赛克的问题的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: [vue] SSR解决了什么问题?有做过
- 下一篇: iOS硬解码H264视频流