虚拟摄像头驱动原理及开发

来源:互联网 发布:c标签和js函数 编辑:程序博客网 时间:2024/05/20 08:00

(以下所说的都是基于微软的windows平台)       
        类似功能的产品,如著名的e2eSoft的 VCam,国内新浪的9518虚拟视频,
新浪的虚拟视频是DirectShow应用层上的视频模拟,大概的思路就是:
因为所有的摄像头WDM驱动,都需要通过运行在应用层的ksproxy.ax与DirectShow交互,
ksproxy.ax顾名思义,是WDM驱动同DShow的代理,
因此在应用层模拟虚拟摄像头,可在这上面做文章(但是有个遗憾就是老的VFW不支持这种方式)。
具体怎么实现,有兴趣的可搜索相关文档。
e2esoft的VCam是真正基于WDM驱动的虚拟摄像头,他有一个WDM驱动来模拟摄像头,
在应用程序看来,跟一个真正的摄像头没有分别,所以他的适用范围很广,基本上真正摄像头能干的事情,他都能做到。

这里要阐述的,就是基于WDM开发的虚拟摄像头驱动(基于老的流内核框架)
摄像头驱动是微端口驱动,是跟真正硬件打交道的一类驱动。
在DriverEntry里初始化 HW_INITIALIZATION_DATA 结构,填写相关回调函数,
调用StreamClassRegisterAdapter注册自己到类驱动。
在回调函数里完成大部分的工作,它依靠HW_STREAM_REQUEST_BLOCK(简称SRB块)跟流类驱动通讯。
流类驱动发送SRB_INITIALIZE_DEVICE命令初始化设备,
接着发送SRB_GET_STREAM_INFO命令获得摄像头支持的视频流。
然后发送SRB_INITIALIZATION_COMPLETE表示初始化完成,
当应用程序要打开摄像头的时候,比如DShow程序,会调用ksproxy.ax,ksproxy.ax接着调用流类驱动,
流类驱动发送 SRB_OPEN_STREAM 给摄像头驱动,表示要打开某个视频流,
当要关闭一个流的时候,流类驱动发送SRB_CLOSE_STREAM表示要关闭某个流,
当驱动卸载或者禁用时候,会收到类驱动发来的SRB_UNINITIALIZE_DEVICE,表示卸载设备。
在 SRB_OPEN_STREAM命令中,要填写两个重要的回调函数,一个是收发视频数据的回调,一个是流控制命令的回调。
收发视频数据的回调完成真正的视频数据传输。
摄像头驱动的基本工作流程就如上面所说,原理其实并不复杂,可能只是细节上处理的比较多些。
以上说的只是大致流程,对初学驱动的同学可能根本不理解,所以要完全理解并能自己开发一个虚拟摄像头驱动,
还是得靠自己查阅大量的相关资料。

在收发视频数据回调函数里,一共提供两个命令:
SRB_READ_DATA,从硬件获得视频数据到应用层,由流类驱动定时发送,
SRB_WRITE_DATA  流类驱动提供视频数据,要写到硬件中去。
而一般的摄像头只有从硬件采集数据到电脑,所以只有 SRB_READ_DATA命令。

像VCAM的驱动,两个命令都支持,他提供两个PIN(针脚),
一个输出PIN,用来把”硬件“采集来的数据输出到类驱动,对应SRB_READ_DATA命令。
一个输入PIN,用来把上层发来的数据写到“硬件”里去,对应SRB_WRITE_DATA命令。
在VCAM驱动内部把这两个PIN连接起来,就完成了一个虚拟摄像头,
简单的说就是把输入PIN的数据copy到输出PIN上去,
作为VCAM的视频源,只要在应用层用DShow打开对应的输入PIN,然后直接朝这个PIN写视频数据,
这些都是标准的DirectShow开发。

刚开发虚拟摄像头驱动的时候,我并不知道是采用这种方式来传输视频源数据。
而是想到一个经典的办法:在驱动里创建功能设备,然后依靠功能设备来传输视频源数据。
后来才渐渐体会出VCAM的处理办法,但是我坚持采用自己的办法来解决问题。
于是自己的处理办法如下:

在HW_INITIALIZATION_DATA命令中创建功能设备,在HW_UNINITIALIZATION_DATA中删除功能设备。
最麻烦的是 对这个功能设备的派遣回调函数的处理了, 功能设备只需要处理
IRP_MJ_CREATE,IRP_MJ_CLEANUP,IRP_MJ_CLOSE,IRP_MJ_DEVICE_CONTROL
4个派遣命令, 但是在DriverEntry里调用 StreamClassRegisterAdapter函数之后, 流类驱动已经hook了所有派遣函数。
因此唯一的解决办法,就是在调用StreamClassRegisterAdapter完之后,
保存它原来的派遣函数地址,替换成自己的派遣函数地址。类似如下代码

