网上有很多读取H264的封装类,但是大多数都是提取NAL单元的,而我想要的是提取每一帧的数据。并且,很多解析H264的代码都是有Bug的,不是太完善。在这篇博文里我向大家分享一个比较完善的H264的封装类,该代码可以读取H264(注意是裸流文件),并能获得每一帧的数据,以及获取视频的分辨率。
下面是这个类的头文件定义:
//支持分析H264/MPEG4/MPEG2的裸流文件,获得里面每一帧的数据
class CFrameExtractFilter
{
public:
CFrameExtractFilter();
virtual ~CFrameExtractFilter(void);
public:
void SetLoopMode(BOOL bLoop); //设置是否循环读取文件
int OpenFile(LPCTSTR pszFileName, SSBSIP_MFC_CODEC_TYPE file_codec_type); //读取文件,并获取每一帧的信息
SSBSIP_MFC_CODEC_TYPE GetStreamType(); //获取流的格式,H264/MPEG4/MPEG2
int GetNextFrame(unsigned char *pFrameBuf, unsigned int *pSize); //获得下一个帧的数据,如果是第一次调用则获取第一帧的数据
void ResetFileRead(); //设置读指针到文件头,重新开始读
BOOL HasReadToEnd() { return m_bReadFileEnd; } //文件是否已经读完(如果设置了循环读取,则该函数一直返回FALSE)
int CloseFile(); //关闭文件
// Implementations
protected:
void SetFileName(LPCTSTR pszFileName); //设置文件路径
// member variables
private:
TCHAR m_szFileName[MAX_PATH];
void *m_pFramexCtx; //读取文件数据的对象指针,实际上为CtxType*类型
BOOL m_bLoopAfterEnd; //是否循环读取文件
SSBSIP_MFC_CODEC_TYPE m_StrmType;
int m_first; //如果为1,从第一帧开始读取
unsigned int m_frame_num; //读取的当前帧的编号
//unsigned int m_FrameRate;
BOOL m_bReadFileEnd; // 已经读到文件尾部
};
类里已经对每个接口有注释说明了,我就不一一详述了。讲一下这些接口的调用顺序:
1. 调用SetLoopMode设置是否循环读取文件;
2. 调用OpenFile打开一个文件,并获取每个帧的文件位移信息和大小(还没有分配内存填充数据);
3. 调用GetNextFrame获得每一帧的数据;
4. 读取完毕,调用CloseFile关闭文件。
下面代码是使用这些接口的一个例子:
CFrameExtractFilter fileParser;
fileParser.SetLoopMode(FALSE);
if(fileParser.OpenFile(_T("D:\\videos\\185.1080P.264"), H264_DEC) != 0)
{
printf(("OpenFile failed \n"));
return -1;
}
unsigned int frame_duration = 1000/25; //帧的间隔时间
unsigned char * pFrameBuffer = (unsigned char*)malloc(1000*1024);
unsigned int nFrameSize = 0;
while(1)
{
nFrameSize = 0;
if(fileParser.GetNextFrame(pFrameBuffer, &nFrameSize) <= 0)
{
break;
}
printf(("GetNextFrame got size: %d \n"), nFrameSize);
Sleep(frame_duration/2);
}
free(pFrameBuffer);
其中,OpenFile和GetNextFrame是两个比较重要的函数,下面就分别对这两个函数的实现进行解释说明。
首先附上OpenFile函数的代码:
//
// Read the File and get every frame's info
//
int CFrameExtractFilter::OpenFile(LPCTSTR pszFileName, SSBSIP_MFC_CODEC_TYPE file_codec_type)
{
SSBSIP_MFC_CODEC_TYPE codec_type = UNKNOWN_TYPE;
TCHAR file_ext[6] = {0};
TCHAR file_name[128] = {0};
int nRead;
unsigned int width, height;
width = height = 0;
char * divider = NULL;
char ext[8] = {0};
USES_CONVERSION;
divider = strrchr(T2A(pszFileName), '.');
if(divider)
strcpy(ext, divider+1);
bool bFlag = false;
if(file_codec_type != UNKNOWN_TYPE) //根据输入的编码类型来确定格式
{
bFlag = true;
codec_type = file_codec_type;
}
else
{
bFlag = GetCodecType(ext, (SSBSIP_MFC_CODEC_TYPE&)codec_type); //根据文件后缀名获取视频编码格式
}
if(!bFlag) //不是支持的格式
return -1;
SetFileName(pszFileName); //保存文件路径
m_StrmType = codec_type;
ATLTRACE(_T("OpenFile: %s \n"), pszFileName);
if(m_pFramexCtx != NULL)
{
SsbSipParserDeInit(m_pFramexCtx);
}
MPEG4_CONFIG_DATA conf_data;
memset(&conf_data, 0, sizeof(MPEG4_CONFIG_DATA));
conf_data.bGetWH = TRUE; //是否获取分辨率
//读取整个文件,获得视频分辨率和每一帧的信息
if((m_pFramexCtx = SsbSipParserInit((SSBSIP_MFC_CODEC_TYPE)m_StrmType, T2A(pszFileName) ,MAX_DECODER_INPUT_BUFFER_SIZE, &conf_data)) == NULL)
{
::OutputDebugString(L"SsbSipParserInit Failed\n");
return -2;
}
width = conf_data.width;
height = conf_data.height;
ATLTRACE("Width: %d, Height: %d \n", width, height);
// Validate the width & height value
if ((width > 4096) || (width < 16) || (height > 2048) || (height < 16)) {
ATLTRACE(_T("video size is not vaild. (width=%d, height=%d)\n"), width, height);
return -3;
}
m_bReadFileEnd = FALSE;
return 0;
}
OpenFile函数负责打开文件,读取并获得视频的相关信息。函数需要传入一个文件路径和一个编码类型参数,其中H264_DEC是指H264类型。如果不知道类型,可以将编码类型设置为UNKNOWN_TYPE,这表示格式未知,这种情况下,函数会根据文件的后缀名判断文件的格式,比如把后缀为“.h264”,".264"的文件认为是H264裸流文件,如果判断的格式不合法,则返回。如果文件是支持的格式,则开始读取文件,读取文件的函数是SsbSipParserInit,该函数遍历整个文件数据,从中解析出每一帧的信息,并能获得视频的分辨率。
void *SsbSipParserInit(SSBSIP_MFC_CODEC_TYPE codec_type, char *inputFileName, int size, MPEG4_CONFIG_DATA * pconf_data)
{
char* pFileExt;
void *hParser;
InParam = (ParserInParam*)malloc(sizeof(ParserInParam));
pFileExt = strrchr(inputFileName, '.');
pFileExt++;
if(true == CheckExtension(pFileExt))
{
if((hParser = InitRawFrameExtract(inputFileName, pconf_data)) == NULL)
{
OutputDebugString(L"InitRawFrameExtract fail\n");
return NULL;
}
OutputDebugString(L"SsbSipParserInit\n");
InParam->isRawStream = 1;
return hParser;
}
return NULL;
}
SsbSipParserInit函数的第一个参数是编码类型,第二个参数是文件路径,第三个参数是缓冲区大小,第四个参数是返回的视频信息(分辨率,帧类型),其中第四个参数的类型定义为:
typedef struct
{
BOOL bGetWH; //是否获取分辨率
int width, height; //分辨率
char cType; //帧类型('I' 'P' 'B')
} MPEG4_CONFIG_DATA;
MPEG4_CONFIG_DATA包含视频分辨率、帧类型信息。如果变量bGetWH为True表示需要获取分辨率,则调用SsbSipParserInit函数时会返回分辨率信息(宽、高赋值给width,height);cType记录的是帧的类型,这里的类型分为'I' 'P' 'B'。
SsbSipParserInit函数里面调用了InitRawFrameExtract函数来提取帧信息。下面是InitRawFrameExtract函数的实现:
void *InitRawFrameExtract(char* pFilename, MPEG4_CONFIG_DATA * pconf_data)
{
SSBSIP_MFC_CODEC_TYPE eMode;
char* pFileExt;
u32 uFrameInfo[2];
u32 uRemainSz, uRdPtr, uRet, uOneFrameAddr, uOneFrameSize, uFrameCnt, uFileOffset;
unsigned int parser_loop_cnt=0;
int stuff_byte;
CtxType *pCtx;
bool parseHeader = true; //是否获取视频的Sequence header信息,对H264来说,视频头一般指SEI+SPS+PPS
pCtx = (CtxType *)malloc(sizeof(CtxType));
pCtx->lastFrameNum = -1;
pFileExt = strrchr(pFilename, '.');
pFileExt++;
GetCodecType(pFileExt, eMode);
pCtx->fin = fopen(pFilename,"rb");
if ( pCtx->fin==NULL )
return false;
pCtx->info = (frameInfo*)malloc(INFO_BUFF_SIZE);
pCtx->frameNum = 0;
uRemainSz = fread(pCtx->aInputBuf,1,STREAM_BUF_SIZE,pCtx->fin);
uRdPtr = (u32)pCtx->aInputBuf;
uFrameCnt = 0;
uFileOffset = 0;
uRet = VsParser(eMode, uRdPtr, uRemainSz, &uOneFrameAddr, &uOneFrameSize, parseHeader, pconf_data); //第一次调用VsParser 第6个参数:parseHeader = true,表示获取视频头信息(H264:SEI+SPS+PPS)
parseHeader = false;
if (uRet == 1) //没有获取到一帧,已读到文件尾部
{
pCtx->info[pCtx->frameNum].offset = uFileOffset;
pCtx->info[pCtx->frameNum].size= uRemainSz;
pCtx->frameNum++;
fclose(pCtx->fin);
return pCtx;
}
else if (uRet!=0)
{
OutputDebugString(L"[HeaderParsing Err]\n");
fclose(pCtx->fin);
return NULL;
}
stuff_byte = uOneFrameAddr - uRdPtr;
uRemainSz -= uOneFrameSize+stuff_byte;
uRdPtr = uOneFrameAddr+uOneFrameSize;
pCtx->info[pCtx->frameNum].offset = uFileOffset;
pCtx->info[pCtx->frameNum].size= uOneFrameSize+stuff_byte;
pCtx->frameNum++;
uFileOffset += uOneFrameSize+stuff_byte;
ATLTRACE(L"\nStart Parsing.........\n");
while (1) {
//每次从文件读入一个块的数据到uRdPtr指向的缓冲区地址,然后调用VsParser提取出一帧数据出来,如果返回不足一帧,则继续读取下一个块。
//VsParser返回0表示已经获取到完整的一帧,并记录当前帧的地址和帧的大小,下次读该帧后面的数据;
//返回1或2表示数据不足或者缓冲区里找不到一帧的数据,这时候则需要读入文件的下一个块,如果已经读到文件尾部,则跳出循环。
uRet = VsParser(eMode, uRdPtr, uRemainSz, &uOneFrameAddr, &uOneFrameSize, FALSE, pconf_data);
if ( (uRet == 0) ) { /* Normal case. */
uRdPtr = uOneFrameAddr + uOneFrameSize;
uRemainSz -= uOneFrameSize;
pCtx->info[pCtx->frameNum].offset = uFileOffset;
pCtx->info[pCtx->frameNum].size= uOneFrameSize;
pCtx->frameNum++;
uFileOffset += uOneFrameSize;
ATLTRACE("Frame info--- size: %d, type: %c \n", uOneFrameSize, pconf_data->cType);
}
else if ((uRet == 1) || (uRet == 2)) { /* Last frame or insufficient stream. */
uOneFrameSize = uRemainSz;
fseek(pCtx->fin, uFileOffset, SEEK_SET);
uRemainSz = fread(pCtx->aInputBuf,1,STREAM_BUF_SIZE,pCtx->fin);
uRdPtr = (u32)pCtx->aInputBuf;
if(uRemainSz==uOneFrameSize) {
pCtx->info[pCtx->frameNum].offset = uFileOffset;
pCtx->info[pCtx->frameNum].size= uOneFrameSize;
pCtx->lastFrameNum = (uRet == 1) ? pCtx->frameNum : pCtx->frameNum - 1;
break;
}
continue;
}
else
{
ATLTRACE("[ERR] Parsing Error!!\n");
return NULL;
}
parser_loop_cnt++;
}
ATLTRACE(L">> VsParser loop count : %d <<\n", parser_loop_cnt);
fseek(pCtx->fin, 0l, SEEK_SET);
return (void *)pCtx;
}
该函数读取整个文件,而提取每个帧信息的工作由VsParser函数负责处理。这里有两个地方调用了VsParser函数,一个是在刚进入函数时,另一处是在While循环体里面。第一个VsParser函数是用来提取视频头信息的,以H264为例,H264的视频头为SEI,SPS和PPS,而第一次调用VsParser则只获取SEI+SPS+PPS信息(parseHeader变量设置为true),把它存储起来作为第一帧。而后面在循环体里调用的VsParser则每次返回一帧,这里的帧是指I,P,B帧。VsParser函数需要传入一个读数据的缓冲区的地址:uRdPtr;读取的大小:uRemainSz,这里的读取长度不是等于文件长度,而是一个块的大小,长度等于STREAM_BUF_SIZE。如下面代码所示,在调用VsParser之前,先调用fread读取一个块的数据(长度等于STREAM_BUF_SIZE),之后将uRdPtr指针指向这个块的缓冲区地址,然后再调用VsParser对缓冲区的数据进行解析。
pCtx->fin = fopen(pFilename,"rb");
if ( pCtx->fin==NULL )
return false;
pCtx->info = (frameInfo*)malloc(INFO_BUFF_SIZE);
pCtx->frameNum = 0;
uRemainSz = fread(pCtx->aInputBuf,1,STREAM_BUF_SIZE,pCtx->fin);
uRdPtr = (u32)pCtx->aInputBuf;
uFrameCnt = 0;
uFileOffset = 0;
uRet = VsParser(eMode, uRdPtr, uRemainSz, &uOneFrameAddr, &uOneFrameSize, parseHeader, pconf_data); //第一次调用VsParser 第6个参数:parseHeader = true,表示获取视频头信息(H264:SEI+SPS+PPS)
parseHeader = false;
VsParser函数每次调用会返回当前帧的地址信息,帧大小,以及帧类型(pconf_data),各个返回参数的意义:uOneFrameAddr--帧的内存地址,uOneFrameSize--帧的大小,pconf_data--帧类型和分辨率信息。如果VsParser返回0,则表示成功返回一帧,则将帧的信息记录到pCtx->info[pCtx->frameNum]数组元素里面,如下所示:
uRdPtr = uOneFrameAddr + uOneFrameSize;
uRemainSz -= uOneFrameSize;
pCtx->info[pCtx->frameNum].offset = uFileOffset;
pCtx->info[pCtx->frameNum].size= uOneFrameSize;
pCtx->frameNum++;
uFileOffset += uOneFrameSize;
注意:这里的pCtx->info[pCtx->frameNum].offset并不等于uOneFrameAddr,原因是offset记录的是帧在文件中的指针位移,而uOneFrameAddr是帧在pCtx->aInputBuf缓冲区的地址。前者是文件地址,后者是内存地址,是不同的。读者可能会问:为什么不直接将返回的帧数据存储到内存中,而只记录文件位移?因为如果都记录每个帧的数据到内存,那么就要分配很大的内存空间用来存储这些帧,而如果只记录帧的文件位移信息,则在需要帧数据的时候才从文件中读取那一帧到内存中,这样只需要创建一块内存,每次都把帧数据放到这块内存,从而更节约内存。
VsParser函数里面实际上调用了各种视频格式的解析处理函数,比如,对H264是调用h264_parse子函数来处理。
// 返回值意义:
// 0: one frame size is successfully determined AND there are still more frames
// 1,2: the size of the last frame is successfully determined. 1,2表示不够一帧数据,需要读取更多,或者是到了文件尾部
// 3: error (invalid standard)
u32 VsParser(SSBSIP_MFC_CODEC_TYPE standard, u32 buffer_start_addr, u32 buffer_size, /*u8* src_mem,*/
u32 *frame_start_addr, u32 *frame_size, u8 is_first_seq_header, MPEG4_CONFIG_DATA * conf_data)
{
u32 ret_value;
u8 *src_mem;
src_mem = (u8 *)buffer_start_addr;
char cType = 0;
switch(standard)
{
case MPEG4_DEC :
case DIVX311_DEC :
case DIVX412_DEC :
case DIVX502_DEC :
case DIVX503_DEC :
case XVID_DEC:
case H263_DEC :
ret_value = mpeg4_parse( buffer_start_addr, buffer_size, src_mem,
frame_start_addr, frame_size, is_first_seq_header, conf_data);
if(ret_value==1||ret_value==2) *frame_size=buffer_size;
break ;
case MPEG2_DEC :
ret_value = mpeg2_parse(buffer_start_addr, buffer_size, src_mem,
frame_start_addr, frame_size, is_first_seq_header, conf_data);
if(ret_value==1||ret_value==2) *frame_size=buffer_size;
break ;
case H264_DEC :
ret_value = h264_parse(buffer_start_addr, buffer_size, src_mem,
frame_start_addr, frame_size, is_first_seq_header, conf_data);
if(ret_value==1||ret_value==2) *frame_size=buffer_size;
break ;
default :
ret_value = 3 ;
break ;
}
return ret_value ;
}
h264_parse函数的实现代码:
//读Buffer,直到找到完整的一帧数据才返回
//返回值意义:
// 0-- 已经找到完整的一帧
// 1-- 找到一个帧的开头标志,但是没有确定帧的大小(说明输入的Buffer里不够一帧数据)
// 2-- 没有找到帧的开头标志(可能输入的Buffer数据太少,或者数据非法)
//
u32 h264_parse(u32 buffer_start_addr, u32 buffer_size, u8 *src_mem, u32 *frame_start_addr, u32 *frame_size, u8 is_first_seq_header, MPEG4_CONFIG_DATA *conf_data)
{
u32 i ;
u32 num_start_code ;
u32 hd_start_code ;
int rbsp_base;
u8 tmp_0, tmp_1 ;
u8 *tmp_mem ;
i = 0 ;
num_start_code = 0 ;//帧头的计数(帧头的起始码为:0x000001或0x00000001, 注意一帧并不等同于一个NAL,这里的帧是指I/B/P或SEI/SPS/PPS帧,而NAL是帧的组成部分),
hd_start_code = 0 ; //SEI/SPS/PPS帧的计数器
//找到一帧的条件是num_start_code = 2,即找到两个帧头,第一个帧头到第二个帧头前的数据为一帧。当第一个帧头后跟I/B/P时,num_start_code=1;当第二个帧头后跟I/B/P或SEI/SPS/PPS帧,则num_start_code = 2
//如果第一个帧头后跟SEI/SPS/PPS,num_start_code并不增加,但hd_start_code会增加1,并且记录当前的地址为起始地址;直到遇到下一个帧头为I/B/P帧时,num_start_code才加1
//u8* src_mem = (u8*)de_emulation_prevention(srcmem);
u32 nal_count = 0;
bool bHaveGotWidthHeight = false; //判断有无获取到分辨率,如果获取了则不再获取,这样可以保证只需调用解析分辨率的函数一次
if(conf_data && conf_data->width > 0 && conf_data->height > 0) // 如果进来函数时已经获得了分辨率则不再去获取
bHaveGotWidthHeight = true;
while(1)
{
tmp_mem = src_mem + i;
if (((*(tmp_mem)==0x00) && (*(tmp_mem+1)==0x00) && (*(tmp_mem+2)==0x00) && (*(tmp_mem+3)==0x01)) || //NAL前的起始码00000001||000001
((*(tmp_mem)==0x00) && (*(tmp_mem+1)==0x00) && (*(tmp_mem+2)==0x01)))
{
rbsp_base = buffer_start_addr + i;
if((*(tmp_mem)==0x00) && (*(tmp_mem+1)==0x00) && (*(tmp_mem+2)==0x00) && (*(tmp_mem+3)==0x01))
{
i++;
tmp_mem = src_mem + i;
}
nal_count++;
tmp_0 = *(tmp_mem+3) & 0x1f ;
if (tmp_0==0x06 || tmp_0==0x07 || tmp_0==0x08) //=== SEI,SPS, PPS, ,i_nal_type的值等于0x7表示这个nalu是个sps数据包
{
hd_start_code++ ;
if (num_start_code==0 && hd_start_code==1)
*frame_start_addr = rbsp_base;
if (num_start_code==1) num_start_code++ ;
}
// SPS case
// (Extracting 'log2_max_frame_num_minus4' value for SPS NAL.)
if(tmp_0==0x07) //SPS
{
if (num_start_code ==0 && hd_start_code==1) //找到一个SPS帧头,但是之前没有找到I/B/P帧,说明还要继续读,函数不能马上返回
{
if(conf_data && conf_data->bGetWH && !bHaveGotWidthHeight) //获取分辨率
{
u8 * ptmp;
#if 0
ptmp = tmp_mem + 4;
SPS sps = {0};
h264dec_seq_parameter_set((void*)ptmp, &sps);
conf_data->width = (sps.pic_width_in_mbs_minus1 + 1) << 4;
conf_data->height = (sps.pic_height_in_map_units_minus1 +1) << 4;
ATLTRACE("h264dec_seq_parameter_set, Width: %d, Height: %d \n", conf_data->width, conf_data->height);
#else
ptmp = tmp_mem + 3;
SpsDecodeParser spsParser;
if(spsParser.h264_decode_sps( ptmp, buffer_size - i - 3, conf_data->width, conf_data->height)) //从SPS中拿到视频分辨率
{
bHaveGotWidthHeight = true; //获取分辨率成功
}
ATLTRACE("h264_decode_sps, Width = %d, Height = %d \n", conf_data->width, conf_data->height);
#endif
}
}
}
#ifdef _SLICE_BASED_INTERFACE_
if (is_first_seq_header && tmp_0==0x06) //=== SEI
{
*frame_size = rbsp_base - *frame_start_addr ;
num_start_code = 2 ;
break ;
}
#endif
if (tmp_0==0x01 || tmp_0==0x05) //=== non-IDR picture, IDR picture,0x01为non-IDR片,0x05为I片
{
unsigned char umpt,*temp;
//int frame_type;
int offset = 0;
temp = tmp_mem+4;
umpt = ue_v(temp,&offset);
if (is_first_seq_header) //遇到nal_unit_type为1,5的分片时,并且is_first_seq_header=true,表示只获取视频头信息(H264:SEI+SPS+PPS),马上返回
{ //说明:is_first_seq_header一般在第一次调用VsParser函数时才传入true,目的是提取文件的视频头信息,但是这里有个问题:
//如果文件的前几帧或第一帧不是I帧,比如是P帧,而P帧前面是没有SPS/PPS的,所以返回的可能就是SEI信息里。
*frame_size = rbsp_base - *frame_start_addr ;
num_start_code = 2 ;
break ;
}
#ifdef _SLICE_BASED_INTERFACE_
num_start_code++;
if (num_start_code==1 && hd_start_code==0) *frame_start_addr = rbsp_base;
#else
tmp_1 = *(tmp_mem+4) & 0x80 ; //== first_mb_in_slice,这个标志用来确定帧与帧的边界。判断该字节第一个bit是否为1,如果是1,就是一帧的第一片。
if (tmp_1==0x80) //帧的第一个分片
{
//这里可能是进入函数后遇到的第一个分片,也可能是下一个帧的第一个分片,是哪种情况由num_start_code 和 hd_start_code的值确定
num_start_code++ ;
if (num_start_code==1 && hd_start_code==0) *frame_start_addr = rbsp_base;
}
if (num_start_code == 1) //找到第一个I/B/P帧的开始头(函数还不能返回)
{
umpt = ue_v(temp,&offset); //slice_type
FrameType f_type = NONE;
u8 slice_type = umpt;
switch(slice_type)
{
case 2:case 7:
case 4:case 9:
f_type = I_Frame;
//ATLTRACE("I Frame\n");
if(conf_data) conf_data->cType = 'I';
break;
case 0:case 5:
case 3:case 8:
f_type = P_Frame;
//ATLTRACE("P Frame \n");
if(conf_data) conf_data->cType = 'P';
break;
case 1:case 6:
f_type = B_Frame;
//ATLTRACE("B Frame \n");
if(conf_data) conf_data->cType = 'B';
break;
default:
f_type = NONE;
//ATLTRACE("NON I P B Frame\n");
if(conf_data) conf_data->cType = 0;
break;
}
//if(f_type != NONE) // 检查到帧类型
//{
// if(conf_data && conf_data->bGetWH == 0) //如果不是获取分辨率则跳出
// break;
//}
}
#endif
}
if (num_start_code==2) //找到两个I/B/P帧的帧头,或者第一个是I/B/P帧的帧头,第2个是SEI/SPS/PPS帧的帧头,函数可以返回了
{
*frame_size = rbsp_base - *frame_start_addr;
break ;
}
i+=3;
}
else
{
i++ ;
}
if (i>=buffer_size-4) break ;
}
//ATLTRACE("nal_count: %d, num_start_code: %d, hd_start_code: %d \n", nal_count - 1, num_start_code, hd_start_code); //打印变量信息
if (num_start_code==2) return(0) ;
if (num_start_code==1) return(1) ;
else return(2) ;
}
h264_parse函数每次找到一个帧才返回,而找到一帧的条件是找到两个帧头(起始码为:0x000001或0x00000001),上一帧和下一帧的帧头找到了,那么之间的数据就是返回的帧的数据。事实上,第2个帧头是下一个帧的第一个分片,可以认为这个是上一帧的结束标志。获得的帧至少包含I,P,B帧(当传入参数is_first_seq_header为true时除外),那么对SPS/PPS帧怎么处理呢?如果进入函数后遇到的第一个分片是SPS/PPS帧,那么会将当前帧头的地址作为帧的开始地址,然后去找第二个帧头,但找到后函数不会马上返回,因为它需要获得一个I帧,或P,B帧才返回,所以它会继续往下搜索数据,直到找到第2个I/P/B分片的帧头或一个I/P/B分片的帧头+SEI/SPS/PPS的帧头才结束,所以VsParser返回的帧数据有可能包含了SPS+PPS(一般在I帧前面),即返回的一个I帧数据可能为:SPS+SPS+I Frame Data。
VsParser只是获得每一帧的文件位移信息,而将数据读到内存由另外一个函数负责处理,这个函数就是CFrameExtractFilter类的成员函数GetNextFrame:
//函数:GetNextFrame
//说明: 读取下一个帧的数据,如果是第一次调用,则读取第一帧。
// m_bLoopAfterEnd变量设置是否循环读取文件,会影响GetNextFrame函数的行为。如果m_bLoopAfterEnd为true,则调用GetNextFrame永远都会返回成功;否则,则读到文件尾部最后一帧就结束。
//返回值:大于0返回实际读取的帧长度;==0表示已经读到最后一帧,调用者应该停止继续调用该函数,准备关闭文件;-1表示读取错误。
int CFrameExtractFilter::GetNextFrame(unsigned char *pFrameBuf, unsigned int *pSize)
{
int isLastFrame;
if(m_bReadFileEnd)
return 0;
if(m_pFramexCtx == NULL)
{
USES_CONVERSION;
if((m_pFramexCtx = SsbSipParserInit((SSBSIP_MFC_CODEC_TYPE)m_StrmType, T2A(m_szFileName) ,MAX_DECODER_INPUT_BUFFER_SIZE, NULL)) == NULL) //第一次读取文件,获得每一帧的文件位移和大小(但不存储数据)
{
::OutputDebugString(L"SsbSipParserInit in GetNextFrame Failed\n");
return -1;
}
}
if (m_first) //读第一帧
{
int nFrameLeng = SsbSipParserFrameGet(m_pFramexCtx, m_frame_num++, pFrameBuf, &isLastFrame);
*pSize = nFrameLeng;
m_first = 0;
return nFrameLeng;
}
int nFrameLeng = SsbSipParserFrameGet(m_pFramexCtx, m_frame_num++, pFrameBuf, &isLastFrame);
*pSize = nFrameLeng;
if(isLastFrame) //最后一帧,表示已读到文件尾部
{
if(m_bLoopAfterEnd)
{
m_first = 1; //重新读取
m_frame_num = 0;
}
else
{
m_bReadFileEnd = TRUE;
}
}
return nFrameLeng;
}
具体的实现大家看代码了,代码下载地址:https://download.csdn.net/download/toshiba689/10947893