From 9ed5062e59911d8659b540258437710b8c33f451 Mon Sep 17 00:00:00 2001 From: Developer-Ecosystem-Engineering Date: Sat, 4 Jun 2022 20:35:15 -0700 Subject: [PATCH] mac-capture: Add support for improved window capture in macOS 12.3 Add a new capture plugin called General Capture that allows for capture of an entire desktop, a single window, or all windows of an application --- libobs-opengl/gl-cocoa.m | 5 +- plugins/mac-capture/CMakeLists.txt | 13 + plugins/mac-capture/data/locale/en-US.ini | 1 + plugins/mac-capture/mac-display-capture.m | 2 +- plugins/mac-capture/mac-screen-capture.m | 977 ++++++++++++++++++++++ plugins/mac-capture/plugin-main.c | 8 + 6 files changed, 1002 insertions(+), 4 deletions(-) create mode 100644 plugins/mac-capture/mac-screen-capture.m diff --git a/libobs-opengl/gl-cocoa.m b/libobs-opengl/gl-cocoa.m index cc2a7e9e4..bf9bbac2d 100644 --- a/libobs-opengl/gl-cocoa.m +++ b/libobs-opengl/gl-cocoa.m @@ -419,9 +419,8 @@ bool gs_texture_rebind_iosurface(gs_texture_t *texture, void *iosurf) blog(LOG_ERROR, "Unexpected pixel format: %d (%c%c%c%c)", pf, pf >> 24, pf >> 16, pf >> 8, pf); - if (tex->width != IOSurfaceGetWidth(ref) || - tex->height != IOSurfaceGetHeight(ref)) - return false; + tex->width = IOSurfaceGetWidth(ref); + tex->height = IOSurfaceGetHeight(ref); if (!gl_bind_texture(tex->base.gl_target, tex->base.texture)) return false; diff --git a/plugins/mac-capture/CMakeLists.txt b/plugins/mac-capture/CMakeLists.txt index bb2c1ff8f..2dabcb04d 100644 --- a/plugins/mac-capture/CMakeLists.txt +++ b/plugins/mac-capture/CMakeLists.txt @@ -5,6 +5,9 @@ find_library(AUDIOUNIT AudioUnit) find_library(COREFOUNDATION CoreFoundation) find_library(IOSURF IOSurface) find_library(COCOA Cocoa) +find_library(COREVIDEO CoreVideo) +find_library(COREMEDIA CoreMedia) +find_library(SCREENCAPTUREKIT ScreenCaptureKit) add_library(mac-capture MODULE) add_library(OBS::capture ALIAS mac-capture) @@ -16,6 +19,7 @@ target_sources( audio-device-enum.h mac-audio.c mac-display-capture.m + mac-screen-capture.m mac-window-capture.m window-utils.m window-utils.h) @@ -23,6 +27,15 @@ target_sources( target_link_libraries(mac-capture PRIVATE OBS::libobs ${COREAUDIO} ${AUDIOUNIT} ${COREFOUNDATION} ${IOSURF} ${COCOA}) +if(SCREENCAPTUREKIT) + target_link_libraries(mac-capture PRIVATE OBS::libobs ${COREVIDEO} + ${COREMEDIA}) + + target_link_options(mac-capture PRIVATE SHELL:-weak_framework + ScreenCaptureKit) + target_link_options(libobs PRIVATE SHELL:-weak_framework ScreenCaptureKit) +endif() + set_target_properties(mac-capture PROPERTIES FOLDER "plugins" PREFIX "") setup_plugin_target(mac-capture) diff --git a/plugins/mac-capture/data/locale/en-US.ini b/plugins/mac-capture/data/locale/en-US.ini index ad2f5e2e5..0411fb759 100644 --- a/plugins/mac-capture/data/locale/en-US.ini +++ b/plugins/mac-capture/data/locale/en-US.ini @@ -9,6 +9,7 @@ WindowCapture="Window Capture" WindowCapture.ShowShadow="Show Window shadow" WindowUtils.Window="Window" WindowUtils.ShowEmptyNames="Show Windows with empty names" +WindowUtils.ShowHidden="Show fullscreen and hidden windows" CropMode="Crop" CropMode.None="None" CropMode.Manual="Manual" diff --git a/plugins/mac-capture/mac-display-capture.m b/plugins/mac-capture/mac-display-capture.m index 31a765e2b..45ec90d80 100644 --- a/plugins/mac-capture/mac-display-capture.m +++ b/plugins/mac-capture/mac-display-capture.m @@ -614,7 +614,7 @@ static obs_properties_t *display_capture_properties(void *unused) sprintf(dimension_buffer[3], "%d", (int32_t)[screen frame].origin.y); -#if __MAC_OS_X_VERSION_MAX_ALLOWED >= __MAC_10_15 +#if __MAC_OS_X_VERSION_MAX_ALLOWED >= 101500 // __MAC_10_15 if (__builtin_available(macOS 10.15, *)) { sprintf(name_buffer, "%.200s: %.12sx%.12s @ %.12s,%.12s", diff --git a/plugins/mac-capture/mac-screen-capture.m b/plugins/mac-capture/mac-screen-capture.m new file mode 100644 index 000000000..9307f4609 --- /dev/null +++ b/plugins/mac-capture/mac-screen-capture.m @@ -0,0 +1,977 @@ +#include +#include + +bool is_screen_capture_available(void) +{ + return (NSClassFromString(@"SCStream") != NULL); +} + +#if __MAC_OS_X_VERSION_MAX_ALLOWED >= 120300 // __MAC_12_3 +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wunguarded-availability-new" + +#include +#include +#include +#include + +#include +#include +#include +#include + +#include "window-utils.h" + +#define MACCAP_LOG(level, msg, ...) \ + blog(level, "[ mac-screencapture ]: " msg, ##__VA_ARGS__) +#define MACCAP_ERR(msg, ...) MACCAP_LOG(LOG_ERROR, msg, ##__VA_ARGS__) + +typedef enum { + ScreenCaptureDisplayStream = 0, + ScreenCaptureWindowStream = 1, + ScreenCaptureApplicationStream = 2, +} ScreenCaptureStreamType; + +@interface ScreenCaptureDelegate : NSObject + +@property struct screen_capture *sc; + +@end + +struct screen_capture { + obs_source_t *source; + + gs_samplerstate_t *sampler; + gs_effect_t *effect; + gs_texture_t *tex; + gs_vertbuffer_t *vertbuf; + + NSRect frame; + bool hide_cursor; + bool show_hidden_windows; + bool show_empty_names; + + SCStream *disp; + SCStreamConfiguration *stream_properties; + SCShareableContent *shareable_content; + ScreenCaptureDelegate *capture_delegate; + + os_event_t *disp_finished; + os_event_t *stream_start_completed; + os_sem_t *shareable_content_available; + IOSurfaceRef current, prev; + + pthread_mutex_t mutex; + + unsigned capture_type; + CGDirectDisplayID display; + struct cocoa_window window; + NSString *application_id; +}; + +static void destroy_screen_stream(struct screen_capture *sc) +{ + if (sc->disp) { + [sc->disp stopCaptureWithCompletionHandler:^( + NSError *_Nullable error) { + if (error && error.code != 3808) { + MACCAP_ERR( + "destroy_screen_stream: Failed to stop stream with error %s\n", + [[error localizedFailureReason] + cStringUsingEncoding: + NSUTF8StringEncoding]); + } + os_event_signal(sc->disp_finished); + }]; + os_event_wait(sc->disp_finished); + } + + if (sc->stream_properties) { + [sc->stream_properties release]; + sc->stream_properties = NULL; + } + + if (sc->tex) { + gs_texture_destroy(sc->tex); + sc->tex = NULL; + } + + if (sc->current) { + IOSurfaceDecrementUseCount(sc->current); + CFRelease(sc->current); + sc->current = NULL; + } + + if (sc->prev) { + IOSurfaceDecrementUseCount(sc->prev); + CFRelease(sc->prev); + sc->prev = NULL; + } + + if (sc->disp) { + [sc->disp release]; + sc->disp = NULL; + } + + os_event_destroy(sc->disp_finished); + os_event_destroy(sc->stream_start_completed); +} + +static void screen_capture_destroy(void *data) +{ + struct screen_capture *sc = data; + + if (!sc) + return; + + obs_enter_graphics(); + + destroy_screen_stream(sc); + + if (sc->sampler) + gs_samplerstate_destroy(sc->sampler); + if (sc->vertbuf) + gs_vertexbuffer_destroy(sc->vertbuf); + + obs_leave_graphics(); + + if (sc->shareable_content) { + os_sem_wait(sc->shareable_content_available); + [sc->shareable_content release]; + os_sem_destroy(sc->shareable_content_available); + sc->shareable_content_available = NULL; + } + + if (sc->capture_delegate) { + [sc->capture_delegate release]; + } + + destroy_window(&sc->window); + + pthread_mutex_destroy(&sc->mutex); + bfree(sc); +} + +static inline void screen_stream_update(struct screen_capture *sc, + CMSampleBufferRef sample_buffer) +{ + bool frame_detail_errored = false; + float scale_factor = 1.0f; + CGRect window_rect = {}; + + CFArrayRef attachments_array = + CMSampleBufferGetSampleAttachmentsArray(sample_buffer, false); + if (sc->capture_type == ScreenCaptureWindowStream && + attachments_array != NULL && + CFArrayGetCount(attachments_array) > 0) { + CFDictionaryRef attachments_dict = + CFArrayGetValueAtIndex(attachments_array, 0); + if (attachments_dict != NULL) { + + CFTypeRef frame_scale_factor = CFDictionaryGetValue( + attachments_dict, SCStreamFrameInfoScaleFactor); + if (frame_scale_factor != NULL) { + Boolean result = CFNumberGetValue( + (CFNumberRef)frame_scale_factor, + kCFNumberFloatType, &scale_factor); + if (result == false) { + scale_factor = 1.0f; + frame_detail_errored = true; + } + } + + CFTypeRef content_rect_dict = CFDictionaryGetValue( + attachments_dict, SCStreamFrameInfoContentRect); + CFTypeRef content_scale_factor = CFDictionaryGetValue( + attachments_dict, + SCStreamFrameInfoContentScale); + if ((content_rect_dict != NULL) && + (content_scale_factor != NULL)) { + CGRect content_rect = {}; + float points_to_pixels = 0.0f; + + Boolean result = + CGRectMakeWithDictionaryRepresentation( + (__bridge CFDictionaryRef) + content_rect_dict, + &content_rect); + if (result == false) { + content_rect = CGRectZero; + frame_detail_errored = true; + } + result = CFNumberGetValue( + (CFNumberRef)content_scale_factor, + kCFNumberFloatType, &points_to_pixels); + if (result == false) { + points_to_pixels = 1.0f; + frame_detail_errored = true; + } + + window_rect.origin = content_rect.origin; + window_rect.size.width = + content_rect.size.width / + points_to_pixels * scale_factor; + window_rect.size.height = + content_rect.size.height / + points_to_pixels * scale_factor; + } + } + } + + CVImageBufferRef image_buffer = + CMSampleBufferGetImageBuffer(sample_buffer); + + CVPixelBufferLockBaseAddress(image_buffer, 0); + IOSurfaceRef frame_surface = CVPixelBufferGetIOSurface(image_buffer); + CVPixelBufferUnlockBaseAddress(image_buffer, 0); + + IOSurfaceRef prev_current = NULL; + + if (frame_surface && !pthread_mutex_lock(&sc->mutex)) { + + bool needs_to_update_properties = false; + + if (!frame_detail_errored) { + if (sc->capture_type == ScreenCaptureWindowStream) { + if ((sc->frame.size.width != + window_rect.size.width) || + (sc->frame.size.height != + window_rect.size.height)) { + sc->frame.size.width = + window_rect.size.width; + sc->frame.size.height = + window_rect.size.height; + needs_to_update_properties = true; + } + } else { + size_t width = + CVPixelBufferGetWidth(image_buffer); + size_t height = + CVPixelBufferGetHeight(image_buffer); + + if ((sc->frame.size.width != width) || + (sc->frame.size.height != height)) { + sc->frame.size.width = width; + sc->frame.size.height = height; + needs_to_update_properties = true; + } + } + } + + if (needs_to_update_properties) { + [sc->stream_properties setWidth:sc->frame.size.width]; + [sc->stream_properties setHeight:sc->frame.size.height]; + + [sc->disp + updateConfiguration:sc->stream_properties + completionHandler:^( + NSError *_Nullable error) { + if (error) { + MACCAP_ERR( + "screen_stream_video_update: Failed to update stream properties with error %s\n", + [[error localizedFailureReason] + cStringUsingEncoding: + NSUTF8StringEncoding]); + } + }]; + } + + prev_current = sc->current; + sc->current = frame_surface; + CFRetain(sc->current); + IOSurfaceIncrementUseCount(sc->current); + + pthread_mutex_unlock(&sc->mutex); + } + + if (prev_current) { + IOSurfaceDecrementUseCount(prev_current); + CFRelease(prev_current); + } +} + +static bool init_screen_stream(struct screen_capture *sc) +{ + SCContentFilter *content_filter; + + sc->frame = CGRectZero; + os_sem_wait(sc->shareable_content_available); + + __block SCDisplay *target_display = nil; + { + [sc->shareable_content.displays + indexOfObjectPassingTest:^BOOL( + SCDisplay *_Nonnull display, NSUInteger idx, + BOOL *_Nonnull stop) { + if (display.displayID == sc->display) { + target_display = sc->shareable_content + .displays[idx]; + *stop = TRUE; + } + return *stop; + }]; + } + + __block SCWindow *target_window = nil; + if (sc->window.window_id != 0) { + [sc->shareable_content.windows indexOfObjectPassingTest:^BOOL( + SCWindow *_Nonnull window, + NSUInteger idx, + BOOL *_Nonnull stop) { + if (window.windowID == sc->window.window_id) { + target_window = + sc->shareable_content.windows[idx]; + *stop = TRUE; + } + return *stop; + }]; + } + + __block SCRunningApplication *target_application = nil; + { + [sc->shareable_content.applications + indexOfObjectPassingTest:^BOOL( + SCRunningApplication *_Nonnull application, + NSUInteger idx, BOOL *_Nonnull stop) { + if ([application.bundleIdentifier + isEqualToString:sc-> + application_id]) { + target_application = + sc->shareable_content + .applications[idx]; + *stop = TRUE; + } + return *stop; + }]; + } + NSArray *target_application_array = + [[NSArray alloc] initWithObjects:target_application, nil]; + + switch (sc->capture_type) { + case ScreenCaptureDisplayStream: { + content_filter = [[SCContentFilter alloc] + initWithDisplay:target_display + excludingWindows:[[NSArray alloc] init]]; + } break; + case ScreenCaptureWindowStream: { + content_filter = [[SCContentFilter alloc] + initWithDesktopIndependentWindow:target_window]; + } break; + case ScreenCaptureApplicationStream: { + content_filter = [[SCContentFilter alloc] + initWithDisplay:target_display + includingApplications:target_application_array + exceptingWindows:[[NSArray alloc] init]]; + } break; + } + os_sem_post(sc->shareable_content_available); + + sc->stream_properties = [[SCStreamConfiguration alloc] init]; + [sc->stream_properties setQueueDepth:8]; + [sc->stream_properties setShowsCursor:!sc->hide_cursor]; + [sc->stream_properties setPixelFormat:'BGRA']; + + switch (sc->capture_type) { + case ScreenCaptureDisplayStream: + case ScreenCaptureApplicationStream: + if (target_display) { + CGDisplayModeRef display_mode = + CGDisplayCopyDisplayMode( + target_display.displayID); + [sc->stream_properties + setWidth:CGDisplayModeGetPixelWidth( + display_mode)]; + [sc->stream_properties + setHeight:CGDisplayModeGetPixelHeight( + display_mode)]; + CGDisplayModeRelease(display_mode); + } + break; + case ScreenCaptureWindowStream: + if (target_window) { + [sc->stream_properties + setWidth:target_window.frame.size.width]; + [sc->stream_properties + setHeight:target_window.frame.size.height]; + } + break; + } + + sc->disp = [[SCStream alloc] initWithFilter:content_filter + configuration:sc->stream_properties + delegate:nil]; + + NSError *error = nil; + BOOL did_add_output = [sc->disp addStreamOutput:sc->capture_delegate + type:SCStreamOutputTypeScreen + sampleHandlerQueue:nil + error:&error]; + if (!did_add_output) { + MACCAP_ERR( + "init_screen_stream: Failed to add stream output with error %s\n", + [[error localizedFailureReason] + cStringUsingEncoding:NSUTF8StringEncoding]); + [error release]; + return !did_add_output; + } + + os_event_init(&sc->disp_finished, OS_EVENT_TYPE_MANUAL); + os_event_init(&sc->stream_start_completed, OS_EVENT_TYPE_MANUAL); + + __block BOOL did_stream_start = false; + [sc->disp startCaptureWithCompletionHandler:^( + NSError *_Nullable error) { + did_stream_start = (BOOL)(error == nil); + if (!did_stream_start) { + MACCAP_ERR( + "init_screen_stream: Failed to start capture with error %s\n", + [[error localizedFailureReason] + cStringUsingEncoding: + NSUTF8StringEncoding]); + // Clean up disp so it isn't stopped + [sc->disp release]; + sc->disp = NULL; + } + os_event_signal(sc->stream_start_completed); + }]; + os_event_wait(sc->stream_start_completed); + + return did_stream_start; +} + +bool init_vertbuf_screen_capture(struct screen_capture *sc) +{ + 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; + + sc->vertbuf = gs_vertexbuffer_create(vb_data, GS_DYNAMIC); + return sc->vertbuf != NULL; +} + +static void *screen_capture_build_content_list(struct screen_capture *sc) +{ + typedef void (^shareable_content_callback)(SCShareableContent *, + NSError *); + shareable_content_callback new_content_received = ^void( + SCShareableContent *shareable_content, NSError *error) { + if (error == nil && sc->shareable_content_available != NULL) { + sc->shareable_content = [shareable_content retain]; + } else { + MACCAP_ERR( + "screen_capture_properties: Failed to get shareable content with error %s\n", + [[error localizedFailureReason] + cStringUsingEncoding: + NSUTF8StringEncoding]); + } + os_sem_post(sc->shareable_content_available); + }; + + os_sem_wait(sc->shareable_content_available); + [sc->shareable_content release]; + [SCShareableContent + getShareableContentExcludingDesktopWindows:true + onScreenWindowsOnly:!sc->show_hidden_windows + completionHandler:new_content_received]; +} + +static void *screen_capture_create(obs_data_t *settings, obs_source_t *source) +{ + struct screen_capture *sc = bzalloc(sizeof(struct screen_capture)); + + sc->source = source; + sc->hide_cursor = !obs_data_get_bool(settings, "show_cursor"); + sc->show_empty_names = obs_data_get_bool(settings, "show_empty_names"); + sc->show_hidden_windows = + obs_data_get_bool(settings, "show_hidden_windows"); + + init_window(&sc->window, settings); + update_window(&sc->window, settings); + + os_sem_init(&sc->shareable_content_available, 1); + screen_capture_build_content_list(sc); + + sc->capture_delegate = [[ScreenCaptureDelegate alloc] init]; + sc->capture_delegate.sc = sc; + + sc->effect = obs_get_base_effect(OBS_EFFECT_DEFAULT_RECT); + if (!sc->effect) + goto fail; + + obs_enter_graphics(); + + struct gs_sampler_info info = { + .filter = GS_FILTER_LINEAR, + .address_u = GS_ADDRESS_CLAMP, + .address_v = GS_ADDRESS_CLAMP, + .address_w = GS_ADDRESS_CLAMP, + .max_anisotropy = 1, + }; + sc->sampler = gs_samplerstate_create(&info); + if (!sc->sampler) + goto fail; + + if (!init_vertbuf_screen_capture(sc)) + goto fail; + + obs_leave_graphics(); + + sc->capture_type = obs_data_get_int(settings, "type"); + sc->display = obs_data_get_int(settings, "display"); + sc->application_id = [[NSString alloc] + initWithUTF8String:obs_data_get_string(settings, + "application")]; + pthread_mutex_init(&sc->mutex, NULL); + + if (!init_screen_stream(sc)) + goto fail; + + return sc; + +fail: + obs_leave_graphics(); + screen_capture_destroy(sc); + 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 screen_capture_video_tick(void *data, float seconds) +{ + UNUSED_PARAMETER(seconds); + + struct screen_capture *sc = data; + + if (!sc->current) + return; + if (!obs_source_showing(sc->source)) + return; + + IOSurfaceRef prev_prev = sc->prev; + if (pthread_mutex_lock(&sc->mutex)) + return; + sc->prev = sc->current; + sc->current = NULL; + pthread_mutex_unlock(&sc->mutex); + + if (prev_prev == sc->prev) + return; + + CGPoint origin = {0.f, 0.f}; + CGPoint end = {sc->frame.size.width, sc->frame.size.height}; + + obs_enter_graphics(); + build_sprite_rect(gs_vertexbuffer_get_data(sc->vertbuf), origin.x, + origin.y, end.x, end.y); + + if (sc->tex) + gs_texture_rebind_iosurface(sc->tex, sc->prev); + else + sc->tex = gs_texture_create_from_iosurface(sc->prev); + obs_leave_graphics(); + + if (prev_prev) { + IOSurfaceDecrementUseCount(prev_prev); + CFRelease(prev_prev); + } +} + +static void screen_capture_video_render(void *data, gs_effect_t *effect) +{ + UNUSED_PARAMETER(effect); + struct screen_capture *sc = data; + + if (!sc->tex) + return; + + const bool linear_srgb = gs_get_linear_srgb(); + + const bool previous = gs_framebuffer_srgb_enabled(); + gs_enable_framebuffer_srgb(linear_srgb); + + gs_vertexbuffer_flush(sc->vertbuf); + gs_load_vertexbuffer(sc->vertbuf); + gs_load_indexbuffer(NULL); + gs_load_samplerstate(sc->sampler, 0); + gs_technique_t *tech = gs_effect_get_technique(sc->effect, "Draw"); + gs_eparam_t *param = gs_effect_get_param_by_name(sc->effect, "image"); + if (linear_srgb) + gs_effect_set_texture_srgb(param, sc->tex); + else + gs_effect_set_texture(param, sc->tex); + gs_technique_begin(tech); + gs_technique_begin_pass(tech, 0); + + gs_draw(GS_TRISTRIP, 0, 4); + + gs_technique_end_pass(tech); + gs_technique_end(tech); + + gs_enable_framebuffer_srgb(previous); +} + +static const char *screen_capture_getname(void *unused) +{ + UNUSED_PARAMETER(unused); + return "macOS ScreenCapture"; +} + +static uint32_t screen_capture_getwidth(void *data) +{ + struct screen_capture *sc = data; + + return sc->frame.size.width; +} + +static uint32_t screen_capture_getheight(void *data) +{ + struct screen_capture *sc = data; + + return sc->frame.size.height; +} + +static void screen_capture_defaults(obs_data_t *settings) +{ + CGDirectDisplayID initial_display = 0; + { + NSScreen *mainScreen = [NSScreen mainScreen]; + if (mainScreen) { + NSNumber *screen_num = + mainScreen.deviceDescription[@"NSScreenNumber"]; + if (screen_num) { + initial_display = + (CGDirectDisplayID) + screen_num.pointerValue; + } + } + } + + obs_data_set_default_int(settings, "type", 0); + obs_data_set_default_int(settings, "display", initial_display); + obs_data_set_default_int(settings, "window", kCGNullWindowID); + obs_data_set_default_obj(settings, "application", NULL); + obs_data_set_default_bool(settings, "show_cursor", true); + obs_data_set_default_bool(settings, "show_empty_names", false); + obs_data_set_default_bool(settings, "show_hidden_windows", false); + + window_defaults(settings); +} + +static void screen_capture_update(void *data, obs_data_t *settings) +{ + struct screen_capture *sc = data; + + CGWindowID old_window_id = sc->window.window_id; + update_window(&sc->window, settings); + + ScreenCaptureStreamType capture_type = + (ScreenCaptureStreamType)obs_data_get_int(settings, "type"); + CGDirectDisplayID display = + (CGDirectDisplayID)obs_data_get_int(settings, "display"); + NSString *application_id = [[NSString alloc] + initWithUTF8String:obs_data_get_string(settings, + "application")]; + bool show_cursor = obs_data_get_bool(settings, "show_cursor"); + bool show_empty_names = obs_data_get_bool(settings, "show_empty_names"); + bool show_hidden_windows = + obs_data_get_bool(settings, "show_hidden_windows"); + + if (capture_type == sc->capture_type) { + switch (sc->capture_type) { + case ScreenCaptureDisplayStream: { + if (sc->display == display && + sc->hide_cursor != show_cursor) + return; + } break; + case ScreenCaptureWindowStream: { + if (old_window_id == sc->window.window_id && + sc->hide_cursor != show_cursor) + return; + } break; + case ScreenCaptureApplicationStream: { + if (sc->display == display && + [application_id + isEqualToString:sc->application_id] && + sc->hide_cursor != show_cursor) + return; + } break; + } + } + + obs_enter_graphics(); + + destroy_screen_stream(sc); + sc->capture_type = capture_type; + sc->display = display; + sc->application_id = application_id; + sc->hide_cursor = !show_cursor; + sc->show_empty_names = show_empty_names; + sc->show_hidden_windows = show_hidden_windows; + init_screen_stream(sc); + + obs_leave_graphics(); +} + +static bool build_display_list(struct screen_capture *sc, + obs_properties_t *props) +{ + os_sem_wait(sc->shareable_content_available); + + obs_property_t *display_list = obs_properties_get(props, "display"); + obs_property_list_clear(display_list); + [sc->shareable_content.displays + enumerateObjectsUsingBlock:^(SCDisplay *_Nonnull display, + NSUInteger idx, + BOOL *_Nonnull stop) { + UNUSED_PARAMETER(idx); + UNUSED_PARAMETER(stop); + + NSUInteger screen_index = [NSScreen.screens + indexOfObjectPassingTest:^BOOL( + NSScreen *_Nonnull screen, + NSUInteger index, BOOL *_Nonnull stop) { + UNUSED_PARAMETER(index); + NSNumber *screen_num = + screen.deviceDescription + [@"NSScreenNumber"]; + CGDirectDisplayID screen_display_id = + (CGDirectDisplayID) + screen_num.pointerValue; + stop = (BOOL)(screen_display_id == + display.displayID); + return stop; + }]; + NSScreen *screen = + [NSScreen.screens objectAtIndex:screen_index]; + + char dimension_buffer[4][12] = {}; + char name_buffer[256] = {}; + sprintf(dimension_buffer[0], "%u", + (uint32_t)screen.frame.size.width); + sprintf(dimension_buffer[1], "%u", + (uint32_t)screen.frame.size.height); + sprintf(dimension_buffer[2], "%d", + (int32_t)screen.frame.origin.x); + sprintf(dimension_buffer[3], "%d", + (int32_t)screen.frame.origin.y); + + sprintf(name_buffer, + "%.200s: %.12sx%.12s @ %.12s,%.12s", + screen.localizedName.UTF8String, + dimension_buffer[0], dimension_buffer[1], + dimension_buffer[2], dimension_buffer[3]); + + obs_property_list_add_int(display_list, name_buffer, + display.displayID); + }]; + + os_sem_post(sc->shareable_content_available); + return true; +} + +static bool build_window_list(struct screen_capture *sc, + obs_properties_t *props) +{ + os_sem_wait(sc->shareable_content_available); + + obs_property_t *window_list = obs_properties_get(props, "window"); + obs_property_list_clear(window_list); + [sc->shareable_content.windows enumerateObjectsUsingBlock:^( + SCWindow *_Nonnull window, + NSUInteger idx, + BOOL *_Nonnull stop) { + UNUSED_PARAMETER(idx); + UNUSED_PARAMETER(stop); + NSString *app_name = window.owningApplication.applicationName; + NSString *title = window.title; + + if (!sc->show_empty_names) { + if (app_name == NULL || title == NULL) { + return; + } else if ([app_name isEqualToString:@""] || + [title isEqualToString:@""]) { + return; + } + } + + const char *list_text = + [[NSString stringWithFormat:@"[%@] %@", app_name, title] + UTF8String]; + obs_property_list_add_int(window_list, list_text, + window.windowID); + }]; + + os_sem_post(sc->shareable_content_available); + return true; +} + +static bool build_application_list(struct screen_capture *sc, + obs_properties_t *props) +{ + os_sem_wait(sc->shareable_content_available); + + obs_property_t *application_list = + obs_properties_get(props, "application"); + obs_property_list_clear(application_list); + [sc->shareable_content.applications + enumerateObjectsUsingBlock:^( + SCRunningApplication *_Nonnull application, + NSUInteger idx, BOOL *_Nonnull stop) { + UNUSED_PARAMETER(idx); + UNUSED_PARAMETER(stop); + const char *name = + [application.applicationName UTF8String]; + const char *bundle_id = + [application.bundleIdentifier UTF8String]; + obs_property_list_add_string(application_list, name, + bundle_id); + }]; + + os_sem_post(sc->shareable_content_available); + return true; +} + +static bool content_changed(struct screen_capture *sc, obs_properties_t *props) +{ + screen_capture_build_content_list(sc); + + build_display_list(sc, props); + build_window_list(sc, props); + build_application_list(sc, props); + + return true; +} + +static bool content_settings_changed(void *priv, obs_properties_t *props, + obs_property_t *property, + obs_data_t *settings) +{ + UNUSED_PARAMETER(property); + + struct screen_capture *sc = (struct screen_capture *)priv; + + sc->show_empty_names = obs_data_get_bool(settings, "show_empty_names"); + sc->show_hidden_windows = + obs_data_get_bool(settings, "show_hidden_windows"); + + return content_changed(sc, props); +} + +static obs_properties_t *screen_capture_properties(void *data) +{ + struct screen_capture *sc = data; + + screen_capture_build_content_list(sc); + + obs_properties_t *props = obs_properties_create(); + + obs_property_t *capture_type = obs_properties_add_list( + props, "type", obs_module_text("Method"), OBS_COMBO_TYPE_LIST, + OBS_COMBO_FORMAT_INT); + obs_property_list_add_int(capture_type, + obs_module_text("DisplayCapture"), 0); + obs_property_list_add_int(capture_type, + obs_module_text("WindowCapture"), 1); + obs_property_list_add_int(capture_type, "Application Capture", 2); + + obs_property_t *display_list = obs_properties_add_list( + props, "display", obs_module_text("DisplayCapture.Display"), + OBS_COMBO_TYPE_LIST, OBS_COMBO_FORMAT_INT); + + 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_t *empty = obs_properties_add_bool( + props, "show_empty_names", + obs_module_text("WindowUtils.ShowEmptyNames")); + obs_property_set_modified_callback2(empty, content_settings_changed, + sc); + + obs_property_t *hidden = obs_properties_add_bool( + props, "show_hidden_windows", + obs_module_text("WindowUtils.ShowHidden")); + obs_property_set_modified_callback2(hidden, content_settings_changed, + sc); + + obs_property_t *application_list = obs_properties_add_list( + props, "application", obs_module_text("Application"), + OBS_COMBO_TYPE_LIST, OBS_COMBO_FORMAT_STRING); + + content_changed(sc, props); + + obs_properties_add_bool(props, "show_cursor", + obs_module_text("DisplayCapture.ShowCursor")); + + return props; +} + +struct obs_source_info screen_capture_info = { + .id = "screen_capture", + .type = OBS_SOURCE_TYPE_INPUT, + .get_name = screen_capture_getname, + + .create = screen_capture_create, + .destroy = screen_capture_destroy, + + .output_flags = OBS_SOURCE_VIDEO | OBS_SOURCE_CUSTOM_DRAW | + OBS_SOURCE_DO_NOT_DUPLICATE | OBS_SOURCE_SRGB, + .video_tick = screen_capture_video_tick, + .video_render = screen_capture_video_render, + + .get_width = screen_capture_getwidth, + .get_height = screen_capture_getheight, + + .get_defaults = screen_capture_defaults, + .get_properties = screen_capture_properties, + .update = screen_capture_update, + .icon_type = OBS_ICON_TYPE_GAME_CAPTURE, +}; + +@implementation ScreenCaptureDelegate + +- (void)stream:(SCStream *)stream + didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer + ofType:(SCStreamOutputType)type +{ + if (self.sc != NULL) { + screen_stream_update(self.sc, sampleBuffer); + } +} + +@end + +// "-Wunguarded-availability-new" +#pragma clang diagnostic pop +#endif diff --git a/plugins/mac-capture/plugin-main.c b/plugins/mac-capture/plugin-main.c index 2efa3a074..2f29166ac 100644 --- a/plugins/mac-capture/plugin-main.c +++ b/plugins/mac-capture/plugin-main.c @@ -12,11 +12,19 @@ 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; +extern bool is_screen_capture_available() WEAK_IMPORT_ATTRIBUTE; + 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); +#if __MAC_OS_X_VERSION_MAX_ALLOWED >= 120300 // __MAC_12_3 + if (is_screen_capture_available()) { + extern struct obs_source_info screen_capture_info; + obs_register_source(&screen_capture_info); + } +#endif return true; }