基于AVStream的虚拟摄像头原理简介及实现
在我以前的文章中分析了虚拟麦克风和虚拟摄像头(基于UVC技术实现)的原理和实现,虚拟麦克风和虚拟摄像头是现在直播行业中无人值守(数字人)中最基础的技术,如下示例:
前面我们实现了基于UVC(USB总线)的虚拟摄像头,接下来介绍另外一种技术来实现虚拟摄像头——基于AVStream框架的实现。
1. AVStream简介
AVStream是对内核流(Kernel Stream)驱动开发的扩展,取代Windows 2000下面 Kernel Streaming音视频开发技术,基本架构如下:
AVStream是基于Kernel Stream框架扩展的新框架,在该框架下面我们更加方便开发出音视频相关的驱动(例如虚拟摄像头等)。
2. 技术分析
AVStream 对Kernel Stream进行了封装,我们无需像Kernel Stream一样,需要对各种IRP回调函数和IRP包进行处理,取代他的是一种描述符的结构,在AVStream中有如下几种描述符:
KSDEVICE_DESCRIPTOR
设备描述符,描述设备的基本特性。KSFILTER_DESCRIPTOR
过滤器描述符,描述过滤器的特性。KSPIN_DESCRIPTOR_EX
引脚描述符,描述引脚的特征信息。
例如KSDEVICE_DESCRIPTOR
描述了整个设备的信息,主要包括设备的各种PNP回调函数,该结构声明如下:
typedef struct _KSDEVICE_DESCRIPTOR {
const KSDEVICE_DISPATCH *Dispatch;
ULONG FilterDescriptorsCount;
const KSFILTER_DESCRIPTOR const * * FilterDescriptors;
ULONG Version;
ULONG Flags;
PVOID Alignment;
} KSDEVICE_DESCRIPTOR, *PKSDEVICE_DESCRIPTOR;
KSFILTER_DESCRIPTOR
表示过滤器描述符信息,该结构声明如下:
typedef struct _KSFILTER_DESCRIPTOR {
const KSFILTER_DISPATCH *Dispatch;
const KSAUTOMATION_TABLE *AutomationTable;
ULONG Version;
ULONG Flags;
const GUID *ReferenceGuid;
ULONG PinDescriptorsCount;
ULONG PinDescriptorSize;
const KSPIN_DESCRIPTOR_EX *PinDescriptors;
ULONG CategoriesCount;
const GUID *Categories;
ULONG NodeDescriptorsCount;
ULONG NodeDescriptorSize;
const KSNODE_DESCRIPTOR *NodeDescriptors;
ULONG ConnectionsCount;
const KSTOPOLOGY_CONNECTION *Connections;
const KSCOMPONENTID *ComponentId;
} KSFILTER_DESCRIPTOR, *PKSFILTER_DESCRIPTOR;
KSPIN_DESCRIPTOR_EX
表示引脚的描述信息,如下:
typedef struct _KSPIN_DESCRIPTOR_EX {
const KSPIN_DISPATCH *Dispatch;
const KSAUTOMATION_TABLE *AutomationTable;
KSPIN_DESCRIPTOR PinDescriptor;
ULONG Flags;
ULONG InstancesPossible;
ULONG InstancesNecessary;
const KSALLOCATOR_FRAMING_EX *AllocatorFraming;
PFNKSINTERSECTHANDLEREX IntersectHandler;
} KSPIN_DESCRIPTOR_EX, *PKSPIN_DESCRIPTOR_EX;
对于各种描述符中,都定义了一个分发函数表,来响应各个描述符的回调函数,例如:
KSDEVICE_DISPATCH
表示设备PNP相关的回调函数。KSFILTER_DISPATCH
表示过滤器相关的回调函数信息。KSPIN_DISPATCH
表示对于引脚的各种处理回调函数。
对于AVStream框架下面的开发,其实就是完成对描述符和分发函数的处理就可以了。
3. 技术实现
对于AVStream驱动,非常简单使用KsInitializeDriver
就可以完成对整个驱动的初始化了,该函数如下:
KSDDKAPI NTSTATUS KsInitializeDriver(
[in] PDRIVER_OBJECT DriverObject,
[in] PUNICODE_STRING RegistryPathName,
[in, optional] const KSDEVICE_DESCRIPTOR *Descriptor
);
在这里就需要我们提供设备描述符信息,例如:
KSDEVICE_DISPATCH
CaptureDeviceDispatch = {
CCaptureDevice::DispatchCreate, // Pnp Add Device
CCaptureDevice::DispatchPnpStart, // Pnp Start
NULL, // Post-Start
NULL, // Pnp Query Stop
NULL, // Pnp Cancel Stop
CCaptureDevice::DispatchPnpStop, // Pnp Stop
NULL, // Pnp Query Remove
NULL, // Pnp Cancel Remove
NULL, // Pnp Remove
NULL, // Pnp Query Capabilities
NULL, // Pnp Surprise Removal
NULL, // Power Query Power
NULL, // Power Set Power
NULL // Pnp Query Interface
};
KSDEVICE_DESCRIPTOR
CaptureDeviceDescriptor = {
&CaptureDeviceDispatch,
0,
NULL
};
这样我们就完成了整AVStream的框架搭建了;当然一个虚拟摄像头需要工作,最重要的就是需要过滤器,我们使用KsCreateFilterFactory
完成过滤器的创建,例如:
NTSTATUS
CCaptureDevice::PnpStart(
IN PCM_RESOURCE_LIST TranslatedResourceList,
IN PCM_RESOURCE_LIST UntranslatedResourceList
)
{
PAGED_CODE();
NTSTATUS Status = STATUS_SUCCESS;
UNREFERENCED_PARAMETER(TranslatedResourceList);
UNREFERENCED_PARAMETER(UntranslatedResourceList);
if (!m_Device->Started)
{
KsAcquireDevice(m_Device);
Status = KsCreateFilterFactory(m_Device->FunctionalDeviceObject,
&CaptureFilterDescriptor,
L"GLOBAL",
NULL,
KSCREATE_ITEM_FREEONSTOP,
NULL,
NULL,
NULL);
KsReleaseDevice(m_Device);
}
return Status;
}
同样,这里需要提供过滤器的描述符信息:
const
KSFILTER_DESCRIPTOR
CaptureFilterDescriptor = {
&CaptureFilterDispatch, // Dispatch Table
&CaptureFilterAutomationTable, // Automation Table
KSFILTER_DESCRIPTOR_VERSION, // Version
0, // Flags
&KSNAME_Filter, // Reference GUID
DEFINE_KSFILTER_PIN_DESCRIPTORS(CapturePinDescriptors),
DEFINE_KSFILTER_CATEGORIES(CaptureFilterCategories),
0,
sizeof(KSNODE_DESCRIPTOR),
NULL,
0,
NULL,
NULL // Component ID
};
当我们完成过滤器描述符的定义之后,整个虚拟摄像头就基本完成了框架的开发,后面只需要对数据进行处理了;在虚拟摄像头中,有两种数据处理方式:
- 基于过滤器进行处理。
- 基于引脚进行数据处理。
一般来说我们都是在引脚下面对各自的数据进行处理,数据处理有回调函数来完成:
struct _KSPIN_DISPATCH {
PFNKSPINIRP Create;
PFNKSPINIRP Close;
PFNKSPIN Process;
PFNKSPINVOID Reset;
PFNKSPINSETDATAFORMAT SetDataFormat;
PFNKSPINSETDEVICESTATE SetDeviceState;
PFNKSPIN Connect;
PFNKSPINVOID Disconnect;
const KSCLOCK_DISPATCH* Clock;
const KSALLOCATOR_DISPATCH* Allocator;
};
我们可以在PFNKSPIN Process
函数中对数据帧做处理,使用KsPinGetLeadingEdgeStreamPointer
可以获取到用户层获取摄像头图像帧的请求数据
KSDDKAPI PKSSTREAM_POINTER KsPinGetLeadingEdgeStreamPointer(
PKSPIN Pin,
KSSTREAM_POINTER_STATE State
);
KSSTREAM_POINTER
表示用户层请求的图像帧的内存信息,我们在虚拟摄像头中只需要像该内存中提供图片信息即可,该结构定义如下:
typedef struct _KSSTREAM_POINTER {
PVOID Context;
PKSPIN Pin;
PKSSTREAM_HEADER StreamHeader;
PKSSTREAM_POINTER_OFFSET Offset;
KSSTREAM_POINTER_OFFSET OffsetIn;
KSSTREAM_POINTER_OFFSET OffsetOut;
} KSSTREAM_POINTER, *PKSSTREAM_POINTER;
在KSSTREAM_POINTER_OFFSET OffsetOut
中记录中请求帧的内存信息,该结构如下:
typedef struct _KSSTREAM_POINTER_OFFSET {
union {
PUCHAR Data;
PKSMAPPING Mappings;
};
PUCHAR Data;
PVOID Alignment;
ULONG Count;
ULONG Remaining;
} KSSTREAM_POINTER_OFFSET, *PKSSTREAM_POINTER_OFFSET;
例如,我们只需要通过RtlCopyMemory(ClonePointer->StreamHeader->Data, ImageBuffer, ImageSize)
将图像拷贝到缓存里面,那么用户层通过虚拟摄像头请求到的数据就是我们指定的图像内容了。
4. 实现效果
通过上面实现虚拟摄像头驱动编译之后,得到三个文件
nanovirtualcameradevice.cat
NanoVirtualCameraDevice.inf
NanoVirtualCameraDevice.sys
我们对NanoVirtualCameraDevice.inf
安装驱动,可以发现如下虚拟摄像头设备:
我们可以向虚拟摄像头提交自定义的图片或者播放自己的视频(如下是我播放的一个视频截图):