7.使用directshow采集视音频并进行H264和ACC实时编码再实时用MP4V2封装成MP4

之前博客讲的一些DirectShow的相关应用,可能对很多人来说已经有些旧了,因为更新的MediaFoundation已经替代了DShow的位置。但MediaFoundation只支持win7后的系统,也就是说不支持XP,所以在实际商业应用中,就windows平台而言,dshow依然是实际应用中的主流,ffmpeg、opencv第三方库的采集也都支持dshow,所以现在依然是比较常用的技术。

本篇开始之前,有不少人私信我,问一些工程中存在的问题,本篇打算把这些问题尽可能的解决一下。所以本篇的主题就是对之前的代码进行改进,使得工程更具有实用性,以下是要改进的列表:

1.对编码后的数据进行实时封装;之前进行视音频采集、编码后,都生成了.h264的视频文件和.aac的音频文件,也就是说文件落地了,然后才封装成了MP4,本篇会进行实时封装,不再生成独立的视频和音频文件,自始至终只会有一个MP4文件,这更符合实际应用的场景。

2.对视音频进行同步;之前好多人说同步做的不好,其实针对dshow而言,视音频同步起来还是很简单的,后面会详细讲解。

3.对工程引用的库文件和头文件全部整理到工程目录中来,让大家一次就能编译过;因为之前的工程用到的库和头文件比较散乱,导致大家用起来不方便。

4.对部分代码、结构进行改进,增加一些有用的设置,使得项目尽可能的实用一点;

5.会尽可能地增加注释,以便大家看的更明白。

首先,说一下大致的逻辑:使用Dshow进行视音频采集,采集的过程中将音频和视频都放进同一个队列中去,然后开启一个线程,从队列中一个一个取出来进行实时编码,取到视频就用X264进行编码,取到音频就用faac编码,编码的数据不再落地,而是使用MP4V2写到MP4中,编码和封装的过程中会涉及到视音频同步的问题。下面一一讲解。

本篇的例子程序全部放到线程中做了,防止出现卡顿现象。当然,可能还会有一些问题,真正要实用化可能还需要再继续改进。

DSHOW视音频采集部分跟之前大致相同,但这里要说一下,每个视频采集设备的采集能力是不一样的,比如有的相机采集的源数据只支持YUY2,有的支持YUY2和RGB的,我手上还有相机是支持IJPG的,但大部分相机应该都是支持YUY2或RGB的,所以我在列出视频分辨率的时候,顺便把相机支持的源流类型也列出来了,代码如下:

