#include #include #include "dc-capture.h" #include "window-helpers.h" #include "../../libobs/util/platform.h" #include "../../libobs-winrt/winrt-capture.h" /* clang-format off */ #define TEXT_WINDOW_CAPTURE obs_module_text("WindowCapture") #define TEXT_WINDOW obs_module_text("WindowCapture.Window") #define TEXT_METHOD obs_module_text("WindowCapture.Method") #define TEXT_METHOD_AUTO obs_module_text("WindowCapture.Method.Auto") #define TEXT_METHOD_BITBLT obs_module_text("WindowCapture.Method.BitBlt") #define TEXT_METHOD_WGC obs_module_text("WindowCapture.Method.WindowsGraphicsCapture") #define TEXT_MATCH_PRIORITY obs_module_text("WindowCapture.Priority") #define TEXT_MATCH_TITLE obs_module_text("WindowCapture.Priority.Title") #define TEXT_MATCH_CLASS obs_module_text("WindowCapture.Priority.Class") #define TEXT_MATCH_EXE obs_module_text("WindowCapture.Priority.Exe") #define TEXT_CAPTURE_CURSOR obs_module_text("CaptureCursor") #define TEXT_COMPATIBILITY obs_module_text("Compatibility") #define TEXT_CLIENT_AREA obs_module_text("ClientArea") /* clang-format on */ #define WC_CHECK_TIMER 1.0f struct winrt_exports { bool *(*winrt_capture_supported)(); struct winrt_capture *(*winrt_capture_init)(bool cursor, HWND window, bool client_area); void (*winrt_capture_free)(struct winrt_capture *capture); void (*winrt_capture_render)(struct winrt_capture *capture, gs_effect_t *effect); uint32_t (*winrt_capture_width)(const struct winrt_capture *capture); uint32_t (*winrt_capture_height)(const struct winrt_capture *capture); }; enum window_capture_method { METHOD_AUTO, METHOD_BITBLT, METHOD_WGC, }; struct window_capture { obs_source_t *source; char *title; char *class; char *executable; enum window_capture_method method; enum window_priority priority; bool cursor; bool compatibility; bool client_area; bool use_wildcards; /* TODO */ struct dc_capture capture; bool wgc_supported; void *winrt_module; struct winrt_exports exports; struct winrt_capture *capture_winrt; float resize_timer; float check_window_timer; float cursor_check_time; HWND window; RECT last_rect; }; static const char *wgc_partial_match_classes[] = { "Chrome", "Mozilla", NULL, }; static const char *wgc_whole_match_classes[] = { "ApplicationFrameWindow", "Windows.UI.Core.CoreWindow", "XLMAIN", /* excel*/ "PPTFrameClass", /* powerpoint */ "OpusApp", /* word */ NULL, }; static enum window_capture_method choose_method(enum window_capture_method method, bool wgc_supported, const char *current_class) { if (method != METHOD_AUTO) { if (method == METHOD_WGC && !wgc_supported) method = METHOD_BITBLT; return method; } if (!current_class) return METHOD_BITBLT; const char **class = wgc_partial_match_classes; while (*class) { if (astrstri(current_class, *class) != NULL) { return METHOD_WGC; } class ++; } class = wgc_whole_match_classes; while (*class) { if (astrcmpi(current_class, *class) == 0) { return METHOD_WGC; } class ++; } return METHOD_BITBLT; } static void update_settings(struct window_capture *wc, obs_data_t *s) { int method = (int)obs_data_get_int(s, "method"); const char *window = obs_data_get_string(s, "window"); int priority = (int)obs_data_get_int(s, "priority"); bfree(wc->title); bfree(wc->class); bfree(wc->executable); build_window_strings(window, &wc->class, &wc->title, &wc->executable); if (wc->title != NULL) { blog(LOG_INFO, "[window-capture: '%s'] update settings:\n" "\texecutable: %s", obs_source_get_name(wc->source), wc->executable); blog(LOG_DEBUG, "\tclass: %s", wc->class); } wc->method = choose_method(method, wc->wgc_supported, wc->class); wc->priority = (enum window_priority)priority; wc->cursor = obs_data_get_bool(s, "cursor"); wc->use_wildcards = obs_data_get_bool(s, "use_wildcards"); wc->compatibility = obs_data_get_bool(s, "compatibility"); wc->client_area = obs_data_get_bool(s, "client_area"); } /* ------------------------------------------------------------------------- */ static const char *wc_getname(void *unused) { UNUSED_PARAMETER(unused); return TEXT_WINDOW_CAPTURE; } #define WINRT_IMPORT(func) \ do { \ exports->func = os_dlsym(module, #func); \ if (!exports->func) { \ success = false; \ blog(LOG_ERROR, \ "Could not load function '%s' from " \ "module '%s'", \ #func, module_name); \ } \ } while (false) static bool load_winrt_imports(struct winrt_exports *exports, void *module, const char *module_name) { bool success = true; WINRT_IMPORT(winrt_capture_supported); WINRT_IMPORT(winrt_capture_init); WINRT_IMPORT(winrt_capture_free); WINRT_IMPORT(winrt_capture_render); WINRT_IMPORT(winrt_capture_width); WINRT_IMPORT(winrt_capture_height); return success; } static void *wc_create(obs_data_t *settings, obs_source_t *source) { struct window_capture *wc = bzalloc(sizeof(struct window_capture)); wc->source = source; obs_enter_graphics(); const bool uses_d3d11 = gs_get_device_type() == GS_DEVICE_DIRECT3D_11; obs_leave_graphics(); if (uses_d3d11) { static const char *const module = "libobs-winrt"; bool use_winrt_capture = false; wc->winrt_module = os_dlopen(module); if (wc->winrt_module && load_winrt_imports(&wc->exports, wc->winrt_module, module) && wc->exports.winrt_capture_supported()) { wc->wgc_supported = true; } } update_settings(wc, settings); return wc; } static void wc_destroy(void *data) { struct window_capture *wc = data; if (wc) { obs_enter_graphics(); dc_capture_free(&wc->capture); obs_leave_graphics(); bfree(wc->title); bfree(wc->class); bfree(wc->executable); if (wc->winrt_module) os_dlclose(wc->winrt_module); bfree(wc); } } static void wc_update(void *data, obs_data_t *settings) { struct window_capture *wc = data; update_settings(wc, settings); /* forces a reset */ wc->window = NULL; wc->check_window_timer = WC_CHECK_TIMER; } static uint32_t wc_width(void *data) { struct window_capture *wc = data; return (wc->method == METHOD_WGC) ? wc->exports.winrt_capture_width(wc->capture_winrt) : wc->capture.width; } static uint32_t wc_height(void *data) { struct window_capture *wc = data; return (wc->method == METHOD_WGC) ? wc->exports.winrt_capture_height(wc->capture_winrt) : wc->capture.height; } static void wc_defaults(obs_data_t *defaults) { obs_data_set_default_int(defaults, "method", METHOD_AUTO); obs_data_set_default_bool(defaults, "cursor", true); obs_data_set_default_bool(defaults, "compatibility", false); obs_data_set_default_bool(defaults, "client_area", true); } static void update_settings_visibility(obs_properties_t *props, enum window_capture_method method) { const bool bitblt_options = method == METHOD_BITBLT; const bool wgc_options = method == METHOD_WGC; obs_property_t *p = obs_properties_get(props, "cursor"); obs_property_set_visible(p, bitblt_options); p = obs_properties_get(props, "compatibility"); obs_property_set_visible(p, bitblt_options); p = obs_properties_get(props, "client_area"); obs_property_set_visible(p, wgc_options); } static bool wc_capture_method_changed(obs_properties_t *props, obs_property_t *p, obs_data_t *settings) { struct window_capture *wc = obs_properties_get_param(props); update_settings(wc, settings); update_settings_visibility(props, wc->method); return true; } static bool wc_window_changed(obs_properties_t *props, obs_property_t *p, obs_data_t *settings) { struct window_capture *wc = obs_properties_get_param(props); update_settings(wc, settings); update_settings_visibility(props, wc->method); return true; } static obs_properties_t *wc_properties(void *data) { struct window_capture *wc = data; obs_properties_t *ppts = obs_properties_create(); obs_properties_set_param(ppts, wc, NULL); obs_property_t *p; p = obs_properties_add_list(ppts, "method", TEXT_METHOD, OBS_COMBO_TYPE_LIST, OBS_COMBO_FORMAT_INT); obs_property_list_add_int(p, TEXT_METHOD_AUTO, METHOD_AUTO); obs_property_list_add_int(p, TEXT_METHOD_BITBLT, METHOD_BITBLT); obs_property_list_add_int(p, TEXT_METHOD_WGC, METHOD_WGC); obs_property_list_item_disable(p, 1, !wc->wgc_supported); obs_property_set_modified_callback(p, wc_capture_method_changed); p = obs_properties_add_list(ppts, "window", TEXT_WINDOW, OBS_COMBO_TYPE_LIST, OBS_COMBO_FORMAT_STRING); fill_window_list(p, EXCLUDE_MINIMIZED, NULL); obs_property_set_modified_callback(p, wc_window_changed); p = obs_properties_add_list(ppts, "priority", TEXT_MATCH_PRIORITY, OBS_COMBO_TYPE_LIST, OBS_COMBO_FORMAT_INT); obs_property_list_add_int(p, TEXT_MATCH_TITLE, WINDOW_PRIORITY_TITLE); obs_property_list_add_int(p, TEXT_MATCH_CLASS, WINDOW_PRIORITY_CLASS); obs_property_list_add_int(p, TEXT_MATCH_EXE, WINDOW_PRIORITY_EXE); obs_properties_add_bool(ppts, "cursor", TEXT_CAPTURE_CURSOR); obs_properties_add_bool(ppts, "compatibility", TEXT_COMPATIBILITY); obs_properties_add_bool(ppts, "client_area", TEXT_CLIENT_AREA); return ppts; } static void wc_hide(void *data) { struct window_capture *wc = data; if (wc->capture_winrt) { wc->exports.winrt_capture_free(wc->capture_winrt); wc->capture_winrt = NULL; } memset(&wc->last_rect, 0, sizeof(wc->last_rect)); } #define RESIZE_CHECK_TIME 0.2f #define CURSOR_CHECK_TIME 0.2f static void wc_tick(void *data, float seconds) { struct window_capture *wc = data; RECT rect; bool reset_capture = false; if (!obs_source_showing(wc->source)) return; if (!wc->window || !IsWindow(wc->window)) { if (!wc->title && !wc->class) return; wc->check_window_timer += seconds; if (wc->check_window_timer < WC_CHECK_TIMER) { if (wc->capture.valid) dc_capture_free(&wc->capture); return; } if (wc->capture_winrt) { wc->exports.winrt_capture_free(wc->capture_winrt); wc->capture_winrt = NULL; } wc->check_window_timer = 0.0f; wc->window = (wc->method == METHOD_WGC) ? find_window_top_level(EXCLUDE_MINIMIZED, wc->priority, wc->class, wc->title, wc->executable) : find_window(EXCLUDE_MINIMIZED, wc->priority, wc->class, wc->title, wc->executable); if (!wc->window) { if (wc->capture.valid) dc_capture_free(&wc->capture); return; } reset_capture = true; } else if (IsIconic(wc->window)) { return; } wc->cursor_check_time += seconds; if (wc->cursor_check_time > CURSOR_CHECK_TIME) { DWORD foreground_pid, target_pid; // Can't just compare the window handle in case of app with child windows if (!GetWindowThreadProcessId(GetForegroundWindow(), &foreground_pid)) foreground_pid = 0; if (!GetWindowThreadProcessId(wc->window, &target_pid)) target_pid = 0; wc->capture.cursor_hidden = foreground_pid && target_pid && foreground_pid != target_pid; wc->cursor_check_time = 0.0f; } obs_enter_graphics(); if (wc->method == METHOD_BITBLT) { GetClientRect(wc->window, &rect); if (!reset_capture) { wc->resize_timer += seconds; if (wc->resize_timer >= RESIZE_CHECK_TIME) { if ((rect.bottom - rect.top) != (wc->last_rect.bottom - wc->last_rect.top) || (rect.right - rect.left) != (wc->last_rect.right - wc->last_rect.left)) reset_capture = true; wc->resize_timer = 0.0f; } } if (reset_capture) { wc->resize_timer = 0.0f; wc->last_rect = rect; dc_capture_free(&wc->capture); dc_capture_init(&wc->capture, 0, 0, rect.right - rect.left, rect.bottom - rect.top, wc->cursor, wc->compatibility); } dc_capture_capture(&wc->capture, wc->window); } else if (wc->method == METHOD_WGC) { if (wc->window && (wc->capture_winrt == NULL)) { wc->capture_winrt = wc->exports.winrt_capture_init( wc->cursor, wc->window, wc->client_area); } } obs_leave_graphics(); } static void wc_render(void *data, gs_effect_t *effect) { struct window_capture *wc = data; gs_effect_t *const opaque = obs_get_base_effect(OBS_EFFECT_OPAQUE); if (wc->method == METHOD_WGC) wc->exports.winrt_capture_render(wc->capture_winrt, opaque); else dc_capture_render(&wc->capture, opaque); UNUSED_PARAMETER(effect); } struct obs_source_info window_capture_info = { .id = "window_capture", .type = OBS_SOURCE_TYPE_INPUT, .output_flags = OBS_SOURCE_VIDEO | OBS_SOURCE_CUSTOM_DRAW, .get_name = wc_getname, .create = wc_create, .destroy = wc_destroy, .update = wc_update, .video_render = wc_render, .hide = wc_hide, .video_tick = wc_tick, .get_width = wc_width, .get_height = wc_height, .get_defaults = wc_defaults, .get_properties = wc_properties, .icon_type = OBS_ICON_TYPE_WINDOW_CAPTURE, };