工作中用到了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;