diff --git a/plugins/mac-capture/CMakeLists.txt b/plugins/mac-capture/CMakeLists.txt index 73f321a77..561190aa3 100644 --- a/plugins/mac-capture/CMakeLists.txt +++ b/plugins/mac-capture/CMakeLists.txt @@ -14,15 +14,18 @@ include_directories(${COREAUDIO} set(mac-capture_HEADERS audio-device-enum.h - mac-helpers.h) + mac-helpers.h + window-utils.h) set(mac-capture_SOURCES plugin-main.c audio-device-enum.c mac-audio.c - mac-display-capture.m) + mac-display-capture.m + window-utils.m) set_source_files_properties(mac-display-capture.m + window-utils.m PROPERTIES LANGUAGE C) add_library(mac-capture MODULE diff --git a/plugins/mac-capture/data/locale/en-US.ini b/plugins/mac-capture/data/locale/en-US.ini index d222c6ae5..699e3eafc 100644 --- a/plugins/mac-capture/data/locale/en-US.ini +++ b/plugins/mac-capture/data/locale/en-US.ini @@ -5,3 +5,5 @@ CoreAudio.Device.Default="Default" DisplayCapture="Display Capture" DisplayCapture.Display="Display" DisplayCapture.ShowCursor="Show Cursor" +WindowUtils.Window="Window" +WindowUtils.ShowEmptyNames="Show Windows with empty names" diff --git a/plugins/mac-capture/window-utils.h b/plugins/mac-capture/window-utils.h new file mode 100644 index 000000000..7ca1a2844 --- /dev/null +++ b/plugins/mac-capture/window-utils.h @@ -0,0 +1,32 @@ +#import +#import + +#include +#include + +struct cocoa_window { + CGWindowID window_id; + + pthread_mutex_t name_lock; + NSString *owner_name; + NSString *window_name; + + uint64_t next_search_time; +}; +typedef struct cocoa_window *cocoa_window_t; + +NSArray *enumerate_cocoa_windows(void); + +bool find_window(cocoa_window_t cw, obs_data_t settings, bool force); + +void init_window(cocoa_window_t cw, obs_data_t settings); + +void destroy_window(cocoa_window_t cw); + +void update_window(cocoa_window_t cw, obs_data_t settings); + +void window_defaults(obs_data_t settings); + +void add_window_properties(obs_properties_t props); + +void show_window_properties(obs_properties_t props, bool show); diff --git a/plugins/mac-capture/window-utils.m b/plugins/mac-capture/window-utils.m new file mode 100644 index 000000000..feba6bed4 --- /dev/null +++ b/plugins/mac-capture/window-utils.m @@ -0,0 +1,233 @@ +#include "window-utils.h" + +#include + +#define WINDOW_NAME ((NSString*)kCGWindowName) +#define WINDOW_NUMBER ((NSString*)kCGWindowNumber) +#define OWNER_NAME ((NSString*)kCGWindowOwnerName) +#define OWNER_PID ((NSNumber*)kCGWindowOwnerPID) + +static NSComparator win_info_cmp = ^(NSDictionary *o1, NSDictionary *o2) +{ + NSComparisonResult res = [o1[OWNER_NAME] compare:o2[OWNER_NAME]]; + if (res != NSOrderedSame) + return res; + + res = [o1[OWNER_PID] compare:o2[OWNER_PID]]; + if (res != NSOrderedSame) + return res; + + res = [o1[WINDOW_NAME] compare:o2[WINDOW_NAME]]; + if (res != NSOrderedSame) + return res; + + return [o1[WINDOW_NUMBER] compare:o2[WINDOW_NUMBER]]; +}; + +NSArray *enumerate_windows(void) +{ + NSArray *arr = (NSArray*)CGWindowListCopyWindowInfo( + kCGWindowListOptionOnScreenOnly, + kCGNullWindowID); + + [arr autorelease]; + + return [arr sortedArrayUsingComparator:win_info_cmp]; +} + +#define WAIT_TIME_MS 500 +#define WAIT_TIME_US WAIT_TIME_MS * 1000 +#define WAIT_TIME_NS WAIT_TIME_US * 1000 + +bool find_window(cocoa_window_t cw, obs_data_t settings, bool force) +{ + if (!force && cw->next_search_time > os_gettime_ns()) + return false; + + cw->next_search_time = os_gettime_ns() + WAIT_TIME_NS; + + pthread_mutex_lock(&cw->name_lock); + + if (!cw->window_name.length && !cw->owner_name.length) + goto invalid_name; + + for (NSDictionary *dict in enumerate_windows()) { + if (![cw->owner_name isEqualToString:dict[OWNER_NAME]]) + continue; + + if (![cw->window_name isEqualToString:dict[WINDOW_NAME]]) + continue; + + pthread_mutex_unlock(&cw->name_lock); + + NSNumber *window_id = (NSNumber*)dict[WINDOW_NUMBER]; + cw->window_id = window_id.intValue; + + obs_data_set_int(settings, "window", cw->window_id); + return true; + } + +invalid_name: + pthread_mutex_unlock(&cw->name_lock); + return false; +} + +void init_window(cocoa_window_t cw, obs_data_t settings) +{ + pthread_mutex_init(&cw->name_lock, NULL); + + cw->owner_name = @(obs_data_get_string(settings, "owner_name")); + cw->window_name = @(obs_data_get_string(settings, "window_name")); + [cw->owner_name retain]; + [cw->window_name retain]; + find_window(cw, settings, true); +} + +void destroy_window(cocoa_window_t cw) +{ + pthread_mutex_destroy(&cw->name_lock); + [cw->owner_name release]; + [cw->window_name release]; +} + +void update_window(cocoa_window_t cw, obs_data_t settings) +{ + pthread_mutex_lock(&cw->name_lock); + [cw->owner_name release]; + [cw->window_name release]; + cw->owner_name = @(obs_data_get_string(settings, "owner_name")); + cw->window_name = @(obs_data_get_string(settings, "window_name")); + [cw->owner_name retain]; + [cw->window_name retain]; + pthread_mutex_unlock(&cw->name_lock); + + cw->window_id = obs_data_get_int(settings, "window"); +} + +static inline const char *make_name(NSString *owner, NSString *name) +{ + if (!owner.length) + return ""; + + NSString *str = [NSString stringWithFormat:@"[%@] %@", owner, name]; + return str.UTF8String; +} + +static inline NSDictionary *find_window_dict(NSArray *arr, int window_id) +{ + for (NSDictionary *dict in arr) { + NSNumber *wid = (NSNumber*)dict[WINDOW_NUMBER]; + if (wid.intValue == window_id) + return dict; + } + + return nil; +} + +static inline bool window_changed_internal(obs_property_t p, + obs_data_t settings) +{ + int window_id = obs_data_get_int(settings, "window"); + NSString *window_owner = @(obs_data_get_string(settings, "owner_name")); + NSString *window_name = + @(obs_data_get_string(settings, "window_name")); + + NSDictionary *win_info = @{ + OWNER_NAME: window_owner, + WINDOW_NAME: window_name, + }; + + NSArray *arr = enumerate_windows(); + + bool show_empty_names = obs_data_get_bool(settings, "show_empty_names"); + + NSDictionary *cur = find_window_dict(arr, window_id); + bool window_found = cur != nil; + bool window_added = window_found; + + obs_property_list_clear(p); + for (NSDictionary *dict in arr) { + NSString *owner = (NSString*)dict[OWNER_NAME]; + NSString *name = (NSString*)dict[WINDOW_NAME]; + NSNumber *wid = (NSNumber*)dict[WINDOW_NUMBER]; + + if (!window_added && + win_info_cmp(win_info, dict) == NSOrderedAscending) { + window_added = true; + size_t idx = obs_property_list_add_int(p, + make_name(window_owner, window_name), + window_id); + obs_property_list_item_disable(p, idx, true); + } + + if (!show_empty_names && !name.length && + window_id != wid.intValue) + continue; + + obs_property_list_add_int(p, make_name(owner, name), + wid.intValue); + } + + if (!window_added) { + size_t idx = obs_property_list_add_int(p, + make_name(window_owner, window_name), + window_id); + obs_property_list_item_disable(p, idx, true); + } + + if (!window_found) + return true; + + NSString *owner = (NSString*)cur[OWNER_NAME]; + NSString *window = (NSString*)cur[WINDOW_NAME]; + + obs_data_set_string(settings, "owner_name", owner.UTF8String); + obs_data_set_string(settings, "window_name", window.UTF8String); + + return true; +} + +static bool window_changed(obs_properties_t props, obs_property_t p, + obs_data_t settings) +{ + UNUSED_PARAMETER(props); + + @autoreleasepool { + return window_changed_internal(p, settings); + } +} + +static bool toggle_empty_names(obs_properties_t props, obs_property_t p, + obs_data_t settings) +{ + UNUSED_PARAMETER(p); + + return window_changed(props, obs_properties_get(props, "window"), + settings); +} + +void window_defaults(obs_data_t settings) +{ + obs_data_set_default_int(settings, "window", kCGNullWindowID); + obs_data_set_default_bool(settings, "show_empty_names", false); +} + +void add_window_properties(obs_properties_t props) +{ + obs_property_t window_list = obs_properties_add_list(props, + "window", obs_module_text("WindowUtils.Window"), + OBS_COMBO_TYPE_LIST, OBS_COMBO_FORMAT_INT); + obs_property_set_modified_callback(window_list, window_changed); + + obs_property_t empty = obs_properties_add_bool(props, + "show_empty_names", + obs_module_text("WindowUtils.ShowEmptyNames")); + obs_property_set_modified_callback(empty, toggle_empty_names); +} + +void show_window_properties(obs_properties_t props, bool show) +{ + obs_property_set_visible(obs_properties_get(props, "window"), show); + obs_property_set_visible( + obs_properties_get(props, "show_empty_names"), show); +}