上一篇讲了怎么把视音频采集下来并合成一个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。
本篇使用的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转为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; //用完要释放
当录制结束的时候要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;
}
工程界面如下:
详细工程代码,请到这里下载:完整工程代码下载
本篇中在回调中处理编码可能有一些问题,下一篇会解决这些问题,请参考下一篇《