From bc64fa97a03c5f80df0eb82a042b6fe2338f913c Mon Sep 17 00:00:00 2001 From: Palana Date: Wed, 20 Aug 2014 00:36:24 +0200 Subject: [PATCH 1/3] Add CoreGraphics window utilities --- plugins/mac-capture/CMakeLists.txt | 7 +- plugins/mac-capture/data/locale/en-US.ini | 2 + plugins/mac-capture/window-utils.h | 32 +++ plugins/mac-capture/window-utils.m | 233 ++++++++++++++++++++++ 4 files changed, 272 insertions(+), 2 deletions(-) create mode 100644 plugins/mac-capture/window-utils.h create mode 100644 plugins/mac-capture/window-utils.m 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); +} From 7b6c4a130adc842a7250d1cc5bcfb48b87cfa911 Mon Sep 17 00:00:00 2001 From: Palana Date: Wed, 20 Aug 2014 00:38:26 +0200 Subject: [PATCH 2/3] Add Display Capture crop modes --- plugins/mac-capture/data/locale/en-US.ini | 9 + plugins/mac-capture/mac-display-capture.m | 353 +++++++++++++++++++++- 2 files changed, 348 insertions(+), 14 deletions(-) diff --git a/plugins/mac-capture/data/locale/en-US.ini b/plugins/mac-capture/data/locale/en-US.ini index 699e3eafc..a5fee74ba 100644 --- a/plugins/mac-capture/data/locale/en-US.ini +++ b/plugins/mac-capture/data/locale/en-US.ini @@ -7,3 +7,12 @@ DisplayCapture.Display="Display" DisplayCapture.ShowCursor="Show Cursor" WindowUtils.Window="Window" WindowUtils.ShowEmptyNames="Show Windows with empty names" +CropMode="Crop" +CropMode.None="None" +CropMode.Manual="Manual" +CropMode.ToWindow="To Window" +CropMode.ToWindowAndManual="To Window and Manual" +Crop.origin.x="Crop left" +Crop.origin.y="Crop top" +Crop.size.width="Crop right" +Crop.size.height="Crop bottom" diff --git a/plugins/mac-capture/mac-display-capture.m b/plugins/mac-capture/mac-display-capture.m index 4f6f1f024..c2cd42fcb 100644 --- a/plugins/mac-capture/mac-display-capture.m +++ b/plugins/mac-capture/mac-display-capture.m @@ -6,17 +6,42 @@ #import #import +#include "window-utils.h" + +enum crop_mode { + CROP_NONE, + CROP_MANUAL, + CROP_TO_WINDOW, + CROP_TO_WINDOW_AND_MANUAL, + CROP_INVALID +}; + +static inline bool requires_window(enum crop_mode mode) +{ + return mode == CROP_TO_WINDOW || mode == CROP_TO_WINDOW_AND_MANUAL; +} + struct display_capture { obs_source_t source; gs_samplerstate_t sampler; gs_effect_t draw_effect; gs_texture_t tex; + gs_vertbuffer_t vertbuf; + NSScreen *screen; unsigned display; - uint32_t width, height; + NSRect frame; bool hide_cursor; + enum crop_mode crop; + CGRect crop_rect; + + struct cocoa_window window; + CGRect window_rect; + bool on_screen; + bool hide_when_minimized; + os_event_t disp_finished; CGDisplayStreamRef disp; IOSurfaceRef current, prev; @@ -24,6 +49,11 @@ struct display_capture { pthread_mutex_t mutex; }; +static inline bool crop_mode_valid(enum crop_mode mode) +{ + return CROP_NONE <= mode && mode < CROP_INVALID; +} + static void destroy_display_stream(struct display_capture *dc) { if (dc->disp) { @@ -53,6 +83,11 @@ static void destroy_display_stream(struct display_capture *dc) dc->disp = NULL; } + if (dc->screen) { + [dc->screen release]; + dc->screen = nil; + } + os_event_destroy(dc->disp_finished); } @@ -71,13 +106,45 @@ static void display_capture_destroy(void *data) gs_samplerstate_destroy(dc->sampler); if (dc->draw_effect) gs_effect_destroy(dc->draw_effect); + if (dc->vertbuf) + gs_vertexbuffer_destroy(dc->vertbuf); obs_leave_graphics(); + destroy_window(&dc->window); + pthread_mutex_destroy(&dc->mutex); bfree(dc); } +static inline void update_window_params(struct display_capture *dc) +{ + if (!requires_window(dc->crop)) + return; + + NSArray *arr = (NSArray*)CGWindowListCopyWindowInfo( + kCGWindowListOptionIncludingWindow, + dc->window.window_id); + + if (arr.count) { + NSDictionary *dict = arr[0]; + NSDictionary *ref = dict[(NSString*)kCGWindowBounds]; + CGRectMakeWithDictionaryRepresentation((CFDictionaryRef)ref, + &dc->window_rect); + dc->on_screen = dict[(NSString*)kCGWindowIsOnscreen] != nil; + dc->window_rect = + [dc->screen convertRectToBacking:dc->window_rect]; + + } else { + if (find_window(&dc->window, NULL, false)) + update_window_params(dc); + else + dc->on_screen = false; + } + + [arr release]; +} + static inline void display_stream_update(struct display_capture *dc, CGDisplayStreamFrameStatus status, uint64_t display_time, IOSurfaceRef frame_surface, CGDisplayStreamUpdateRef update_ref) @@ -97,6 +164,9 @@ static inline void display_stream_update(struct display_capture *dc, dc->current = frame_surface; CFRetain(dc->current); IOSurfaceIncrementUseCount(dc->current); + + update_window_params(dc); + pthread_mutex_unlock(&dc->mutex); } @@ -117,21 +187,18 @@ static bool init_display_stream(struct display_capture *dc) if (dc->display >= [NSScreen screens].count) return false; - NSScreen *screen = [NSScreen screens][dc->display]; + dc->screen = [[NSScreen screens][dc->display] retain]; - NSRect frame = [screen convertRectToBacking:screen.frame]; + dc->frame = [dc->screen convertRectToBacking:dc->screen.frame]; - dc->width = frame.size.width; - dc->height = frame.size.height; - - NSNumber *screen_num = screen.deviceDescription[@"NSScreenNumber"]; + NSNumber *screen_num = dc->screen.deviceDescription[@"NSScreenNumber"]; CGDirectDisplayID disp_id = (CGDirectDisplayID)screen_num.pointerValue; NSDictionary *rect_dict = CFBridgingRelease( CGRectCreateDictionaryRepresentation( CGRectMake(0, 0, - screen.frame.size.width, - screen.frame.size.height))); + dc->screen.frame.size.width, + dc->screen.frame.size.height))); NSDictionary *dict = @{ (__bridge NSString*)kCGDisplayStreamSourceRect: rect_dict, @@ -142,8 +209,9 @@ static bool init_display_stream(struct display_capture *dc) os_event_init(&dc->disp_finished, OS_EVENT_TYPE_MANUAL); + const CGSize *size = &dc->frame.size; dc->disp = CGDisplayStreamCreateWithDispatchQueue(disp_id, - dc->width, dc->height, 'BGRA', + size->width, size->height, 'BGRA', (__bridge CFDictionaryRef)dict, dispatch_queue_create(NULL, NULL), ^(CGDisplayStreamFrameStatus status, @@ -159,6 +227,30 @@ static bool init_display_stream(struct display_capture *dc) return !CGDisplayStreamStart(dc->disp); } +bool init_vertbuf(struct display_capture *dc) +{ + struct gs_vb_data *vb_data = gs_vbdata_create(); + vb_data->num = 4; + vb_data->points = bzalloc(sizeof(struct vec3) * 4); + if (!vb_data->points) + return false; + + vb_data->num_tex = 1; + vb_data->tvarray = bzalloc(sizeof(struct gs_tvertarray)); + if (!vb_data->tvarray) + return false; + + vb_data->tvarray[0].width = 2; + vb_data->tvarray[0].array = bzalloc(sizeof(struct vec2) * 4); + if (!vb_data->tvarray[0].array) + return false; + + dc->vertbuf = gs_vertexbuffer_create(vb_data, GS_DYNAMIC); + return dc->vertbuf != NULL; +} + +void load_crop(struct display_capture *dc, obs_data_t settings); + static void *display_capture_create(obs_data_t settings, obs_source_t source) { @@ -188,8 +280,14 @@ static void *display_capture_create(obs_data_t settings, if (!dc->draw_effect) goto fail; + if (!init_vertbuf(dc)) + goto fail; + obs_leave_graphics(); + init_window(&dc->window, settings); + load_crop(dc, settings); + dc->display = obs_data_get_int(settings, "display"); pthread_mutex_init(&dc->mutex, NULL); @@ -204,6 +302,28 @@ fail: return NULL; } +static void build_sprite(struct gs_vb_data *data, float fcx, float fcy, + float start_u, float end_u, float start_v, float end_v) +{ + struct vec2 *tvarray = data->tvarray[0].array; + + vec3_set(data->points+1, fcx, 0.0f, 0.0f); + vec3_set(data->points+2, 0.0f, fcy, 0.0f); + vec3_set(data->points+3, fcx, fcy, 0.0f); + vec2_set(tvarray, start_u, start_v); + vec2_set(tvarray+1, end_u, start_v); + vec2_set(tvarray+2, start_u, end_v); + vec2_set(tvarray+3, end_u, end_v); +} + +static inline void build_sprite_rect(struct gs_vb_data *data, + float origin_x, float origin_y, float end_x, float end_y) +{ + build_sprite(data, fabs(end_x - origin_x), fabs(end_y - origin_y), + origin_x, end_x, + origin_y, end_y); +} + static void display_capture_video_tick(void *data, float seconds) { UNUSED_PARAMETER(seconds); @@ -223,13 +343,52 @@ static void display_capture_video_tick(void *data, float seconds) if (prev_prev == dc->prev) return; + if (requires_window(dc->crop) && !dc->on_screen) + goto cleanup; + + CGPoint origin = { 0.f }; + CGPoint end = { 0.f }; + + switch (dc->crop) { + float x, y; + case CROP_INVALID: + break; + + case CROP_MANUAL: + origin.x += dc->crop_rect.origin.x; + origin.y += dc->crop_rect.origin.y; + end.y -= dc->crop_rect.size.height; + end.x -= dc->crop_rect.size.width; + case CROP_NONE: + end.y += dc->frame.size.height; + end.x += dc->frame.size.width; + break; + + case CROP_TO_WINDOW_AND_MANUAL: + origin.x += dc->crop_rect.origin.x; + origin.y += dc->crop_rect.origin.y; + end.y -= dc->crop_rect.size.height; + end.x -= dc->crop_rect.size.width; + case CROP_TO_WINDOW: + origin.x += x = dc->window_rect.origin.x - dc->frame.origin.x; + origin.y += y = dc->window_rect.origin.y - dc->frame.origin.y; + end.y += dc->window_rect.size.height + y; + end.x += dc->window_rect.size.width + x; + break; + } + + obs_enter_graphics(); + build_sprite_rect(gs_vertexbuffer_get_data(dc->vertbuf), + origin.x, origin.y, end.x, end.y); + if (dc->tex) gs_texture_rebind_iosurface(dc->tex, dc->prev); else dc->tex = gs_texture_create_from_iosurface(dc->prev); obs_leave_graphics(); +cleanup: if (prev_prev) { IOSurfaceDecrementUseCount(prev_prev); CFRelease(prev_prev); @@ -242,9 +401,12 @@ static void display_capture_video_render(void *data, gs_effect_t effect) struct display_capture *dc = data; - if (!dc->tex) + if (!dc->tex || (requires_window(dc->crop) && !dc->on_screen)) return; + gs_vertexbuffer_flush(dc->vertbuf); + gs_load_vertexbuffer(dc->vertbuf); + gs_load_indexbuffer(NULL); gs_load_samplerstate(dc->sampler, 0); gs_technique_t tech = gs_effect_get_technique(dc->draw_effect, "Default"); @@ -253,7 +415,7 @@ static void display_capture_video_render(void *data, gs_effect_t effect) gs_technique_begin(tech); gs_technique_begin_pass(tech, 0); - gs_draw_sprite(dc->tex, 0, 0, 0); + gs_draw(GS_TRISTRIP, 0, 4); gs_technique_end_pass(tech); gs_technique_end(tech); @@ -264,27 +426,114 @@ static const char *display_capture_getname(void) return obs_module_text("DisplayCapture"); } +#define CROPPED_LENGTH(rect, origin_, length) \ + fabs((rect ## .size. ## length - dc->crop_rect.size. ## length) - \ + (rect ## .origin. ## origin_ + dc->crop_rect.origin. ## origin_)) + static uint32_t display_capture_getwidth(void *data) { struct display_capture *dc = data; - return dc->width; + + float crop = dc->crop_rect.origin.x + dc->crop_rect.size.width; + switch (dc->crop) { + case CROP_NONE: + return dc->frame.size.width; + + case CROP_MANUAL: + return fabs(dc->frame.size.width - crop); + + case CROP_TO_WINDOW: + return dc->window_rect.size.width; + + case CROP_TO_WINDOW_AND_MANUAL: + return fabs(dc->window_rect.size.width - crop); + + case CROP_INVALID: + break; + } + return 0; } static uint32_t display_capture_getheight(void *data) { struct display_capture *dc = data; - return dc->height; + + float crop = dc->crop_rect.origin.y + dc->crop_rect.size.height; + switch (dc->crop) { + case CROP_NONE: + return dc->frame.size.height; + + case CROP_MANUAL: + return fabs(dc->frame.size.height - crop); + + case CROP_TO_WINDOW: + return dc->window_rect.size.height; + + case CROP_TO_WINDOW_AND_MANUAL: + return fabs(dc->window_rect.size.height - crop); + + case CROP_INVALID: + break; + } + return 0; } static void display_capture_defaults(obs_data_t settings) { obs_data_set_default_int(settings, "display", 0); obs_data_set_default_bool(settings, "show_cursor", true); + obs_data_set_default_int(settings, "crop_mode", CROP_NONE); + + window_defaults(settings); +} + +void load_crop_mode(enum crop_mode *mode, obs_data_t settings) +{ + *mode = obs_data_get_int(settings, "crop_mode"); + if (!crop_mode_valid(*mode)) + *mode = CROP_NONE; +} + +void load_crop(struct display_capture *dc, obs_data_t settings) +{ + load_crop_mode(&dc->crop, settings); + +#define CROP_VAR_NAME(var, mode) (mode "." #var) +#define LOAD_CROP_VAR(var, mode) \ + dc->crop_rect.var = obs_data_get_double(settings, \ + CROP_VAR_NAME(var, mode)); + switch (dc->crop) { + case CROP_MANUAL: + LOAD_CROP_VAR(origin.x, "manual"); + LOAD_CROP_VAR(origin.y, "manual"); + LOAD_CROP_VAR(size.width, "manual"); + LOAD_CROP_VAR(size.height, "manual"); + break; + + case CROP_TO_WINDOW_AND_MANUAL: + LOAD_CROP_VAR(origin.x, "window"); + LOAD_CROP_VAR(origin.y, "window"); + LOAD_CROP_VAR(size.width, "window"); + LOAD_CROP_VAR(size.height, "window"); + break; + + case CROP_NONE: + case CROP_TO_WINDOW: + case CROP_INVALID: + break; + } +#undef LOAD_CROP_VAR } static void display_capture_update(void *data, obs_data_t settings) { struct display_capture *dc = data; + + load_crop(dc, settings); + + if (requires_window(dc->crop)) + update_window(&dc->window, settings); + unsigned display = obs_data_get_int(settings, "display"); bool show_cursor = obs_data_get_bool(settings, "show_cursor"); if (dc->display == display && dc->hide_cursor != show_cursor) @@ -300,6 +549,47 @@ static void display_capture_update(void *data, obs_data_t settings) obs_leave_graphics(); } +static bool switch_crop_mode(obs_properties_t props, obs_property_t p, + obs_data_t settings) +{ + UNUSED_PARAMETER(p); + + enum crop_mode crop; + load_crop_mode(&crop, settings); + + const char *name; + bool visible; +#define LOAD_CROP_VAR(var, mode) \ + name = CROP_VAR_NAME(var, mode); \ + obs_property_set_visible(obs_properties_get(props, name), visible); + + visible = crop == CROP_MANUAL; + LOAD_CROP_VAR(origin.x, "manual"); + LOAD_CROP_VAR(origin.y, "manual"); + LOAD_CROP_VAR(size.width, "manual"); + LOAD_CROP_VAR(size.height, "manual"); + + visible = crop == CROP_TO_WINDOW_AND_MANUAL; + LOAD_CROP_VAR(origin.x, "window"); + LOAD_CROP_VAR(origin.y, "window"); + LOAD_CROP_VAR(size.width, "window"); + LOAD_CROP_VAR(size.height, "window"); +#undef LOAD_CROP_VAR + + show_window_properties(props, visible || crop == CROP_TO_WINDOW); + return true; +} + +static const char *crop_names[] = { + "CropMode.None", + "CropMode.Manual", + "CropMode.ToWindow", + "CropMode.ToWindowAndManual" +}; + +#ifndef COUNTOF +#define COUNTOF(x) (sizeof(x)/sizeof(x[0])) +#endif static obs_properties_t display_capture_properties(void) { obs_properties_t props = obs_properties_create(); @@ -317,6 +607,41 @@ static obs_properties_t display_capture_properties(void) obs_properties_add_bool(props, "show_cursor", obs_module_text("DisplayCapture.ShowCursor")); + obs_property_t crop = obs_properties_add_list(props, "crop_mode", + obs_module_text("CropMode"), + OBS_COMBO_TYPE_LIST, OBS_COMBO_FORMAT_INT); + obs_property_set_modified_callback(crop, switch_crop_mode); + + for (unsigned i = 0; i < COUNTOF(crop_names); i++) { + const char *name = obs_module_text(crop_names[i]); + obs_property_list_add_int(crop, name, i); + } + + add_window_properties(props); + show_window_properties(props, false); + + obs_property_t p; + const char *name; + float min; +#define LOAD_CROP_VAR(var, mode) \ + name = CROP_VAR_NAME(var, mode); \ + p = obs_properties_add_float(props, name, \ + obs_module_text("Crop."#var), min, 4096.f, .5f); \ + obs_property_set_visible(p, false); + + min = 0.f; + LOAD_CROP_VAR(origin.x, "manual"); + LOAD_CROP_VAR(origin.y, "manual"); + LOAD_CROP_VAR(size.width, "manual"); + LOAD_CROP_VAR(size.height, "manual"); + + min = -4096.f; + LOAD_CROP_VAR(origin.x, "window"); + LOAD_CROP_VAR(origin.y, "window"); + LOAD_CROP_VAR(size.width, "window"); + LOAD_CROP_VAR(size.height, "window"); +#undef LOAD_CROP_VAR + return props; } From f8ed37dec46582ff0a7735535bd3a9e7a9a9dc4a Mon Sep 17 00:00:00 2001 From: Palana Date: Wed, 20 Aug 2014 00:39:41 +0200 Subject: [PATCH 3/3] Add Window Capture to mac-capture plugin --- plugins/mac-capture/CMakeLists.txt | 2 + plugins/mac-capture/data/locale/en-US.ini | 2 + plugins/mac-capture/mac-window-capture.m | 220 ++++++++++++++++++++++ plugins/mac-capture/plugin-main.c | 2 + 4 files changed, 226 insertions(+) create mode 100644 plugins/mac-capture/mac-window-capture.m diff --git a/plugins/mac-capture/CMakeLists.txt b/plugins/mac-capture/CMakeLists.txt index 561190aa3..6e7b2ebd1 100644 --- a/plugins/mac-capture/CMakeLists.txt +++ b/plugins/mac-capture/CMakeLists.txt @@ -22,9 +22,11 @@ set(mac-capture_SOURCES audio-device-enum.c mac-audio.c mac-display-capture.m + mac-window-capture.m window-utils.m) set_source_files_properties(mac-display-capture.m + mac-window-capture.m window-utils.m PROPERTIES LANGUAGE C) diff --git a/plugins/mac-capture/data/locale/en-US.ini b/plugins/mac-capture/data/locale/en-US.ini index a5fee74ba..ad2f5e2e5 100644 --- a/plugins/mac-capture/data/locale/en-US.ini +++ b/plugins/mac-capture/data/locale/en-US.ini @@ -5,6 +5,8 @@ CoreAudio.Device.Default="Default" DisplayCapture="Display Capture" DisplayCapture.Display="Display" DisplayCapture.ShowCursor="Show Cursor" +WindowCapture="Window Capture" +WindowCapture.ShowShadow="Show Window shadow" WindowUtils.Window="Window" WindowUtils.ShowEmptyNames="Show Windows with empty names" CropMode="Crop" diff --git a/plugins/mac-capture/mac-window-capture.m b/plugins/mac-capture/mac-window-capture.m new file mode 100644 index 000000000..27cb11154 --- /dev/null +++ b/plugins/mac-capture/mac-window-capture.m @@ -0,0 +1,220 @@ +#include +#include +#include +#include + +#import +#import + +#include "window-utils.h" + +struct window_capture { + obs_source_t source; + + struct cocoa_window window; + + //CGRect bounds; + //CGWindowListOption window_option; + CGWindowImageOption image_option; + + CGColorSpaceRef color_space; + + DARRAY(uint8_t) buffer; + + pthread_t capture_thread; + os_event_t capture_event; + os_event_t stop_event; +}; + +static CGImageRef get_image(struct window_capture *wc) +{ + NSArray *arr = (NSArray*)CGWindowListCreate( + kCGWindowListOptionIncludingWindow, + wc->window.window_id); + [arr autorelease]; + + if (arr.count) + return CGWindowListCreateImage(CGRectNull, + kCGWindowListOptionIncludingWindow, + wc->window.window_id, wc->image_option); + + if (!find_window(&wc->window, NULL, false)) + return NULL; + + return CGWindowListCreateImage(CGRectNull, + kCGWindowListOptionIncludingWindow, + wc->window.window_id, wc->image_option); +} + +static inline void capture_frame(struct window_capture *wc) +{ + uint64_t ts = os_gettime_ns(); + CGImageRef img = get_image(wc); + if (!img) + return; + + size_t width = CGImageGetWidth(img); + size_t height = CGImageGetHeight(img); + + CGRect rect = {{0, 0}, {width, height}}; + da_reserve(wc->buffer, width * height * 4); + uint8_t *data = wc->buffer.array; + + CGContextRef cg_context = CGBitmapContextCreate(data, width, height, + 8, width * 4, wc->color_space, + kCGBitmapByteOrder32Host | + kCGImageAlphaPremultipliedFirst); + CGContextSetBlendMode(cg_context, kCGBlendModeCopy); + CGContextDrawImage(cg_context, rect, img); + CGContextRelease(cg_context); + CGImageRelease(img); + + struct obs_source_frame frame = { + .format = VIDEO_FORMAT_BGRA, + .width = width, + .height = height, + .data[0] = data, + .linesize[0] = width * 4, + .timestamp = ts, + }; + + obs_source_output_video(wc->source, &frame); +} + +static void *capture_thread(void *data) +{ + struct window_capture *wc = data; + + for (;;) { + os_event_wait(wc->capture_event); + if (os_event_try(wc->stop_event) != EAGAIN) + break; + + @autoreleasepool { + capture_frame(wc); + } + } + + return NULL; +} + +static inline void *window_capture_create_internal(obs_data_t settings, + obs_source_t source) +{ + struct window_capture *wc = bzalloc(sizeof(struct window_capture)); + + wc->source = source; + + wc->color_space = CGColorSpaceCreateDeviceRGB(); + + da_init(wc->buffer); + + init_window(&wc->window, settings); + + wc->image_option = obs_data_get_bool(settings, "show_shadow") ? + kCGWindowImageDefault : kCGWindowImageBoundsIgnoreFraming; + + os_event_init(&wc->capture_event, OS_EVENT_TYPE_AUTO); + os_event_init(&wc->stop_event, OS_EVENT_TYPE_MANUAL); + + pthread_create(&wc->capture_thread, NULL, capture_thread, wc); + + return wc; +} + +static void *window_capture_create(obs_data_t settings, obs_source_t source) +{ + @autoreleasepool { + return window_capture_create_internal(settings, source); + } +} + +static void window_capture_destroy(void *data) +{ + struct window_capture *cap = data; + + os_event_signal(cap->stop_event); + os_event_signal(cap->capture_event); + + pthread_join(cap->capture_thread, NULL); + + CGColorSpaceRelease(cap->color_space); + + da_free(cap->buffer); + + os_event_destroy(cap->capture_event); + os_event_destroy(cap->stop_event); + + destroy_window(&cap->window); + + bfree(cap); +} + +static void window_capture_defaults(obs_data_t settings) +{ + obs_data_set_default_bool(settings, "show_shadow", false); + window_defaults(settings); +} + +static obs_properties_t window_capture_properties(void) +{ + obs_properties_t props = obs_properties_create(); + + add_window_properties(props); + + obs_properties_add_bool(props, "show_shadow", + obs_module_text("WindowCapture.ShowShadow")); + + return props; +} + +static inline void window_capture_update_internal(struct window_capture *wc, + obs_data_t settings) +{ + wc->image_option = obs_data_get_bool(settings, "show_shadow") ? + kCGWindowImageDefault : kCGWindowImageBoundsIgnoreFraming; + + update_window(&wc->window, settings); +} + +static void window_capture_update(void *data, obs_data_t settings) +{ + @autoreleasepool { + return window_capture_update_internal(data, settings); + } +} + +static const char *window_capture_getname(void) +{ + return obs_module_text("WindowCapture"); +} + +static inline void window_capture_tick_internal(struct window_capture *wc, + float seconds) +{ + UNUSED_PARAMETER(seconds); + os_event_signal(wc->capture_event); +} + +static void window_capture_tick(void *data, float seconds) +{ + @autoreleasepool { + return window_capture_tick_internal(data, seconds); + } +} + +struct obs_source_info window_capture_info = { + .id = "window_capture", + .type = OBS_SOURCE_TYPE_INPUT, + .get_name = window_capture_getname, + + .create = window_capture_create, + .destroy = window_capture_destroy, + + .output_flags = OBS_SOURCE_ASYNC_VIDEO, + .video_tick = window_capture_tick, + + .get_defaults = window_capture_defaults, + .get_properties = window_capture_properties, + .update = window_capture_update, +}; diff --git a/plugins/mac-capture/plugin-main.c b/plugins/mac-capture/plugin-main.c index 5e0450ca5..4f6698dcb 100644 --- a/plugins/mac-capture/plugin-main.c +++ b/plugins/mac-capture/plugin-main.c @@ -6,11 +6,13 @@ OBS_MODULE_USE_DEFAULT_LOCALE("mac-capture", "en-US") extern struct obs_source_info coreaudio_input_capture_info; extern struct obs_source_info coreaudio_output_capture_info; extern struct obs_source_info display_capture_info; +extern struct obs_source_info window_capture_info; bool obs_module_load(void) { obs_register_source(&coreaudio_input_capture_info); obs_register_source(&coreaudio_output_capture_info); obs_register_source(&display_capture_info); + obs_register_source(&window_capture_info); return true; }