diff --git a/plugins/win-capture/data/locale/en-US.ini b/plugins/win-capture/data/locale/en-US.ini index 28383e5a2..ef04ec151 100644 --- a/plugins/win-capture/data/locale/en-US.ini +++ b/plugins/win-capture/data/locale/en-US.ini @@ -12,8 +12,12 @@ Monitor="Display" PrimaryMonitor="Primary Monitor" GameCapture="Game Capture" GameCapture.AnyFullscreen="Capture any fullscreen application" +GameCapture.CaptureWindow="Capture specific window" +GameCapture.UseHotkey="Capture foreground window with hotkey" GameCapture.ForceScaling="Force Scaling" GameCapture.ScaleRes="Scale Resolution" GameCapture.LimitFramerate="Limit capture framerate" GameCapture.CaptureOverlays="Capture third-party overlays (such as steam)" GameCapture.AntiCheatHook="Use anti-cheat compatibility hook" +GameCapture.HotkeyStart="Capture foreground window" +GameCapture.HotkeyStop="Deactivate capture" diff --git a/plugins/win-capture/game-capture.c b/plugins/win-capture/game-capture.c index 2e2879a0a..ab7168929 100644 --- a/plugins/win-capture/game-capture.c +++ b/plugins/win-capture/game-capture.c @@ -1,6 +1,8 @@ #include #include +#include #include +#include #include #include #include @@ -19,7 +21,7 @@ #define info(format, ...) do_log(LOG_INFO, format, ##__VA_ARGS__) #define debug(format, ...) do_log(LOG_DEBUG, format, ##__VA_ARGS__) -#define SETTING_ANY_FULLSCREEN "capture_any_fullscreen" +#define SETTING_MODE "capture_mode" #define SETTING_CAPTURE_WINDOW "window" #define SETTING_ACTIVE_WINDOW "active_window" #define SETTING_WINDOW_PRIORITY "priority" @@ -32,6 +34,17 @@ #define SETTING_CAPTURE_OVERLAYS "capture_overlays" #define SETTING_ANTI_CHEAT_HOOK "anti_cheat_hook" +/* deprecated */ +#define SETTING_ANY_FULLSCREEN "capture_any_fullscreen" + +#define SETTING_MODE_ANY "any_fullscreen" +#define SETTING_MODE_WINDOW "window" +#define SETTING_MODE_HOTKEY "hotkey" + +#define HOTKEY_START "hotkey_start" +#define HOTKEY_STOP "hotkey_stop" + +#define TEXT_MODE obs_module_text("Mode") #define TEXT_GAME_CAPTURE obs_module_text("GameCapture") #define TEXT_ANY_FULLSCREEN obs_module_text("GameCapture.AnyFullscreen") #define TEXT_SLI_COMPATIBILITY obs_module_text("Compatibility") @@ -48,19 +61,32 @@ #define TEXT_CAPTURE_OVERLAYS obs_module_text("GameCapture.CaptureOverlays") #define TEXT_ANTI_CHEAT_HOOK obs_module_text("GameCapture.AntiCheatHook") +#define TEXT_MODE_ANY TEXT_ANY_FULLSCREEN +#define TEXT_MODE_WINDOW obs_module_text("GameCapture.CaptureWindow") +#define TEXT_MODE_HOTKEY obs_module_text("GameCapture.UseHotkey") + +#define TEXT_HOTKEY_START obs_module_text("GameCapture.HotkeyStart") +#define TEXT_HOTKEY_STOP obs_module_text("GameCapture.HotkeyStop") + #define DEFAULT_RETRY_INTERVAL 2.0f #define ERROR_RETRY_INTERVAL 4.0f +enum capture_mode { + CAPTURE_MODE_ANY, + CAPTURE_MODE_WINDOW, + CAPTURE_MODE_HOTKEY +}; + struct game_capture_config { char *title; char *class; char *executable; enum window_priority priority; + enum capture_mode mode; uint32_t scale_cx; uint32_t scale_cy; bool cursor : 1; bool force_shmem : 1; - bool capture_any_fullscreen : 1; bool force_scaling : 1; bool allow_transparency : 1; bool limit_framerate : 1; @@ -83,6 +109,14 @@ struct game_capture { float retry_time; float fps_reset_time; float retry_interval; + struct dstr title; + struct dstr class; + struct dstr executable; + enum window_priority priority; + obs_hotkey_pair_id hotkey_pair; + volatile long hotkey_window; + volatile bool deactivate_hook; + volatile bool activate_hook_now; bool wait_for_target_startup : 1; bool showing : 1; bool active : 1; @@ -222,25 +256,50 @@ static void game_capture_destroy(void *data) struct game_capture *gc = data; stop_capture(gc); + if (gc->hotkey_pair) + obs_hotkey_pair_unregister(gc->hotkey_pair); + obs_enter_graphics(); cursor_data_free(&gc->cursor_data); obs_leave_graphics(); + dstr_free(&gc->title); + dstr_free(&gc->class); + dstr_free(&gc->executable); free_config(&gc->config); bfree(gc); } +static inline bool using_older_non_mode_format(obs_data_t *settings) +{ + return obs_data_has_user_value(settings, SETTING_ANY_FULLSCREEN) && + !obs_data_has_user_value(settings, SETTING_MODE); +} + static inline void get_config(struct game_capture_config *cfg, obs_data_t *settings, const char *window) { int ret; const char *scale_str; + const char *mode_str = NULL; build_window_strings(window, &cfg->class, &cfg->title, &cfg->executable); - cfg->capture_any_fullscreen = obs_data_get_bool(settings, - SETTING_ANY_FULLSCREEN); + if (using_older_non_mode_format(settings)) { + bool any = obs_data_get_bool(settings, SETTING_ANY_FULLSCREEN); + mode_str = any ? SETTING_MODE_ANY : SETTING_MODE_WINDOW; + } else { + mode_str = obs_data_get_string(settings, SETTING_MODE); + } + + if (mode_str && strcmp(mode_str, SETTING_MODE_WINDOW) == 0) + cfg->mode = CAPTURE_MODE_WINDOW; + else if (mode_str && strcmp(mode_str, SETTING_MODE_HOTKEY) == 0) + cfg->mode = CAPTURE_MODE_HOTKEY; + else + cfg->mode = CAPTURE_MODE_ANY; + cfg->priority = (enum window_priority)obs_data_get_int(settings, SETTING_WINDOW_PRIORITY); cfg->force_shmem = obs_data_get_bool(settings, @@ -283,10 +342,10 @@ static inline int s_cmp(const char *str1, const char *str2) static inline bool capture_needs_reset(struct game_capture_config *cfg1, struct game_capture_config *cfg2) { - if (cfg1->capture_any_fullscreen != cfg2->capture_any_fullscreen) { + if (cfg1->mode != cfg2->mode) { return true; - } else if (!cfg1->capture_any_fullscreen && + } else if (cfg1->mode == CAPTURE_MODE_WINDOW && (s_cmp(cfg1->class, cfg2->class) != 0 || s_cmp(cfg1->title, cfg2->title) != 0 || s_cmp(cfg1->executable, cfg2->executable) != 0 || @@ -314,6 +373,33 @@ static inline bool capture_needs_reset(struct game_capture_config *cfg1, return false; } +static bool hotkey_start(void *data, obs_hotkey_pair_id id, + obs_hotkey_t *hotkey, bool pressed) +{ + if (pressed) { + struct game_capture *gc = data; + info("Activate hotkey pressed"); + os_atomic_set_long(&gc->hotkey_window, + (long)GetForegroundWindow()); + os_atomic_set_bool(&gc->deactivate_hook, true); + os_atomic_set_bool(&gc->activate_hook_now, true); + } + + return true; +} + +static bool hotkey_stop(void *data, obs_hotkey_pair_id id, + obs_hotkey_t *hotkey, bool pressed) +{ + if (pressed) { + struct game_capture *gc = data; + info("Deactivate hotkey pressed"); + os_atomic_set_bool(&gc->deactivate_hook, true); + } + + return true; +} + static void game_capture_update(void *data, obs_data_t *settings) { struct game_capture *gc = data; @@ -338,6 +424,30 @@ static void game_capture_update(void *data, obs_data_t *settings) gc->retry_interval = DEFAULT_RETRY_INTERVAL; gc->wait_for_target_startup = false; + if (cfg.mode == CAPTURE_MODE_HOTKEY) { + if (!gc->hotkey_pair) { + gc->hotkey_pair = obs_hotkey_pair_register_source( + gc->source, + HOTKEY_START, TEXT_HOTKEY_START, + HOTKEY_STOP, TEXT_HOTKEY_STOP, + hotkey_start, hotkey_stop, gc, gc); + } + } else if (gc->hotkey_pair) { + obs_hotkey_pair_unregister(gc->hotkey_pair); + gc->hotkey_pair = 0; + } + + dstr_free(&gc->title); + dstr_free(&gc->class); + dstr_free(&gc->executable); + + if (cfg.mode == CAPTURE_MODE_WINDOW) { + dstr_copy(&gc->title, gc->config.title); + dstr_copy(&gc->class, gc->config.class); + dstr_copy(&gc->executable, gc->config.executable); + gc->priority = gc->config.priority; + } + if (!gc->initial_config) { if (reset_capture) { stop_capture(gc); @@ -714,7 +824,7 @@ cleanup: static bool init_hook(struct game_capture *gc) { - if (gc->config.capture_any_fullscreen) { + if (gc->config.mode == CAPTURE_MODE_ANY) { struct dstr name = {0}; if (get_window_exe(&name, gc->next_window)) { info("attempting to hook fullscreen process: %s", @@ -722,7 +832,7 @@ static bool init_hook(struct game_capture *gc) dstr_free(&name); } } else { - info("attempting to hook process: %s", gc->config.executable); + info("attempting to hook process: %s", gc->executable.array); } if (!open_target_process(gc)) { @@ -826,16 +936,16 @@ static void get_selected_window(struct game_capture *gc) { HWND window; - if (strcmpi(gc->config.class, "dwm") == 0) { + if (dstr_cmpi(&gc->class, "dwm") == 0) { wchar_t class_w[512]; - os_utf8_to_wcs(gc->config.class, 0, class_w, 512); + os_utf8_to_wcs(gc->class.array, 0, class_w, 512); window = FindWindowW(class_w, NULL); } else { window = find_window(INCLUDE_MINIMIZED, - gc->config.priority, - gc->config.class, - gc->config.title, - gc->config.executable); + gc->priority, + gc->class.array, + gc->title.array, + gc->executable.array); } if (window) { @@ -847,7 +957,7 @@ static void get_selected_window(struct game_capture *gc) static void try_hook(struct game_capture *gc) { - if (gc->config.capture_any_fullscreen) { + if (gc->config.mode == CAPTURE_MODE_ANY) { get_fullscreen_window(gc); } else { get_selected_window(gc); @@ -1279,6 +1389,26 @@ static inline bool capture_valid(struct game_capture *gc) static void game_capture_tick(void *data, float seconds) { struct game_capture *gc = data; + bool deactivate = os_atomic_set_bool(&gc->deactivate_hook, false); + bool activate_now = os_atomic_set_bool(&gc->activate_hook_now, false); + + if (activate_now) { + HWND hwnd = (HWND)os_atomic_load_long(&gc->hotkey_window); + + if (get_window_exe(&gc->executable, hwnd)) { + get_window_title(&gc->title, hwnd); + get_window_class(&gc->class, hwnd); + + gc->priority = WINDOW_PRIORITY_CLASS; + gc->retry_time = 10.0f; + gc->activate_hook = true; + } else { + deactivate = false; + activate_now = false; + } + } else if (deactivate) { + gc->activate_hook = false; + } if (!obs_source_showing(gc->source)) { if (gc->showing) { @@ -1295,6 +1425,9 @@ static void game_capture_tick(void *data, float seconds) if (gc->hook_stop && object_signalled(gc->hook_stop)) { stop_capture(gc); } + if (gc->active && deactivate) { + stop_capture(gc); + } if (gc->active && !gc->hook_ready && gc->process_id) { gc->hook_ready = get_event_plus_id(EVENT_HOOK_READY, @@ -1334,7 +1467,7 @@ static void game_capture_tick(void *data, float seconds) if (!gc->active) { if (!gc->error_acquiring && gc->retry_time > gc->retry_interval) { - if (gc->config.capture_any_fullscreen || + if (gc->config.mode == CAPTURE_MODE_ANY || gc->activate_hook) { try_hook(gc); gc->retry_time = 0.0f; @@ -1438,7 +1571,7 @@ static const char *game_capture_name(void *unused) static void game_capture_defaults(obs_data_t *settings) { - obs_data_set_default_bool(settings, SETTING_ANY_FULLSCREEN, true); + obs_data_set_default_string(settings, SETTING_MODE, SETTING_MODE_ANY); obs_data_set_default_int(settings, SETTING_WINDOW_PRIORITY, (int)WINDOW_PRIORITY_EXE); obs_data_set_default_bool(settings, SETTING_COMPATIBILITY, false); @@ -1451,17 +1584,24 @@ static void game_capture_defaults(obs_data_t *settings) obs_data_set_default_bool(settings, SETTING_ANTI_CHEAT_HOOK, true); } -static bool any_fullscreen_callback(obs_properties_t *ppts, +static bool mode_callback(obs_properties_t *ppts, obs_property_t *p, obs_data_t *settings) { - bool any_fullscreen = obs_data_get_bool(settings, - SETTING_ANY_FULLSCREEN); + bool capture_window; + + if (using_older_non_mode_format(settings)) { + capture_window = !obs_data_get_bool(settings, + SETTING_ANY_FULLSCREEN); + } else { + const char *mode = obs_data_get_string(settings, SETTING_MODE); + capture_window = strcmp(mode, SETTING_MODE_WINDOW) == 0; + } p = obs_properties_get(ppts, SETTING_CAPTURE_WINDOW); - obs_property_set_enabled(p, !any_fullscreen); + obs_property_set_visible(p, capture_window); p = obs_properties_get(ppts, SETTING_WINDOW_PRIORITY); - obs_property_set_enabled(p, !any_fullscreen); + obs_property_set_visible(p, capture_window); return true; } @@ -1568,13 +1708,32 @@ static obs_properties_t *game_capture_properties(void *data) } } + /* update from deprecated settings */ + if (data) { + struct game_capture *gc = data; + obs_data_t *settings = obs_source_get_settings(gc->source); + if (using_older_non_mode_format(settings)) { + bool any = obs_data_get_bool(settings, + SETTING_ANY_FULLSCREEN); + const char *mode = any ? + SETTING_MODE_ANY : SETTING_MODE_WINDOW; + + obs_data_set_string(settings, SETTING_MODE, mode); + } + obs_data_release(settings); + } + obs_properties_t *ppts = obs_properties_create(); obs_property_t *p; - p = obs_properties_add_bool(ppts, SETTING_ANY_FULLSCREEN, - TEXT_ANY_FULLSCREEN); + p = obs_properties_add_list(ppts, SETTING_MODE, TEXT_MODE, + OBS_COMBO_TYPE_LIST, OBS_COMBO_FORMAT_STRING); - obs_property_set_modified_callback(p, any_fullscreen_callback); + obs_property_list_add_string(p, TEXT_MODE_ANY, SETTING_MODE_ANY); + obs_property_list_add_string(p, TEXT_MODE_WINDOW, SETTING_MODE_WINDOW); + obs_property_list_add_string(p, TEXT_MODE_HOTKEY, SETTING_MODE_HOTKEY); + + obs_property_set_modified_callback(p, mode_callback); p = obs_properties_add_list(ppts, SETTING_CAPTURE_WINDOW, TEXT_WINDOW, OBS_COMBO_TYPE_LIST, OBS_COMBO_FORMAT_STRING);