之前博客讲的一些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;
}
音频类似,这里不累述。
//处理线程
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;
}
//------------------------------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);
}
其他如白平衡、饱和度、亮度都可以设置。
最后做成的样子如下:
完整的工程代码见这里:完整工程代码下载
注:代码中OnBnClickedBtnOpencam方法中848行后少了一行代码 :pConfig->SetFormat(pmt); 如果没有这一行代码,分辨率等于没设置,则录制出来的分辨率会一直是默认分辨率。
如需转载本文,请注明出处,谢谢!