基于Directshow的USB视频捕获Delphi篇(一)
参考:https://blog.csdn.net/dbyoung/article/details/78256226
工作中用到了USB Camera 来获取图像。用dspack控件,发现有BUG,使用不了。只好自己写了。参考了dspack源码,但实现方法不同。
也在网上查看了很多资料,终于能顺利的运行。记录一下。也给需要的人有点帮助。
分下面六个部分:
第一步:枚举所有视频输入设备;
第二步:枚举视频支持格式;
第三步:视频预览;
第四步:截图
第五步:相机参数调整;
第六步:视频录像;
第一步:枚举所有视频输入设备;
枚举所有视频输入设备,保存到 TStrings 中。注意保存用的是 AddObject,保存了相机的名称、序列、GUID。因为相机有可能有多个,名称、GUID都可能重复。
上代码。使用函数方式,不使用面向对象的方式。面向对象掩盖了很多细节,不容易了解到重点。
{ 枚举所有视频输入设备 } procedure EnumAllUSBCamera(strsList: TStrings); var SysDevEnum: ICreateDevEnum; EnumCat : IEnumMoniker; hr : Integer; Moniker : IMoniker; Fetched : ULONG; PropBag : IPropertyBag; strName : OleVariant; strGuid : OleVariant; III : Integer; puInfo : PVideoInputInfo; intIndex : Integer; begin { 创建系统枚举器对象 } hr := CocreateInstance(CLSID_SystemDeviceEnum, nil, CLSCTX_INPROC, IID_ICreateDevEnum, SysDevEnum); if hr <> S_OK then Exit; { 用指定的 Filter 目录类型创建一个枚举器,并获得 IEnumMoniker 接口; } hr := SysDevEnum.CreateClassEnumerator(CLSID_VideoInputDeviceCategory, EnumCat, 0); if hr <> S_OK then Exit; try { 释放内存 } if strsList.Count > 0 then begin for III := 0 to strsList.Count - 1 do begin FreeMem(PVideoFormatInfo(strsList.Objects[III])); end; end; strsList.Clear; { 获取指定类型目录下所有设备标识 } while (EnumCat.Next(1, Moniker, @Fetched) = S_OK) do begin Moniker.BindToStorage(nil, nil, IID_IPropertyBag, PropBag); PropBag.Read('CLSID', strGuid, nil); PropBag.Read('FriendlyName', strName, nil); New(puInfo); puInfo^.id := TGUID(strGuid); puInfo^.strName := ShortString(strName); puInfo^.index := 0; if strsList.IndexOf(strName) = -1 then begin strsList.AddObject(strName, TObject(puInfo)); end else begin { 相同名称的 USBCamera 相机,<有可能有多个名称重复的相机> } intIndex := GetMaxIndex(strsList, strName); puInfo^.index := intIndex + 1; strsList.AddObject(strName + format('(%d)', [puInfo^.index]), TObject(puInfo)); end; PropBag := nil; Moniker := nil; end; finally EnumCat := nil; SysDevEnum := nil; end; end;
第二步:枚举视频支持格式;
选择了某个相机之后,我希望知道这个相机支持的所有视频格式,并可以选择用不同的格式来进行视频预览和视频录像。
上代码。
{ 枚举视频支持格式 } function EnumVideoFormat(const strFriendlyName: String; const intIndex: Integer; strsList: TStrings): Boolean; var SysDevEnum : IBaseFilter; CaptureGraphBuilder2: ICaptureGraphBuilder2; iunk : IUnknown; fStreamConfig : IAMStreamConfig; piCount, piSize : Integer; III : Integer; pmt : PAMMediaType; pSCC : PVideoStreamConfigCaps; pvInfo : PVideoFormatInfo; begin Result := False; { 获取指定USB摄像头的 Filter } SysDevEnum := CreateFilter(CLSID_VideoInputDeviceCategory, AnsiString(strFriendlyName), intIndex); if SysDevEnum = nil then Exit; { 释放内存 } if strsList.Count > 0 then begin for III := 0 to strsList.Count - 1 do begin FreeMem(PVideoFormatInfo(strsList.Objects[III])); end; end; strsList.Clear; { 创建 ICaptureGraphBuilder2 接口 } if Failed(CocreateInstance(CLSID_CaptureGraphBuilder2, nil, CLSCTX_INPROC, IID_ICaptureGraphBuilder2, CaptureGraphBuilder2)) then Exit; { 获取 IID_IAMStreamConfig 接口 } if Failed(CaptureGraphBuilder2.FindInterface(nil, nil, SysDevEnum, IID_IAMStreamConfig, iunk)) then Exit; { 获取 IAMStreamConfig 媒体类型接口 } if Failed(iunk.QueryInterface(IID_IAMStreamConfig, fStreamConfig)) then Exit; if Failed(fStreamConfig.GetNumberOfCapabilities(piCount, piSize)) then Exit; if piCount <= 0 then Exit; { 枚举支持的视频格式 } pSCC := AllocMem(piSize); try for III := 0 to piCount - 1 do begin if fStreamConfig.GetStreamCaps(III, pmt, pSCC) = S_OK then begin try New(pvInfo); { 注意释放内存 } pvInfo^.Frame := PVIDEOINFOHEADER(pmt^.pbFormat)^.AvgTimePerFrame; pvInfo^.id := pmt^.formattype; pvInfo^.iWidth := pSCC^.MaxOutputSize.cx; pvInfo^.iHeight := pSCC^.MaxOutputSize.cy; pvInfo^.iMod := pmt^.subtype; pvInfo^.format := VideoMediaSubTypeToStr(pmt^.subtype); strsList.AddObject(format('类型:%s 分辨率:%4d×%4d', [pvInfo^.format, pvInfo^.iWidth, pvInfo^.iHeight]), TObject(pvInfo)); finally DeleteMediaType(pmt); end; end; end; finally FreeMem(pSCC); end; SysDevEnum := nil; CaptureGraphBuilder2 := nil; fStreamConfig := nil; Result := True; end;
第三步:视频预览;
选择了视频设备,和视频格式后,启动视频预览。
function USBCameraPreview(var FIGraphBuilder: IGraphBuilder; // var FICaptureGraphBuilder2: ICaptureGraphBuilder2; // var FSysDevEnum: IBaseFilter; // var FIVideoWindow: IVideoWindow; // var FIMediaControl: IMediaControl; // var FISampleGrabber: ISampleGrabber; // pv: PVideoInputInfo; pf: PVideoFormatInfo; // pnl: TPanel // ): Boolean; var SampleGrabberFilter: IBaseFilter; mt : TAMMediaType; multiplexer : IBaseFilter; Writer : IFileSinkFilter; begin Result := False; { 创建 IGraphBuilder 接口 } if Failed(CocreateInstance(CLSID_FilterGraph, nil, CLSCTX_INPROC, IID_IGraphBuilder, FIGraphBuilder)) then Exit; { 创建 ICaptureGraphBuilder2 接口 } if Failed(CocreateInstance(CLSID_CaptureGraphBuilder2, nil, CLSCTX_INPROC, IID_ICaptureGraphBuilder2, FICaptureGraphBuilder2)) then Exit; { 调用 ICaptureGraphBuilder2 的 SetFilterGraph 方法将 FilterGraph 加入到Builder中 } if Failed(FICaptureGraphBuilder2.SetFiltergraph(FIGraphBuilder)) then Exit; { 获取指定USB摄像头的 Filter } FSysDevEnum := CreateFilter(CLSID_VideoInputDeviceCategory, AnsiString(pv^.strName), pv^.index); if FSysDevEnum = nil then Exit; { 设置指定 Filter 的媒体格式类型 } if not SetMediaType(FSysDevEnum, pf^.iWidth, pf^.iHeight, pf^.format) then Exit; { 将视频捕捉 Filter 添加到 Filter 图中 } if Failed(FIGraphBuilder.AddFilter(FSysDevEnum, 'VideoCapture')) then Exit; { 渲染预览视频PIN } if Failed(FICaptureGraphBuilder2.RenderStream(@PIN_CATEGORY_PREVIEW, @MEDIATYPE_Video, FSysDevEnum, nil, nil)) then Exit; { 设置视频预览窗口 } if Failed(FIGraphBuilder.QueryInterface(IID_IVideoWindow, FIVideoWindow)) then Exit; { 设置视频播放的WINDOWS窗口 } if Failed(FIVideoWindow.put_Owner(pnl.Handle)) then Exit; if Failed(FIVideoWindow.put_windowstyle(WS_CHILD or WS_Clipsiblings)) then Exit; { 设置视频尺寸 } if Failed(FIVideoWindow.SetWindowposition(0, 0, pnl.Width, pnl.Height)) then Exit; { 得到IMediaControl接口,用于控制流播放 } if Failed(FIGraphBuilder.QueryInterface(IID_IMediaControl, FIMediaControl)) then Exit; Result := True; end;
第四步:截图
截图方式有两种,一种是从缓冲区中获取图像,一种是用回调的方式获取图像。
现在使用第一张方法,从缓冲区中获取图像。回调方式下一篇文件介绍。
要从缓冲区中获取图像,需要先设置,允许从缓冲区获取图像。
修改上面的视频预览函数,USBCameraPreview。
这一句:
{ 渲染预览视频PIN } if Failed(FICaptureGraphBuilder2.RenderStream(@PIN_CATEGORY_PREVIEW, @MEDIATYPE_Video, FSysDevEnum, nil, nil)) then Exit;
修改为:
{ 如果需要截图功能 } if bSnapBmp then begin CocreateInstance(CLSID_SampleGrabber, nil, CLSCTX_INPROC, IID_IBaseFilter, SampleGrabberFilter); FIGraphBuilder.AddFilter(SampleGrabberFilter, 'SampleGrabber'); SampleGrabberFilter.QueryInterface(IID_ISampleGrabber, FISampleGrabber); zeromemory(@mt, sizeof(AM_MEDIA_TYPE)); mt.majortype := MEDIATYPE_Video; mt.subtype := MEDIASUBTYPE_RGB24; // 24位,位图格式输出 FISampleGrabber.SetMediaType(@mt); // FISampleGrabber.SetBufferSamples(True); // 允许从 Buffer 中获取数据 { 渲染预览视频PIN } if Failed(FICaptureGraphBuilder2.RenderStream(@PIN_CATEGORY_PREVIEW, @MEDIATYPE_Video, FSysDevEnum, SampleGrabberFilter, nil)) then Exit; end else begin { 渲染预览视频PIN } if Failed(FICaptureGraphBuilder2.RenderStream(@PIN_CATEGORY_PREVIEW, @MEDIATYPE_Video, FSysDevEnum, nil, nil)) then Exit; end;
函数声明中添加一个布尔类型 bSnapBmp。
能看出来,只是修改FICaptureGraphBuilder2.RenderStream 了第四个参数。由原来的 nil 变成了 SampleGrabberFilter,是不是很简单。
(以后我们还会对 USBCameraPreview 这个函数进行修改 。这是个基本函数。)
开启了允许截图功能,我们就可以截图了。
{ 截图 } procedure TForm1.btnSnapBmpClick(Sender: TObject); var pfs : TFilterState; mt : TAMMediaType; hr : HResult; pBufferSize: Integer; pBuffer : PByte; bmp : TBitmap; vi : PVideoInfoHeader; begin if FIMediaControl = nil then Exit; FIMediaControl.GetState(1000, pfs); if pfs = State_Stopped then Exit; { 获取媒体类型 } hr := FISampleGrabber.GetConnectedMediaType(mt); if hr <> S_OK then Exit; if mt.pbFormat = nil then Exit; vi := PVideoInfoHeader(mt.pbFormat); { 获取当前帧数据大小 } hr := FISampleGrabber.GetCurrentBuffer(pBufferSize, nil); if hr <> S_OK then Exit; { 分配内存大小 } pBuffer := AllocMem(pBufferSize); try { 再一次获取当前帧,获取图像数据 } hr := FISampleGrabber.GetCurrentBuffer(pBufferSize, pBuffer); if hr <> S_OK then Exit; { 创建位图 } bmp := TBitmap.Create; try bmp.PixelFormat := pf24bit; bmp.width := vi^.bmiHeader.biWidth; bmp.height := vi^.bmiHeader.biHeight; SetBitmapBits(bmp.Handle, vi^.bmiHeader.biSizeImage, pBuffer); bmp.Canvas.CopyRect(bmp.Canvas.ClipRect, bmp.Canvas, Rect(0, bmp.height, bmp.width, 0)); img1.Picture.Bitmap.Assign(bmp); finally bmp.Free; end; finally FreeMem(pBuffer); end; end;
第五步:相机参数调整;
这一步比较简单,USB CAMERA 有两种参数可以调节,一个是视频参数,一个是格式参数。格式参数我们已经枚举出来了,调不调节无所谓了。
下面给出这两个参数调节的代码。两个函数。
{ 视频参数调节 } function ShowFilterPropertyPages(filter: IBaseFilter; hFormHandle: THandle): Boolean; var pSpecify: ISpecifyPropertyPages; caGUID : TCAGUID; begin Result := False; pSpecify := nil; filter.QueryInterface(ISpecifyPropertyPages, pSpecify); if pSpecify <> nil then begin pSpecify.GetPages(caGUID); pSpecify := nil; Result := OleCreatePropertyFrame(hFormHandle, 0, 0, '', 1, Pointer(@filter), caGUID.cElems, PGUID(caGUID.pElems), 0, 0, nil) = S_OK; CoTaskMemFree(caGUID.pElems); end; end; { 格式参数调节 } function ShowPinPropertyPages(pin: IPin; hFormHandle: THandle): Boolean; var pSpecify: ISpecifyPropertyPages; caGUID : TCAGUID; begin Result := False; pSpecify := nil; pin.QueryInterface(ISpecifyPropertyPages, pSpecify); if pSpecify <> nil then begin pSpecify.GetPages(caGUID); pSpecify := nil; Result := OleCreatePropertyFrame(hFormHandle, 0, 0, '', 1, Pointer(@pin), caGUID.cElems, PGUID(caGUID.pElems), 0, 0, nil) = S_OK; CoTaskMemFree(caGUID.pElems); end; end;
调用:
{ 视频参数调节}
ShowFilterPropertyPages(FSysDevEnum, Handle);
{格式参数调节}
var
pin: IPin;
begin
FICaptureGraphBuilder2.FindPin(FSysDevEnum, PINDIR_OUTPUT, nil, nil, False, 0, pin);
ShowPinPropertyPages(pin, Handle);
end;
第六步:视频录像;
视频录像就是将视频保存到磁盘上。这就要使用视频编码器,将视频以何种格式保存到磁盘上。avi,mov,mp4,等等。
不同的格式,需要你的机器上安装对应的编码器。我们以所有的机器上都支持的avi格式来保存文件。
(缺点:文件比较大。保存是原始的YUV图像。其实也可以将YUV转化为BMP,下一篇文章中介绍)
修改上面的视频预览函数,让它可以进行视频录制。
在截图功能代码之后,添加代码:
{ 如果是视频录制 } if bRecord then begin { 视频录制文件保持路径 } if Failed(FICaptureGraphBuilder2.SetOutputFileName(MEDIASUBTYPE_Avi, PWideChar(strSaveFileName), multiplexer, Writer)) then Exit; if Failed(FICaptureGraphBuilder2.RenderStream(@PIN_CATEGORY_CAPTURE, @MEDIATYPE_Video, FSysDevEnum, nil, multiplexer)) then Exit; end;
这样就多了两个参数,bRecord,strSaveFileName。意思很明白。
这样这个视频预览函数,既可以预览,又支持截图、又支持视频录像。
完整代码如下:
function CommonUSBCamera(var FIGraphBuilder: IGraphBuilder; // var FICaptureGraphBuilder2: ICaptureGraphBuilder2; // var FSysDevEnum: IBaseFilter; // var FIVideoWindow: IVideoWindow; // var FIMediaControl: IMediaControl; // var FISampleGrabber: ISampleGrabber; // pv: PVideoInputInfo; pf: PVideoFormatInfo; // pnl: TPanel; // const strSaveFileName: string = ''; const bRecord: Boolean = False; // 录像 const bSnapBmp: Boolean = False // 截图 ): Boolean; var SampleGrabberFilter: IBaseFilter; mt : TAMMediaType; multiplexer : IBaseFilter; Writer : IFileSinkFilter; begin Result := False; { 创建 IGraphBuilder 接口 } if Failed(CocreateInstance(CLSID_FilterGraph, nil, CLSCTX_INPROC, IID_IGraphBuilder, FIGraphBuilder)) then Exit; { 创建 ICaptureGraphBuilder2 接口 } if Failed(CocreateInstance(CLSID_CaptureGraphBuilder2, nil, CLSCTX_INPROC, IID_ICaptureGraphBuilder2, FICaptureGraphBuilder2)) then Exit; { 调用 ICaptureGraphBuilder2 的 SetFilterGraph 方法将 FilterGraph 加入到Builder中 } if Failed(FICaptureGraphBuilder2.SetFiltergraph(FIGraphBuilder)) then Exit; { 获取指定USB摄像头的 Filter } FSysDevEnum := CreateFilter(CLSID_VideoInputDeviceCategory, AnsiString(pv^.strName), pv^.index); if FSysDevEnum = nil then Exit; { 设置指定 Filter 的媒体格式类型 } if not SetMediaType(FSysDevEnum, pf^.iWidth, pf^.iHeight, pf^.format) then Exit; { 将视频捕捉 Filter 添加到 Filter 图中 } if Failed(FIGraphBuilder.AddFilter(FSysDevEnum, 'VideoCapture')) then Exit; { 如果需要截图功能 } if bSnapBmp then begin CocreateInstance(CLSID_SampleGrabber, nil, CLSCTX_INPROC, IID_IBaseFilter, SampleGrabberFilter); FIGraphBuilder.AddFilter(SampleGrabberFilter, 'SampleGrabber'); SampleGrabberFilter.QueryInterface(IID_ISampleGrabber, FISampleGrabber); zeromemory(@mt, sizeof(AM_MEDIA_TYPE)); mt.majortype := MEDIATYPE_Video; mt.subtype := MEDIASUBTYPE_RGB24; // 24位,位图格式输出 FISampleGrabber.SetMediaType(@mt); // FISampleGrabber.SetBufferSamples(True); // 允许从 Buffer 中获取数据 { 渲染预览视频PIN } if Failed(FICaptureGraphBuilder2.RenderStream(@PIN_CATEGORY_PREVIEW, @MEDIATYPE_Video, FSysDevEnum, SampleGrabberFilter, nil)) then Exit; end else begin { 渲染预览视频PIN } if Failed(FICaptureGraphBuilder2.RenderStream(@PIN_CATEGORY_PREVIEW, @MEDIATYPE_Video, FSysDevEnum, nil, nil)) then Exit; end; { 如果是视频录制 } if bRecord then begin { 视频录制文件保持路径 } if Failed(FICaptureGraphBuilder2.SetOutputFileName(MEDIASUBTYPE_Avi, PWideChar(strSaveFileName), multiplexer, Writer)) then Exit; if Failed(FICaptureGraphBuilder2.RenderStream(@PIN_CATEGORY_CAPTURE, @MEDIATYPE_Video, FSysDevEnum, nil, multiplexer)) then Exit; end; { 设置视频预览窗口 } if Failed(FIGraphBuilder.QueryInterface(IID_IVideoWindow, FIVideoWindow)) then Exit; { 设置视频播放的WINDOWS窗口 } if Failed(FIVideoWindow.put_Owner(pnl.Handle)) then Exit; if Failed(FIVideoWindow.put_windowstyle(WS_CHILD or WS_Clipsiblings)) then Exit; { 设置视频尺寸 } if Failed(FIVideoWindow.SetWindowposition(0, 0, pnl.Width, pnl.Height)) then Exit; { 得到IMediaControl接口,用于控制流播放 } if Failed(FIGraphBuilder.QueryInterface(IID_IMediaControl, FIMediaControl)) then Exit; Result := True; end; { 视频预览 } function USBVideoPreview(var FIGraphBuilder: IGraphBuilder; var FICaptureGraphBuilder2: ICaptureGraphBuilder2; var FSysDevEnum: IBaseFilter; var FIVideoWindow: IVideoWindow; var FIMediaControl: IMediaControl; var FISampleGrabber: ISampleGrabber; pv: PVideoInputInfo; pf: PVideoFormatInfo; pnl: TPanel; const bSnapBmp: Boolean = False): Boolean; begin Result := CommonUSBCamera(FIGraphBuilder, FICaptureGraphBuilder2, FSysDevEnum, FIVideoWindow, FIMediaControl, FISampleGrabber, pv, pf, pnl, '', False, True); end; { 视频录制 } function USBVideoRecord(var FIGraphBuilder: IGraphBuilder; var FICaptureGraphBuilder2: ICaptureGraphBuilder2; var FSysDevEnum: IBaseFilter; var FIVideoWindow: IVideoWindow; var FIMediaControl: IMediaControl; var FISampleGrabber: ISampleGrabber; pv: PVideoInputInfo; pf: PVideoFormatInfo; pnl: TPanel; const strSaveFileName: String): Boolean; begin Result := CommonUSBCamera(FIGraphBuilder, FICaptureGraphBuilder2, FSysDevEnum, FIVideoWindow, FIMediaControl, FISampleGrabber, pv, pf, pnl, strSaveFileName, True, True); end;
完整工程代码:
http://download.csdn.net/download/dbyoung/10025100