基于AVStream的虚拟摄像头原理简介及实现

本文介绍了基于AVStream技术的虚拟摄像头原理,包括其与UVC的关系,AVStream对KernelStream的封装,以及如何使用设备描述符、过滤器描述符和引脚描述符进行驱动开发。通过实例展示了驱动初始化、过滤器创建和数据处理的过程,最后展示了实际的驱动文件和使用效果。
摘要由CSDN通过智能技术生成

基于AVStream的虚拟摄像头原理简介及实现

在我以前的文章中分析了虚拟麦克风和虚拟摄像头(基于UVC技术实现)的原理和实现,虚拟麦克风和虚拟摄像头是现在直播行业中无人值守(数字人)中最基础的技术,如下示例:

  1. 虚拟摄像头:基于USB总线的虚拟摄像头(UVC)原理简介
  2. 虚拟麦克风:如何在云电脑串流中实现声音输入输出虚拟声卡(虚拟扬声器和虚拟麦克风)开发

前面我们实现了基于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安装驱动,可以发现如下虚拟摄像头设备:
在这里插入图片描述

我们可以向虚拟摄像头提交自定义的图片或者播放自己的视频(如下是我播放的一个视频截图):
在这里插入图片描述

  • 12
    点赞
  • 31
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
要使用Qt和FFmpeg实现网络摄像头视频录制,您需要做以下几个步骤: 1. 安装FFmpeg库。您可以从官方网站下载预编译的二进制文件,或者自己编译源代码。 2. 在Qt项目中导入FFmpeg库。您可以使用Qt的插件系统或手动链接库文件。 3. 创建一个Qt界面,用于显示摄像头捕捉到的视频流。 4. 使用FFmpeg库编写代码,将摄像头捕捉到的视频流转换为所需的格式并保存到本地文件。 以下是一个简单的示例代码,演示如何使用Qt和FFmpeg实现网络摄像头视频录制: ```cpp #include <QApplication> #include <QMainWindow> #include <QVBoxLayout> #include <QLabel> #include <QTimer> #include <QDebug> #include <QDateTime> #include <QThread> extern "C" { #include "libavcodec/avcodec.h" #include "libavformat/avformat.h" #include "libswscale/swscale.h" } #define WIDTH 640 #define HEIGHT 480 #define FPS 25 class CameraWidget : public QWidget { public: CameraWidget(QWidget *parent = nullptr) : QWidget(parent) { label_ = new QLabel(this); QVBoxLayout *layout = new QVBoxLayout(this); layout->addWidget(label_); setLayout(layout); } virtual ~CameraWidget() {} void start() { timer_ = new QTimer(this); connect(timer_, SIGNAL(timeout()), this, SLOT(update())); timer_->start(1000/FPS); } void stop() { timer_->stop(); delete timer_; timer_ = nullptr; } protected: void update() { // Capture camera frame QImage image(WIDTH, HEIGHT, QImage::Format_RGB888); for (int y = 0; y < HEIGHT; y++) { for (int x = 0; x < WIDTH; x++) { QRgb color = qRgb(qrand() % 256, qrand() % 256, qrand() % 256); image.setPixel(x, y, color); } } // Display camera frame label_->setPixmap(QPixmap::fromImage(image)); // Save camera frame to file if (recording_) { if (!formatContext_) { initFormatContext(); } if (formatContext_ && videoStream_) { AVPacket packet; av_init_packet(&packet); packet.data = nullptr; packet.size = 0; if (av_new_packet(&packet, WIDTH * HEIGHT * 3) < 0) { qWarning() << "Failed to allocate packet"; } SwsContext *swsContext = sws_getContext(WIDTH, HEIGHT, AV_PIX_FMT_RGB24, WIDTH, HEIGHT, videoStream_->codecpar->format, SWS_BILINEAR, nullptr, nullptr, nullptr); if (swsContext) { uint8_t *srcData[4] = {(uint8_t *)image.bits(), nullptr, nullptr, nullptr}; int srcLinesize[4] = {image.bytesPerLine(), 0, 0, 0}; uint8_t *dstData[4] = {nullptr, nullptr, nullptr, nullptr}; int dstLinesize[4] = {0, 0, 0, 0}; av_image_alloc(dstData, dstLinesize, videoStream_->codecpar->width, videoStream_->codecpar->height, videoStream_->codecpar->format, 1); if (dstData[0] && av_image_fill_arrays(videoFrame_->data, videoFrame_->linesize, dstData[0], videoStream_->codecpar->format, videoStream_->codecpar->width, videoStream_->codecpar->height, 1) >= 0) { sws_scale(swsContext, srcData, srcLinesize, 0, HEIGHT, videoFrame_->data, videoFrame_->linesize); videoFrame_->pts = pts_++; if (avcodec_send_frame(videoCodecContext_, videoFrame_) == 0) { while (avcodec_receive_packet(videoCodecContext_, &packet) == 0) { packet.stream_index = videoStream_->index; packet.pts = av_rescale_q(packet.pts, videoCodecContext_->time_base, videoStream_->time_base); packet.dts = av_rescale_q(packet.dts, videoCodecContext_->time_base, videoStream_->time_base); packet.duration = av_rescale_q(packet.duration, videoCodecContext_->time_base, videoStream_->time_base); packet.pos = -1; if (av_interleaved_write_frame(formatContext_, &packet) < 0) { qWarning() << "Failed to write packet"; } av_packet_unref(&packet); } } } av_freep(&dstData[0]); sws_freeContext(swsContext); } av_packet_unref(&packet); } } } void initFormatContext() { QString filename = QDateTime::currentDateTime().toString("yyyy-MM-dd_hh-mm-ss"); filename = QDir::tempPath() + "/" + filename + ".avi"; if (avformat_alloc_output_context2(&formatContext_, nullptr, "avi", filename.toUtf8().constData()) < 0) { qWarning() << "Failed to allocate output format context"; return; } if (avio_open(&formatContext_->pb, filename.toUtf8().constData(), AVIO_FLAG_WRITE) < 0) { qWarning() << "Failed to open output file"; avformat_free_context(formatContext_); formatContext_ = nullptr; return; } videoStream_ = avformat_new_stream(formatContext_, nullptr); if (!videoStream_) { qWarning() << "Failed to create video stream"; avformat_free_context(formatContext_); formatContext_ = nullptr; return; } videoCodecContext_ = avcodec_alloc_context3(nullptr); if (!videoCodecContext_) { qWarning() << "Failed to allocate codec context"; avformat_free_context(formatContext_); formatContext_ = nullptr; return; } videoCodecContext_->codec_id = AV_CODEC_ID_H264; videoCodecContext_->codec_type = AVMEDIA_TYPE_VIDEO; videoCodecContext_->width = WIDTH; videoCodecContext_->height = HEIGHT; videoCodecContext_->pix_fmt = AV_PIX_FMT_YUV420P; videoCodecContext_->time_base = av_make_q(1, FPS); videoStream_->codecpar->codec_id = AV_CODEC_ID_H264; videoStream_->codecpar->codec_type = AVMEDIA_TYPE_VIDEO; videoStream_->codecpar->width = WIDTH; videoStream_->codecpar->height = HEIGHT; videoStream_->codecpar->format = videoCodecContext_->pix_fmt; videoStream_->time_base = av_make_q(1, FPS); if (avcodec_open2(videoCodecContext_, avcodec_find_encoder(videoCodecContext_->codec_id), nullptr) < 0) { qWarning() << "Failed to open codec"; avformat_free_context(formatContext_); formatContext_ = nullptr; return; } if (avformat_write_header(formatContext_, nullptr) < 0) { qWarning() << "Failed to write header"; avformat_free_context(formatContext_); formatContext_ = nullptr; return; } videoFrame_ = av_frame_alloc(); if (!videoFrame_) { qWarning() << "Failed to allocate frame"; avformat_free_context(formatContext_); formatContext_ = nullptr; return; } videoFrame_->format = videoCodecContext_->pix_fmt; videoFrame_->width = WIDTH; videoFrame_->height = HEIGHT; if (av_frame_get_buffer(videoFrame_, 32) < 0) { qWarning() << "Failed to allocate frame buffer"; avformat_free_context(formatContext_); formatContext_ = nullptr; return; } pts_ = 0; } private: QLabel *label_; QTimer *timer_ = nullptr; bool recording_ = false; AVFormatContext *formatContext_ = nullptr; AVStream *videoStream_ = nullptr; AVCodecContext *videoCodecContext_ = nullptr; AVFrame *videoFrame_ = nullptr; int64_t pts_ = 0; }; int main(int argc, char *argv[]) { QApplication a(argc, argv); QMainWindow mainWindow; CameraWidget cameraWidget(&mainWindow); mainWindow.setCentralWidget(&cameraWidget); cameraWidget.start(); QThread::sleep(10); cameraWidget.stop(); return a.exec(); } ``` 该示例代码使用Qt中的QTimer类定期捕获摄像头的视频流,并将其显示在界面上。如果设置recording_为true,则使用FFmpeg库将当前帧转换为指定格式并写入文件。请注意,此示例代码仅是演示如何使用Qt和FFmpeg实现网络摄像头视频录制的示例,您需要根据您的实际需求进行修改和调整。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值