void CMainDlg::GetVideoResolution()
{
	if (m_pCapture)
	{
		m_arrCamResolutionArr.RemoveAll();
		m_cbxResolutionCtrl.ResetContent();
		IAMStreamConfig *pConfig = NULL;  
		//&MEDIATYPE_Video,如果包括其他媒体类型,第二个参数设置为0
		HRESULT hr = m_pCapture->FindInterface(&PIN_CATEGORY_CAPTURE, &MEDIATYPE_Video, 
			m_pVideoFilter, IID_IAMStreamConfig, (void **)&pConfig);

		int iCount = 0, iSize = 0;
		hr = pConfig->GetNumberOfCapabilities(&iCount, &iSize);
		// Check the size to make sure we pass in the correct structure.
		if (iSize == sizeof(VIDEO_STREAM_CONFIG_CAPS))
		{
			// Use the video capabilities structure.
			for (int iFormat = 0; iFormat < iCount; iFormat++)
			{
				VIDEO_STREAM_CONFIG_CAPS scc;
				AM_MEDIA_TYPE *pmtConfig = NULL;
				hr = pConfig->GetStreamCaps(iFormat, &pmtConfig, (BYTE*)&scc);
				if (SUCCEEDED(hr))
				{
					//(pmtConfig->subtype == MEDIASUBTYPE_RGB24) &&
					if ((pmtConfig->majortype == MEDIATYPE_Video) &&
						(pmtConfig->formattype == FORMAT_VideoInfo) &&
						(pmtConfig->cbFormat >= sizeof (VIDEOINFOHEADER)) &&
						(pmtConfig->pbFormat != NULL))
					{
						VIDEOINFOHEADER *pVih = (VIDEOINFOHEADER*)pmtConfig->pbFormat;
						// pVih contains the detailed format information.
						LONG lWidth = pVih->bmiHeader.biWidth;
						LONG lHeight = pVih->bmiHeader.biHeight;
						BOOL bFind = FALSE;
						//是否已经存在这个分辨率,不存在就加入array
						for (int n=0; n < m_arrCamResolutionArr.GetSize(); n++)
						{
							CamResolutionInfo sInfo = m_arrCamResolutionArr.GetAt(n);
							if (sInfo.nWidth == lWidth && sInfo.nHeight == lHeight)
							{
								bFind = TRUE;
								break;
							}
						}
						if (!bFind)
						{
							CamResolutionInfo camInfo;
							camInfo.nResolutionIndex = iFormat;
							camInfo.nWidth = lWidth;
							camInfo.nHeight = lHeight;
							m_arrCamResolutionArr.Add(camInfo);

							CString strSubType = _T("");
							if (MEDIASUBTYPE_RGB24 == pmtConfig->subtype)
							{
								strSubType = _T("RGB24");
							}
							else if (MEDIASUBTYPE_RGB555 == pmtConfig->subtype)
							{
								strSubType = _T("RGB555");
							}
							else if (MEDIASUBTYPE_RGB32 == pmtConfig->subtype)
							{
								strSubType = _T("RGB32");
							}
							else if (MEDIASUBTYPE_RGB565 == pmtConfig->subtype)
							{
								strSubType = _T("RGB565");
							}
							else if (MEDIASUBTYPE_RGB8 == pmtConfig->subtype)
							{
								strSubType = _T("RGB8");
							}
							else if (MEDIASUBTYPE_IJPG == pmtConfig->subtype)
							{
								strSubType = _T("IJPG");
							}
							else if (MEDIASUBTYPE_YUY2 == pmtConfig->subtype)
							{
								strSubType = _T("YUY2");
							}
							else if (MEDIASUBTYPE_YUYV == pmtConfig->subtype)
							{
								strSubType = _T("YUYV");
							}
							else if (MEDIASUBTYPE_H264 == pmtConfig->subtype)
							{
								strSubType = _T("H264");
							}
							else if (MEDIASUBTYPE_MJPG == pmtConfig->subtype)
							{
								strSubType = _T("MJPG");
							}
							else if (MEDIASUBTYPE_Y41P == pmtConfig->subtype)
							{
								strSubType = _T("Y41P");
							}
							else
							{
								strSubType = _T("其他");
							}
							CString strFormat = _T("");
							strFormat.Format(_T("%d * %d , %s, 采样大小:%ld"), lWidth, lHeight, strSubType, pmtConfig->lSampleSize);
							m_cbxResolutionCtrl.AddString(strFormat);
						}
					}

					// Delete the media type when you are done.
					FreeMediaType(pmtConfig);
				}
			}
		}
		if (m_cbxResolutionCtrl.GetCount() > 0)
		{
			m_cbxResolutionCtrl.SetCurSel(0);
		}
	}
}
之所以列出源流的类型,是因为只有知道了源流类型,才能将其转成X264支持的YUV,如果源流本身就是YUV,那就不用转了,直接将其输送给X264进行编码。

初始化后,选择了指定的分辨率,源流的类型也确定了,就可以打开相机了。

打开相机后根据需要可以预览,也可以不要预览,如果不要预览,则在打开相机之前,先设置预览是否显示,代码如下:

m_pVW->put_AutoShow(OAFALSE);
m_pVW->put_Visible(OAFALSE);

