#import #import #import "syphon-framework/Syphon.h" #include #define LOG(level, message, ...) \ blog(level, "%s: " message, obs_source_get_name(s->source), \ ##__VA_ARGS__) struct syphon { SYPHON_CLIENT_UNIQUE_CLASS_NAME *client; IOSurfaceRef ref; gs_samplerstate_t *sampler; gs_effect_t *effect; gs_vertbuffer_t *vertbuffer; gs_texture_t *tex; uint32_t width, height; bool crop; CGRect crop_rect; bool allow_transparency; obs_source_t *source; bool active; bool uuid_changed; id new_server_listener; id retire_listener; NSString *app_name; NSString *name; NSString *uuid; obs_data_t *inject_info; NSString *inject_app; NSString *inject_uuid; bool inject_active; id launch_listener; bool inject_server_found; float inject_wait_time; }; typedef struct syphon *syphon_t; static inline void objc_release(NSObject **obj) { [*obj release]; *obj = nil; } static inline void update_properties(syphon_t s) { obs_source_update_properties(s->source); } static inline void find_and_inject_target(syphon_t s, NSArray *arr, bool retry); @interface OBSSyphonKVObserver : NSObject - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context; @end static inline void handle_application_launch(syphon_t s, NSArray *new) { if (!s->inject_active) return; if (!new) return; find_and_inject_target(s, new, false); } @implementation OBSSyphonKVObserver - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { UNUSED_PARAMETER(keyPath); UNUSED_PARAMETER(object); syphon_t s = context; if (!s) return; handle_application_launch(s, change[NSKeyValueChangeNewKey]); update_properties(s); } @end static const char *syphon_get_name(void *unused) { UNUSED_PARAMETER(unused); return obs_module_text("Syphon"); } static void stop_client(syphon_t s) { obs_enter_graphics(); if (s->client) { @autoreleasepool { [s->client stop]; objc_release(&s->client); } } if (s->tex) { gs_texture_destroy(s->tex); s->tex = NULL; } if (s->ref) { IOSurfaceDecrementUseCount(s->ref); CFRelease(s->ref); s->ref = NULL; } s->width = 0; s->height = 0; obs_leave_graphics(); } static inline NSDictionary *find_by_uuid(NSArray *arr, NSString *uuid) { for (NSDictionary *dict in arr) { if ([dict[SyphonServerDescriptionUUIDKey] isEqual:uuid]) return dict; } return nil; } static inline void check_version(syphon_t s, NSDictionary *desc) { extern const NSString *SyphonServerDescriptionDictionaryVersionKey; NSNumber *version = desc[SyphonServerDescriptionDictionaryVersionKey]; if (!version) return LOG(LOG_WARNING, "Server description does not contain " "VersionKey"); if (version.unsignedIntValue > 0) LOG(LOG_WARNING, "Got server description version %d, " "expected 0", version.unsignedIntValue); } static inline void check_description(syphon_t s, NSDictionary *desc) { extern const NSString *SyphonSurfaceType; extern const NSString *SyphonSurfaceTypeIOSurface; extern const NSString *SyphonServerDescriptionSurfacesKey; NSArray *surfaces = desc[SyphonServerDescriptionSurfacesKey]; if (!surfaces) return LOG(LOG_WARNING, "Server description does not contain " "SyphonServerDescriptionSurfacesKey"); if (!surfaces.count) return LOG(LOG_WARNING, "Server description contains empty " "SyphonServerDescriptionSurfacesKey"); for (NSDictionary *surface in surfaces) { NSString *type = surface[SyphonSurfaceType]; if (type && [type isEqual:SyphonSurfaceTypeIOSurface]) return; } NSString *surfaces_string = [NSString stringWithFormat:@"%@", surfaces]; LOG(LOG_WARNING, "SyphonSurfaces does not contain" "'SyphonSurfaceTypeIOSurface': %s", surfaces_string.UTF8String); } static inline bool update_string(NSString **str, NSString *new) { if (!new) return false; [*str release]; *str = [new retain]; return true; } static inline void handle_new_frame(syphon_t s, SYPHON_CLIENT_UNIQUE_CLASS_NAME *client) { IOSurfaceRef ref = [client IOSurface]; if (!ref) return; if (ref == s->ref) { CFRelease(ref); return; } IOSurfaceIncrementUseCount(ref); obs_enter_graphics(); if (s->ref) { gs_texture_destroy(s->tex); IOSurfaceDecrementUseCount(s->ref); CFRelease(s->ref); } s->ref = ref; s->tex = gs_texture_create_from_iosurface(s->ref); s->width = gs_texture_get_width(s->tex); s->height = gs_texture_get_height(s->tex); obs_leave_graphics(); } static void create_client(syphon_t s) { stop_client(s); if (!s->app_name.length && !s->name.length && !s->uuid.length) return; SyphonServerDirectory *ssd = [SyphonServerDirectory sharedDirectory]; NSArray *servers = [ssd serversMatchingName:s->name appName:s->app_name]; if (!servers.count) return; NSDictionary *desc = find_by_uuid(servers, s->uuid); if (!desc) { desc = servers[0]; if (update_string(&s->uuid, desc[SyphonServerDescriptionUUIDKey])) s->uuid_changed = true; } check_version(s, desc); check_description(s, desc); @autoreleasepool { s->client = [[SYPHON_CLIENT_UNIQUE_CLASS_NAME alloc] initWithServerDescription:desc options:nil newFrameHandler:^( SYPHON_CLIENT_UNIQUE_CLASS_NAME *client) { handle_new_frame(s, client); }]; } s->active = true; } static inline void release_settings(syphon_t s) { [s->app_name release]; [s->name release]; [s->uuid release]; } static inline bool load_syphon_settings(syphon_t s, obs_data_t *settings) { NSString *app_name = @(obs_data_get_string(settings, "app_name")); NSString *name = @(obs_data_get_string(settings, "name")); bool equal_names = [app_name isEqual:s->app_name] && [name isEqual:s->name]; if (s->uuid_changed && equal_names) return false; NSString *uuid = @(obs_data_get_string(settings, "uuid")); if ([uuid isEqual:s->uuid] && equal_names) return false; release_settings(s); s->app_name = [app_name retain]; s->name = [name retain]; s->uuid = [uuid retain]; s->uuid_changed = false; return true; } static inline void update_from_announce(syphon_t s, NSDictionary *info) { if (s->active) return; if (!info) return; NSString *app_name = info[SyphonServerDescriptionAppNameKey]; NSString *name = info[SyphonServerDescriptionNameKey]; NSString *uuid = info[SyphonServerDescriptionUUIDKey]; if (![uuid isEqual:s->uuid] && !([app_name isEqual:s->app_name] && [name isEqual:s->name])) return; update_string(&s->app_name, app_name); update_string(&s->name, name); if (update_string(&s->uuid, uuid)) s->uuid_changed = true; create_client(s); } static inline void update_inject_state(syphon_t s, NSDictionary *info, bool announce) { if (!info) return; NSString *app_name = info[SyphonServerDescriptionAppNameKey]; NSString *name = info[SyphonServerDescriptionNameKey]; NSString *uuid = info[SyphonServerDescriptionUUIDKey]; if (![uuid isEqual:s->inject_uuid] && (![app_name isEqual:s->inject_app] || ![name isEqual:@"InjectedSyphon"])) return; if (!(s->inject_server_found = announce)) { s->inject_wait_time = 0.f; objc_release(&s->inject_uuid); LOG(LOG_INFO, "Injected server retired: " "[%s] InjectedSyphon (%s)", s->inject_app.UTF8String, uuid.UTF8String); return; } if (s->inject_uuid) //TODO: track multiple injected instances? return; s->inject_uuid = [uuid retain]; LOG(LOG_INFO, "Injected server found: [%s] %s (%s)", app_name.UTF8String, name.UTF8String, uuid.UTF8String); } static inline void handle_announce(syphon_t s, NSNotification *note) { if (!note) return; update_from_announce(s, note.object); update_inject_state(s, note.object, true); update_properties(s); } static inline void update_from_retire(syphon_t s, NSDictionary *info) { if (!info) return; NSString *uuid = info[SyphonServerDescriptionUUIDKey]; if (!uuid) return; if (![uuid isEqual:s->uuid]) return; s->active = false; } static inline void handle_retire(syphon_t s, NSNotification *note) { if (!note) return; update_from_retire(s, note.object); update_inject_state(s, note.object, false); update_properties(s); } static inline gs_vertbuffer_t *create_vertbuffer() { 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 NULL; vb_data->num_tex = 1; vb_data->tvarray = bzalloc(sizeof(struct gs_tvertarray)); if (!vb_data->tvarray) goto fail_tvarray; vb_data->tvarray[0].width = 2; vb_data->tvarray[0].array = bzalloc(sizeof(struct vec2) * 4); if (!vb_data->tvarray[0].array) goto fail_array; gs_vertbuffer_t *vbuff = gs_vertexbuffer_create(vb_data, GS_DYNAMIC); if (vbuff) return vbuff; bfree(vb_data->tvarray[0].array); fail_array: bfree(vb_data->tvarray); fail_tvarray: bfree(vb_data->points); return NULL; } static inline bool init_obs_graphics_objects(syphon_t s) { 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, }; obs_enter_graphics(); s->sampler = gs_samplerstate_create(&info); s->vertbuffer = create_vertbuffer(); obs_leave_graphics(); s->effect = obs_get_base_effect(OBS_EFFECT_DEFAULT_RECT); return s->sampler != NULL && s->vertbuffer != NULL && s->effect != NULL; } static inline bool create_syphon_listeners(syphon_t s) { NSNotificationCenter *nc = [NSNotificationCenter defaultCenter]; s->new_server_listener = [nc addObserverForName:SyphonServerAnnounceNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *note) { handle_announce(s, note); }]; s->retire_listener = [nc addObserverForName:SyphonServerRetireNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *note) { handle_retire(s, note); }]; return s->new_server_listener != nil && s->retire_listener != nil; } static inline bool create_applications_observer(syphon_t s, NSWorkspace *ws) { s->launch_listener = [[OBSSyphonKVObserver alloc] init]; if (!s->launch_listener) return false; [ws addObserver:s->launch_listener forKeyPath:NSStringFromSelector(@selector(runningApplications)) options:NSKeyValueObservingOptionNew context:s]; return true; } static inline void load_crop(syphon_t s, obs_data_t *settings) { s->crop = obs_data_get_bool(settings, "crop"); #define LOAD_CROP(x) s->crop_rect.x = obs_data_get_double(settings, "crop." #x) LOAD_CROP(origin.x); LOAD_CROP(origin.y); LOAD_CROP(size.width); LOAD_CROP(size.height); #undef LOAD_CROP } static inline void syphon_destroy_internal(syphon_t s); static void *syphon_create_internal(obs_data_t *settings, obs_source_t *source) { UNUSED_PARAMETER(source); syphon_t s = bzalloc(sizeof(struct syphon)); if (!s) return s; s->source = source; if (!init_obs_graphics_objects(s)) goto fail; if (!load_syphon_settings(s, settings)) goto fail; const char *inject_info = obs_data_get_string(settings, "application"); s->inject_info = obs_data_create_from_json(inject_info); s->inject_active = obs_data_get_bool(settings, "inject"); s->inject_app = @(obs_data_get_string(s->inject_info, "name")); if (s->inject_app) [s->inject_app retain]; if (!create_syphon_listeners(s)) goto fail; NSWorkspace *ws = [NSWorkspace sharedWorkspace]; if (!create_applications_observer(s, ws)) goto fail; if (s->inject_active) find_and_inject_target(s, ws.runningApplications, false); create_client(s); load_crop(s, settings); s->allow_transparency = obs_data_get_bool(settings, "allow_transparency"); return s; fail: syphon_destroy_internal(s); return NULL; } static void *syphon_create(obs_data_t *settings, obs_source_t *source) { @autoreleasepool { return syphon_create_internal(settings, source); } } static inline void stop_listener(id listener) { if (!listener) return; NSNotificationCenter *nc = [NSNotificationCenter defaultCenter]; [nc removeObserver:listener]; } static inline void syphon_destroy_internal(syphon_t s) { stop_listener(s->new_server_listener); stop_listener(s->retire_listener); NSWorkspace *ws = [NSWorkspace sharedWorkspace]; [ws removeObserver:s->launch_listener forKeyPath:NSStringFromSelector(@selector (runningApplications))]; objc_release(&s->launch_listener); objc_release(&s->inject_app); objc_release(&s->inject_uuid); obs_data_release(s->inject_info); release_settings(s); obs_enter_graphics(); stop_client(s); if (s->sampler) gs_samplerstate_destroy(s->sampler); if (s->vertbuffer) gs_vertexbuffer_destroy(s->vertbuffer); obs_leave_graphics(); bfree(s); } static void syphon_destroy(void *data) { @autoreleasepool { syphon_destroy_internal(data); } } static inline NSString *get_string(obs_data_t *settings, const char *name) { if (!settings) return nil; return @(obs_data_get_string(settings, name)); } static inline void update_strings_from_context(syphon_t s, obs_data_t *settings, NSString **app, NSString **name, NSString **uuid) { if (!s || !s->uuid_changed) return; s->uuid_changed = false; *app = s->app_name; *name = s->name; *uuid = s->uuid; obs_data_set_string(settings, "app_name", s->app_name.UTF8String); obs_data_set_string(settings, "name", s->name.UTF8String); obs_data_set_string(settings, "uuid", s->uuid.UTF8String); } static inline void add_servers(syphon_t s, obs_property_t *list, obs_data_t *settings) { bool found_current = settings == NULL; NSString *set_app = get_string(settings, "app_name"); NSString *set_name = get_string(settings, "name"); NSString *set_uuid = get_string(settings, "uuid"); update_strings_from_context(s, settings, &set_app, &set_name, &set_uuid); obs_property_list_add_string(list, "", ""); NSArray *arr = [[SyphonServerDirectory sharedDirectory] servers]; for (NSDictionary *server in arr) { NSString *app = server[SyphonServerDescriptionAppNameKey]; NSString *name = server[SyphonServerDescriptionNameKey]; NSString *uuid = server[SyphonServerDescriptionUUIDKey]; NSString *serv = [NSString stringWithFormat:@"[%@] %@", app, name]; obs_property_list_add_string(list, serv.UTF8String, uuid.UTF8String); if (!found_current) found_current = [uuid isEqual:set_uuid]; } if (found_current || !set_uuid.length || !set_app.length) return; NSString *serv = [NSString stringWithFormat:@"[%@] %@", set_app, set_name]; size_t idx = obs_property_list_add_string(list, serv.UTF8String, set_uuid.UTF8String); obs_property_list_item_disable(list, idx, true); } static bool servers_changed(obs_properties_t *props, obs_property_t *list, obs_data_t *settings) { @autoreleasepool { obs_property_list_clear(list); add_servers(obs_properties_get_param(props), list, settings); return true; } } static inline NSString *get_inject_application_path() { static NSString *ident = @"zakk.lol.SyphonInject"; NSWorkspace *ws = [NSWorkspace sharedWorkspace]; return [ws absolutePathForAppBundleWithIdentifier:ident]; } static inline bool is_inject_available_in_lib_dir(NSFileManager *fm, NSURL *url) { if (!url.isFileURL) return false; for (NSString *path in [fm contentsOfDirectoryAtPath:url.path error:nil]) { NSURL *bundle_url = [url URLByAppendingPathComponent:path]; NSBundle *bundle = [NSBundle bundleWithURL:bundle_url]; if (!bundle) continue; if ([bundle.bundleIdentifier isEqual:@"zakk.lol.SASyphonInjector"]) return true; } return false; } static inline bool is_inject_available() { if (get_inject_application_path()) return true; NSFileManager *fm = [NSFileManager defaultManager]; for (NSURL *url in [fm URLsForDirectory:NSLibraryDirectory inDomains:NSAllDomainsMask]) { NSURL *scripting = [url URLByAppendingPathComponent:@"ScriptingAdditions" isDirectory:true]; if (is_inject_available_in_lib_dir(fm, scripting)) return true; } return false; } static inline void launch_syphon_inject_internal() { NSString *path = get_inject_application_path(); NSWorkspace *ws = [NSWorkspace sharedWorkspace]; if (path) [ws launchApplication:path]; } static bool launch_syphon_inject(obs_properties_t *props, obs_property_t *prop, void *data) { UNUSED_PARAMETER(props); UNUSED_PARAMETER(prop); UNUSED_PARAMETER(data); @autoreleasepool { launch_syphon_inject_internal(); return false; } } static int describes_app(obs_data_t *info, NSRunningApplication *app) { int score = 0; if ([app.localizedName isEqual:get_string(info, "name")]) score += 2; if ([app.bundleIdentifier isEqual:get_string(info, "bundle")]) score += 2; if ([app.executableURL isEqual:get_string(info, "executable")]) score += 2; if (score && app.processIdentifier == obs_data_get_int(info, "pid")) score += 1; return score; } static inline void app_to_data(NSRunningApplication *app, obs_data_t *app_data) { obs_data_set_string(app_data, "name", app.localizedName.UTF8String); obs_data_set_string(app_data, "bundle", app.bundleIdentifier.UTF8String); // Until we drop 10.8, use path.fileSystemRepsentation obs_data_set_string(app_data, "executable", app.executableURL.path.fileSystemRepresentation); obs_data_set_int(app_data, "pid", app.processIdentifier); } static inline NSDictionary *get_duplicate_names(NSArray *apps) { NSMutableDictionary *result = [NSMutableDictionary dictionaryWithCapacity:apps.count]; for (NSRunningApplication *app in apps) { if (result[app.localizedName]) result[app.localizedName] = @(true); else result[app.localizedName] = @(false); } return result; } static inline size_t add_app(obs_property_t *prop, NSDictionary *duplicates, NSString *name, const char *bundle, const char *json_data, bool is_duplicate, pid_t pid) { if (!is_duplicate) { NSNumber *val = duplicates[name]; is_duplicate = val && val.boolValue; } if (is_duplicate) name = [NSString stringWithFormat:@"%@ (%s: %d)", name, bundle, pid]; return obs_property_list_add_string(prop, name.UTF8String, json_data); } static void update_inject_list_internal(obs_properties_t *props, obs_property_t *prop, obs_data_t *settings) { UNUSED_PARAMETER(props); const char *current_str = obs_data_get_string(settings, "application"); obs_data_t *current = obs_data_create_from_json(current_str); NSString *current_name = @(obs_data_get_string(current, "name")); bool current_found = !obs_data_has_user_value(current, "name"); obs_property_list_clear(prop); obs_property_list_add_string(prop, "", ""); NSWorkspace *ws = [NSWorkspace sharedWorkspace]; NSArray *apps = ws.runningApplications; NSDictionary *duplicates = get_duplicate_names(apps); NSMapTable *candidates = [NSMapTable weakToStrongObjectsMapTable]; obs_data_t *app_data = obs_data_create(); for (NSRunningApplication *app in apps) { app_to_data(app, app_data); int score = describes_app(current, app); NSString *name = app.localizedName; add_app(prop, duplicates, name, app.bundleIdentifier.UTF8String, obs_data_get_json(app_data), [name isEqual:current_name] && score < 4, app.processIdentifier); if (score >= 4) { [candidates setObject:@(score) forKey:app]; current_found = true; } } obs_data_release(app_data); if (!current_found) { size_t idx = add_app(prop, duplicates, current_name, obs_data_get_string(current, "bundle"), current_str, duplicates[current_name] != nil, obs_data_get_int(current, "pid")); obs_property_list_item_disable(prop, idx, true); } else if (candidates.count > 0) { NSRunningApplication *best_match = nil; NSNumber *best_match_score = @(0); for (NSRunningApplication *app in candidates.keyEnumerator) { NSNumber *score = [candidates objectForKey:app]; if ([score compare:best_match_score] == NSOrderedDescending) { best_match = app; best_match_score = score; } } // Update settings in case of PID/executable updates if (best_match_score.intValue >= 4) { app_to_data(best_match, current); obs_data_set_string(settings, "application", obs_data_get_json(current)); } } obs_data_release(current); } static void toggle_inject_internal(obs_properties_t *props, obs_property_t *prop, obs_data_t *settings) { bool enabled = obs_data_get_bool(settings, "inject"); obs_property_t *inject_list = obs_properties_get(props, "application"); bool inject_enabled = obs_property_enabled(prop); obs_property_set_enabled(inject_list, enabled && inject_enabled); } static bool toggle_inject(obs_properties_t *props, obs_property_t *prop, obs_data_t *settings) { @autoreleasepool { toggle_inject_internal(props, prop, settings); return true; } } static bool update_inject_list(obs_properties_t *props, obs_property_t *prop, obs_data_t *settings) { @autoreleasepool { update_inject_list_internal(props, prop, settings); return true; } } static bool update_crop(obs_properties_t *props, obs_property_t *prop, obs_data_t *settings) { bool enabled = obs_data_get_bool(settings, "crop"); #define LOAD_CROP(x) \ prop = obs_properties_get(props, "crop." #x); \ obs_property_set_enabled(prop, enabled); LOAD_CROP(origin.x); LOAD_CROP(origin.y); LOAD_CROP(size.width); LOAD_CROP(size.height); #undef LOAD_CROP return true; } static void show_syphon_license_internal(void) { char *path = obs_module_file("syphon_license.txt"); if (!path) return; NSWorkspace *ws = [NSWorkspace sharedWorkspace]; [ws openFile:@(path)]; bfree(path); } static bool show_syphon_license(obs_properties_t *props, obs_property_t *prop, void *data) { UNUSED_PARAMETER(props); UNUSED_PARAMETER(prop); UNUSED_PARAMETER(data); @autoreleasepool { show_syphon_license_internal(); return false; } } static void syphon_release(void *param) { if (!param) return; obs_source_release(((syphon_t)param)->source); } static inline obs_properties_t *syphon_properties_internal(syphon_t s) { if (s) obs_source_addref(s->source); obs_properties_t *props = obs_properties_create_param(s, syphon_release); obs_property_t *list = obs_properties_add_list( props, "uuid", obs_module_text("Source"), OBS_COMBO_TYPE_LIST, OBS_COMBO_FORMAT_STRING); obs_property_set_modified_callback(list, servers_changed); obs_properties_add_bool(props, "allow_transparency", obs_module_text("AllowTransparency")); obs_property_t *launch = obs_properties_add_button( props, "launch inject", obs_module_text("LaunchSyphonInject"), launch_syphon_inject); obs_property_t *inject = obs_properties_add_bool( props, "inject", obs_module_text("Inject")); obs_property_set_modified_callback(inject, toggle_inject); obs_property_t *inject_list = obs_properties_add_list( props, "application", obs_module_text("Application"), OBS_COMBO_TYPE_LIST, OBS_COMBO_FORMAT_STRING); obs_property_set_modified_callback(inject_list, update_inject_list); if (!get_inject_application_path()) obs_property_set_enabled(launch, false); if (!is_inject_available()) { obs_property_set_enabled(inject, false); obs_property_set_enabled(inject_list, false); } obs_property_t *crop = obs_properties_add_bool(props, "crop", obs_module_text("Crop")); obs_property_set_modified_callback(crop, update_crop); #define LOAD_CROP(x) \ obs_properties_add_float(props, "crop." #x, \ obs_module_text("Crop." #x), 0., 4096.f, \ .5f); LOAD_CROP(origin.x); LOAD_CROP(origin.y); LOAD_CROP(size.width); LOAD_CROP(size.height); #undef LOAD_CROP obs_properties_add_button(props, "syphon license", obs_module_text("SyphonLicense"), show_syphon_license); return props; } static obs_properties_t *syphon_properties(void *data) { @autoreleasepool { return syphon_properties_internal(data); } } static inline void syphon_save_internal(syphon_t s, obs_data_t *settings) { if (!s->uuid_changed) return; obs_data_set_string(settings, "app_name", s->app_name.UTF8String); obs_data_set_string(settings, "name", s->name.UTF8String); obs_data_set_string(settings, "uuid", s->uuid.UTF8String); } static void syphon_save(void *data, obs_data_t *settings) { @autoreleasepool { syphon_save_internal(data, settings); } } static inline 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 inline void tick_inject_state(syphon_t s, float seconds) { s->inject_wait_time -= seconds; if (s->inject_wait_time > 0.f) return; s->inject_wait_time = 1.f; NSWorkspace *ws = [NSWorkspace sharedWorkspace]; find_and_inject_target(s, ws.runningApplications, true); } static void syphon_video_tick(void *data, float seconds) { UNUSED_PARAMETER(seconds); syphon_t s = data; if (s->inject_active && !s->inject_server_found) tick_inject_state(s, seconds); if (!s->tex) return; static const CGRect null_crop = {{0.f}}; const CGRect *crop = &null_crop; if (s->crop) crop = &s->crop_rect; obs_enter_graphics(); build_sprite_rect(gs_vertexbuffer_get_data(s->vertbuffer), crop->origin.x, s->height - crop->origin.y, s->width - crop->size.width, crop->size.height); obs_leave_graphics(); } static void syphon_video_render(void *data, gs_effect_t *effect) { UNUSED_PARAMETER(effect); syphon_t s = data; if (!s->tex) return; bool disable_blending = !s->allow_transparency; if (disable_blending) { gs_enable_blending(false); gs_enable_color(true, true, true, false); } gs_vertexbuffer_flush(s->vertbuffer); gs_load_vertexbuffer(s->vertbuffer); gs_load_indexbuffer(NULL); gs_load_samplerstate(s->sampler, 0); gs_technique_t *tech = gs_effect_get_technique(s->effect, "Draw"); gs_effect_set_texture(gs_effect_get_param_by_name(s->effect, "image"), s->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); if (disable_blending) { gs_enable_color(true, true, true, true); gs_enable_blending(true); } } static uint32_t syphon_get_width(void *data) { syphon_t s = (syphon_t)data; if (!s->crop) return s->width; int32_t width = s->width - s->crop_rect.origin.x - s->crop_rect.size.width; return MAX(0, width); } static uint32_t syphon_get_height(void *data) { syphon_t s = (syphon_t)data; if (!s->crop) return s->height; int32_t height = s->height - s->crop_rect.origin.y - s->crop_rect.size.height; return MAX(0, height); } static inline void inject_app(syphon_t s, NSRunningApplication *app, bool retry) { SBApplication *sbapp = nil; if (app.processIdentifier != -1) sbapp = [SBApplication applicationWithProcessIdentifier:app.processIdentifier]; else if (app.bundleIdentifier) sbapp = [SBApplication applicationWithBundleIdentifier:app.bundleIdentifier]; if (!sbapp) return LOG(LOG_ERROR, "Could not inject %s", app.localizedName.UTF8String); sbapp.timeout = 10 * 60; sbapp.sendMode = kAEWaitReply; [sbapp sendEvent:'ascr' id:'gdut' parameters:0]; sbapp.sendMode = kAENoReply; [sbapp sendEvent:'SASI' id:'injc' parameters:0]; if (retry) return; LOG(LOG_INFO, "Injected '%s' (%d, '%s')", app.localizedName.UTF8String, app.processIdentifier, app.bundleIdentifier.UTF8String); } static inline void find_and_inject_target(syphon_t s, NSArray *arr, bool retry) { NSMutableArray *best_matches = [NSMutableArray arrayWithCapacity:1]; int best_score = 0; for (NSRunningApplication *app in arr) { int score = describes_app(s->inject_info, app); if (!score) continue; if (score > best_score) { best_score = score; [best_matches removeAllObjects]; } if (score >= best_score) [best_matches addObject:app]; } for (NSRunningApplication *app in best_matches) inject_app(s, app, retry); } static inline bool inject_info_equal(obs_data_t *prev, obs_data_t *new) { if (![get_string(prev, "name") isEqual:get_string(new, "name")]) return false; if (![get_string(prev, "bundle") isEqual:get_string(new, "bundle")]) return false; if (![get_string(prev, "executable") isEqual:get_string(new, "executable")]) return false; if (![get_string(prev, "pid") isEqual:get_string(new, "pid")]) return false; return true; } static inline void update_inject(syphon_t s, obs_data_t *settings) { bool try_injecting = s->inject_active; s->inject_active = obs_data_get_bool(settings, "inject"); const char *inject_str = obs_data_get_string(settings, "application"); try_injecting = !try_injecting && s->inject_active; obs_data_t *prev = s->inject_info; s->inject_info = obs_data_create_from_json(inject_str); NSString *prev_app = s->inject_app; s->inject_app = [@(obs_data_get_string(s->inject_info, "name")) retain]; [prev_app release]; objc_release(&s->inject_uuid); SyphonServerDirectory *ssd = [SyphonServerDirectory sharedDirectory]; NSArray *servers = [ssd serversMatchingName:@"InjectedSyphon" appName:s->inject_app]; s->inject_server_found = false; for (NSDictionary *server in servers) update_inject_state(s, server, true); if (!try_injecting) try_injecting = s->inject_active && !inject_info_equal(prev, s->inject_info); obs_data_release(prev); if (!try_injecting) return; NSWorkspace *ws = [NSWorkspace sharedWorkspace]; find_and_inject_target(s, ws.runningApplications, false); } static inline bool update_syphon(syphon_t s, obs_data_t *settings) { NSArray *arr = [[SyphonServerDirectory sharedDirectory] servers]; if (!load_syphon_settings(s, settings)) return false; NSDictionary *dict = find_by_uuid(arr, s->uuid); if (dict) { NSString *app = dict[SyphonServerDescriptionAppNameKey]; NSString *name = dict[SyphonServerDescriptionNameKey]; obs_data_set_string(settings, "app_name", app.UTF8String); obs_data_set_string(settings, "name", name.UTF8String); load_syphon_settings(s, settings); } else if (!dict && !s->uuid.length) { obs_data_set_string(settings, "app_name", ""); obs_data_set_string(settings, "name", ""); load_syphon_settings(s, settings); } return true; } static void syphon_update_internal(syphon_t s, obs_data_t *settings) { s->allow_transparency = obs_data_get_bool(settings, "allow_transparency"); load_crop(s, settings); update_inject(s, settings); if (update_syphon(s, settings)) create_client(s); } static void syphon_update(void *data, obs_data_t *settings) { @autoreleasepool { syphon_update_internal(data, settings); } } struct obs_source_info syphon_info = { .id = "syphon-input", .type = OBS_SOURCE_TYPE_INPUT, .output_flags = OBS_SOURCE_VIDEO | OBS_SOURCE_CUSTOM_DRAW | OBS_SOURCE_DO_NOT_DUPLICATE, .get_name = syphon_get_name, .create = syphon_create, .destroy = syphon_destroy, .video_render = syphon_video_render, .video_tick = syphon_video_tick, .get_properties = syphon_properties, .get_width = syphon_get_width, .get_height = syphon_get_height, .update = syphon_update, .save = syphon_save, };