解码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或完整的一帧数据,所以不用设置这个属性。

你可能感兴趣的:(音视频开发)