解码H264视频出现花屏或马赛克的问题
常見的引起花屏或馬賽克問題的原因是因為丟包,這時候,開發(fā)者應(yīng)該檢查自己的接收緩沖區(qū)是否太小,還有打印RTP的SeqNumber看有沒有不連續(xù)或亂序的問題,如果是用UDP傳輸,則RTP包容易發(fā)生亂序,需要開發(fā)者對包按順序進行重組再解碼。
我說的花屏問題的情況是假設(shè)網(wǎng)絡(luò)沒有數(shù)據(jù)丟包也沒有亂序的情況,假設(shè)輸入的網(wǎng)絡(luò)包是正常的。那問題出在哪里?是在程序去RTP頭、拿到Payload數(shù)據(jù)之后的處理流程有問題。
當我們從網(wǎng)絡(luò)中接收到RTP包,去了包頭,拿到Payload數(shù)據(jù)之后一般就會送去解碼,但是如果直接送去解碼器解碼,很可能會出現(xiàn)花屏。這個問題我很早就遇到過,當時查閱過資料,發(fā)現(xiàn)送給H264解碼器的必須是一個NALU單元,或者是完整的一幀數(shù)據(jù)(包含H264 StartCode),也就是說我們拿到Payload數(shù)據(jù)之后,還要將分片的數(shù)據(jù)組成一個NALU或完整的一幀之后才送給解碼器。怎么知道哪些RTP包屬于一個NALU呢?RTP協(xié)議對H264格式根據(jù)包的大小定義了幾種不同的封包規(guī)則:
三種打包方式:
1 .單一 NAL 單元模式
對于 NALU 的長度小于 MTU 大小的包, 一般采用單一 NAL 單元模式.
2 .組合封包模式
其次, 當 NALU 的長度特別小時, 可以把幾個 NALU 單元封在一個 RTP 包中.
3. FragmentationUnits (FUs).
而當 NALU 的長度超過 MTU 時, 就必須對 NALU 單元進行分片封包. 也稱為 Fragmentation Units (FUs)。這種封包方式有FU-A,FU-B。
關(guān)于如何對RTP H264解包的詳細過程,可參考我的一篇文章:《如何發(fā)送和接收RTP封包的H264,用FFmpeg解碼》
因此,關(guān)鍵是如何對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的某些成員能知道包是一個分片的開頭還是結(jié)尾,這樣就知道了NALU的起始和結(jié)束的邊界了。這個處理方法是在RTP解析層做的,另外還有一種方法--通過FFmpeg的拼幀函數(shù),就是下面要介紹的這一種。
FFmpeg有專門的接口對多個不連續(xù)的數(shù)據(jù)塊組成一幀,這個強大的API就是:av_parser_parse2,讓我們看看如何使用它,下面是示例代碼:
首先要創(chuàng)建一個AVCodecParserContext結(jié)構(gòu):
m_avParserContext = av_parser_init(CODEC_ID_H264);然后,調(diào)用?av_parser_parse2函數(shù)對輸入的數(shù)據(jù)拼幀。
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函數(shù)內(nèi)部會對送進來的數(shù)據(jù)塊進行拼裝處理,組成完成的一幀,然后將數(shù)據(jù)拷貝到另外一個內(nèi)存地址(可能分配了新的內(nèi)存,需要調(diào)用者在外部釋放),新的內(nèi)存地址通過參數(shù)返回給調(diào)用者。然后調(diào)用者就可以將返回的新內(nèi)存地址里的數(shù)據(jù)(完整一幀)拿去解碼了。
使用完該接口對象,記得還要釋放對象:
av_parser_close(m_avParserContext);另外,我們還要注意:不要設(shè)置解碼器的CODEC_FLAG_TRUNCATED屬性,比如下面這樣設(shè)置是沒有必要的,并且會有惡劣影響。
if(codec->capabilities&CODEC_CAP_TRUNCATED)c->flags|= CODEC_FLAG_TRUNCATED; /* we do not send complete frames */設(shè)置這個屬性是告訴FFmpeg解碼器輸入的數(shù)據(jù)是碎片的或不完整的單元幀。而我們送去解碼器的已經(jīng)是一個NALU或完整的一幀數(shù)據(jù),所以不用設(shè)置這個屬性。
總結(jié)
以上是生活随笔為你收集整理的解码H264视频出现花屏或马赛克的问题的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: [vue] SSR解决了什么问题?有做过
- 下一篇: iOS硬解码H264视频流