如何读取H264文件获得每一帧的数据(VsParserPro)

网上有很多读取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);

其中,OpenFileGetNextFrame是两个比较重要的函数,下面就分别对这两个函数的实现进行解释说明。

首先附上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

你可能感兴趣的:(音视频技术)