win-dshow: Add Virtual Camera (Windows)
The virtual camera adds the ability to use the output of OBS itself as a camera that can be selected within other Windows applications. This is very loosely based upon the catxfish virtual camera plugin design. There is a shared memory queue, but instead of having 10-20 frames in the queue, there are now only 3 frames in the queue to minimize latency and reduce memory usage. The third frame is mostly to ensure that writing does not occur on the same frame being read; the delay is merely one frame at all times. The frames of the shared memory queue are NV12 instead of YUYV, which reduces the memory and data copied, as well as eliminate unnecessary conversion from NV12. Some programs (such as chrome, which uses webrtc to capture) do not support NV12 however, so an I420 conversion is provided, which is far less expensive than YUYV. The CPU cost of NV12 -> I420 is negligible in comparison. The virtual camera filter itself is based upon the output filter within the libdshowcapture library, which was originally implemented for other purposes. This is more ideal than the Microsoft example code because for one, it's far less convoluted, two, allows us to be able to customize the filter to our needs a bit more easily, and three, has much better RAII. The Microsoft CBaseFilter/etc code comprised of about 30 source files, where as the output filter comprises of two or three required source files which we already had, so it's a huge win to compile time. Scaling is avoided whenever possible to minimize CPU usage. When the virtual camera is activated in OBS, the width, height, and frame interval are saved, that way if the filter is activated, it will always remember the last OBS resolution/interval that the virtual camera was activated with, even if OBS is not active. If for some reason the filter activates before OBS starts up, and OBS starts up with a different resolution, it will use simple point scaling intermittently, and then will remember the new scaling in the future. The scaler could use some optimization. FFmpeg was not opted for because the FFmpeg DLLs would have to be provided for both architectures, which would be about 30 megabytes in total, and would make writing the plugin much more painful. Thus a simple point scaling algorithm is used, and scaling is avoided whenever possible. (If another willing participant wants to have a go at improving the scaling then go for it. But otherwise, it avoids scaling whenever possible anyway, so it's not a huge deal)
This commit is contained in:
parent
79fff2e13b
commit
6377fe3177
@ -21,6 +21,14 @@ set(win-dshow_SOURCES
|
||||
ffmpeg-decode.c
|
||||
win-dshow.rc)
|
||||
|
||||
set(virtualcam-output_SOURCES
|
||||
tiny-nv12-scale.c
|
||||
shared-memory-queue.c
|
||||
virtualcam.c)
|
||||
set(virtualcam-output_HEADERS
|
||||
tiny-nv12-scale.h
|
||||
shared-memory-queue.h)
|
||||
|
||||
set(libdshowcapture_SOURCES
|
||||
libdshowcapture/source/capture-filter.cpp
|
||||
libdshowcapture/source/output-filter.cpp
|
||||
@ -54,6 +62,8 @@ set(libdshowcapture_HEADERS
|
||||
add_library(win-dshow MODULE
|
||||
${win-dshow_SOURCES}
|
||||
${win-dshow_HEADERS}
|
||||
${virtualcam-output_SOURCES}
|
||||
${virtualcam-output_HEADERS}
|
||||
${libdshowcapture_SOURCES}
|
||||
${libdshowcapture_HEADERS})
|
||||
target_link_libraries(win-dshow
|
||||
@ -61,7 +71,13 @@ target_link_libraries(win-dshow
|
||||
strmiids
|
||||
ksuser
|
||||
wmcodecdspuuid
|
||||
w32-pthreads
|
||||
${FFMPEG_LIBRARIES})
|
||||
set_target_properties(win-dshow PROPERTIES FOLDER "plugins")
|
||||
set_target_properties(win-dshow PROPERTIES FOLDER "plugins/win-dshow")
|
||||
|
||||
source_group("libdshowcapture\\Source Files" FILES ${libdshowcapture_SOURCES})
|
||||
source_group("libdshowcapture\\Header Files" FILES ${libdshowcapture_HEADERS})
|
||||
|
||||
install_obs_plugin_with_data(win-dshow data)
|
||||
|
||||
add_subdirectory(virtualcam-module)
|
||||
|
BIN
plugins/win-dshow/data/placeholder.png
Normal file
BIN
plugins/win-dshow/data/placeholder.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 451 KiB |
@ -1,4 +1,7 @@
|
||||
#include <obs-module.h>
|
||||
#include <strsafe.h>
|
||||
#include <strmif.h>
|
||||
#include "virtualcam-guid.h"
|
||||
|
||||
OBS_DECLARE_MODULE()
|
||||
OBS_MODULE_USE_DEFAULT_LOCALE("win-dshow", "en-US")
|
||||
@ -10,9 +13,41 @@ MODULE_EXPORT const char *obs_module_description(void)
|
||||
extern void RegisterDShowSource();
|
||||
extern void RegisterDShowEncoders();
|
||||
|
||||
extern "C" struct obs_output_info virtualcam_info;
|
||||
|
||||
static bool vcam_installed(bool b64)
|
||||
{
|
||||
wchar_t cls_str[CHARS_IN_GUID];
|
||||
wchar_t temp[MAX_PATH];
|
||||
HKEY key = nullptr;
|
||||
|
||||
StringFromGUID2(CLSID_OBS_VirtualVideo, cls_str, CHARS_IN_GUID);
|
||||
StringCbPrintf(temp, sizeof(temp), L"CLSID\\%s", cls_str);
|
||||
|
||||
DWORD flags = KEY_READ;
|
||||
flags |= b64 ? KEY_WOW64_64KEY : KEY_WOW64_32KEY;
|
||||
|
||||
LSTATUS status = RegOpenKeyExW(HKEY_CLASSES_ROOT, temp, 0, flags, &key);
|
||||
if (status != ERROR_SUCCESS) {
|
||||
return false;
|
||||
}
|
||||
|
||||
RegCloseKey(key);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool obs_module_load(void)
|
||||
{
|
||||
RegisterDShowSource();
|
||||
RegisterDShowEncoders();
|
||||
obs_register_output(&virtualcam_info);
|
||||
|
||||
if (vcam_installed(false)) {
|
||||
obs_data_t *obs_settings = obs_data_create();
|
||||
obs_data_set_bool(obs_settings, "vcamEnabled", true);
|
||||
obs_apply_private_data(obs_settings);
|
||||
obs_data_release(obs_settings);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
@ -1 +1 @@
|
||||
Subproject commit 03fbb3814b2ad678f5be4b23b8ca05fb4172e12e
|
||||
Subproject commit 7a495a9d3844152e3c43d14855a386d044ecd411
|
209
plugins/win-dshow/shared-memory-queue.c
Normal file
209
plugins/win-dshow/shared-memory-queue.c
Normal file
@ -0,0 +1,209 @@
|
||||
#include <windows.h>
|
||||
#include "shared-memory-queue.h"
|
||||
#include "tiny-nv12-scale.h"
|
||||
|
||||
#define VIDEO_NAME L"OBSVirtualCamVideo"
|
||||
|
||||
enum queue_type {
|
||||
SHARED_QUEUE_TYPE_VIDEO,
|
||||
};
|
||||
|
||||
struct queue_header {
|
||||
volatile uint32_t write_idx;
|
||||
volatile uint32_t read_idx;
|
||||
volatile uint32_t state;
|
||||
|
||||
uint32_t offsets[3];
|
||||
|
||||
uint32_t type;
|
||||
|
||||
uint32_t cx;
|
||||
uint32_t cy;
|
||||
uint64_t interval;
|
||||
|
||||
uint32_t reserved[8];
|
||||
};
|
||||
|
||||
struct video_queue {
|
||||
HANDLE handle;
|
||||
bool ready_to_read;
|
||||
struct queue_header *header;
|
||||
uint64_t *ts[3];
|
||||
uint8_t *frame[3];
|
||||
long last_inc;
|
||||
int dup_counter;
|
||||
bool is_writer;
|
||||
};
|
||||
|
||||
#define ALIGN_SIZE(size, align) size = (((size) + (align - 1)) & (~(align - 1)))
|
||||
#define FRAME_HEADER_SIZE 32
|
||||
|
||||
video_queue_t *video_queue_create(uint32_t cx, uint32_t cy, uint64_t interval)
|
||||
{
|
||||
struct video_queue vq = {0};
|
||||
struct video_queue *pvq;
|
||||
DWORD frame_size = cx * cy * 3 / 2;
|
||||
uint32_t offset_frame[3];
|
||||
DWORD size;
|
||||
|
||||
size = sizeof(struct queue_header);
|
||||
|
||||
ALIGN_SIZE(size, 32);
|
||||
|
||||
offset_frame[0] = size;
|
||||
size += frame_size + FRAME_HEADER_SIZE;
|
||||
ALIGN_SIZE(size, 32);
|
||||
|
||||
offset_frame[1] = size;
|
||||
size += frame_size + FRAME_HEADER_SIZE;
|
||||
ALIGN_SIZE(size, 32);
|
||||
|
||||
offset_frame[2] = size;
|
||||
size += frame_size + FRAME_HEADER_SIZE;
|
||||
ALIGN_SIZE(size, 32);
|
||||
|
||||
struct queue_header header = {0};
|
||||
|
||||
header.state = SHARED_QUEUE_STATE_STARTING;
|
||||
header.cx = cx;
|
||||
header.cy = cy;
|
||||
header.interval = interval;
|
||||
vq.is_writer = true;
|
||||
|
||||
for (size_t i = 0; i < 3; i++) {
|
||||
uint32_t off = offset_frame[i];
|
||||
header.offsets[i] = off;
|
||||
}
|
||||
|
||||
/* fail if already in use */
|
||||
vq.handle = OpenFileMappingW(FILE_MAP_READ, false, VIDEO_NAME);
|
||||
if (vq.handle) {
|
||||
CloseHandle(vq.handle);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
vq.handle = CreateFileMappingW(INVALID_HANDLE_VALUE, NULL,
|
||||
PAGE_READWRITE, 0, size, VIDEO_NAME);
|
||||
if (!vq.handle) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
vq.header = (struct queue_header *)MapViewOfFile(
|
||||
vq.handle, FILE_MAP_ALL_ACCESS, 0, 0, 0);
|
||||
memcpy(vq.header, &header, sizeof(header));
|
||||
|
||||
for (size_t i = 0; i < 3; i++) {
|
||||
uint32_t off = offset_frame[i];
|
||||
vq.ts[i] = (uint64_t *)(((uint8_t *)vq.header) + off);
|
||||
vq.frame[i] = ((uint8_t *)vq.header) + off + FRAME_HEADER_SIZE;
|
||||
}
|
||||
pvq = malloc(sizeof(vq));
|
||||
memcpy(pvq, &vq, sizeof(vq));
|
||||
return pvq;
|
||||
}
|
||||
|
||||
video_queue_t *video_queue_open()
|
||||
{
|
||||
struct video_queue vq = {0};
|
||||
|
||||
vq.handle = OpenFileMappingW(FILE_MAP_READ, false, VIDEO_NAME);
|
||||
if (!vq.handle) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
vq.header = (struct queue_header *)MapViewOfFile(
|
||||
vq.handle, FILE_MAP_READ, 0, 0, 0);
|
||||
|
||||
struct video_queue *pvq = malloc(sizeof(vq));
|
||||
memcpy(pvq, &vq, sizeof(vq));
|
||||
return pvq;
|
||||
}
|
||||
|
||||
void video_queue_close(video_queue_t *vq)
|
||||
{
|
||||
if (!vq) {
|
||||
return;
|
||||
}
|
||||
if (vq->is_writer) {
|
||||
vq->header->state = SHARED_QUEUE_STATE_STOPPING;
|
||||
}
|
||||
|
||||
UnmapViewOfFile(vq->header);
|
||||
CloseHandle(vq->handle);
|
||||
free(vq);
|
||||
}
|
||||
|
||||
void video_queue_get_info(video_queue_t *vq, uint32_t *cx, uint32_t *cy,
|
||||
uint64_t *interval)
|
||||
{
|
||||
struct queue_header *qh = vq->header;
|
||||
*cx = qh->cx;
|
||||
*cy = qh->cy;
|
||||
*interval = qh->interval;
|
||||
}
|
||||
|
||||
#define get_idx(inc) ((unsigned long)inc % 3)
|
||||
|
||||
void video_queue_write(video_queue_t *vq, uint8_t **data, uint32_t *linesize,
|
||||
uint64_t timestamp)
|
||||
{
|
||||
struct queue_header *qh = vq->header;
|
||||
long inc = ++qh->write_idx;
|
||||
|
||||
unsigned long idx = get_idx(inc);
|
||||
size_t size = linesize[0] * qh->cy;
|
||||
|
||||
*vq->ts[idx] = timestamp;
|
||||
memcpy(vq->frame[idx], data[0], size);
|
||||
memcpy(vq->frame[idx] + size, data[1], size / 2);
|
||||
|
||||
qh->read_idx = inc;
|
||||
qh->state = SHARED_QUEUE_STATE_READY;
|
||||
}
|
||||
|
||||
enum queue_state video_queue_state(video_queue_t *vq)
|
||||
{
|
||||
if (!vq) {
|
||||
return SHARED_QUEUE_STATE_INVALID;
|
||||
}
|
||||
|
||||
enum queue_state state = (enum queue_state)vq->header->state;
|
||||
if (!vq->ready_to_read && state == SHARED_QUEUE_STATE_READY) {
|
||||
for (size_t i = 0; i < 3; i++) {
|
||||
size_t off = vq->header->offsets[i];
|
||||
vq->ts[i] = (uint64_t *)(((uint8_t *)vq->header) + off);
|
||||
vq->frame[i] = ((uint8_t *)vq->header) + off +
|
||||
FRAME_HEADER_SIZE;
|
||||
}
|
||||
vq->ready_to_read = true;
|
||||
}
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
bool video_queue_read(video_queue_t *vq, nv12_scale_t *scale, void *dst,
|
||||
uint64_t *ts)
|
||||
{
|
||||
struct queue_header *qh = vq->header;
|
||||
long inc = qh->read_idx;
|
||||
|
||||
if (qh->state == SHARED_QUEUE_STATE_STOPPING) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (inc == vq->last_inc) {
|
||||
if (++vq->dup_counter == 10) {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
vq->dup_counter = 0;
|
||||
vq->last_inc = inc;
|
||||
}
|
||||
|
||||
unsigned long idx = get_idx(inc);
|
||||
|
||||
*ts = *vq->ts[idx];
|
||||
|
||||
nv12_do_scale(scale, dst, vq->frame[idx]);
|
||||
return true;
|
||||
}
|
37
plugins/win-dshow/shared-memory-queue.h
Normal file
37
plugins/win-dshow/shared-memory-queue.h
Normal file
@ -0,0 +1,37 @@
|
||||
#pragma once
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
struct video_queue;
|
||||
struct nv12_scale;
|
||||
typedef struct video_queue video_queue_t;
|
||||
typedef struct nv12_scale nv12_scale_t;
|
||||
|
||||
enum queue_state {
|
||||
SHARED_QUEUE_STATE_INVALID,
|
||||
SHARED_QUEUE_STATE_STARTING,
|
||||
SHARED_QUEUE_STATE_READY,
|
||||
SHARED_QUEUE_STATE_STOPPING,
|
||||
};
|
||||
|
||||
extern video_queue_t *video_queue_create(uint32_t cx, uint32_t cy,
|
||||
uint64_t interval);
|
||||
extern video_queue_t *video_queue_open();
|
||||
extern void video_queue_close(video_queue_t *vq);
|
||||
|
||||
extern void video_queue_get_info(video_queue_t *vq, uint32_t *cx, uint32_t *cy,
|
||||
uint64_t *interval);
|
||||
extern void video_queue_write(video_queue_t *vq, uint8_t **data,
|
||||
uint32_t *linesize, uint64_t timestamp);
|
||||
extern enum queue_state video_queue_state(video_queue_t *vq);
|
||||
extern bool video_queue_read(video_queue_t *vq, nv12_scale_t *scale, void *dst,
|
||||
uint64_t *ts);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
134
plugins/win-dshow/tiny-nv12-scale.c
Normal file
134
plugins/win-dshow/tiny-nv12-scale.c
Normal file
@ -0,0 +1,134 @@
|
||||
#include <string.h>
|
||||
#include "tiny-nv12-scale.h"
|
||||
|
||||
/* TODO: optimize this stuff later, or replace with something better. it's
|
||||
* kind of garbage. although normally it shouldn't be called that often. plus
|
||||
* it's nearest neighbor so not really a huge deal. at the very least it
|
||||
* should be sse2 at some point. */
|
||||
|
||||
void nv12_scale_init(nv12_scale_t *s, bool convert_to_i420, int dst_cx,
|
||||
int dst_cy, int src_cx, int src_cy)
|
||||
{
|
||||
s->convert_to_i420 = convert_to_i420;
|
||||
|
||||
s->src_cx = src_cx;
|
||||
s->src_cy = src_cy;
|
||||
|
||||
s->dst_cx = dst_cx;
|
||||
s->dst_cy = dst_cy;
|
||||
}
|
||||
|
||||
static void nv12_scale_nearest(nv12_scale_t *s, uint8_t *dst_start,
|
||||
const uint8_t *src)
|
||||
{
|
||||
register uint8_t *dst = dst_start;
|
||||
const int src_cx = s->src_cx;
|
||||
const int src_cy = s->src_cy;
|
||||
const int dst_cx = s->dst_cx;
|
||||
const int dst_cy = s->dst_cy;
|
||||
|
||||
/* lum */
|
||||
for (int y = 0; y < dst_cy; y++) {
|
||||
const int src_line = y * src_cy / dst_cy * s->src_cx;
|
||||
|
||||
for (int x = 0; x < dst_cx; x++) {
|
||||
const int src_x = x * src_cx / dst_cx;
|
||||
|
||||
*(dst++) = src[src_line + src_x];
|
||||
}
|
||||
}
|
||||
|
||||
src += src_cx * src_cy;
|
||||
|
||||
/* uv */
|
||||
const int dst_cx_d2 = dst_cx / 2;
|
||||
const int dst_cy_d2 = dst_cy / 2;
|
||||
|
||||
for (int y = 0; y < dst_cy_d2; y++) {
|
||||
const int src_line = y * src_cy / dst_cy * src_cx;
|
||||
|
||||
for (int x = 0; x < dst_cx_d2; x++) {
|
||||
const int src_x = x * src_cx / dst_cx * 2;
|
||||
const int pos = src_line + src_x;
|
||||
|
||||
*(dst++) = src[pos];
|
||||
*(dst++) = src[pos + 1];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void nv12_scale_nearest_to_i420(nv12_scale_t *s, uint8_t *dst_start,
|
||||
const uint8_t *src)
|
||||
{
|
||||
register uint8_t *dst = dst_start;
|
||||
const int src_cx = s->src_cx;
|
||||
const int src_cy = s->src_cy;
|
||||
const int dst_cx = s->dst_cx;
|
||||
const int dst_cy = s->dst_cy;
|
||||
const int size = src_cx * src_cy;
|
||||
|
||||
/* lum */
|
||||
for (int y = 0; y < dst_cy; y++) {
|
||||
const int src_line = y * src_cy / dst_cy * s->src_cx;
|
||||
|
||||
for (int x = 0; x < dst_cx; x++) {
|
||||
const int src_x = x * src_cx / dst_cx;
|
||||
|
||||
*(dst++) = src[src_line + src_x];
|
||||
}
|
||||
}
|
||||
|
||||
src += size;
|
||||
|
||||
/* uv */
|
||||
const int dst_cx_d2 = dst_cx / 2;
|
||||
const int dst_cy_d2 = dst_cy / 2;
|
||||
|
||||
register uint8_t *dst2 = dst + dst_cx * dst_cy / 4;
|
||||
|
||||
for (int y = 0; y < dst_cy_d2; y++) {
|
||||
const int src_line = y * src_cy / dst_cy * src_cx;
|
||||
|
||||
for (int x = 0; x < dst_cx_d2; x++) {
|
||||
const int src_x = x * src_cx / dst_cx * 2;
|
||||
const int pos = src_line + src_x;
|
||||
|
||||
*(dst++) = src[pos];
|
||||
*(dst2++) = src[pos + 1];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void nv12_convert_to_i420(nv12_scale_t *s, uint8_t *dst_start,
|
||||
const uint8_t *src_start)
|
||||
{
|
||||
const int size = s->src_cx * s->src_cy;
|
||||
const int size_d4 = size / 4;
|
||||
|
||||
memcpy(dst_start, src_start, size);
|
||||
|
||||
register uint8_t *dst1 = dst_start + size;
|
||||
register uint8_t *dst2 = dst1 + size_d4;
|
||||
register uint8_t *dst_end = dst2 + size_d4;
|
||||
register const uint8_t *src = src_start + size;
|
||||
|
||||
while (dst2 < dst_end) {
|
||||
*(dst1++) = *(src++);
|
||||
*(dst2++) = *(src++);
|
||||
}
|
||||
}
|
||||
|
||||
void nv12_do_scale(nv12_scale_t *s, uint8_t *dst, const uint8_t *src)
|
||||
{
|
||||
if (s->src_cx == s->dst_cx && s->src_cy == s->dst_cy) {
|
||||
if (s->convert_to_i420)
|
||||
nv12_convert_to_i420(s, dst, src);
|
||||
else
|
||||
memcpy(dst, src, s->src_cx * s->src_cy * 3 / 2);
|
||||
} else {
|
||||
if (s->convert_to_i420)
|
||||
nv12_scale_nearest_to_i420(s, dst, src);
|
||||
else
|
||||
nv12_scale_nearest(s, dst, src);
|
||||
}
|
||||
}
|
28
plugins/win-dshow/tiny-nv12-scale.h
Normal file
28
plugins/win-dshow/tiny-nv12-scale.h
Normal file
@ -0,0 +1,28 @@
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
struct nv12_scale {
|
||||
bool convert_to_i420;
|
||||
|
||||
int src_cx;
|
||||
int src_cy;
|
||||
|
||||
int dst_cx;
|
||||
int dst_cy;
|
||||
};
|
||||
|
||||
typedef struct nv12_scale nv12_scale_t;
|
||||
|
||||
extern void nv12_scale_init(nv12_scale_t *s, bool convert_to_i420, int dst_cx,
|
||||
int dst_cy, int src_cx, int src_cy);
|
||||
extern void nv12_do_scale(nv12_scale_t *s, uint8_t *dst, const uint8_t *src);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
8
plugins/win-dshow/virtualcam-guid.h
Normal file
8
plugins/win-dshow/virtualcam-guid.h
Normal file
@ -0,0 +1,8 @@
|
||||
#pragma once
|
||||
|
||||
#include <windows.h>
|
||||
#include <initguid.h>
|
||||
|
||||
// {A3FCE0F5-3493-419F-958A-ABA1250EC20B}
|
||||
DEFINE_GUID(CLSID_OBS_VirtualVideo, 0xa3fce0f5, 0x3493, 0x419f, 0x95, 0x8a,
|
||||
0xab, 0xa1, 0x25, 0xe, 0xc2, 0xb);
|
78
plugins/win-dshow/virtualcam-module/CMakeLists.txt
Normal file
78
plugins/win-dshow/virtualcam-module/CMakeLists.txt
Normal file
@ -0,0 +1,78 @@
|
||||
cmake_minimum_required(VERSION 3.5)
|
||||
project(obs-virtualcam-module)
|
||||
|
||||
if(CMAKE_SIZEOF_VOID_P EQUAL 8)
|
||||
set(_output_suffix "64")
|
||||
else()
|
||||
set(_output_suffix "32")
|
||||
endif()
|
||||
|
||||
configure_file(
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/virtualcam-module.def.in"
|
||||
"${CMAKE_CURRENT_BINARY_DIR}/virtualcam-module.def")
|
||||
|
||||
set(libdshowcapture_SOURCES
|
||||
../libdshowcapture/source/log.cpp
|
||||
../libdshowcapture/source/dshow-base.cpp
|
||||
../libdshowcapture/source/dshow-enum.cpp
|
||||
../libdshowcapture/source/dshow-formats.cpp
|
||||
../libdshowcapture/source/dshow-media-type.cpp
|
||||
../libdshowcapture/source/output-filter.cpp
|
||||
)
|
||||
|
||||
set(libdshowcapture_HEADERS
|
||||
../libdshowcapture/source/ComPtr.hpp
|
||||
../libdshowcapture/source/CoTaskMemPtr.hpp
|
||||
../libdshowcapture/source/log.hpp
|
||||
../libdshowcapture/source/dshow-base.hpp
|
||||
../libdshowcapture/source/dshow-enum.hpp
|
||||
../libdshowcapture/source/dshow-formats.hpp
|
||||
../libdshowcapture/source/dshow-media-type.hpp
|
||||
../libdshowcapture/source/output-filter.hpp
|
||||
../libdshowcapture/dshowcapture.hpp
|
||||
)
|
||||
|
||||
set(obs-virtualcam-module_SOURCES
|
||||
"${CMAKE_CURRENT_BINARY_DIR}/virtualcam-module.def"
|
||||
sleepto.c
|
||||
placeholder.cpp
|
||||
virtualcam-module.cpp
|
||||
virtualcam-filter.cpp
|
||||
../shared-memory-queue.c
|
||||
../tiny-nv12-scale.c
|
||||
)
|
||||
|
||||
set(obs-virtualcam-module_HEADERS
|
||||
sleepto.h
|
||||
virtualcam-filter.hpp
|
||||
../shared-memory-queue.h
|
||||
../tiny-nv12-scale.h
|
||||
)
|
||||
|
||||
if(MSVC)
|
||||
add_compile_options("$<IF:$<CONFIG:Debug>,/MTd,/MT>")
|
||||
endif()
|
||||
|
||||
include_directories(${CMAKE_SOURCE_DIR}/libobs/util)
|
||||
|
||||
source_group("libdshowcapture\\Source Files" FILES ${libdshowcapture_SOURCES})
|
||||
source_group("libdshowcapture\\Header Files" FILES ${libdshowcapture_HEADERS})
|
||||
|
||||
set(CMAKE_MODULE_LINKER_FLAGS "${MAKE_MODULE_LINKER_FLAGS} /ignore:4104")
|
||||
|
||||
add_library(obs-virtualcam-module MODULE
|
||||
${libdshowcapture_SOURCES}
|
||||
${libdshowcapture_HEADERS}
|
||||
${obs-virtualcam-module_SOURCES}
|
||||
${obs-virtualcam-module_HEADERS})
|
||||
target_link_libraries(obs-virtualcam-module
|
||||
winmm
|
||||
strmiids
|
||||
gdiplus
|
||||
)
|
||||
set_target_properties(obs-virtualcam-module PROPERTIES FOLDER "plugins/win-dshow")
|
||||
|
||||
set_target_properties(obs-virtualcam-module
|
||||
PROPERTIES
|
||||
OUTPUT_NAME "obs-virtualcam-module${_output_suffix}")
|
||||
install_obs_datatarget(obs-virtualcam-module "obs-plugins/win-dshow")
|
146
plugins/win-dshow/virtualcam-module/placeholder.cpp
Normal file
146
plugins/win-dshow/virtualcam-module/placeholder.cpp
Normal file
@ -0,0 +1,146 @@
|
||||
#include <windows.h>
|
||||
#include <strsafe.h>
|
||||
#include <gdiplus.h>
|
||||
#include <stdint.h>
|
||||
#include <vector>
|
||||
|
||||
using namespace Gdiplus;
|
||||
|
||||
extern HINSTANCE dll_inst;
|
||||
static std::vector<uint8_t> placeholder;
|
||||
|
||||
/* XXX: optimize this later. or don't, it's only called once. */
|
||||
|
||||
static void convert_placeholder(const uint8_t *rgb_in, int width, int height)
|
||||
{
|
||||
size_t size = width * height * 3;
|
||||
size_t linesize = width * 3;
|
||||
|
||||
std::vector<uint8_t> yuv_out;
|
||||
yuv_out.resize(size);
|
||||
|
||||
const uint8_t *in = rgb_in;
|
||||
const uint8_t *end = in + size;
|
||||
uint8_t *out = &yuv_out[0];
|
||||
|
||||
while (in < end) {
|
||||
const int16_t b = *(in++);
|
||||
const int16_t g = *(in++);
|
||||
const int16_t r = *(in++);
|
||||
|
||||
*(out++) = (uint8_t)(((66 * r + 129 * g + 25 * b + 128) >> 8) +
|
||||
16);
|
||||
*(out++) = (uint8_t)(((-38 * r - 74 * g + 112 * b + 128) >> 8) +
|
||||
128);
|
||||
*(out++) = (uint8_t)(((112 * r - 94 * g - 18 * b + 128) >> 8) +
|
||||
128);
|
||||
}
|
||||
|
||||
placeholder.resize(width * height * 3 / 2);
|
||||
|
||||
in = &yuv_out[0];
|
||||
end = in + size;
|
||||
|
||||
out = &placeholder[0];
|
||||
uint8_t *chroma = out + width * height;
|
||||
|
||||
while (in < end) {
|
||||
const uint8_t *in2 = in + linesize;
|
||||
const uint8_t *end2 = in2;
|
||||
uint8_t *out2 = out + width;
|
||||
|
||||
while (in < end2) {
|
||||
int16_t u;
|
||||
int16_t v;
|
||||
|
||||
*(out++) = *(in++);
|
||||
u = *(in++);
|
||||
v = *(in++);
|
||||
|
||||
*(out++) = *(in++);
|
||||
u += *(in++);
|
||||
v += *(in++);
|
||||
|
||||
*(out2++) = *(in2++);
|
||||
u += *(in2++);
|
||||
v += *(in2++);
|
||||
|
||||
*(out2++) = *(in2++);
|
||||
u += *(in2++);
|
||||
v += *(in2++);
|
||||
|
||||
*(chroma++) = (uint8_t)(u / 4);
|
||||
*(chroma++) = (uint8_t)(v / 4);
|
||||
}
|
||||
|
||||
in = in2;
|
||||
out = out2;
|
||||
}
|
||||
}
|
||||
|
||||
static bool load_placeholder_internal()
|
||||
{
|
||||
Status s;
|
||||
|
||||
wchar_t file[MAX_PATH];
|
||||
if (!GetModuleFileNameW(dll_inst, file, MAX_PATH)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
wchar_t *slash = wcsrchr(file, '\\');
|
||||
if (!slash) {
|
||||
return false;
|
||||
}
|
||||
|
||||
slash[1] = 0;
|
||||
|
||||
StringCbCat(file, sizeof(file), L"placeholder.png");
|
||||
|
||||
Bitmap bmp(file);
|
||||
if (bmp.GetLastStatus() != Status::Ok) {
|
||||
return false;
|
||||
}
|
||||
|
||||
BitmapData bmd = {};
|
||||
Rect r(0, 0, bmp.GetWidth(), bmp.GetHeight());
|
||||
|
||||
s = bmp.LockBits(&r, ImageLockModeRead, PixelFormat24bppRGB, &bmd);
|
||||
if (s != Status::Ok) {
|
||||
return false;
|
||||
}
|
||||
|
||||
convert_placeholder((const uint8_t *)bmd.Scan0, bmp.GetWidth(),
|
||||
bmp.GetHeight());
|
||||
|
||||
bmp.UnlockBits(&bmd);
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool load_placeholder()
|
||||
{
|
||||
GdiplusStartupInput si;
|
||||
ULONG_PTR token;
|
||||
GdiplusStartup(&token, &si, nullptr);
|
||||
|
||||
bool success = load_placeholder_internal();
|
||||
|
||||
GdiplusShutdown(token);
|
||||
return success;
|
||||
}
|
||||
|
||||
const uint8_t *get_placeholder()
|
||||
{
|
||||
static bool failed = false;
|
||||
static bool initialized = false;
|
||||
|
||||
if (initialized) {
|
||||
return placeholder.data();
|
||||
} else if (failed) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
initialized = load_placeholder();
|
||||
failed = !initialized;
|
||||
|
||||
return initialized ? placeholder.data() : nullptr;
|
||||
}
|
50
plugins/win-dshow/virtualcam-module/sleepto.c
Normal file
50
plugins/win-dshow/virtualcam-module/sleepto.c
Normal file
@ -0,0 +1,50 @@
|
||||
#include <windows.h>
|
||||
#include <stdbool.h>
|
||||
#include "sleepto.h"
|
||||
|
||||
static bool have_clockfreq = false;
|
||||
static LARGE_INTEGER clock_freq;
|
||||
|
||||
static inline uint64_t get_clockfreq(void)
|
||||
{
|
||||
if (!have_clockfreq) {
|
||||
QueryPerformanceFrequency(&clock_freq);
|
||||
have_clockfreq = true;
|
||||
}
|
||||
|
||||
return clock_freq.QuadPart;
|
||||
}
|
||||
|
||||
uint64_t gettime_100ns(void)
|
||||
{
|
||||
LARGE_INTEGER current_time;
|
||||
double time_val;
|
||||
|
||||
QueryPerformanceCounter(¤t_time);
|
||||
time_val = (double)current_time.QuadPart;
|
||||
time_val *= 10000000.0;
|
||||
time_val /= (double)get_clockfreq();
|
||||
|
||||
return (uint64_t)time_val;
|
||||
}
|
||||
|
||||
bool sleepto_100ns(uint64_t time_target)
|
||||
{
|
||||
uint64_t t = gettime_100ns();
|
||||
uint32_t milliseconds;
|
||||
|
||||
if (t >= time_target)
|
||||
return false;
|
||||
|
||||
milliseconds = (uint32_t)((time_target - t) / 10000);
|
||||
if (milliseconds > 1)
|
||||
Sleep(milliseconds - 1);
|
||||
|
||||
for (;;) {
|
||||
t = gettime_100ns();
|
||||
if (t >= time_target)
|
||||
return true;
|
||||
|
||||
Sleep(0);
|
||||
}
|
||||
}
|
14
plugins/win-dshow/virtualcam-module/sleepto.h
Normal file
14
plugins/win-dshow/virtualcam-module/sleepto.h
Normal file
@ -0,0 +1,14 @@
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
extern uint64_t gettime_100ns(void);
|
||||
extern bool sleepto_100ns(uint64_t time_target);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
241
plugins/win-dshow/virtualcam-module/virtualcam-filter.cpp
Normal file
241
plugins/win-dshow/virtualcam-module/virtualcam-filter.cpp
Normal file
@ -0,0 +1,241 @@
|
||||
#include "virtualcam-filter.hpp"
|
||||
#include "sleepto.h"
|
||||
|
||||
#include <shlobj_core.h>
|
||||
#include <strsafe.h>
|
||||
#include <inttypes.h>
|
||||
|
||||
using namespace DShow;
|
||||
|
||||
extern const uint8_t *get_placeholder();
|
||||
|
||||
/* ========================================================================= */
|
||||
|
||||
VCamFilter::VCamFilter()
|
||||
: OutputFilter(VideoFormat::NV12, DEFAULT_CX, DEFAULT_CY,
|
||||
DEFAULT_INTERVAL)
|
||||
{
|
||||
thread_start = CreateEvent(nullptr, true, false, nullptr);
|
||||
thread_stop = CreateEvent(nullptr, true, false, nullptr);
|
||||
|
||||
AddVideoFormat(VideoFormat::I420, DEFAULT_CX, DEFAULT_CY,
|
||||
DEFAULT_INTERVAL);
|
||||
|
||||
/* ---------------------------------------- */
|
||||
/* load placeholder image */
|
||||
|
||||
placeholder = get_placeholder();
|
||||
|
||||
/* ---------------------------------------- */
|
||||
/* detect if this filter is within obs */
|
||||
|
||||
wchar_t file[MAX_PATH];
|
||||
if (!GetModuleFileNameW(nullptr, file, MAX_PATH)) {
|
||||
file[0] = 0;
|
||||
}
|
||||
|
||||
#ifdef _WIN64
|
||||
const wchar_t *obs_process = L"obs64.exe";
|
||||
#else
|
||||
const wchar_t *obs_process = L"obs32.exe";
|
||||
#endif
|
||||
|
||||
in_obs = !!wcsstr(file, obs_process);
|
||||
|
||||
/* ---------------------------------------- */
|
||||
/* add last/current obs res/interval */
|
||||
|
||||
uint32_t new_cx = cx;
|
||||
uint32_t new_cy = cy;
|
||||
uint64_t new_interval = interval;
|
||||
|
||||
vq = video_queue_open();
|
||||
if (vq) {
|
||||
if (video_queue_state(vq) == SHARED_QUEUE_STATE_READY) {
|
||||
video_queue_get_info(vq, &new_cx, &new_cy,
|
||||
&new_interval);
|
||||
}
|
||||
|
||||
/* don't keep it open until the filter actually starts */
|
||||
video_queue_close(vq);
|
||||
vq = nullptr;
|
||||
} else {
|
||||
wchar_t res_file[MAX_PATH];
|
||||
SHGetFolderPathW(nullptr, CSIDL_APPDATA, nullptr,
|
||||
SHGFP_TYPE_CURRENT, res_file);
|
||||
StringCbCat(res_file, sizeof(res_file),
|
||||
L"\\obs-virtualcam.txt");
|
||||
|
||||
HANDLE file = CreateFileW(res_file, GENERIC_READ, 0, nullptr,
|
||||
OPEN_EXISTING, 0, nullptr);
|
||||
if (file) {
|
||||
char res[128];
|
||||
DWORD len = 0;
|
||||
|
||||
ReadFile(file, res, sizeof(res), &len, nullptr);
|
||||
CloseHandle(file);
|
||||
|
||||
res[len] = 0;
|
||||
int vals = sscanf(res,
|
||||
"%" PRIu32 "x%" PRIu32 "x%" PRIu64,
|
||||
&new_cx, &new_cy, &new_interval);
|
||||
if (vals != 3) {
|
||||
new_cx = cx;
|
||||
new_cy = cy;
|
||||
new_interval = interval;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (new_cx != cx || new_cy != cy || new_interval != interval) {
|
||||
AddVideoFormat(VideoFormat::NV12, new_cx, new_cy, new_interval);
|
||||
AddVideoFormat(VideoFormat::I420, new_cx, new_cy, new_interval);
|
||||
SetVideoFormat(VideoFormat::NV12, new_cx, new_cy, new_interval);
|
||||
cx = new_cx;
|
||||
cy = new_cy;
|
||||
interval = new_interval;
|
||||
}
|
||||
|
||||
nv12_scale_init(&scaler, false, cx, cy, cx, cy);
|
||||
|
||||
/* ---------------------------------------- */
|
||||
|
||||
th = std::thread([this] { Thread(); });
|
||||
|
||||
AddRef();
|
||||
}
|
||||
|
||||
VCamFilter::~VCamFilter()
|
||||
{
|
||||
SetEvent(thread_stop);
|
||||
th.join();
|
||||
video_queue_close(vq);
|
||||
}
|
||||
|
||||
const wchar_t *VCamFilter::FilterName() const
|
||||
{
|
||||
return L"VCamFilter";
|
||||
}
|
||||
|
||||
STDMETHODIMP VCamFilter::Pause()
|
||||
{
|
||||
HRESULT hr;
|
||||
|
||||
hr = OutputFilter::Pause();
|
||||
if (FAILED(hr)) {
|
||||
return hr;
|
||||
}
|
||||
|
||||
SetEvent(thread_start);
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
inline uint64_t VCamFilter::GetTime()
|
||||
{
|
||||
if (!!clock) {
|
||||
REFERENCE_TIME rt;
|
||||
HRESULT hr = clock->GetTime(&rt);
|
||||
if (SUCCEEDED(hr)) {
|
||||
return (uint64_t)rt;
|
||||
}
|
||||
}
|
||||
|
||||
return gettime_100ns();
|
||||
}
|
||||
|
||||
void VCamFilter::Thread()
|
||||
{
|
||||
HANDLE h[2] = {thread_start, thread_stop};
|
||||
DWORD ret = WaitForMultipleObjects(2, h, false, INFINITE);
|
||||
if (ret != WAIT_OBJECT_0)
|
||||
return;
|
||||
|
||||
uint64_t cur_time = gettime_100ns();
|
||||
uint64_t filter_time = GetTime();
|
||||
|
||||
cx = GetCX();
|
||||
cy = GetCY();
|
||||
interval = GetInterval();
|
||||
|
||||
nv12_scale_init(&scaler, false, GetCX(), GetCY(), cx, cy);
|
||||
|
||||
while (!stopped()) {
|
||||
Frame(filter_time);
|
||||
sleepto_100ns(cur_time += interval);
|
||||
filter_time += interval;
|
||||
}
|
||||
}
|
||||
|
||||
void VCamFilter::Frame(uint64_t ts)
|
||||
{
|
||||
uint32_t new_cx = cx;
|
||||
uint32_t new_cy = cy;
|
||||
uint64_t new_interval = interval;
|
||||
|
||||
if (!vq) {
|
||||
vq = video_queue_open();
|
||||
}
|
||||
|
||||
enum queue_state state = video_queue_state(vq);
|
||||
if (state != prev_state) {
|
||||
if (state == SHARED_QUEUE_STATE_READY) {
|
||||
video_queue_get_info(vq, &new_cx, &new_cy,
|
||||
&new_interval);
|
||||
} else if (state == SHARED_QUEUE_STATE_STOPPING) {
|
||||
video_queue_close(vq);
|
||||
vq = nullptr;
|
||||
}
|
||||
|
||||
prev_state = state;
|
||||
}
|
||||
|
||||
if (state != SHARED_QUEUE_STATE_READY) {
|
||||
new_cx = DEFAULT_CX;
|
||||
new_cy = DEFAULT_CY;
|
||||
new_interval = DEFAULT_INTERVAL;
|
||||
}
|
||||
|
||||
if (new_cx != cx || new_cy != cy || new_interval != interval) {
|
||||
if (in_obs) {
|
||||
SetVideoFormat(GetVideoFormat(), new_cx, new_cy,
|
||||
new_interval);
|
||||
}
|
||||
|
||||
nv12_scale_init(&scaler, false, GetCX(), GetCY(), new_cx,
|
||||
new_cy);
|
||||
|
||||
cx = new_cx;
|
||||
cy = new_cy;
|
||||
interval = new_interval;
|
||||
}
|
||||
|
||||
scaler.convert_to_i420 = GetVideoFormat() == VideoFormat::I420;
|
||||
|
||||
uint8_t *ptr;
|
||||
if (LockSampleData(&ptr)) {
|
||||
if (state == SHARED_QUEUE_STATE_READY)
|
||||
ShowOBSFrame(ptr);
|
||||
else
|
||||
ShowDefaultFrame(ptr);
|
||||
|
||||
UnlockSampleData(ts, ts + interval);
|
||||
}
|
||||
}
|
||||
|
||||
void VCamFilter::ShowOBSFrame(uint8_t *ptr)
|
||||
{
|
||||
uint64_t temp;
|
||||
if (!video_queue_read(vq, &scaler, ptr, &temp)) {
|
||||
video_queue_close(vq);
|
||||
vq = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
void VCamFilter::ShowDefaultFrame(uint8_t *ptr)
|
||||
{
|
||||
if (placeholder) {
|
||||
nv12_do_scale(&scaler, ptr, placeholder);
|
||||
} else {
|
||||
memset(ptr, 127, GetCX() * GetCY() * 3 / 2);
|
||||
}
|
||||
}
|
51
plugins/win-dshow/virtualcam-module/virtualcam-filter.hpp
Normal file
51
plugins/win-dshow/virtualcam-module/virtualcam-filter.hpp
Normal file
@ -0,0 +1,51 @@
|
||||
#pragma once
|
||||
|
||||
#include <windows.h>
|
||||
#include <cstdint>
|
||||
#include <thread>
|
||||
#include "../shared-memory-queue.h"
|
||||
#include "../tiny-nv12-scale.h"
|
||||
#include "../libdshowcapture/source/output-filter.hpp"
|
||||
#include "../../../libobs/util/windows/WinHandle.hpp"
|
||||
|
||||
#define DEFAULT_CX 1920
|
||||
#define DEFAULT_CY 1080
|
||||
#define DEFAULT_INTERVAL 333333ULL
|
||||
|
||||
class VCamFilter : public DShow::OutputFilter {
|
||||
std::thread th;
|
||||
|
||||
video_queue_t *vq = nullptr;
|
||||
int queue_mode = 0;
|
||||
bool in_obs = false;
|
||||
enum queue_state prev_state = SHARED_QUEUE_STATE_INVALID;
|
||||
const uint8_t *placeholder;
|
||||
uint32_t cx = DEFAULT_CX;
|
||||
uint32_t cy = DEFAULT_CY;
|
||||
uint64_t interval = DEFAULT_INTERVAL;
|
||||
WinHandle thread_start;
|
||||
WinHandle thread_stop;
|
||||
|
||||
nv12_scale_t scaler = {};
|
||||
|
||||
inline bool stopped() const
|
||||
{
|
||||
return WaitForSingleObject(thread_stop, 0) != WAIT_TIMEOUT;
|
||||
}
|
||||
|
||||
inline uint64_t GetTime();
|
||||
|
||||
void Thread();
|
||||
void Frame(uint64_t ts);
|
||||
void ShowOBSFrame(uint8_t *ptr);
|
||||
void ShowDefaultFrame(uint8_t *ptr);
|
||||
|
||||
protected:
|
||||
const wchar_t *FilterName() const override;
|
||||
|
||||
public:
|
||||
VCamFilter();
|
||||
~VCamFilter() override;
|
||||
|
||||
STDMETHODIMP Pause() override;
|
||||
};
|
298
plugins/win-dshow/virtualcam-module/virtualcam-module.cpp
Normal file
298
plugins/win-dshow/virtualcam-module/virtualcam-module.cpp
Normal file
@ -0,0 +1,298 @@
|
||||
#include "virtualcam-filter.hpp"
|
||||
#include "../virtualcam-guid.h"
|
||||
|
||||
/* ========================================================================= */
|
||||
|
||||
static const REGPINTYPES AMSMediaTypesV = {&MEDIATYPE_Video,
|
||||
&MEDIASUBTYPE_NV12};
|
||||
|
||||
static const REGFILTERPINS AMSPinVideo = {L"Output", false, true,
|
||||
false, false, &CLSID_NULL,
|
||||
nullptr, 1, &AMSMediaTypesV};
|
||||
|
||||
HINSTANCE dll_inst = nullptr;
|
||||
static volatile long locks = 0;
|
||||
|
||||
/* ========================================================================= */
|
||||
|
||||
class VCamFactory : public IClassFactory {
|
||||
volatile long refs = 1;
|
||||
CLSID cls;
|
||||
|
||||
public:
|
||||
inline VCamFactory(CLSID cls_) : cls(cls_) {}
|
||||
|
||||
// IUnknown
|
||||
STDMETHODIMP QueryInterface(REFIID riid, void **p_ptr);
|
||||
STDMETHODIMP_(ULONG) AddRef();
|
||||
STDMETHODIMP_(ULONG) Release();
|
||||
|
||||
// IClassFactory
|
||||
STDMETHODIMP CreateInstance(LPUNKNOWN parent, REFIID riid,
|
||||
void **p_ptr);
|
||||
STDMETHODIMP LockServer(BOOL lock);
|
||||
};
|
||||
|
||||
STDMETHODIMP VCamFactory::QueryInterface(REFIID riid, void **p_ptr)
|
||||
{
|
||||
if (!p_ptr) {
|
||||
return E_POINTER;
|
||||
}
|
||||
|
||||
if ((riid == IID_IUnknown) || (riid == IID_IClassFactory)) {
|
||||
AddRef();
|
||||
*p_ptr = (void *)this;
|
||||
return S_OK;
|
||||
} else {
|
||||
*p_ptr = nullptr;
|
||||
return E_NOINTERFACE;
|
||||
}
|
||||
}
|
||||
|
||||
STDMETHODIMP_(ULONG) VCamFactory::AddRef()
|
||||
{
|
||||
return InterlockedIncrement(&refs);
|
||||
}
|
||||
|
||||
STDMETHODIMP_(ULONG) VCamFactory::Release()
|
||||
{
|
||||
long new_refs = InterlockedDecrement(&refs);
|
||||
if (new_refs == 0) {
|
||||
delete this;
|
||||
return 0;
|
||||
}
|
||||
|
||||
return (ULONG)new_refs;
|
||||
}
|
||||
|
||||
STDMETHODIMP VCamFactory::CreateInstance(LPUNKNOWN parent, REFIID, void **p_ptr)
|
||||
{
|
||||
if (!p_ptr) {
|
||||
return E_POINTER;
|
||||
}
|
||||
|
||||
*p_ptr = nullptr;
|
||||
|
||||
/* don't bother supporting the "parent" functionality */
|
||||
if (parent) {
|
||||
return E_NOINTERFACE;
|
||||
}
|
||||
|
||||
if (IsEqualCLSID(cls, CLSID_OBS_VirtualVideo)) {
|
||||
*p_ptr = (void *)new VCamFilter();
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
return E_NOINTERFACE;
|
||||
}
|
||||
|
||||
STDMETHODIMP VCamFactory::LockServer(BOOL lock)
|
||||
{
|
||||
if (lock) {
|
||||
InterlockedIncrement(&locks);
|
||||
} else {
|
||||
InterlockedDecrement(&locks);
|
||||
}
|
||||
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
/* ========================================================================= */
|
||||
|
||||
static inline DWORD string_size(const wchar_t *str)
|
||||
{
|
||||
return (DWORD)(wcslen(str) + 1) * sizeof(wchar_t);
|
||||
}
|
||||
|
||||
static bool RegServer(CLSID cls, const wchar_t *desc, const wchar_t *file,
|
||||
const wchar_t *model = L"Both",
|
||||
const wchar_t *type = L"InprocServer32")
|
||||
{
|
||||
wchar_t cls_str[CHARS_IN_GUID];
|
||||
wchar_t temp[MAX_PATH];
|
||||
HKEY key = nullptr;
|
||||
HKEY subkey = nullptr;
|
||||
bool success = false;
|
||||
|
||||
StringFromGUID2(cls, cls_str, CHARS_IN_GUID);
|
||||
|
||||
StringCbPrintf(temp, sizeof(temp), L"CLSID\\%s", cls_str);
|
||||
|
||||
if (RegCreateKey(HKEY_CLASSES_ROOT, temp, &key) != ERROR_SUCCESS) {
|
||||
goto fail;
|
||||
}
|
||||
|
||||
RegSetValueW(key, nullptr, REG_SZ, desc, string_size(desc));
|
||||
|
||||
if (RegCreateKey(key, type, &subkey) != ERROR_SUCCESS) {
|
||||
goto fail;
|
||||
}
|
||||
|
||||
RegSetValueW(subkey, nullptr, REG_SZ, file, string_size(file));
|
||||
RegSetValueExW(subkey, L"ThreadingModel", 0, REG_SZ,
|
||||
(const BYTE *)model, string_size(model));
|
||||
|
||||
success = true;
|
||||
|
||||
fail:
|
||||
if (key) {
|
||||
RegCloseKey(key);
|
||||
}
|
||||
if (key) {
|
||||
RegCloseKey(subkey);
|
||||
}
|
||||
|
||||
return success;
|
||||
}
|
||||
|
||||
static bool UnregServer(CLSID cls)
|
||||
{
|
||||
wchar_t cls_str[CHARS_IN_GUID];
|
||||
wchar_t temp[MAX_PATH];
|
||||
|
||||
StringFromGUID2(cls, cls_str, CHARS_IN_GUID);
|
||||
StringCbPrintf(temp, sizeof(temp), L"CLSID\\%s", cls_str);
|
||||
|
||||
return RegDeleteTreeW(HKEY_CLASSES_ROOT, temp) == ERROR_SUCCESS;
|
||||
}
|
||||
|
||||
static bool RegServers(bool reg)
|
||||
{
|
||||
wchar_t file[MAX_PATH];
|
||||
|
||||
if (!GetModuleFileNameW(dll_inst, file, MAX_PATH)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (reg) {
|
||||
return RegServer(CLSID_OBS_VirtualVideo, L"OBS Virtual Camera",
|
||||
file);
|
||||
} else {
|
||||
return UnregServer(CLSID_OBS_VirtualVideo);
|
||||
}
|
||||
}
|
||||
|
||||
static bool RegFilters(bool reg)
|
||||
{
|
||||
ComPtr<IFilterMapper2> fm;
|
||||
HRESULT hr;
|
||||
|
||||
hr = CoCreateInstance(CLSID_FilterMapper2, nullptr,
|
||||
CLSCTX_INPROC_SERVER, IID_IFilterMapper2,
|
||||
(void **)&fm);
|
||||
if (FAILED(hr)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (reg) {
|
||||
ComPtr<IMoniker> moniker;
|
||||
REGFILTER2 rf2;
|
||||
rf2.dwVersion = 1;
|
||||
rf2.dwMerit = MERIT_DO_NOT_USE;
|
||||
rf2.cPins = 1;
|
||||
rf2.rgPins = &AMSPinVideo;
|
||||
|
||||
hr = fm->RegisterFilter(CLSID_OBS_VirtualVideo,
|
||||
L"OBS Video Output", &moniker,
|
||||
&CLSID_VideoInputDeviceCategory,
|
||||
nullptr, &rf2);
|
||||
if (FAILED(hr)) {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
hr = fm->UnregisterFilter(&CLSID_VideoInputDeviceCategory, 0,
|
||||
CLSID_OBS_VirtualVideo);
|
||||
if (FAILED(hr)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/* ========================================================================= */
|
||||
|
||||
STDAPI DllRegisterServer()
|
||||
{
|
||||
if (!RegServers(true)) {
|
||||
RegServers(false);
|
||||
return E_FAIL;
|
||||
}
|
||||
|
||||
CoInitialize(0);
|
||||
|
||||
if (!RegFilters(true)) {
|
||||
RegFilters(false);
|
||||
RegServers(false);
|
||||
CoUninitialize();
|
||||
return E_FAIL;
|
||||
}
|
||||
|
||||
CoUninitialize();
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
STDAPI DllUnregisterServer()
|
||||
{
|
||||
CoInitialize(0);
|
||||
RegFilters(false);
|
||||
RegServers(false);
|
||||
CoUninitialize();
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
STDAPI DllInstall(BOOL install, LPCWSTR)
|
||||
{
|
||||
if (!install) {
|
||||
return DllUnregisterServer();
|
||||
} else {
|
||||
return DllRegisterServer();
|
||||
}
|
||||
}
|
||||
|
||||
STDAPI DllCanUnloadNow()
|
||||
{
|
||||
return InterlockedOr(&locks, 0) == 0 ? S_OK : S_FALSE;
|
||||
}
|
||||
|
||||
STDAPI DllGetClassObject(REFCLSID cls, REFIID riid, void **p_ptr)
|
||||
{
|
||||
if (!p_ptr) {
|
||||
return E_POINTER;
|
||||
}
|
||||
|
||||
*p_ptr = nullptr;
|
||||
|
||||
if (riid != IID_IClassFactory && riid != IID_IUnknown) {
|
||||
return E_NOINTERFACE;
|
||||
}
|
||||
if (!IsEqualCLSID(cls, CLSID_OBS_VirtualVideo)) {
|
||||
return E_INVALIDARG;
|
||||
}
|
||||
|
||||
*p_ptr = (void *)new VCamFactory(cls);
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
//#define ENABLE_LOGGING
|
||||
|
||||
#ifdef ENABLE_LOGGING
|
||||
void logcallback(DShow::LogType, const wchar_t *msg, void *)
|
||||
{
|
||||
OutputDebugStringW(msg);
|
||||
OutputDebugStringW(L"\n");
|
||||
}
|
||||
#endif
|
||||
|
||||
BOOL WINAPI DllMain(HINSTANCE inst, DWORD reason, LPVOID)
|
||||
{
|
||||
if (reason == DLL_PROCESS_ATTACH) {
|
||||
DisableThreadLibraryCalls(inst);
|
||||
#ifdef ENABLE_LOGGING
|
||||
DShow::SetLogCallback(logcallback, nullptr);
|
||||
#endif
|
||||
dll_inst = inst;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
@ -0,0 +1,8 @@
|
||||
LIBRARY obs-virtualcam-module@_output_suffix@.dll
|
||||
EXPORTS
|
||||
DllMain PRIVATE
|
||||
DllGetClassObject PRIVATE
|
||||
DllCanUnloadNow PRIVATE
|
||||
DllRegisterServer PRIVATE
|
||||
DllUnregisterServer PRIVATE
|
||||
DllInstall PRIVATE
|
102
plugins/win-dshow/virtualcam.c
Normal file
102
plugins/win-dshow/virtualcam.c
Normal file
@ -0,0 +1,102 @@
|
||||
#include <obs-module.h>
|
||||
#include <util/platform.h>
|
||||
#include "shared-memory-queue.h"
|
||||
|
||||
struct virtualcam_data {
|
||||
obs_output_t *output;
|
||||
video_queue_t *vq;
|
||||
};
|
||||
|
||||
static const char *virtualcam_name(void *unused)
|
||||
{
|
||||
UNUSED_PARAMETER(unused);
|
||||
return "Virtual Camera Output";
|
||||
}
|
||||
|
||||
static void virtualcam_destroy(void *data)
|
||||
{
|
||||
struct virtualcam_data *vcam = (struct virtualcam_data *)data;
|
||||
video_queue_close(vcam->vq);
|
||||
bfree(data);
|
||||
}
|
||||
|
||||
static void *virtualcam_create(obs_data_t *settings, obs_output_t *output)
|
||||
{
|
||||
struct virtualcam_data *vcam =
|
||||
(struct virtualcam_data *)bzalloc(sizeof(*vcam));
|
||||
vcam->output = output;
|
||||
|
||||
UNUSED_PARAMETER(settings);
|
||||
return vcam;
|
||||
}
|
||||
|
||||
static bool virtualcam_start(void *data)
|
||||
{
|
||||
struct virtualcam_data *vcam = (struct virtualcam_data *)data;
|
||||
uint32_t width = obs_output_get_width(vcam->output);
|
||||
uint32_t height = obs_output_get_height(vcam->output);
|
||||
|
||||
struct obs_video_info ovi;
|
||||
obs_get_video_info(&ovi);
|
||||
|
||||
uint64_t interval = ovi.fps_den * 10000000ULL / ovi.fps_num;
|
||||
|
||||
char res[64];
|
||||
snprintf(res, sizeof(res), "%dx%dx%lld", (int)width, (int)height,
|
||||
(long long)interval);
|
||||
|
||||
char *res_file = os_get_config_path_ptr("obs-virtualcam.txt");
|
||||
os_quick_write_utf8_file_safe(res_file, res, strlen(res), false, "tmp",
|
||||
NULL);
|
||||
bfree(res_file);
|
||||
|
||||
vcam->vq = video_queue_create(width, height, interval);
|
||||
if (!vcam->vq) {
|
||||
blog(LOG_WARNING, "starting virtual-output failed");
|
||||
return false;
|
||||
}
|
||||
|
||||
struct video_scale_info vsi = {0};
|
||||
vsi.format = VIDEO_FORMAT_NV12;
|
||||
vsi.width = width;
|
||||
vsi.height = height;
|
||||
obs_output_set_video_conversion(vcam->output, &vsi);
|
||||
|
||||
blog(LOG_INFO, "Virtual output started");
|
||||
obs_output_begin_data_capture(vcam->output, 0);
|
||||
return true;
|
||||
}
|
||||
|
||||
static void virtualcam_stop(void *data, uint64_t ts)
|
||||
{
|
||||
struct virtualcam_data *vcam = (struct virtualcam_data *)data;
|
||||
obs_output_end_data_capture(vcam->output);
|
||||
video_queue_close(vcam->vq);
|
||||
vcam->vq = NULL;
|
||||
|
||||
blog(LOG_INFO, "Virtual output stopped");
|
||||
|
||||
UNUSED_PARAMETER(ts);
|
||||
}
|
||||
|
||||
static void virtual_video(void *param, struct video_data *frame)
|
||||
{
|
||||
struct virtualcam_data *vcam = (struct virtualcam_data *)param;
|
||||
|
||||
if (!vcam->vq)
|
||||
return;
|
||||
|
||||
video_queue_write(vcam->vq, frame->data, frame->linesize,
|
||||
frame->timestamp);
|
||||
}
|
||||
|
||||
struct obs_output_info virtualcam_info = {
|
||||
.id = "virtualcam_output",
|
||||
.flags = OBS_OUTPUT_VIDEO,
|
||||
.get_name = virtualcam_name,
|
||||
.create = virtualcam_create,
|
||||
.destroy = virtualcam_destroy,
|
||||
.start = virtualcam_start,
|
||||
.stop = virtualcam_stop,
|
||||
.raw_video = virtual_video,
|
||||
};
|
Loading…
x
Reference in New Issue
Block a user