首页 > 学院 > 开发设计 > 正文

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

2019-11-08 18:50:24
字体:
来源:转载
供稿:网友

上一篇讲了怎么把视音频采集下来并合成一个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库拷贝到自己的工程中,再到源码中把下面这三个头文件拷贝到你的工程中

注意,编出来的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转为YUV420RGB2YUV(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打开看看是否正常。

工程界面如下:

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


发表评论 共有条评论
用户名: 密码:
验证码: 匿名发表