如果打开相机后,想看回显,代码如下:

	//设置视频显示窗口
	m_hShowWnd = GetDlgItem(IDC_STC_SHOW)->m_hWnd ;		//picture控件的句柄
	HRESULT hr = m_pVW->put_Owner((OAHWND)m_hShowWnd);
	if (FAILED(hr)) 
		return;
	hr = m_pVW->put_WindowStyle(WS_CHILD | WS_CLIPCHILDREN);
	if (FAILED(hr)) 
		return;

	long nFrameWidth = 0, nFrameHeight = 0;
	m_pVW->get_Width(&nFrameWidth);
	m_pVW->get_Height(&nFrameHeight);

	//图像显示位置,下面的写法是为了让视频显示在当前picture控件的正中央
	if (m_pVW)
	{
		CRect rc;
		::GetClientRect(m_hShowWnd, &rc);
		double fWndWidth = rc.Width();
		double fWndHeight = rc.Height();
		double fWndScale = fWndWidth/fWndHeight;

		double fFrameWidth = nFrameWidth;
		double fFrameHeight = nFrameHeight;
		double fFrameScale = fFrameWidth/fFrameHeight;

		int nShowWidth = fWndWidth;
		int nShowHeight = fWndHeight;
		int xPos = 0, yPos = 0;
		if (fWndScale >= fFrameScale)  //控件窗口宽高比例比视频的宽高大
		{
			if (fWndHeight <= fFrameHeight)
			{
				nShowHeight = fWndHeight;
				nShowWidth = nFrameWidth*fWndHeight/nFrameHeight;
			}
			else
			{
				nShowHeight = fFrameHeight;
				nShowWidth = nFrameWidth;
			}
		}
		else
		{
			if (fWndWidth <= fFrameWidth)
			{
				nShowWidth = fWndWidth;
				nShowHeight = fWndWidth*nFrameHeight/nFrameWidth;
			}
			else
			{
				nShowHeight = fFrameHeight;
				nShowWidth = nFrameWidth;
			}
		}

		xPos = (fWndWidth - nShowWidth)/2;
		yPos = (fWndHeight - nShowHeight)/2;

		m_pVW->SetWindowPosition(xPos, yPos, nShowWidth, nShowHeight);

		m_pVW->put_Visible(OATRUE);

打开相机后,其实视音频的两个回调就已经在运行了,采集的话,可随时截取数据,跟之前不一样的是,我没有在回调中启动处理线程,而是先把数据暂存在一个全局的队列中,视音频存在同一队列,注意,跟之前不一样的地方是还存了一个数据,叫采样时间,每一帧的视音频的采样时间都有,这个时间用来做视音频同步再合适不过。

视频的回调代码如下:

HRESULT STDMETHODCALLTYPE CSampleGrabberCB::BufferCB(double SampleTime, BYTE *pBuffer, long BufferLen)
{
	//开始采集
	if (m_bBeginEncode)
	{		
		BYTE* pByte = new BYTE[BufferLen];
		memcpy(pByte, pBuffer, BufferLen);

		GrabDataInfo sData;
		sData.pData = pByte;
		sData.nDataSize = BufferLen;
		sData.dSampleTime = SampleTime; //curTime.time + ((double)(curTime.millitm) / 1000.0);
		sData.nType = 0;

		theApp.m_mxGlobalMutex.Lock();
		theApp.m_arrGrabData.Add(sData);
		theApp.m_mxGlobalMutex.Unlock();

		CString str;
		str.Format(_T("\n Video--BufferLen:%ld, SampleTime:%f "), BufferLen, sData.dSampleTime);
		OutputDebugString(str);
	}

	return 0;
}
音频类似,这里不累述。
在开始采集后开启了一个处理线程,专门处理视音频编码和同步问题。大致的逻辑是:从全局的队列中去一帧,判断是音频还是视频,因为音频编码有延迟,所以必须等到音频正式开始编了以后,才进行视频的编码,这样是为了保证视音频封装的质量。由于视音频每一帧都有采样时间,使用采样时间进行前后相减,就可以知道每一帧播放时间,使用mp4v2进行封装就比较方便,下面是具体代码,日志非常详细,相信很容易看明白。

//处理线程
void CMainDlg::AVMuxEncodeDeal()
{
	//------------------------------视频编码初始化------------------------------------
	//--------------------------------------------------------------------------------
	int csp = X264_CSP_I420;
	int width = m_nFrameWidth;    
	int height = m_nFrameHeight;
	int y_size = width * height;
	ULONG nYUVLen = width * height + (width * height)/2;   //每帧YUV420数据的大小,这么写是为了后面使用方便

	m_pParam = (x264_param_t*)malloc(sizeof(x264_param_t));

	//初始化,是对不正确的参数进行修改,并对各结构体参数和cabac编码,预测等需要的参数进行初始化
	x264_param_default(m_pParam);

	//如果有编码延迟,即时编码设置为zerolatency
	x264_param_default_preset(m_pParam, x264_preset_names[2], "zerolatency"); 

	m_pParam->i_width = width;        //帧宽
	m_pParam->i_height = height;      //帧高
	m_pParam->i_csp = X264_CSP_I420;  //表示用来编码的源视频帧为YUV420的

	//针对本程序,b_annexb必须为1,编出来的NALU前4个字节为分隔用的前缀码,后面mp4v2在判断NALU类型的时候要用到,设置为0则表示NALU的长度,会导致无法判断是什么类型的帧
	m_pParam->b_annexb = 1;         // 值为true,则NALU之前是4字节前缀码0x00000001;
	// 值为false,则NALU之前的4个字节为NALU长度
	m_pParam->b_repeat_headers = 1; //每个关键帧前都发送sps和pps  
	m_pParam->b_cabac = 1;			//自适应上下文算术编码,baseline不支持  
	m_pParam->i_threads = 1;		//处理线程个数
	m_pParam->i_keyint_max = 50;    //设置IDR关键帧的间隔
	m_pParam->i_fps_den=1;			//帧率
	m_pParam->i_fps_num=25;			//帧率 
	m_pParam->rc.b_mb_tree=0;		//不为0导致编码延时帧,所以最好为0  
	m_pParam->rc.i_rc_method = X264_RC_CRF; //码率控制参数,CQP:恒定质量,CRF:恒定码率,ABR:平均码率
	m_pParam->rc.f_rf_constant = 25.0;		//CRF下调整此参数会影响编码速度和图像质量,比如为20,数据速率为2500kbps左右,设置30数据速率1520kbps左右
	m_pParam->rc.f_rf_constant_max = 45.0;  
	m_pParam->i_level_idc = 30;				//表示编码复杂度,具体多复杂,还需再研究

	//设置Profile,这里有7种级别(编码出来的码流规格),级别越高,清晰度越高,耗费资源越大
	//如果用baseline则没有B帧,但baseline通用性很好,所以请斟酌选择
	x264_param_apply_profile(m_pParam, x264_profile_names[1]);

	//x264_picture_t存储压缩编码前的像素数据
	m_pPic_in = (x264_picture_t*)malloc(sizeof(x264_picture_t));
	m_pPic_out = (x264_picture_t*)malloc(sizeof(x264_picture_t));

	x264_picture_init(m_pPic_out);

	//为图像结构体x264_picture_t分配内存
	x264_picture_alloc(m_pPic_in, csp, m_pParam->i_width, m_pParam->i_height);

	//打开编码器
	m_pHandle = x264_encoder_open(m_pParam);
	if (m_pHandle == NULL)
	{
		MessageBox(_T("视频编码器打开失败!"));
		return;
	}

	m_nFrameIndex = 0;
	//------------------------------------------------------------------------------------------------

	//-------------------------------------------音频编码初始化开始------------------------------------
	//-----------------------------------------------------------------------------------------------
	//这里的m_nSamplesPerSec和m_nChannels都是通过dshow根据实际的音频设备获取到的,不要乱写
	//m_nInputSamples是m_nChannels个通道的采样数,
	m_hFaacEncHandle = faacEncOpen(m_nSamplesPerSec, m_nChannels, &m_nInputSamples, &m_nMaxOutputBytes);
	if(m_hFaacEncHandle == NULL)
	{
		printf("[ERROR] Failed to call faacEncOpen()\n");
		MessageBox(_T("音频编码器打开失败!"));
		return;
	}

	//获取配置
	m_faacConfigurePtr = faacEncGetCurrentConfiguration(m_hFaacEncHandle);
	m_faacConfigurePtr->inputFormat = FAAC_INPUT_16BIT;
	// 0 = Raw,1 = ADTS
	m_faacConfigurePtr->outputFormat = 0;   //裸流还是前面加ADTS
	m_faacConfigurePtr->aacObjectType = LOW;
	m_faacConfigurePtr->allowMidside = 0;
	m_faacConfigurePtr->useLfe = 0;

	//设置配置
	faacEncSetConfiguration(m_hFaacEncHandle, m_faacConfigurePtr);

	unsigned char *chDecodeInfo = NULL;
	GetDecoderSpecificInfo(chDecodeInfo);

	//目标mp4的全路径
	USES_CONVERSION;
	string sFullPathName = T2CA(m_strEncodedFullName);

	//注意参数的传递
	CMp4Encoder *pMp4Encoder = new CMp4Encoder();
	pMp4Encoder->m_sFilePathName = sFullPathName;
	pMp4Encoder->m_vWidth = m_nFrameWidth;      //帧宽
	pMp4Encoder->m_vHeight = m_nFrameHeight;	//帧高
	bool bSuc = pMp4Encoder->InitMp4Encoder(chDecodeInfo, m_nInputSamples/m_nChannels, m_nSamplesPerSec);
	if (!bSuc)
	{
		MessageBox(_T("编码器初始化失败!"));
		return;
	}

	CString strLog = _T("");
	strLog.Format(_T("\n -----Audio m_nInputSamples:%ld,m_nChannels:%d,m_nSamplesPerSec:%ld,m_nMaxOutputBytes:%ld \n"), m_nInputSamples, m_nChannels, m_nSamplesPerSec, m_nMaxOutputBytes);
	OutputDebugString(strLog);

	double videoSampletime = 0.0;
	double dTempTakeTime = 0.0;
	m_bFirst = TRUE;

	while (1)
	{
		DWORD dwRet = WaitForSingleObject(m_hManageExitEvent, 5);  
		if(dwRet == WAIT_OBJECT_0)
		{
			if (theApp.m_arrGrabData.GetSize() <= 0)
			{
				break;
			}
		}
		//因为theApp.m_arrGrabData一直在接收数据,这里要加锁
		theApp.m_mxGlobalMutex.Lock();
		int nCount = theApp.m_arrGrabData.GetSize();
		if(nCount<=0)
		{
			theApp.m_mxGlobalMutex.Unlock();
			continue;
		}

		//每次都取第一个
		GrabDataInfo sDataInfo = theApp.m_arrGrabData.GetAt(0);
		theApp.m_arrGrabData.RemoveAt(0);
		theApp.m_mxGlobalMutex.Unlock();

		if (sDataInfo.nType == 0) //视频编码
		{
			if (!m_bCanEncode)  
			{
				//如果音频还没开始编,则此音频前的所有视频帧都抛弃
				delete[] sDataInfo.pData;
				continue;
			}

			//视频的采样时间,用来做同步
			m_dLastVSampleTime = sDataInfo.dSampleTime;

			strLog.Format(_T("\n -----#####Video sampletime:%f \n"), sDataInfo.dSampleTime);
			OutputDebugString(strLog);

			if (m_bFirst)
			{
				m_bFirst = FALSE;
				//这样做就是为了给videoSampletime赋个初值,只能在sDataInfo获取到后做
				videoSampletime = sDataInfo.dSampleTime;
			}

			//每一帧大小
			BYTE * yuvByte = new BYTE[nYUVLen];
			//每一帧大小中Y、U、V的数据大小
			BYTE * yByte = new BYTE[y_size];
			BYTE * uByte = new BYTE[y_size/4];
			BYTE * vByte = new BYTE[y_size/4];
			//先初始化一下
			memset(yuvByte, 0, nYUVLen);
			memset(yByte, 0, y_size);
			memset(uByte, 0, y_size/4);
			memset(vByte, 0, y_size/4);

			if (m_nSourceVideoType == 1) //来源是YUY2
			{
				//YUY2转YUV420
				YUV422To420(sDataInfo.pData, yByte, uByte, vByte, width, height);
			}
			else   //来源是RGB24
			{
				//把RGB24转为YUV420
				RGB2YUV(sDataInfo.pData, width, height, yuvByte, &nYUVLen);
			}

			//转换后,就用不到原始数据了,可以先释放
			delete[] sDataInfo.pData;

			int iNal = 0;

			//x264_nal_t存储压缩编码后的码流数据
			x264_nal_t* pNals = NULL;

			if (m_nSourceVideoType == 1) 
			{
				//注意,这里是YUV420(或叫I420),注意写的起始位置和大小,前y_size是Y的数据,然后y_size/4是U的数据,最后y_size/4是V的数据
				memcpy(m_pPic_in->img.plane[0], yByte, y_size);		//先写Y
				memcpy(m_pPic_in->img.plane[1], uByte, y_size/4);	//再写U
				memcpy(m_pPic_in->img.plane[2], vByte, y_size/4);	//再写V
			}
			else
			{
				//注意,这里是YUV420(或叫I420),注意写的起始位置和大小,前y_size是Y的数据,然后y_size/4是U的数据,最后y_size/4是V的数据
				memcpy(m_pPic_in->img.plane[0], yuvByte, y_size);						//先写Y
				memcpy(m_pPic_in->img.plane[1], yuvByte + y_size, y_size/4);			//再写U
				memcpy(m_pPic_in->img.plane[2], yuvByte + y_size + y_size/4, y_size/4); //再写V
			}

			m_pPic_in->i_pts = m_nFrameIndex++; //时钟

			//编码一帧图像,pNals为返回的码流数据,iNal是返回的pNals中的NAL单元的数目
			int ret = x264_encoder_encode(m_pHandle, &pNals, &iNal, m_pPic_in, m_pPic_out);
			if (ret < 0)
			{
				OutputDebugString(_T("\n x264_encoder_encode err"));
				delete[] yuvByte;
				delete[] yByte;
				delete[] uByte;
				delete[] vByte;

				continue;
			}

			//计算两帧之间的间隔时间,单位为毫秒
			dTempTakeTime = (sDataInfo.dSampleTime - videoSampletime)*1000;

			//把本帧的采样时间暂存
			videoSampletime = sDataInfo.dSampleTime;

			//写入,注意这里虽然循环多次写入,但只是写入一帧,千万不要在循环里加入时间片间隔,所以j == 0时将间隔时间置为0,否则会出现花屏现象
			for (int j = 0; j < iNal; ++j)
			{
				pMp4Encoder->Mp4VEncode(pNals[j].p_payload, pNals[j].i_payload, dTempTakeTime);

				if (j == 0)  //仅此帧第一次写入的片段需要加上与前一帧的时间间隔,本帧后面的片段是不需要间隔的
				{
					dTempTakeTime = 0.0;
				}
			}

			//用完要释放
			delete[] yuvByte; 
			delete[] yByte;
			delete[] uByte;
			delete[] vByte;
		}
		else if (sDataInfo.nType == 1)//音频
		{
			BOOL bFlag = FALSE;
			//采样时间比刚刚编过码的视频帧小,说明当前音频帧已经过时,需要往后面找合适的音频帧
			if(m_bCanEncode && sDataInfo.dSampleTime - m_dLastVSampleTime < 0.0) 
			{
				bFlag = TRUE;  //后面数据释放与否的标志

				//当前过时的帧可以直接释放
				delete [] sDataInfo.pData;

				//往后找可以用的音频帧
				int nIndex = -1;
				while (nIndex == -1)
				{
					nIndex = -1;
					theApp.m_mxGlobalMutex.Lock();
					int nCount = theApp.m_arrGrabData.GetSize();
					for (int k=0;k= 0.0)
						{
							nIndex = k;  //找到,退出while循环
							break;
						}
					}
					theApp.m_mxGlobalMutex.Unlock();

					if (m_bEndRecord) //
					{
						break;
					}

					Sleep(300);
				}

				//如果用户已经点击了结束采集,则确保theApp.m_arrGrabData里已经没有剩余帧
				if (m_bEndRecord && nIndex == -1)
				{
					continue;
				}
			}

			//设定每次能编码的数据大小
			int nPCMBufferSize = m_nInputSamples*m_wBitsPerSample / 8;

			//编码前后的buffer数据,后面编码时用
			BYTE* pbPCMBuffer = new BYTE[nPCMBufferSize];
			BYTE *pbAACBuffer = new BYTE [m_nMaxOutputBytes];

			ULONG ulTotalEncode = 0;
			int nTime = 0;

			while (1)
			{
				//pBuffer大小为BufferLen,远大于编码能力nPCMBufferSize,所以这里多分几次编
				//每次从pBuffer中取出nPCMBufferSize的大小,直到取完
				memcpy(pbPCMBuffer, sDataInfo.pData+ulTotalEncode, nPCMBufferSize);
				ulTotalEncode += nPCMBufferSize;
				nTime++;
				int nRet = faacEncEncode(m_hFaacEncHandle, (int*) pbPCMBuffer, m_nInputSamples, pbAACBuffer, m_nMaxOutputBytes);
				if (nRet <= 0) //faac一般需要3、4个样本缓存,所以相当于丢弃
				{
					break;
				}
				m_bCanEncode = TRUE;  //等音频能正式用了,视频再正式编码

				strLog.Format(_T("\n -----@@@@@Audio encode sampletime:%f \n"), sDataInfo.dSampleTime);
				OutputDebugString(strLog);

				//写入音频数据
				pMp4Encoder->Mp4AEncode(pbAACBuffer, nRet);

				//取到最后一次要注意,大小不是nPCMBufferSize了,而是BufferLen - ulTotalEncode
				if (sDataInfo.nDataSize < ulTotalEncode + nPCMBufferSize) 
				{
					int nEndDataSize = sDataInfo.nDataSize - ulTotalEncode;
					if (nEndDataSize > 0) //剩余的
					{
						delete[] pbPCMBuffer;
						pbPCMBuffer = new BYTE[nEndDataSize];
						memcpy(pbPCMBuffer, sDataInfo.pData+ulTotalEncode, nEndDataSize);

						//要修改一下输入采样
						int nInputSamples = nEndDataSize / (m_wBitsPerSample/8);
						//对剩余的数据编码
						nRet = faacEncEncode(m_hFaacEncHandle, (int*)pbPCMBuffer, nInputSamples, pbAACBuffer, m_nMaxOutputBytes);
						if (nRet <= 0)
						{
							break;
						}

						pMp4Encoder->Mp4AEncode(pbAACBuffer, nRet);
					}
					break;
				}
			}

			//释放
			delete [] pbPCMBuffer;
			delete [] pbAACBuffer;
			if(!bFlag)
				delete [] sDataInfo.pData;
		}
	}
	//关闭音频编码器
	faacEncClose(m_hFaacEncHandle);
	m_hFaacEncHandle = NULL;

	//释放内存
	x264_picture_clean(m_pPic_in);

	//关闭编码器
	x264_encoder_close(m_pHandle);
	m_pHandle = NULL;

	free(m_pPic_in);
	m_pPic_in = NULL;
	free(m_pPic_out);
	m_pPic_out = NULL;

	free(m_pParam);
	m_pParam = NULL;

	//释放
	pMp4Encoder->CloseMp4Encoder();
	delete pMp4Encoder;

}

