目录
简述:
delphi7 下进行USB摄像头开发网上的办法一般两种1。基于AVICAP32的windowsAPI 2.基于DSPack 控件。本文将介绍利用FFMPEG+SDL2实现USB摄像头的播放及拍照。
实现效果图如下
准备工作
注:以下所需资源可以直接从我的网盘下载。
链接:https://pan.baidu.com/s/1IoJvgI8Y5_3DqhCGTvs4Cw
提取码:pdga
- FFMPEG Delphi7 的DLL封装及头文件。这里利用了FFVCL对FFMPEG的封装。可以在 其官网下载FFVCL – Delphi FFmpeg VCL Components(Video Encoder and Video Player)。可以从我的网盘下载。
- SDL2 Delphi7的DLL封装及头文件。可以在官网Simple DirectMedia Layer - Homepage下载。也可以从我的网盘下载。
- 因为FFMPEG枚举视频设备列表时对中文的不友好。而有些媒体设备的名称为中文,因此采用DSP控件中DirectShow9实现。可以从我的网盘下载。也可以自行百度下载。
获取视频设备类表
这里借助DSPACK,因为方便。当然也可以直接使用COM访问DirectShow。必须引用的单元文件DSUtil,DirectShow9,ActiveX,mmsystem。
主要方法:
unit rocmediadev;
interface
uses
classes,DSUtil,DirectShow9,ActiveX,mmsystem;
type
//媒体格式定义
TRocMediaFormat = class
End;
//视频媒体格式
TVideoFormat=class(TRocMediaFormat)
private
FWidth:Integer; //视频分辨率
FHeight:Integer; //视频分辨率
FAvgTimePerFrame:Integer; //每帧时间
FBitRate:Integer; //码率
public
procedure setFormat(const w,h,atpf,b:Integer);
constructor Create(const w,h,atpf,b:Integer);
published
property Width:Integer read FWidth;
property Height:Integer read FHeight;
property AvgTimePerFrame:Integer read FAvgTimePerFrame;
property BitRate:Integer read FBitRate;
End;
//音频媒体格式
TAudioFormat=class(TRocMediaFormat)
private
FAudioFormat:TWAVEFORMATEX;
public
procedure setFormat(const fmt:TWAVEFORMATEX);
constructor Create(const fmt:TWAVEFORMATEX);
published
property AudioFormat:TWAVEFORMATEX read FAudioFormat;
End;
//设备媒体格式列表
TRocMediaFormats=class(TList)
private
function getFormats(const index: Integer): TRocMediaFormat;
public
procedure Add(const frm:TRocMediaFormat);overload;
procedure Clear;override;
property Formats[const index:Integer]:TRocMediaFormat read getFormats;
End;
//媒体设备
TRocMediaDevice=class
private
FfriendlyName:String;
FmonikerName:String;
FFormats:TRocMediaFormats;
procedure DsGetOptionDevice(moniker:IMoniker);
public
constructor Create(const friendlyName:String;const monikerName:String;const moniker:IMoniker);
destructor destroy;override;
published
property Formats:TRocMediaFormats read FFormats;
property FriendlyName:String read FfriendlyName;
property MonikerName:String read FmonikerName;
End;
//设备媒体管理类
TRocMediaDeviceManager=class(TList)
private
function getDevices(const Index: Integer): TRocMediaDevice;
protected
procedure DsGetInputDevices(guidValue:TGUID);
public
procedure Clear;override;
property Devices[const Index:Integer]:TRocMediaDevice read getDevices;
End;
//视频媒体管理类
TRocVideoDeviceManager=class(TRocMediaDeviceManager)
public
constructor Create;
End;
//音频频媒体管理类
TRocAudioDeviceManager=class(TRocMediaDeviceManager)
public
constructor Create;
End;
implementation
{ TVideoFormat }
constructor TVideoFormat.Create(const w, h, atpf, b: Integer);
begin
setFormat(w,h,atpf,b);
end;
procedure TVideoFormat.setFormat(const w, h, atpf, b: Integer);
begin
FWidth := w;
FHeight := h;
FAvgTimePerFrame := atpf;
FBitRate := b;
end;
{ TAudioFormat }
constructor TAudioFormat.Create(const fmt: TWAVEFORMATEX);
begin
setFormat(fmt);
end;
procedure TAudioFormat.setFormat(const fmt: TWAVEFORMATEX);
begin
FAudioFormat := fmt;
end;
{ TRocMediaFormats }
procedure TRocMediaFormats.Add(const frm: TRocMediaFormat);
begin
inherited Add(frm);
end;
procedure TRocMediaFormats.Clear;
var
i:Integer;
begin
for i:=Count-1 downto 0 do
Begin
Formats[i].Free;
inherited Delete(i);
End;
inherited;
end;
function TRocMediaFormats.getFormats(
const index: Integer): TRocMediaFormat;
begin
result:= TRocMediaFormat(inherited Items[index]);
end;
{ TRocMediaDevice }
constructor TRocMediaDevice.Create(const friendlyName:String;const monikerName:String;const moniker:IMoniker);
begin
FfriendlyName := friendlyName;
FmonikerName := monikerName;
FFormats:=TRocMediaFormats.Create;
DsGetOptionDevice(moniker);
end;
destructor TRocMediaDevice.destroy;
begin
FFormats.Free;
end;
procedure TRocMediaDevice.DsGetOptionDevice(moniker: IMoniker);
var
filter:IBaseFilter;
pinEnum:IEnumPins;
Pin:IPin;
pinInfo:PIN_INFO;
pinFetched:longword;
mtFetched:Longword;
mtEnum :IEnumMediaTypes;
mt:PAMMediaType;
begin
repeat
moniker.BindToObject(nil,nil,IID_IBaseFilter,filter);
If FAILED(filter.EnumPins(pinEnum))then break;
pinEnum.Reset();
pinFetched:=0;
while (SUCCEEDED(pinEnum.Next(1,pin,@pinFetched)) and (pinFetched>0)) do
Begin
if (pin=nil) then continue;
if Failed(pin.QueryPinInfo(pinInfo)) then continue;
if (pinInfo.dir <> PINDIR_OUTPUT) then continue;
if Failed(pin.EnumMediaTypes(mtEnum)) then break;
while (SUCCEEDED(mtEnum.Next(1,mt,@mtFetched)) and (mtFetched>0)) do
Begin
if IsEqualGUID(mt.formattype,FORMAT_VideoInfo2) then
Begin
if (mt.cbFormat >= sizeof(VIDEOINFOHEADER2)) then
begin
with PVideoInfoHeader2(mt.pbFormat)^ do
Begin
Formats.Add(TVideoFormat.Create(bmiHeader.biWidth,bmiHeader.biHeight,AvgTimePerFrame,dwBitRate));
End;
end;
End Else Begin
if IsEqualGUID(mt.formattype,FORMAT_VideoInfo) then
Begin
if (mt.cbFormat >= sizeof(VIDEOINFOHEADER)) then
begin
with PVIDEOINFOHEADER(mt.pbFormat)^ do
Begin
Formats.Add(TVideoFormat.Create(bmiHeader.biWidth,bmiHeader.biHeight,AvgTimePerFrame,dwBitRate));
End;
end;
End Else Begin
if IsEqualGUID(mt.formattype,FORMAT_WaveFormatEx) then
Begin
if(mt.cbFormat>=sizeof(TWAVEFORMATEX)) then
Begin
Formats.Add(TAudioFormat.Create(PWAVEFORMATEX(mt.pbFormat)^) );
End;
End;
End;
End;
End;
End;
until 1 = 1;
Pin:=nil;
filter:=nil;
pinEnum:=nil;
end;
{ TRocMediaDeviceManager }
procedure TRocMediaDeviceManager.Clear;
var
I:Integer;
begin
for i:=Count-1 downto 0 Do
Begin
Devices[i].Free;
inherited Delete(i);
End;
inherited;
end;
procedure TRocMediaDeviceManager.DsGetInputDevices(guidValue: TGUID);
var
sysDev:TSysDevEnum;
monike:IMoniker;
I:Integer;
pOleDisplayName:PWideChar;
device:TRocMediaDevice;
begin
sysDev:=TSysDevenum.Create(guidValue);
for I:=0 to sysDev.CountFilters-1 Do
Begin
monike:=SysDev.GetMoniker(I);
pOleDisplayName := PWideChar(CoTaskMemAlloc(255));
try
monike.GetDisplayName(nil,nil,pOleDisplayName);
device := TRocMediaDevice.Create(sysDev.Filters[I].FriendlyName,
pOleDisplayName,monike);
inherited Add(device);
finally
CoTaskMemFree(pOleDisplayName);
monike:=nil;
end;
End;
sysDev.Free;
end;
function TRocMediaDeviceManager.getDevices(
const Index: Integer): TRocMediaDevice;
begin
Result:= TRocMediaDevice(inherited Items[index]);
end;
{ TRocVideoDeviceManager }
constructor TRocVideoDeviceManager.Create;
begin
DsGetInputDevices(CLSID_VideoInputDeviceCategory);
end;
{ TRocAudioDeviceManager }
constructor TRocAudioDeviceManager.Create;
begin
DsGetInputDevices(CLSID_AudioInputDeviceCategory);
end;
end.
以上单元文件使用方法:
var
VideoDeviceManager:TRocVideoDeviceManager; //视频媒体设备管理类
i:Integer;
device:TRocMediaDevice;
Begin
VideoDeviceManager:=TRocVideoDeviceManager.Create;
try
for i:=0 to VideoDeviceManager.Count-1 do
Begin
device:=VideoDeviceManager.Devices[i];
//插入设备信息到combobox1下拉列表中
//combobox2中显示combobox1中当前选中设备的设备格式
combobox1.Items.AddObject(device.friendlyName,device);
End;
finally
VideoDeviceManager.free;
end;
End;
//设备被重新选中时,刷新combobox2列表中设备格式信息。
procedure ComboBox1Change(Sender: TObject);
var
device:TRocMediaDevice;
j:Integer;
fmt:TVideoFormat;
begin
ComboBox2.Items.Clear;
device:=TRocMediaDevice(ComboBox1.Items.Objects[ComboBox1.ItemIndex]);
for j:=0 to device.Formats.Count-1 do
Begin
fmt := TVideoFormat(device.Formats.Formats[j]);
ComboBox2.Items.AddObject(Format('%dx%d',[fmt.Width,fmt.Height]),fmt);
End;
if(ComboBox2.Items.Count>0) then ComboBox2.ItemIndex:=0;
end;
FFMPEG视频解码
- FFMPEG+SDL应用级别初始化
av_register_all() 这是ffmpeg注册复用器。所有基于FFMPEG的应用程序首先必须调用的函数。主要对所支持的“封装/解封”格式,所支持的协议(protocol).所支持的编码及解码器...的注册。
avdevice_register_all() 这里还需要调用设备注册函数。因为我们需要打开USB摄像头。
avformat_network_init() 打开网络流注册函数。主要是读取网络流数据。例如RTSP网络实时视频。在以后的文章中另开一篇介绍RTSP有关的视频实现。
SDL_INIT(Uint32 flags) 这也是所有基于SDL的应用程序所必须调用的初始化函数。
flags取值:
SDL_INIT_TIMER:定时器
SDL_INIT_AUDIO:音频
SDL_INIT_VIDEO:视频
SDL_INIT_JOYSTICK:摇杆
SDL_INIT_HAPTIC:触摸屏
SDL_INIT_GAMECONTROLLER:游戏控制器
SDL_INIT_EVENTS:事件
SDL_INIT_NOPARACHUTE:不捕获关键信号(这个不理解)
SDL_INIT_EVERYTHING:包含上述所有选项
用法举例:SDL_INIT(SDL_INIT_VIDEO or SDL_INIT_AUDIO or SDL_INIT_TIMER);
- 初始化USB摄像头
var
AVFormatContext:PAVFormatContext;
AVCodecContext:PAVCodecContext;
Video_s_index:Integer; //视频流索引号
Devicename:String; //摄像头名称
FormatName:String; //vfwcap or dshow 采集方式
// dshow 摄像头、采集卡、麦克风等
// vfwcap 主要时摄像头设备
//初始化视频对象
function InitializeVideoObj:boolean;
var
dn:String;
InputFromat:PAVInputFormat;
options:PAVDictionary;
retCode,i:Integer;
videoS:PAVStream;
AVcodec:PAVCodec;
begin
result := false;
repeat
AVFormatContext := avformat_alloc_context;
if not assigned(AVFormatContext) Then break;
dn := 'video=' + Devicename;
//采集方式 dshow / vfwcap
InputFromat := av_find_input_format(pchar(FormatName));
if not assigned(inputFromat) then break;
options:=nil;
//设置视频大小
av_dict_set(@options,'video_size',
pchar(inttostr(FWidth)+'x'+inttostr(FHeight)),0);
//打开摄像头
retCode := avformat_open_input(@AVFormatContext,
pchar(UTF8EnCode(dn)),InputFromat,@options);
if retCode<0 Then break;
options:=nil;
//查找视频流索引
retCode := avformat_find_stream_info(AVFormatContext,@options);
if retCode < 0 Then break;
video_s_index := -1;
for i := 0 to AVFormatContext^.nb_streams - 1 do
Begin
videoS := PPtrIdx(AVFormatContext^.streams,i);
if videoS^.codec^.codec_type=AVMEDIA_TYPE_VIDEO then
Begin
video_s_index:=i;
break;
End;
End;
if video_s_index = -1 break;
//获取解码器并打开编码器
AVcodec:=avcodec_find_decoder(videoS^.codec^.codec_id);
if not assigned(AVcodec) then break;
AVCodecContext:=avcodec_alloc_context3(AVcodec);
if not assigned(AVCodecContext) then break;
avcodec_parameters_to_context(AVCodecContext,videoS^.codecpar);
retCode:=AVCodec_Open2(AVCodecContext,AVcodec,nil);
if retCode<0 Then break;
result := true;
until 1=1;
end;
调用avformat_open_input() 打开输入流(这里只USB摄像头),并读取视频流头部信息。这里需要注意的是摄像头名称必须转换为UTF8。
- 初始化过滤器
采集到的视频数据统一转换为BGR24格式。方便拍照转为位图图片。这里采用了AVFilter过滤器实现。
var
filter_graph: PAVFilterGraph;
buffersink_ctx: PAVFilterContext;
buffersrc_ctx: PAVFilterContext;
function initializeFilter:boolean;
const
pix_fmts:TAVPixelFormats = (AV_PIX_FMT_BGR24,AV_PIX_FMT_NONE);
var
args: array[0..512-1] of AnsiChar;
buffersrc: PAVFilter;
buffersink: PAVFilter;
outputs: PAVFilterInOut;
inputs: PAVFilterInOut;
ret:integer;
videoS:PAVStream;
filters_descr:String;
begin
result := false;
buffersrc := avfilter_get_by_name('buffer');
buffersink := avfilter_get_by_name('buffersink');
outputs := avfilter_inout_alloc();
inputs := avfilter_inout_alloc();
filter_graph := avfilter_graph_alloc();
try
repeat
if not Assigned(outputs) or not Assigned(inputs) or
not Assigned(filter_graph) then break;
videoS := PPtrIdx(AVFormatContext.streams,video_s_index);
snprintf(@args[0], SizeOf(args),
'video_size=%dx%d:pix_fmt=%d:time_base=%d/%d:pixel_aspect=%d/%d',
AVCodecContext.width,AVCodecContext.height,AVCodecContext.pix_fmt,
videoS.time_base.num, videoS.time_base.den,
AVCodecContext.sample_aspect_ratio.num,
AVCodecContext.sample_aspect_ratio.den);
ret := avfilter_graph_create_filter(@buffersrc_ctx,buffersrc,
'in',@args[0], nil, filter_graph);
if(ret < 0) then break;
ret := avfilter_graph_create_filter(@buffersink_ctx, buffersink, 'out',
nil, nil, filter_graph);
if(ret < 0) then break;
ret := av_opt_set_int_list(buffersink_ctx, 'pix_fmts', @pix_fmts[0],
SizeOf(TAVPixelFormat),
Int64(AV_PIX_FMT_NONE), AV_OPT_SEARCH_CHILDREN);
if(ret < 0) then break;
outputs.name := av_strdup('in');
outputs.filter_ctx := buffersrc_ctx;
outputs.pad_idx := 0;
outputs.next := nil;
inputs.name := av_strdup('out');
inputs.filter_ctx := buffersink_ctx;
inputs.pad_idx := 0;
inputs.next := nil;
filters_descr:= 'scale='+inttostr(FWidth)+':'+
inttostr(FHeight)+',fps=fps='+inttostr(FVideo_frame_rate);
ret := avfilter_graph_parse_ptr(filter_graph, pchar(filters_descr),
@inputs, @outputs, nil);
if( ret < 0) then break;
ret := avfilter_graph_config(filter_graph, nil);
if(ret < 0) then break;
result := true;
until 1 = 1;
finally
if assigned(inputs) then avfilter_inout_free(@inputs);
if assigned(outputs) then avfilter_inout_free(@outputs);
end;
end;
过滤器使用起来简单而且功能强大。不同种类的过滤器作用不同,使用流程主要是这几步
1.分配AVFilterGraph avfilter_graph_alloc()
2.创建源过滤器 和 接收过滤器。
int avfilter_graph_create_filter(AVFilterContext **filt_ctx, const AVFilter *filt, const char *name,
const char *args, void *opaque, AVFilterGraph *graph_ctx);
通过name 'in' 或者 'out' 创建不同类的过滤器
3.生成源和接收过滤器的输入输出
outputs.name := av_strdup('in');
....
inputs.name := av_strdup('out');
....
4. 通过解析过滤器字符串添加过滤器
avfilter_graph_parse_ptr(filter_graph, pchar(filters_descr), @inputs, @outputs, nil)
5.检查过滤器的完整性
avfilter_graph_config(filter_graph, nil);
- 初始化SDL渲染对象
var
FPreviewWnd:THandle; //视频预览窗体句柄
SDL_Wnd:PSDL_Window; //根据FPreviewWnd创建的SDL窗体句柄
SDL_Renderer:PSDL_Renderer;//渲染对象
SDL_Texture:PSDL_Texture;
FPreviewRect:TRect; //视频预览的大小 width,height
function initializeSDLObj:boolean;
begin
result : = false;
repeat
SDL_Wnd := SDL_CreateWindowFrom(Pointer(FPreviewWnd));
if not assigned(SDL_Wnd) then break;
SDL_Renderer := SDL_CreateRenderer(SDL_Wnd,-1,0);
if not assigned(SDL_Renderer) then break;
SDL_Texture := SDL_CreateTexture(SDL_Renderer,SDL_PIXELFORMAT_BGR24,
SDL_TEXTUREACCESS_STREAMING,FWidth,FHeight);
if not assigned(SDL_Texture) then break;
result : = true;
until 1 = 1;
end;
视频播放
//视频解码播放在线程中运行
var
Playhandle:THandle;
FQuitEvt:THandle; //退出线程事件
FCapturePicEvt:THandle; //拍照命令事件
FCaptureNotify:THandle; //照片抓拍完成通知事件
FCaptureData:TMemoryStream; //抓拍照片缓存
CS_Capture:TRTLCriticalSection;//照片缓冲保护临界量
procedure OnCameraRenderer;
var
iAVFrame,oAVFrame:PAVFrame;
Events:TWOHandleArray;
waitResult:longword;
AVPacket:TAVPacket;
avRet:integer;
SDL_Rect:TSDL_Rect;
begin
iAVFrame := AV_Frame_Alloc;
oAVFrame := AV_Frame_Alloc;
try
iAVFrame.format := Ord(AVCodecContext^.pix_fmt);
iAVFrame.width := AVCodecContext^.width;
iAVFrame.height := AVCodecContext^.height;
Events[0] := FQuitEvt;
Events[1] := FCapturePicEvt;
repeat
waitResult:=WaitForMultipleObjects(2,@Events,False,0);
DoCapture := False;
If waitResult = WAIT_OBJECT_0 Then Break; //接收到退出视频解码播放事件
If waitResult = WAIT_OBJECT_0+1 Then
Begin //接收到拍照事件
ResetEvent(FCapturePicEvt);
DoCapture:=True;
End;
av_init_packet(@AVPacket);
try
avRet := av_read_frame(AVFormatContext,@AVPacket);
if(avRet<0) then break;
if avRet = 0 then
Begin
if AVPacket.stream_index <> video_s_index then continue;
//开始解码
avRet := avcodec_send_packet(AVCodecContext,@AVPacket);
if(avRet < 0) then
Begin
if (avRet = AVERROR_EAGAIN) or (avRet = AVERROR_EOF) Then continue;
break;//解码失败退出
End;
while true do
Begin
//获取解码后的数据
avRet := avcodec_receive_frame(AVCodecContext,iAVFrame);
if(avRet < 0) then
Begin
if (avRet = AVERROR_EAGAIN) or (avRet = AVERROR_EOF) Then break;
Exit;
End;
//视频数据转为BGR24
avRet := av_buffersrc_add_frame_flags(buffersrc_ctx,
iAVFrame,AV_BUFFERSRC_FLAG_KEEP_REF);
if(avRet < 0) then Exit;//过滤器转换失败
repeat
avRet := av_buffersink_get_frame(buffersink_ctx, oAVFrame);
try
if(avRet<0) then
Begin
if (avRet = AVERROR_EAGAIN) or
(avRet = AVERROR_EOF) Then break;
Exit;//转码失败
End;
if DoCapture Then //抓拍照片
Begin
EnterCriticalSection(CS_Capture);
try
FCaptureData.Clear;
FCaptureData.Write(oAVFrame^.linesize[0],sizeof(Integer));
FCaptureData.Write(oAVFrame^.width,sizeof(Integer));
FCaptureData.Write(oAVFrame^.height,sizeof(Integer));
FCaptureData.Write(OAVFrame^.data[0]^,
oAVFrame^.linesize[0]*oAVFrame^.height);
SetEvent(FCaptureNotify);
finally
DoCapture := False;
LeaveCriticalSection(CS_Capture);
End;
End;
//渲染视频到指定窗体上
SDL_Rect.x:=FPreviewRect.Left;
SDL_Rect.y:=FPreviewRect.Top;
SDL_Rect.w:=FPreviewRect.Right-FPreviewRect.Left;
SDL_Rect.h:=FPreviewRect.Bottom-FPreviewRect.Top;
SDL_UpdateTexture(SDL_Texture,nil,
Pbyte(oAVFrame^.data[0]),oAVFrame^.linesize[0]);
SDL_RenderClear(SDL_Renderer);
SDL_RenderCopy(SDL_Renderer, SDL_Texture, nil,@SDL_Rect);
SDL_RenderPresent(SDL_Renderer);
finally
av_frame_unref(oAVFrame); //释放缓存
end;
until 1 = 2;
End;
End;
finally
av_packet_unref(@AVPacket);//释放缓存
end;
until 1 = 2;
finally
av_frame_free(@iAVFrame);
av_frame_free(@oAVFrame);
End;
end;
视频播放在另启一个线程运行。 CreateThread();
拍照
上面的方法函数OnCameraRenderer。中接收拍照命令事件,完成抓拍图片并保存到缓存数据FCaptureData。 缓存数据包含。lineSize.图片的每行字节数。Width 图片的宽度 Height 图片的高度 图片的BGR24数据。 通过函数 BGR24ToBitmap() 转成bmp图片。
function BGR24ToBitmap(const frame: Pbyte; const linesize,
width, height: integer):TBitmap;
var
P:PByte;
I:integer;
begin
Result:=TBitmap.Create;
Result.Width:=Width;
Result.Height:=Height;
Result.PixelFormat:=pf24bit;
P:=frame;
for I:=0 To Result.Height-1 Do
Begin
Move(P^,Result.ScanLine[I]^,linesize);
Inc(P,linesize);
End;
end;
参数frame 为图片BGR24格式的数据指针。
Demo下载
上面的代码实现的主要源代码。具体的实现Demo 重这里下载