#import #import #import #import #import #import #import "SyphonOBSClient.h" #define LOG(level, message, ...) blog(level, "%s: " message, obs_source_get_name(s->source), ##__VA_ARGS__) struct syphon { SyphonOBSClient *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; }; typedef struct syphon *syphon_t; static inline void update_properties(syphon_t s) { obs_source_update_properties(s->source); } static const char *syphon_get_name(void *unused __attribute((unused))) { return obs_module_text("Syphon"); } static void stop_client(syphon_t s) { obs_enter_graphics(); if (s->client) { [s->client stop]; } 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; } /* If you see this and think "surely these must be defined in some type of * public header!": They don't. Or at least I couldnt't find anything. The * definitions inside of Syphon are only in SyphonPrivate.h/m, and nowhere * else. When we had Syphon as a submodule we abused this by using extern, but * now that we use a prebuilt framework and as such no longer can acceess the * private headers and sources directly, that's no longer possible. Other * projects sometimes copy SyphonPrivate.h entirely (with the full definitions * of everything), but for our purpose these strings are enough. */ const NSString *SyphonServerDescriptionDictionaryVersionKey = @"SyphonServerDescriptionDictionaryVersionKey"; const NSString *SyphonSurfaceType = @"SyphonSurfaceType"; const NSString *SyphonSurfaceTypeIOSurface = @"SyphonSurfaceTypeIOSurface"; const NSString *SyphonServerDescriptionSurfacesKey = @"SyphonServerDescriptionSurfacesKey"; static inline void check_version(syphon_t s, NSDictionary *desc) { 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) { 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 void handle_new_frame(syphon_t s, SyphonOBSClient *client) { IOSurfaceRef ref = [client newFrameImage]; 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 (![s->uuid isEqualToString:desc[SyphonServerDescriptionUUIDKey]]) { s->uuid_changed = true; } } check_version(s, desc); check_description(s, desc); s->client = [[SyphonOBSClient alloc] initWithServerDescription:desc options:nil newFrameHandler:^(SyphonOBSClient *client) { handle_new_frame(s, client); }]; s->active = true; } 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; s->app_name = app_name; s->name = name; s->uuid = uuid; 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; s->app_name = app_name; s->name = name; if (![s->uuid isEqualToString:uuid]) { s->uuid = uuid; s->uuid_changed = true; } create_client(s); } static inline void handle_announce(syphon_t s, NSNotification *note) { if (!note) return; update_from_announce(s, note.userInfo); 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.userInfo); 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 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) { syphon_t s = bzalloc(sizeof(struct syphon)); if (!s) return s; s->source = source; if (!init_obs_graphics_objects(s)) { syphon_destroy_internal(s); return NULL; } if (!load_syphon_settings(s, settings)) { syphon_destroy_internal(s); return NULL; } if (!create_syphon_listeners(s)) { syphon_destroy_internal(s); return NULL; } create_client(s); load_crop(s, settings); s->allow_transparency = obs_data_get_bool(settings, "allow_transparency"); return s; } 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); 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 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]; NSURL *url = [NSURL URLWithString:[@"file://" stringByAppendingString:[NSString stringWithCString:path encoding:NSUTF8StringEncoding]]]; [ws openURL:url]; 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_get_ref(s->source) == NULL) { s = NULL; } 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 *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, fabsf(end_x - origin_x), fabsf(end_y - origin_y), origin_x, end_x, origin_y, end_y); } static void syphon_video_tick(void *data, float seconds) { UNUSED_PARAMETER(seconds); syphon_t s = data; 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), (float) crop->origin.x, s->height - (float) crop->origin.y, s->width - (float) crop->size.width, (float) 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; gs_vertexbuffer_flush(s->vertbuffer); gs_load_vertexbuffer(s->vertbuffer); gs_load_indexbuffer(NULL); gs_load_samplerstate(s->sampler, 0); const char *tech_name = s->allow_transparency ? "Draw" : "DrawOpaque"; gs_technique_t *tech = gs_effect_get_technique(s->effect, tech_name); 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); } 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 - (int) s->crop_rect.origin.x - (int) 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 - (int) s->crop_rect.origin.y - (int) s->crop_rect.size.height; return MAX(0, height); } 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); 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, .icon_type = OBS_ICON_TYPE_GAME_CAPTURE, };