CMp4Encoder类是封装了MP4V2处理过程的类,视频部分的处理会复杂一点,要根据H264的编码信息去处理每一帧,具体视频的处理如下:

//------------------------------h264码流分析------------------------------------------
//	h264 NALU:  0x00 00 00 01 | nalu_type(1字节)| nalu_data (N 字节) | 0x00 00 00 01 | ...
//				起始码(4字节)          类型             数据           下一个NALU起始码
//  nalu_type有以下种类,长度为1个字节,8位分别表示:【 1比特 禁止位,恒为0 | 2比特 重要性指示位  | 5比特 类型 】
//	0x67 (0 11 00111) SPS		非常重要			
//	0x68 (0 11 01000) PPS		非常重要			
//	0x65 (0 11 00101) IDR帧		非常重要,关键帧
//	0x61 (0 11 00001) I帧		重要	,非IDR的关键帧
//	0x41 (0 10 00001) P帧		重要				 
//	0x01 (0 00 00001) B帧		不重要				
//	0x06 (0 00 00110) SEI		不重要				
// 这里对每个NAL单元的前4或5个字节进行解析,从而查找是什么类型的NAL单元
// 第一帧 SPS【0 0 0 1 0x67】 PPS【0 0 0 1 0x68】 SEI【0 0 1 0x6】 IDR【0 0 1 0x65】
// p帧      P【0 0 0 1 0x41】
// I帧    SPS【0 0 0 1 0x67】 PPS【0 0 0 1 0x68】 IDR【0 0 1 0x65】
// 除非编码器和解码器进行特定的语法协商,解码器一般不对SEI包进行解析,所以这里不处理SEI数据
//
// MP4WriteSample接收I、P帧 nalu,该nalu需要用4字节的数据大小头替换原有的起始头,并且数据大小为big-endian格式(即将高字节存储在起始地址)
//-------------------------------------------------------------------------------------------------

