文章目录
基于AVStream框架的虚拟摄像头开发实现
在前面文章中,我们分析过虚拟摄像头有两种实现方式:
- 基于DirectShow框架的虚拟摄像头实现。
- 基于驱动的虚拟摄像头实现。
并且在前面文章中分析了基于DirectShow框架的方案(可以参见基于DirectShow框架的虚拟摄像头开发实现),在这里我们分析基于AVStream驱动框架的摄像头方案,该方案可以做到稳定性好,兼容几乎所有的应用软件。
1. AVStream技术概览
AVStream是用来取代Kernel Stream的音视频开发的一种新框架,其大致架构如下:
如上图我们可以发现其都是基于KS框架来扩展的,扩展基于Port Class的驱动框架(用来开发音频驱动),基于AVStream的驱动框架(用来开发视频驱动)。这个框架有两个比较明显的优势:
- 开发更加便捷和简单。
- 为音视频的处理提供了统一的内核流处理模型。
在AVStream框架中,都是基于对象来进行抽象和管理,整个对象形成一个类似树状的结构,框架如下:
如上图中所示:
- 对于一个AVStream驱动的设备,使用AVStream Device对象作为整个结构的树根。
- AVStream Device对象可以设置多个AVStream Filter Factory对象,用来创建过滤器。
- 对于一个过滤器可以可以包含多个AVStream Pin Factory。
- AVStream Pin Factory可以用来创建引脚实例。
AVStream的主要工作就是组织这些对象,让其协同工作。
2. 描述符
通过上面我们可以看到整个AVStream的核心工作原理是各自的对象来实现的,AVStream框架已经为我们做好了所有的处理流程(例如IRP数据的处理),我们只需要定义这些对象即可。在AVStream框架中,使用描述符来定义各自的对象,可以分为三种描述符:
- 设备描述符。
- 过滤器描述符。
- 引脚描述符。
设备描述符包括设备的信息,例如设备的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;
通过这几个描述符,我们就定义了整个驱动的对象属性信息,AVStream框架就可以通过这些属性创建各自的设备对象,过滤器工厂,过滤器实例,引脚工厂,引脚实例等结构。
3. 分发表
在AVStrem架构中框架已经替我们做好了大部分的事情了,但是我们怎么和框架进行交互呢?这个就是分发表的作用,在每个描述符中都有一个KSXXX_DISPATCH
定义着各种回调函数。
KSDEVICE_DISPATCH
表示设备创建和PNP处理相关的回调接口,定义如下:
typedef struct _KSDEVICE_DISPATCH {
PFNKSDEVICECREATE Add;
PFNKSDEVICEPNPSTART Start;
PFNKSDEVICE PostStart;
PFNKSDEVICEIRP QueryStop;
PFNKSDEVICEIRPVOID CancelStop;
PFNKSDEVICEIRPVOID Stop;
PFNKSDEVICEIRP QueryRemove;
PFNKSDEVICEIRPVOID CancelRemove;
PFNKSDEVICEIRPVOID Remove;
PFNKSDEVICEQUERYCAPABILITIES QueryCapabilities;
PFNKSDEVICEIRPVOID SurpriseRemoval;
PFNKSDEVICEQUERYPOWER QueryPower;
PFNKSDEVICESETPOWER SetPower;
PFNKSDEVICEIRP QueryInterface;
} KSDEVICE_DISPATCH, *PKSDEVICE_DISPATCH;
KSFILTER_DISPATCH
和KSPIN_DISPATCH
分别表示过滤器和引脚相关的回调函数,定义如下:
typedef struct _KSFILTER_DISPATCH {
PFNKSFILTERIRP Create;
PFNKSFILTERIRP Close;
PFNKSFILTERPROCESS Process;
PFNKSFILTERVOID Reset;
} KSFILTER_DISPATCH, *PKSFILTER_DISPATCH;
typedef 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;
} KSPIN_DISPATCH, *PKSPIN_DISPATCH;
通过上面KSFILTER_DISPATCH
和KSPIN_DISPATCH
的定义,我们可以发现两个分发表中都有一个Process
处理函数,那么具体处理数据的是哪一个呢?这里就涉及到数据的两种处理模式:
- 基于过滤器的处理模式。
- 基于引脚的处理模式。
在我们的实际情况中,我们很少使用基于过滤器的处理模式,都是在各自的引脚中处理其数据。
4. KSOBJECT_BAG
KSOBJECT_BAG
是用来管理对象的一个容器,在上述的KSDEVICE
、KSFILTERFACTORY
、 KSFILTER
和 KSPIN
结构中都存在KSOBJECT_BAG
,我们以其中一个为例,如下:
typedef struct _KSDEVICE {
const KSDEVICE_DESCRIPTOR *Descriptor;
KSOBJECT_BAG Bag;
PVOID Context;
PDEVICE_OBJECT FunctionalDeviceObject;
PDEVICE_OBJECT PhysicalDeviceObject;
PDEVICE_OBJECT NextDeviceObject;
BOOLEAN Started;
SYSTEM_POWER_STATE SystemPowerState;
DEVICE_POWER_STATE DevicePowerState;
} KSDEVICE, *PKSDEVICE;
我们使用KsAddItemToObjectBag
将对象添加到KSOBJECT_BAG
,并且指定释放函数,当KSOBJECT_BAG
被删除的时候,释放函数就会自动调用,该函数声明如下:
KSDDKAPI NTSTATUS KsAddItemToObjectBag(
KSOBJECT_BAG ObjectBag,
__drv_aliasesMem PVOID Item,
PFNKSFREE Free
);
一般来说我们可以使用如下:
Status = KsAddItemToObjectBag(Pin->Bag,
reinterpret_cast <PVOID> (CapPin),
reinterpret_cast <PFNKSFREE>(CCapturePin::Cleanup));
5. StreamPoint
当我们用户层发起读取(写入)的IRP请求之后,AVStream就会将IRP分解成多个数据帧,然后放入队列,并且使用流指针来指向数据帧的抽象,整个框架如下:
如上图,整个队列中存在一个头和尾的指针,分别使用如下函数获取:
KSDDKAPI PKSSTREAM_POINTER KsPinGetLeadingEdgeStreamPointer(
[in] PKSPIN Pin,
[in] KSSTREAM_POINTER_STATE State
);
KSDDKAPI PKSSTREAM_POINTER KsPinGetTrailingEdgeStreamPointer(
[in] PKSPIN Pin,
[in] 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;
typedef struct {
ULONG Size;
ULONG TypeSpecificFlags;
KSTIME PresentationTime;
LONGLONG Duration;
ULONG FrameExtent;
ULONG DataUsed;
PVOID Data;
ULONG OptionsFlags;
ULONG Reserved;
} KSSTREAM_HEADER, *PKSSTREAM_HEADER;
其中KSSTREAM_HEADER
表示数据帧,里面有数据的地址和其他信息。对于KSSTREAM_POINTER
指向的数据,我们可以分多次处理,如果我们处理了部分数据,那么就需要对KSSTREAM_POINTER_OFFSET
进行处理,如下:
KSDDKAPI NTSTATUS KsStreamPointerAdvanceOffsets(
[in] PKSSTREAM_POINTER StreamPointer,
[in] ULONG InUsed,
[in] ULONG OutUsed,
[in] BOOLEAN Eject
);
6. 实现
虚拟摄像头的开发大致可以分为三个部分:
- 整个驱动框架的处理。
- 各个描述符的定义。
- 数据的模拟处理。
下面分别看一下这三个模块的实现过程。
6.1 框架实现
基于AVStream框架实现的虚拟摄像头实现特别简单,使用KsInitializeDriver
初始化驱动即可,如下:
extern "C"
NTSTATUS
DriverEntry(
IN PDRIVER_OBJECT DriverObject,
IN PUNICODE_STRING RegistryPath
)
{
return KsInitializeDriver(DriverObject, RegistryPath, &CaptureDeviceDescriptor);
}
这里我们只需要提供好设备描述符信息即可,我们定义设备描述符如下:
const
KSDEVICE_DESCRIPTOR
CaptureDeviceDescriptor =
{
&CaptureDeviceDispatch,
0,
NULL
};
6.2 描述符的实现
我们在设备启动IRP_MN_START_DEVICE
的回调函数CCaptureDevice::PnpStart
中,动态创建过滤器工厂,代码如下:
Status = KsCreateFilterFactory(m_Device->FunctionalDeviceObject,
&CaptureFilterDescriptor,
L"GLOBAL",
NULL,
KSCREATE_ITEM_FREEONSTOP,
NULL,
NULL,
NULL);
这里我们只需要提供过滤器工厂的描述符,框架就会完成其他工作了,描述定义如下:
const
KSFILTER_DESCRIPTOR
CaptureFilterDescriptor =
{
&CaptureFilterDispatch, // Dispatch Table
NULL, // Automation Table
KSFILTER_DESCRIPTOR_VERSION, // Version
0, // Flags
&KSNAME_Filter, // Reference GUID
DEFINE_KSFILTER_PIN_DESCRIPTORS(CaptureFilterPinDescriptors),
DEFINE_KSFILTER_CATEGORIES(CaptureFilterCategories),
0,
sizeof(KSNODE_DESCRIPTOR),
NULL,
0,
NULL,
NULL // Component ID
};
6.3 数据处理
这里我们分析基于引脚的处理模型,在引脚中提供了分发函数如下:
const
KSPIN_DISPATCH
CapturePinDispatch = {
CCapturePin::DispatchCreate, // Pin Create
NULL, // Pin Close
CCapturePin::DispatchProcess, // Pin Process
NULL, // Pin Reset
CCapturePin::DispatchSetFormat, // Pin Set Data Format
CCapturePin::DispatchSetState, // Pin Set Device State
NULL, // Pin Connect
NULL, // Pin Disconnect
NULL, // Clock Dispatch
NULL // Allocator Dispatch
};
只需要在CCapturePin::DispatchProcess
对数据帧进行图像数据提供即可,如下:
NTSTATUS
CCapturePin::Process(
)
{
PAGED_CODE();
NTSTATUS Status = STATUS_SUCCESS;
PKSSTREAM_POINTER Leading;
Leading = KsPinGetLeadingEdgeStreamPointer(m_Pin, KSSTREAM_POINTER_STATE_LOCKED);
while (NT_SUCCESS(Status) && Leading)
{
PKSSTREAM_POINTER ClonePointer;
PSTREAM_POINTER_CONTEXT SPContext = NULL;
if (NULL == Leading->StreamHeader->Data)
{
Status = KsStreamPointerAdvance(Leading);
continue;
}
if (!m_PreviousStreamPointer)
{
Status = KsStreamPointerClone(Leading,
NULL,
sizeof(STREAM_POINTER_CONTEXT),
&ClonePointer);
if (NT_SUCCESS(Status))
{
ClonePointer->StreamHeader->DataUsed = 0;
SPContext = reinterpret_cast<PSTREAM_POINTER_CONTEXT>(ClonePointer->Context);
SPContext->BufferVirtual = reinterpret_cast <PUCHAR>(ClonePointer->StreamHeader->Data);
}
}
else
{
ClonePointer = m_PreviousStreamPointer;
SPContext = reinterpret_cast<PSTREAM_POINTER_CONTEXT>(ClonePointer->Context);
Status = STATUS_SUCCESS;
}
if (!NT_SUCCESS(Status))
{
KsStreamPointerUnlock(Leading, FALSE);
break;
}
ULONG MappingsUsed = m_Device->ProgramScatterGatherMappings(ClonePointer,
&(SPContext->BufferVirtual),
Leading->OffsetOut.Mappings,
Leading->OffsetOut.Remaining);
if (MappingsUsed == Leading->OffsetOut.Remaining)
{
m_PreviousStreamPointer = NULL;
}
else
{
m_PreviousStreamPointer = ClonePointer;
}
if (MappingsUsed)
{
Status = KsStreamPointerAdvanceOffsets(Leading,
0,
MappingsUsed,
FALSE);
}
else
{
Status = STATUS_PENDING;
break;
}
}
if (!Leading)
{
m_PendIo = TRUE;
Status = STATUS_PENDING;
}
if (NT_SUCCESS(Status) && Leading)
{
KsStreamPointerUnlock(Leading, FALSE);
}
else
{
if (Status == STATUS_DEVICE_NOT_READY)
{
Status = STATUS_SUCCESS;
}
}
if (!NT_SUCCESS(Status) || Status == STATUS_PENDING)
{
m_PendIo = TRUE;
}
return Status;
}
7. 总结
在数据处理函数CCapturePin::Process
中,我们提供自己的图像数据,就可以完成定制化的虚拟摄像头了,虚拟摄像头如下:
可以使用GraphEdit来渲染虚拟摄像头的数据,如下: