5.使用DirectShow进行摄像头采集并进行H264实时编码

上一篇讲了怎么把视音频采集下来并合成一个AVI文件,但我们看这个AVI文件就发现,虽然很清晰,但就是大小太大了,录制短短10秒,可能就有100M以上,而且还有一个问题,就是录制只能是打开采集时开始,停止采集时停止,不能预览的时候随心所欲地录制。本篇就是要解决这些问题。

之前有一篇(使用DShow进行采集拍照)在讲实时拍照时曾用到过ISampleGrabber来抓取图像,然后设置缓存,从缓存中取数据然后生成图片,本篇也使用ISampleGrabber,但不使用缓存的方式,而是使用回调的方式抓取图像,在回调中先将RGB24的帧转换为YUV420,然后使用第三方的编码器X264对其进行编码。下面我们来做做看。大致的代码跟实时拍照那一篇差不多,不过设置回调的地方不一样,代码如下:

		//设置视频分辨率、格式
		IAMStreamConfig *pConfig = NULL;  
		m_pCapture->FindInterface(&PIN_CATEGORY_CAPTURE, &MEDIATYPE_Video, 
							m_pVideoFilter, IID_IAMStreamConfig, (void **) &pConfig);

		AM_MEDIA_TYPE *pmt = NULL; 
		VIDEO_STREAM_CONFIG_CAPS scc;
		pConfig->GetStreamCaps(nResolutionIndex, &pmt, (BYTE*)&scc); //nResolutionIndex就是选择的分辨率序号

		pmt->majortype = MEDIATYPE_Video;	
		pmt->subtype = MEDIASUBTYPE_RGB24;  //抓取RGB24
		pmt->formattype = FORMAT_VideoInfo;

		pConfig->SetFormat(pmt);

		m_pGrabberFilter->QueryInterface(IID_ISampleGrabber, (void **)&m_pGrabber);
		HRESULT hr = m_pGrabber->SetMediaType(pmt);
		if(FAILED(hr))
		{
			AfxMessageBox(_T("Fail to set media type!"));
			return;
		}
		//是否缓存数据,缓存的话,可以给后面做其他处理,不缓存的话,图像处理就放在回调中
		m_pGrabber->SetBufferSamples(FALSE); 
		m_pGrabber->SetOneShot(FALSE);
		mCB.lWidth = nSetWidth;
		mCB.lHeight = nSetHeight;
		//设置回调,在回调中处理每一帧
		m_pGrabber->SetCallback(&mCB, 1);

		hr = m_pCapture->RenderStream(&PIN_CATEGORY_PREVIEW, &MEDIATYPE_Video, m_pVideoFilter, m_pGrabberFilter, NULL);
		if( FAILED(hr))
		{
			AfxMessageBox(_T("RenderStream failed"));
			return;
		}
代码中mCB是一个类的实例,这个类是继承至ISampleGrabberCB的,所以程序中要新建一个类,让其继承至ISampleGrabberCB。
其他代码都差不多,启动预览后,回调中的BufferCB函数就不断能收到数据,这些数据是每收到一次就是一帧的数据,所以编码的工作主要在这里进行。

本篇使用的H264编码器是大名鼎鼎的X264,编码效率高而小巧,源码下载地址:http://www.videolan.org/developers/x264.html。Windows环境下要下载mingw编译器来编一下,生成一个DLL和一个lib库拷贝到自己的工程中,再到源码中把下面这三个头文件拷贝到你的工程中

5.使用DirectShow进行摄像头采集并进行H264实时编码_第1张图片

注意,编出来的dll可能带版本后缀,请去掉,否则你的程序可能不认,比如我编出来的dll是libx264-148.dll,改成libx264.dll

你在程序中使用X264,下面这样调用即可(路径问题请自己添加好)

extern "C"
{
#include "x264.h"  
};
#pragma comment(lib,"libx264.lib")

下面说说怎么进行编码吧,当录制开始的时候,收到一帧后,要先转换为YUV420,我们知道,之前抓取图像的时候已经设置了抓取的为RGB24。具体转换按照一定的算法进行即可,网上这样的算法很多,我也下了一个,具体就不展示了。

//每一帧大小
ULONG nYUVLen = lWidth * lHeight + (lWidth * lHeight)/2;
BYTE * yuvByte = new BYTE[nYUVLen];
//先把RGB24转为YUV420
RGB2YUV(pBuffer, lWidth, lHeight, yuvByte, &nYUVLen);

转换后,使用X264进行编码,代码如下:

		int csp = X264_CSP_I420;
		int width = lWidth;
		int height = lHeight;
		int y_size = width * height;

		//刚开始打开要初始化一些参数
		if (m_bFirst)
		{
			m_bFirst = FALSE;

			CTime time = CTime::GetCurrentTime();
			CString szTime = time.Format("%Y%m%d_%H%M%S.h264");
			CString strSavePath = _T("");
			strSavePath.Format(_T("%s%s"), m_sSavePath, szTime);
			USES_CONVERSION;
			string strFullPath = W2A(strSavePath);
			m_fp_dst = fopen(strFullPath.c_str(), "wb");

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

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

			//如果有编码延迟,可以这样设置就能即时编码
			x264_param_default_preset(m_pParam, "fast", "zerolatency"); 

			m_pParam->i_width = width;
			m_pParam->i_height = height;
			m_pParam->i_csp = X264_CSP_I420;  

			//设置Profile,这里有5种级别(编码出来的码流规格),级别越高,清晰度越高,耗费资源越大
			x264_param_apply_profile(m_pParam, x264_profile_names[5]);

			//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_pPic_in == NULL || m_pPic_out == NULL || m_pHandle == NULL || m_pParam == NULL)
		{
			return 2;
		}

		int iNal = 0;

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

		//注意写的起始位置和大小,前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"));
			return 1;
		}

		//写入目标文件
		for (int j = 0; j < iNal; ++j)
		{
			fwrite(pNals[j].p_payload, 1, pNals[j].i_payload, m_fp_dst);
		}

		delete[] yuvByte; //用完要释放

第一次执行要执行一下m_bFirst中的初始化参数的代码,代码具体的解释见代码中的注释。

当录制结束的时候要flush一下编码器中剩余的帧,然后释放相关参数,代码如下:

	//结束编码
	if (m_bEndEncode)
	{
		m_bEndEncode = FALSE;

		int iNal = 0;

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

		//flush encoder 
		//把编码器中剩余的码流数据输出
		while (1)
		{
			int ret = x264_encoder_encode(m_pHandle, &pNals, &iNal, NULL, m_pPic_out);
			if (ret == 0)
			{
				break;
			}
			printf("Flush 1 frame.\n");
			for (int j = 0; j < iNal; ++j)
			{
				fwrite(pNals[j].p_payload, 1, pNals[j].i_payload, m_fp_dst);
			}
		}

		//释放内存
		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;

		//关闭文件
		fclose(m_fp_dst);
		m_fp_dst = NULL;
		
		m_nFrameIndex = 0;
	}

录制结束后会在设置的目录下产生一个H264为后缀的文件,可以用VLC打开看看是否正常。

工程界面如下:

5.使用DirectShow进行摄像头采集并进行H264实时编码_第2张图片


详细工程代码,请到这里下载:完整工程代码下载

本篇中在回调中处理编码可能有一些问题,下一篇会解决这些问题,请参考下一篇《 

6.使用DirecrShow采集摄像头视音频并实时进行H264和AAC编码后封装成MP4





你可能感兴趣的:(DShow)