基于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