上一篇讲了怎么把视音频采集下来并合成一个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打开看看是否正常。工程界面如下:
详细工程代码,请到这里下载:完整工程代码下载
新闻热点
疑难解答