status = StreamClassRegisterAdapter( DriverObject, RegistryPath, &HwInitData );
if( NT_SUCCESS(status)){
        org_create_function = DriverObject->MajorFunction[IRP_MJ_CREATE];
        org_cleanup_function = DriverObject->MajorFunction[IRP_MJ_CLEANUP];
        org_close_function = DriverObject->MajorFunction[IRP_MJ_CLOSE];
        org_ioctl_function = DriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL];
        ////
        DriverObject->MajorFunction[IRP_MJ_CREATE] =
        DriverObject->MajorFunction[IRP_MJ_CLEANUP] =
        DriverObject->MajorFunction[IRP_MJ_CLOSE] = ioctl_createclose;
        DriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] = ioctl_control;

}

在自己的派遣函数里,如果不是本功能设备的命令,就调用原来的派遣函数,否则就自己处理。
至于如何判断命令是本功能设备发送的,还是其他设备发送的,
我最早使用的办法是保存本功能设备的PDEVICE_OBJECT对象指针,
在派遣函数里判断传递的DEVICE对象是不是自己创建的设备,从而判定是不是本设备发送的命令,
但是这种做法,每次当应用程序打开这个设备,系统卸载驱动的时候,就会造成系统蓝屏崩溃,
最后想到一个更好的处理办法:在IoCreateDevice的时候,创建一个自定义的设备类型,
每次在派遣函数里判断传递进来的设备类型是不是自己创建的功能设备,
是的话就自己处理,否则交给原始派遣函数处理,类似如下代码:
NTSTATUS ioctl_control( PDEVICE_OBJECT obj, PIRP Irp)
{
       if(obj->DeviceType != FILE_DEVICE_MY_VCAMERA_DEVICE)
           return org_ioctl_function( obj, Irp); //FILE_DEVICE_MY_VCAMERA_DEVICE 是自定义的一个宏
        ///////是自己设备发送的命令,处理...
        ........      
}
从而这个问题得到很好的解决。

接着就是视频数据传输的问题了,视频数据都是相当庞大的,不可能把数据从用户空间copy到内核空间,
再从内核空间copy到流类驱动请求的 SRB_READ_DATA的SRB块里,如果这样做的话,跟找死差不多。
因此采用共享内存的办法, 直接把SRB块里的空间映射到应用层程序空间里,具体做法就是:
每当应用程序调用DeviceIoControl发送请求地址命令,
驱动的派遣函数查找排队的SRB块(每个流类驱动发送的SRB_READ_DATA的SRB块,都会被排队等待处理)
如果找到,调用MmMapLockedPagesSpecifyCache等函数,把SRB块里的视频数据内存地址,映射到用户空间地址。
并把这个地址返回给用户, 用户获得这个地址之后,就可以直接朝这个地址填写视频数据了。
当用户填写完视频数据之后,再用DeviceIoControl发送完成命令,
驱动的派遣函数接着把这个Srb提交给流类驱动,这样就完成了一次视频数据帧的操作。

应用程序代码大致如下:
hFile = CreateFile(功能设备名,...);
///
struct ioctl_vcamarg_t
{
    ULARGE_INTEGER   inter_handle; 
    ////
    int                stream_number;   
    int                video_type;//视频类型,比如RGB24,YV12等类型
    int                width;//视频帧的宽度
    int                height; //高度
    int                bit_count;//一个数据点占用多少位
    int                buffer_size;//映射的地址空间大小
    ULARGE_INTEGER   buffer_address;//映射的用户空间地址
};
struct ioctl_vcamarg_t arg;//用于同驱动传递信息的数据结构,里边有映射的用户空间地址
DeviceIoCrontrol( hFile, IOCTL_USE_VCAM_BUFFER,&arg,...); //获得视频数据地址
//接着就可以直接操作这个地址地址空间,比如如下,把整个视频帧填写为灰色。
memcpy( (VOID*)(ULONG_PTR)arg.buffer_address.Quad, 0x80, arg.buffer_size);

DeviceIoControl(和File,IOCTL_END_VCAM_BUFFER,&arg.inter_handle,....); //完成视频数据帧。

因此我对视频源的这种做法,跟通用的做法是有些不同的,但是实际上完成之后,发现效率一点也不差,
而且在视频源的用户层程序的开发,简单了许多,因为没使用一堆的DirectShow的乱七八糟的复杂的接口。

如上,这是整个虚拟摄像头驱动的开发,如果觉得这种这种做法有不对劲的地方,请高手指出,不吝赐教。