diff --git a/plugins/win-dshow/CMakeLists.txt b/plugins/win-dshow/CMakeLists.txt index 355dba7d5..387167d90 100644 --- a/plugins/win-dshow/CMakeLists.txt +++ b/plugins/win-dshow/CMakeLists.txt @@ -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) diff --git a/plugins/win-dshow/data/placeholder.png b/plugins/win-dshow/data/placeholder.png new file mode 100644 index 000000000..fca3bf4c6 Binary files /dev/null and b/plugins/win-dshow/data/placeholder.png differ diff --git a/plugins/win-dshow/dshow-plugin.cpp b/plugins/win-dshow/dshow-plugin.cpp index e438b7005..a26914d31 100644 --- a/plugins/win-dshow/dshow-plugin.cpp +++ b/plugins/win-dshow/dshow-plugin.cpp @@ -1,4 +1,7 @@ #include +#include +#include +#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; } diff --git a/plugins/win-dshow/libdshowcapture b/plugins/win-dshow/libdshowcapture index 03fbb3814..7a495a9d3 160000 --- a/plugins/win-dshow/libdshowcapture +++ b/plugins/win-dshow/libdshowcapture @@ -1 +1 @@ -Subproject commit 03fbb3814b2ad678f5be4b23b8ca05fb4172e12e +Subproject commit 7a495a9d3844152e3c43d14855a386d044ecd411 diff --git a/plugins/win-dshow/shared-memory-queue.c b/plugins/win-dshow/shared-memory-queue.c new file mode 100644 index 000000000..87b8ffac6 --- /dev/null +++ b/plugins/win-dshow/shared-memory-queue.c @@ -0,0 +1,209 @@ +#include +#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; +} diff --git a/plugins/win-dshow/shared-memory-queue.h b/plugins/win-dshow/shared-memory-queue.h new file mode 100644 index 000000000..5e6435299 --- /dev/null +++ b/plugins/win-dshow/shared-memory-queue.h @@ -0,0 +1,37 @@ +#pragma once + +#include +#include + +#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 diff --git a/plugins/win-dshow/tiny-nv12-scale.c b/plugins/win-dshow/tiny-nv12-scale.c new file mode 100644 index 000000000..e576b78f4 --- /dev/null +++ b/plugins/win-dshow/tiny-nv12-scale.c @@ -0,0 +1,134 @@ +#include +#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); + } +} diff --git a/plugins/win-dshow/tiny-nv12-scale.h b/plugins/win-dshow/tiny-nv12-scale.h new file mode 100644 index 000000000..3e9a2fa37 --- /dev/null +++ b/plugins/win-dshow/tiny-nv12-scale.h @@ -0,0 +1,28 @@ +#pragma once + +#include +#include + +#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 diff --git a/plugins/win-dshow/virtualcam-guid.h b/plugins/win-dshow/virtualcam-guid.h new file mode 100644 index 000000000..4c488d0c3 --- /dev/null +++ b/plugins/win-dshow/virtualcam-guid.h @@ -0,0 +1,8 @@ +#pragma once + +#include +#include + +// {A3FCE0F5-3493-419F-958A-ABA1250EC20B} +DEFINE_GUID(CLSID_OBS_VirtualVideo, 0xa3fce0f5, 0x3493, 0x419f, 0x95, 0x8a, + 0xab, 0xa1, 0x25, 0xe, 0xc2, 0xb); diff --git a/plugins/win-dshow/virtualcam-module/CMakeLists.txt b/plugins/win-dshow/virtualcam-module/CMakeLists.txt new file mode 100644 index 000000000..4dddfc498 --- /dev/null +++ b/plugins/win-dshow/virtualcam-module/CMakeLists.txt @@ -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("$,/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") diff --git a/plugins/win-dshow/virtualcam-module/placeholder.cpp b/plugins/win-dshow/virtualcam-module/placeholder.cpp new file mode 100644 index 000000000..744181acf --- /dev/null +++ b/plugins/win-dshow/virtualcam-module/placeholder.cpp @@ -0,0 +1,146 @@ +#include +#include +#include +#include +#include + +using namespace Gdiplus; + +extern HINSTANCE dll_inst; +static std::vector 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 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; +} diff --git a/plugins/win-dshow/virtualcam-module/sleepto.c b/plugins/win-dshow/virtualcam-module/sleepto.c new file mode 100644 index 000000000..9ca23f0d4 --- /dev/null +++ b/plugins/win-dshow/virtualcam-module/sleepto.c @@ -0,0 +1,50 @@ +#include +#include +#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); + } +} diff --git a/plugins/win-dshow/virtualcam-module/sleepto.h b/plugins/win-dshow/virtualcam-module/sleepto.h new file mode 100644 index 000000000..07b882f4d --- /dev/null +++ b/plugins/win-dshow/virtualcam-module/sleepto.h @@ -0,0 +1,14 @@ +#pragma once + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +extern uint64_t gettime_100ns(void); +extern bool sleepto_100ns(uint64_t time_target); + +#ifdef __cplusplus +} +#endif diff --git a/plugins/win-dshow/virtualcam-module/virtualcam-filter.cpp b/plugins/win-dshow/virtualcam-module/virtualcam-filter.cpp new file mode 100644 index 000000000..42f43d897 --- /dev/null +++ b/plugins/win-dshow/virtualcam-module/virtualcam-filter.cpp @@ -0,0 +1,241 @@ +#include "virtualcam-filter.hpp" +#include "sleepto.h" + +#include +#include +#include + +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); + } +} diff --git a/plugins/win-dshow/virtualcam-module/virtualcam-filter.hpp b/plugins/win-dshow/virtualcam-module/virtualcam-filter.hpp new file mode 100644 index 000000000..d5724de4d --- /dev/null +++ b/plugins/win-dshow/virtualcam-module/virtualcam-filter.hpp @@ -0,0 +1,51 @@ +#pragma once + +#include +#include +#include +#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; +}; diff --git a/plugins/win-dshow/virtualcam-module/virtualcam-module.cpp b/plugins/win-dshow/virtualcam-module/virtualcam-module.cpp new file mode 100644 index 000000000..573b1cbdc --- /dev/null +++ b/plugins/win-dshow/virtualcam-module/virtualcam-module.cpp @@ -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 fm; + HRESULT hr; + + hr = CoCreateInstance(CLSID_FilterMapper2, nullptr, + CLSCTX_INPROC_SERVER, IID_IFilterMapper2, + (void **)&fm); + if (FAILED(hr)) { + return false; + } + + if (reg) { + ComPtr 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; +} diff --git a/plugins/win-dshow/virtualcam-module/virtualcam-module.def.in b/plugins/win-dshow/virtualcam-module/virtualcam-module.def.in new file mode 100644 index 000000000..943ebc515 --- /dev/null +++ b/plugins/win-dshow/virtualcam-module/virtualcam-module.def.in @@ -0,0 +1,8 @@ +LIBRARY obs-virtualcam-module@_output_suffix@.dll +EXPORTS + DllMain PRIVATE + DllGetClassObject PRIVATE + DllCanUnloadNow PRIVATE + DllRegisterServer PRIVATE + DllUnregisterServer PRIVATE + DllInstall PRIVATE diff --git a/plugins/win-dshow/virtualcam.c b/plugins/win-dshow/virtualcam.c new file mode 100644 index 000000000..5125b201b --- /dev/null +++ b/plugins/win-dshow/virtualcam.c @@ -0,0 +1,102 @@ +#include +#include +#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, +};