void CMp4Encoder::Mp4VEncode(BYTE* _naluData,int _naluSize, double nTakeTime)
{
	int index = -1;
	//
	if(_naluData[0]==0 && _naluData[1]==0 && _naluData[2]==0 && _naluData[3]==1 && _naluData[4]==0x67)
	{
		index = 0x67;
	}
	if(index!=0x67 && m_vTrackId==MP4_INVALID_TRACK_ID)
	{
		return;
	}
	if(_naluData[0]==0 && _naluData[1]==0 && _naluData[2]==0 && _naluData[3]==1 && _naluData[4]==0x68)
	{
		index = 0x68;
	}
	if(_naluData[0]==0 && _naluData[1]==0 && _naluData[2]==1 && _naluData[3]==0x65)
	{
		index = 0x65;
	}
	if(_naluData[0]==0 && _naluData[1]==0 && _naluData[2]==0 && _naluData[3]==1 && _naluData[4]==0x41)
	{
		index = 0x41;
	}
	if (_naluData[0]==0 && _naluData[1]==0 && _naluData[2]==0 && _naluData[3]==1 && _naluData[4]==0x01)
	{
		index = 0x01;
	}
	//
	switch(index)
	{
	case 0x67:        //SPS 
		if(m_vTrackId == MP4_INVALID_TRACK_ID)
		{
			m_vTrackId = MP4AddH264VideoTrack  
				(m_mp4FHandle,   
				m_vTimeScale,   
				m_vTimeScale / m_vFrateR,  
				m_vWidth,     // width  
				m_vHeight,    // height  
				_naluData[5], // sps[1] AVCProfileIndication  
				_naluData[6], // sps[2] profile_compat  
				_naluData[7], // sps[3] AVCLevelIndication  
				3);           // 4 bytes length before each NAL unit  
			if (m_vTrackId == MP4_INVALID_TRACK_ID)  
			{  
				MessageBoxA(NULL,"add video track failed.\n","ERROR!",MB_OK);  
				return;  
			} 
			MP4SetVideoProfileLevel(m_mp4FHandle, 0x7F);
		}

		MP4AddH264SequenceParameterSet(m_mp4FHandle,m_vTrackId,_naluData+4,_naluSize-4);  
		break;
	case 0x68:  //PPS
		MP4AddH264PictureParameterSet(m_mp4FHandle,m_vTrackId,_naluData+4,_naluSize-4);  
		break;
	case 0x65:  //I
		{
			BYTE* data = new BYTE[_naluSize+1];
			data[0] = (_naluSize-3)>>24;  
			data[1] = (_naluSize-3)>>16;  
			data[2] = (_naluSize-3)>>8;  
			data[3] = (_naluSize-3)&0xff;  
			memcpy(data+4,_naluData+3,_naluSize-3);
 
			//注意这里第五个参数,是指两个视频帧之间的tick数目,nTakeTime是时间间隔(毫秒),90000/1000是指每秒的tick数
			if(!MP4WriteSample(m_mp4FHandle, m_vTrackId, data, _naluSize+1, nTakeTime*90000/1000, 0, 1))
			{  
				return;  
			} 

			delete [] data;
			break;
		}
	case 0x41: //P
		{
			_naluData[0] = (_naluSize-4) >>24;  
			_naluData[1] = (_naluSize-4) >>16;  
			_naluData[2] = (_naluSize-4) >>8;  
			_naluData[3] = (_naluSize-4) &0xff; 

			//注意这里第五个参数,是指两个视频帧之间的tick数目,nTakeTime是时间间隔(毫秒),90000/1000是指每秒的tick数
			if(!MP4WriteSample(m_mp4FHandle, m_vTrackId, _naluData, _naluSize, nTakeTime*90000/1000, 0, 1))
			{  
				return;  
			} 

			break;
		}
	case 0x01:
		{

			break;
		}
	}
}
还有一点要注意,采集前最好关闭自动曝光,除非是在一个非常稳定、亮度够的环境中,因为环境的影响,会影响采集的数据,相机的相关调整可以调用相关的接口,代码如下,下面是是否自动曝光设置:

	long Val, Flags;
	IAMCameraControl *m_pCtrl;  
	m_pTempVideoFilter->QueryInterface(IID_IAMCameraControl, (void **)&m_pCtrl); 
	m_pCtrl->Get(CameraControl_Exposure, &Val, &Flags); 
	if (nCheck == 1) //自动曝光
	{
		m_pCtrl->Set(CameraControl_Exposure, Val, CameraControl_Flags_Auto);
	}
	else
	{
		m_pCtrl->Set(CameraControl_Exposure, Val, CameraControl_Flags_Manual);
	}
其他如白平衡、饱和度、亮度都可以设置。

最后做成的样子如下:

7.使用directshow采集视音频并进行H264和ACC实时编码再实时用MP4V2封装成MP4_第1张图片

完整的工程代码见这里:完整工程代码下载

注:代码中OnBnClickedBtnOpencam方法中848行后少了一行代码 :pConfig->SetFormat(pmt); 如果没有这一行代码,分辨率等于没设置,则录制出来的分辨率会一直是默认分辨率。

如需转载本文,请注明出处,谢谢!




你可能感兴趣的:(DShow)