本文已参与 [新人创作礼] 活动,一起开启掘金创作之路。
《使用gpac封装mp4代码解析(一)》详细分析了ParseNalu函数,本文分析调用ParseNalu函数的函数WriteH264。另一个调用ParseNalu函数的函数WriteH265放在后面篇章进行解析。
函数WriteH264源码如下:
int AF_MP4Writer::WriteH264(unsigned char *pData, int nSize, bool bKey, long nTimeStamp)
{
if ((nTimeStamp == 0) || (pData == NULL) || (nSize <= 0))
return -1;
int lenIn = nSize;
int lenOut = 0;
unsigned char *pIn = pData;
unsigned char *pOut = NULL;
int nBufSize = 0;
unsigned char *pBuf = (unsigned char *)malloc(nSize + 4);
memset(pBuf, 0, nSize + 4);
do {
int nNalStart = 0;
int nNalEnd = 0;
lenOut = ParseNalu(pIn, lenIn, &nNalStart, &nNalEnd);
if (lenOut <= 0)
break;
pOut = pIn + nNalStart;
if (pOut)
{
unsigned int nNalType = (pOut[0] & 0x1F);
switch (nNalType)
{
case 0x07: // SPS
if (!m_bReady)
{
m_pNaluData[AF_NALU_SPS] = new unsigned char[lenOut];
memcpy(m_pNaluData[AF_NALU_SPS], pOut, lenOut);
m_pNaluLength[AF_NALU_SPS] = lenOut;
}
break;
case 0x08: // PPS
if (!m_bReady)
{
m_pNaluData[AF_NALU_PPS] = new unsigned char[lenOut];
memcpy(m_pNaluData[AF_NALU_PPS], pOut, lenOut);
m_pNaluLength[AF_NALU_PPS] = lenOut;
}
break;
case 0x06: // SEI
break;
default :
memcpy(pBuf + nBufSize, &lenOut, 4);
SWAP(pBuf[nBufSize], pBuf[nBufSize + 3]);
SWAP(pBuf[nBufSize + 1], pBuf[nBufSize + 2]);
nBufSize += 4;
memcpy(pBuf + nBufSize, pOut, lenOut);
nBufSize += lenOut;
break;
}
//lenIn -= (lenOut - (pOut - pIn));
lenIn -= (lenOut + (pOut - pIn));
pIn = pOut + lenOut;
}
} while (1);
if (!m_bReady && m_pNaluData[AF_NALU_SPS] && m_pNaluData[AF_NALU_PPS])
{
int nPPSLen = m_pNaluLength[AF_NALU_PPS];
int nZeroCount = 0;
for (int ni = nPPSLen - 1; ni >= 0; ni--)
{
if (m_pNaluData[AF_NALU_PPS][ni])
break;
nZeroCount++;
}
m_pNaluLength[AF_NALU_PPS] -= nZeroCount;
WriteH264Nalu(m_pNaluData, m_pNaluLength);
m_bReady = true;
}
if (m_bReady && (nBufSize > 0))
WriteFrame(pBuf, nBufSize, bKey, nTimeStamp);
if (pBuf)
{
free(pBuf);
pBuf = NULL;
}
return 0;
}
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
if ((nTimeStamp == 0) || (pData == NULL) || (nSize <= 0))
return -1;
int lenIn = nSize;
int lenOut = 0;
unsigned char *pIn = pData;
unsigned char *pOut = NULL;
int nBufSize = 0;
unsigned char *pBuf = (unsigned char *)malloc(nSize + 4);
memset(pBuf, 0, nSize + 4);
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
这段代码的意思是:
参数检查。如果时间戳为0,或者pData为空或者长度不大于0,直接返回错误。保证了代码的健壮性。
定义变量并(根据函数参数)赋初值。
分配一段内存,比输入长度nSize多4个字节,应该是用于保存完整NAL。
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
do {
int nNalStart = 0;
int nNalEnd = 0;
lenOut = ParseNalu(pIn, lenIn, &nNalStart, &nNalEnd);
if (lenOut <= 0)
break;
……
} while (1);
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
do-while循环比较大,功能相对分散,因此拆分为各个子部分依次说。
ParseNalu函数前文刚刚分析完,算是老相识了。pIn指向了函数参数pData,lenIn为函数参数nSize。根据传入的数组和长度获取一个NAL,返回其长度,保存于lenOut中,并且将起始位置和结束位置分别保存于nNalStart和nNalEnd中。如果lenOut小于等于0(实际上异常的时候绝大多数情况应该是0),说明没有找到起始码,或者出现了其他异常,跳出循环。
如果没有出错,则继续循环,找下一个NAL。
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
pOut = pIn + nNalStart;
if (pOut)
{
unsigned int nNalType = (pOut[0] & 0x1F);
switch (nNalType)
{
case 0x07: // SPS
if (!m_bReady)
{
m_pNaluData[AF_NALU_SPS] = new unsigned char[lenOut];
memcpy(m_pNaluData[AF_NALU_SPS], pOut, lenOut);
m_pNaluLength[AF_NALU_SPS] = lenOut;
}
break;
case 0x08: // PPS
if (!m_bReady)
{
m_pNaluData[AF_NALU_PPS] = new unsigned char[lenOut];
memcpy(m_pNaluData[AF_NALU_PPS], pOut, lenOut);
m_pNaluLength[AF_NALU_PPS] = lenOut;
}
break;
case 0x06: // SEI
break;
default :
memcpy(pBuf + nBufSize, &lenOut, 4);
SWAP(pBuf[nBufSize], pBuf[nBufSize + 3]);
SWAP(pBuf[nBufSize + 1], pBuf[nBufSize + 2]);
nBufSize += 4;
memcpy(pBuf + nBufSize, pOut, lenOut);
nBufSize += lenOut;
break;
}
//lenIn -= (lenOut - (pOut - pIn));
lenIn -= (lenOut + (pOut - pIn));
pIn = pOut + lenOut;
}
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
这段代码的意思是:
如果运行到这里,说明上边找到了一个NAL且没有出错。那么将pOut指向pIn + nNalStart,即指向起始码之后紧跟着的第一个字节数据(nal头)。
对于pOut的判断,笔者认为是完全是为了增强代码健壮性了,因为走到这里pIn一定不是空,哪就那么寸,nNalStart正好是-pIn,两者相加为0。因此,这句话加与不加问题不大。
下边就该开始关键性的代码了。在分析代码前需要做知识补充。
##############################################################################################
nal头结构
1个字节:forbidden_bit(1bit) + nal_reference_bit(2bit) + nal_unit_type(5bit)
forbidden_bit:
禁止位,初始为0,当网络发现NAL单元有比特错误时可设置该比特为1,以便接收方纠错或丢掉该单元。
nal_reference_bit:
nal重要性指示,标志该NAL单元的重要性,值越大,越重要,解码器在解码处理不过来的时候,可以丢掉重要性为0的NALU。
nal_unit_type:
0:未规定
1:非IDR图像中不采用数据划分的片段
2:非IDR图像中A类数据划分片段
3:非IDR图像中B类数据划分片段
4:非IDR图像中C类数据划分片段
5:IDR图像的片段
6:补充增强信息 (SEI)
7:序列参数集 (SPS)
8:图像参数集 (PPS)
9:分割符
10:序列结束符
11:流结束符
12:填充数据
13 – 23:保留
24 – 31:未规定
##############################################################################################
补充了上述知识后,我们继续看代码:
unsigned int nNalType = (pOut[0] & 0x1F);
取pOut[0]的低5位,即nal_unit_type。对于7(SPS)、8(PPS)和6(SEI)进行单独处理,其余值统一处理。
当nNalType为7时,如果m_bReady为False,则新分配一段内存(m_pNaluData[AF_NALU_SPS]指向这段内存),并将pOut中的数据拷贝给m_pNaluData[AF_NALU_SPS](AF_NALU_SPS值为1),用于保存本次NAL数据,同时记录数据长度于m_pNaluLength[AF_NALU_SPS];如果m_bReady为True,则不做任何处理。
当nNalType为8时,如果m_bReady为false,则新分配一段内存(m_pNaluData[AF_NALU_PPS]指向这段内存),并将pOut中的数据拷贝给m_pNaluData[AF_NALU_PPS](AF_NALU_PPS值为2),用于保存本次NAL数据,同时记录数据长度于m_pNaluLength[AF_NALU_PPS];如果m_bReady为True,则不做任何处理。
当nNalType为6时,不做任何处理。
当nNalType为除上述3个值外的其他值时,先将lenOut的值拷贝到前4个字节(第1次是pBuf[0]~pBuf[3],以后每次是pBuf[nBufSize]~pBuf[nBufSize+3])。之后0和3交换,1和2交换,应该是调整大小端字节序,不过不知arm上是否也需要调整。
接下来nBufSize的值加4,因为已经拷贝了长度的4个字节。之后将pOut开始的数据拷贝到pBuf + nBufSize中,拷贝lenOut个字节。相应地,nBufSize的也要增加lenOut。
接下来的一句比较令人费解:lenIn -= (lenOut + (pOut - pIn));
lenOut的值是本次NAL的长度,即从NAL头开始到下一个起始码之间的长度(当然,也有一小部分是到nSize),(pOut-pIn)的值为nNalStart,因为上边有一句pOut = pIn + nNalStart;。这样lenOut和nNalStart两部分相加,得到的是起始码和起始码前边的一些字节的长度,再加上本次NAL长度,一共的字节数。lenIn开始被赋值为了nSize,每循环一次,找到一个NAL,就减去一段长度,所以lenIn实际上代表了剩余字节数。
pIn = pOut + lenOut;
这一句就相对好理解了,将指针指向了下一个NAL的起始码处。
/////////////////////////////////////////////////////////////////////////////////////////////////////////////
if (!m_bReady && m_pNaluData[AF_NALU_SPS] && m_pNaluData[AF_NALU_PPS])
{
int nPPSLen = m_pNaluLength[AF_NALU_PPS];
int nZeroCount = 0;
for (int ni = nPPSLen - 1; ni >= 0; ni--)
{
if (m_pNaluData[AF_NALU_PPS][ni])
break;
nZeroCount++;
}
m_pNaluLength[AF_NALU_PPS] -= nZeroCount;
WriteH264Nalu(m_pNaluData, m_pNaluLength);
m_bReady = true;
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////
这段代码的意思是:
如果m_bReady为false,并且m_pNaluData[AF_NALU_SPS]不为空(已经获得),且m_pNaluData[AF_NALU_PPS]也不为空(也已获得),则要从m_pNaluData[AF_NALU_PPS]的最后一个字节开始,依次检查,如果为0,则将nZeroCount加1,直到有不为0的数据为止。这实际上是要计算出无用的0的个数,进而计算出实际有效的m_pNaluLength[AF_NALU_PPS] 。比如,原来的m_pNaluLength[AF_NALU_PPS] 有10个字节,经过检查发现最后2个为0(即使中间有0也不计入其中),则调整后的m_pNaluLength[AF_NALU_PPS] 的长度为10-2=8个字节。
调用WriteH264Nalu函数将m_pNaluData写入,即写入SPS和和PPS。
将m_bReady置为true。
/////////////////////////////////////////////////////////////////////////////////////////////////////////////
if (m_bReady && (nBufSize > 0))
WriteFrame(pBuf, nBufSize, bKey, nTimeStamp);
if (pBuf)
{
free(pBuf);
pBuf = NULL;
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////
如果m_bReady为true,即已经写入了SPS和PPS,并且nBufSize大于0,则调用WriteFrame函数。
写入后将分配的pBuf释放。
至此,WriteH264函数就结束了,对其的解析也就完成了。严格来说,这个函数在健壮性方面存在一定不足,应该加入分配内存是否成功的判断以及分配失败时的处理。