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