1#import <Cocoa/Cocoa.h> 2#import <ScriptingBridge/ScriptingBridge.h> 3#import "syphon-framework/Syphon.h" 4#include <obs-module.h> 5 6#define LOG(level, message, ...) \ 7 blog(level, "%s: " message, obs_source_get_name(s->source), \ 8 ##__VA_ARGS__) 9 10struct syphon { 11 SYPHON_CLIENT_UNIQUE_CLASS_NAME *client; 12 IOSurfaceRef ref; 13 14 gs_samplerstate_t *sampler; 15 gs_effect_t *effect; 16 gs_vertbuffer_t *vertbuffer; 17 gs_texture_t *tex; 18 uint32_t width, height; 19 bool crop; 20 CGRect crop_rect; 21 bool allow_transparency; 22 23 obs_source_t *source; 24 25 bool active; 26 bool uuid_changed; 27 id new_server_listener; 28 id retire_listener; 29 30 NSString *app_name; 31 NSString *name; 32 NSString *uuid; 33 34 obs_data_t *inject_info; 35 NSString *inject_app; 36 NSString *inject_uuid; 37 bool inject_active; 38 id launch_listener; 39 bool inject_server_found; 40 float inject_wait_time; 41}; 42typedef struct syphon *syphon_t; 43 44static inline void update_properties(syphon_t s) 45{ 46 obs_source_update_properties(s->source); 47} 48 49static inline void find_and_inject_target(syphon_t s, NSArray *arr, bool retry); 50 51@interface OBSSyphonKVObserver : NSObject 52- (void)observeValueForKeyPath:(NSString *)keyPath 53 ofObject:(id)object 54 change:(NSDictionary *)change 55 context:(void *)context; 56@end 57 58static inline void handle_application_launch(syphon_t s, NSArray *new) 59{ 60 if (!s->inject_active) 61 return; 62 63 if (!new) 64 return; 65 66 find_and_inject_target(s, new, false); 67} 68 69@implementation OBSSyphonKVObserver 70- (void)observeValueForKeyPath:(NSString *)keyPath 71 ofObject:(id)object 72 change:(NSDictionary *)change 73 context:(void *)context 74{ 75 UNUSED_PARAMETER(keyPath); 76 UNUSED_PARAMETER(object); 77 78 syphon_t s = context; 79 if (!s) 80 return; 81 82 handle_application_launch(s, change[NSKeyValueChangeNewKey]); 83 update_properties(s); 84} 85@end 86 87static const char *syphon_get_name(void *unused) 88{ 89 UNUSED_PARAMETER(unused); 90 return obs_module_text("Syphon"); 91} 92 93static void stop_client(syphon_t s) 94{ 95 obs_enter_graphics(); 96 97 if (s->client) { 98 [s->client stop]; 99 } 100 101 if (s->tex) { 102 gs_texture_destroy(s->tex); 103 s->tex = NULL; 104 } 105 106 if (s->ref) { 107 IOSurfaceDecrementUseCount(s->ref); 108 CFRelease(s->ref); 109 s->ref = NULL; 110 } 111 112 s->width = 0; 113 s->height = 0; 114 115 obs_leave_graphics(); 116} 117 118static inline NSDictionary *find_by_uuid(NSArray *arr, NSString *uuid) 119{ 120 for (NSDictionary *dict in arr) { 121 if ([dict[SyphonServerDescriptionUUIDKey] isEqual:uuid]) 122 return dict; 123 } 124 125 return nil; 126} 127 128static inline void check_version(syphon_t s, NSDictionary *desc) 129{ 130 extern const NSString *SyphonServerDescriptionDictionaryVersionKey; 131 132 NSNumber *version = desc[SyphonServerDescriptionDictionaryVersionKey]; 133 if (!version) 134 return LOG(LOG_WARNING, "Server description does not contain " 135 "VersionKey"); 136 137 if (version.unsignedIntValue > 0) 138 LOG(LOG_WARNING, 139 "Got server description version %d, " 140 "expected 0", 141 version.unsignedIntValue); 142} 143 144static inline void check_description(syphon_t s, NSDictionary *desc) 145{ 146 extern const NSString *SyphonSurfaceType; 147 extern const NSString *SyphonSurfaceTypeIOSurface; 148 extern const NSString *SyphonServerDescriptionSurfacesKey; 149 150 NSArray *surfaces = desc[SyphonServerDescriptionSurfacesKey]; 151 if (!surfaces) 152 return LOG(LOG_WARNING, "Server description does not contain " 153 "SyphonServerDescriptionSurfacesKey"); 154 155 if (!surfaces.count) 156 return LOG(LOG_WARNING, "Server description contains empty " 157 "SyphonServerDescriptionSurfacesKey"); 158 159 for (NSDictionary *surface in surfaces) { 160 NSString *type = surface[SyphonSurfaceType]; 161 if (type && [type isEqual:SyphonSurfaceTypeIOSurface]) 162 return; 163 } 164 165 NSString *surfaces_string = [NSString stringWithFormat:@"%@", surfaces]; 166 LOG(LOG_WARNING, 167 "SyphonSurfaces does not contain" 168 "'SyphonSurfaceTypeIOSurface': %s", 169 surfaces_string.UTF8String); 170} 171 172static inline void handle_new_frame(syphon_t s, 173 SYPHON_CLIENT_UNIQUE_CLASS_NAME *client) 174{ 175 IOSurfaceRef ref = [client IOSurface]; 176 177 if (!ref) 178 return; 179 180 if (ref == s->ref) { 181 CFRelease(ref); 182 return; 183 } 184 185 IOSurfaceIncrementUseCount(ref); 186 187 obs_enter_graphics(); 188 if (s->ref) { 189 gs_texture_destroy(s->tex); 190 IOSurfaceDecrementUseCount(s->ref); 191 CFRelease(s->ref); 192 } 193 194 s->ref = ref; 195 s->tex = gs_texture_create_from_iosurface(s->ref); 196 s->width = gs_texture_get_width(s->tex); 197 s->height = gs_texture_get_height(s->tex); 198 obs_leave_graphics(); 199} 200 201static void create_client(syphon_t s) 202{ 203 stop_client(s); 204 205 if (!s->app_name.length && !s->name.length && !s->uuid.length) 206 return; 207 208 SyphonServerDirectory *ssd = [SyphonServerDirectory sharedDirectory]; 209 NSArray *servers = [ssd serversMatchingName:s->name 210 appName:s->app_name]; 211 if (!servers.count) 212 return; 213 214 NSDictionary *desc = find_by_uuid(servers, s->uuid); 215 if (!desc) { 216 desc = servers[0]; 217 if (![s->uuid isEqualToString: 218 desc[SyphonServerDescriptionUUIDKey]]) { 219 s->uuid_changed = true; 220 } 221 } 222 223 check_version(s, desc); 224 check_description(s, desc); 225 226 s->client = [[SYPHON_CLIENT_UNIQUE_CLASS_NAME alloc] 227 initWithServerDescription:desc 228 options:nil 229 newFrameHandler:^( 230 SYPHON_CLIENT_UNIQUE_CLASS_NAME *client) { 231 handle_new_frame(s, client); 232 }]; 233 234 s->active = true; 235} 236 237static inline bool load_syphon_settings(syphon_t s, obs_data_t *settings) 238{ 239 NSString *app_name = @(obs_data_get_string(settings, "app_name")); 240 NSString *name = @(obs_data_get_string(settings, "name")); 241 bool equal_names = [app_name isEqual:s->app_name] && 242 [name isEqual:s->name]; 243 if (s->uuid_changed && equal_names) 244 return false; 245 246 NSString *uuid = @(obs_data_get_string(settings, "uuid")); 247 if ([uuid isEqual:s->uuid] && equal_names) 248 return false; 249 250 s->app_name = app_name; 251 s->name = name; 252 s->uuid = uuid; 253 s->uuid_changed = false; 254 return true; 255} 256 257static inline void update_from_announce(syphon_t s, NSDictionary *info) 258{ 259 if (s->active) 260 return; 261 262 if (!info) 263 return; 264 265 NSString *app_name = info[SyphonServerDescriptionAppNameKey]; 266 NSString *name = info[SyphonServerDescriptionNameKey]; 267 NSString *uuid = info[SyphonServerDescriptionUUIDKey]; 268 269 if (![uuid isEqual:s->uuid] && 270 !([app_name isEqual:s->app_name] && [name isEqual:s->name])) 271 return; 272 273 s->app_name = app_name; 274 s->name = name; 275 if (![s->uuid isEqualToString:uuid]) { 276 s->uuid = uuid; 277 s->uuid_changed = true; 278 } 279 280 create_client(s); 281} 282 283static inline void update_inject_state(syphon_t s, NSDictionary *info, 284 bool announce) 285{ 286 if (!info) 287 return; 288 289 NSString *app_name = info[SyphonServerDescriptionAppNameKey]; 290 NSString *name = info[SyphonServerDescriptionNameKey]; 291 NSString *uuid = info[SyphonServerDescriptionUUIDKey]; 292 293 if (![uuid isEqual:s->inject_uuid] && 294 (![app_name isEqual:s->inject_app] || 295 ![name isEqual:@"InjectedSyphon"])) 296 return; 297 298 if (!(s->inject_server_found = announce)) { 299 s->inject_wait_time = 0.f; 300 LOG(LOG_INFO, 301 "Injected server retired: " 302 "[%s] InjectedSyphon (%s)", 303 s->inject_app.UTF8String, uuid.UTF8String); 304 return; 305 } 306 307 if (s->inject_uuid) //TODO: track multiple injected instances? 308 return; 309 310 s->inject_uuid = uuid; 311 LOG(LOG_INFO, "Injected server found: [%s] %s (%s)", 312 app_name.UTF8String, name.UTF8String, uuid.UTF8String); 313} 314 315static inline void handle_announce(syphon_t s, NSNotification *note) 316{ 317 if (!note) 318 return; 319 320 update_from_announce(s, note.object); 321 update_inject_state(s, note.object, true); 322 update_properties(s); 323} 324 325static inline void update_from_retire(syphon_t s, NSDictionary *info) 326{ 327 if (!info) 328 return; 329 330 NSString *uuid = info[SyphonServerDescriptionUUIDKey]; 331 if (!uuid) 332 return; 333 334 if (![uuid isEqual:s->uuid]) 335 return; 336 337 s->active = false; 338} 339 340static inline void handle_retire(syphon_t s, NSNotification *note) 341{ 342 if (!note) 343 return; 344 345 update_from_retire(s, note.object); 346 update_inject_state(s, note.object, false); 347 update_properties(s); 348} 349 350static inline gs_vertbuffer_t *create_vertbuffer() 351{ 352 struct gs_vb_data *vb_data = gs_vbdata_create(); 353 vb_data->num = 4; 354 vb_data->points = bzalloc(sizeof(struct vec3) * 4); 355 if (!vb_data->points) 356 return NULL; 357 358 vb_data->num_tex = 1; 359 vb_data->tvarray = bzalloc(sizeof(struct gs_tvertarray)); 360 if (!vb_data->tvarray) 361 goto fail_tvarray; 362 363 vb_data->tvarray[0].width = 2; 364 vb_data->tvarray[0].array = bzalloc(sizeof(struct vec2) * 4); 365 if (!vb_data->tvarray[0].array) 366 goto fail_array; 367 368 gs_vertbuffer_t *vbuff = gs_vertexbuffer_create(vb_data, GS_DYNAMIC); 369 if (vbuff) 370 return vbuff; 371 372 bfree(vb_data->tvarray[0].array); 373fail_array: 374 bfree(vb_data->tvarray); 375fail_tvarray: 376 bfree(vb_data->points); 377 378 return NULL; 379} 380 381static inline bool init_obs_graphics_objects(syphon_t s) 382{ 383 struct gs_sampler_info info = { 384 .filter = GS_FILTER_LINEAR, 385 .address_u = GS_ADDRESS_CLAMP, 386 .address_v = GS_ADDRESS_CLAMP, 387 .address_w = GS_ADDRESS_CLAMP, 388 .max_anisotropy = 1, 389 }; 390 391 obs_enter_graphics(); 392 s->sampler = gs_samplerstate_create(&info); 393 s->vertbuffer = create_vertbuffer(); 394 obs_leave_graphics(); 395 396 s->effect = obs_get_base_effect(OBS_EFFECT_DEFAULT_RECT); 397 398 return s->sampler != NULL && s->vertbuffer != NULL && s->effect != NULL; 399} 400 401static inline bool create_syphon_listeners(syphon_t s) 402{ 403 NSNotificationCenter *nc = [NSNotificationCenter defaultCenter]; 404 s->new_server_listener = [nc 405 addObserverForName:SyphonServerAnnounceNotification 406 object:nil 407 queue:[NSOperationQueue mainQueue] 408 usingBlock:^(NSNotification *note) { 409 handle_announce(s, note); 410 }]; 411 412 s->retire_listener = [nc 413 addObserverForName:SyphonServerRetireNotification 414 object:nil 415 queue:[NSOperationQueue mainQueue] 416 usingBlock:^(NSNotification *note) { 417 handle_retire(s, note); 418 }]; 419 420 return s->new_server_listener != nil && s->retire_listener != nil; 421} 422 423static inline bool create_applications_observer(syphon_t s, NSWorkspace *ws) 424{ 425 s->launch_listener = [[OBSSyphonKVObserver alloc] init]; 426 if (!s->launch_listener) 427 return false; 428 429 [ws addObserver:s->launch_listener 430 forKeyPath:NSStringFromSelector(@selector(runningApplications)) 431 options:NSKeyValueObservingOptionNew 432 context:s]; 433 434 return true; 435} 436 437static inline void load_crop(syphon_t s, obs_data_t *settings) 438{ 439 s->crop = obs_data_get_bool(settings, "crop"); 440 441#define LOAD_CROP(x) s->crop_rect.x = obs_data_get_double(settings, "crop." #x) 442 LOAD_CROP(origin.x); 443 LOAD_CROP(origin.y); 444 LOAD_CROP(size.width); 445 LOAD_CROP(size.height); 446#undef LOAD_CROP 447} 448 449static inline void syphon_destroy_internal(syphon_t s); 450 451static void *syphon_create_internal(obs_data_t *settings, obs_source_t *source) 452{ 453 UNUSED_PARAMETER(source); 454 455 syphon_t s = bzalloc(sizeof(struct syphon)); 456 if (!s) 457 return s; 458 459 s->source = source; 460 461 if (!init_obs_graphics_objects(s)) { 462 syphon_destroy_internal(s); 463 return NULL; 464 } 465 466 if (!load_syphon_settings(s, settings)) { 467 syphon_destroy_internal(s); 468 return NULL; 469 } 470 471 const char *inject_info = obs_data_get_string(settings, "application"); 472 s->inject_info = obs_data_create_from_json(inject_info); 473 s->inject_active = obs_data_get_bool(settings, "inject"); 474 s->inject_app = @(obs_data_get_string(s->inject_info, "name")); 475 476 if (!create_syphon_listeners(s)) { 477 syphon_destroy_internal(s); 478 return NULL; 479 } 480 481 NSWorkspace *ws = [NSWorkspace sharedWorkspace]; 482 if (!create_applications_observer(s, ws)) { 483 syphon_destroy_internal(s); 484 return NULL; 485 } 486 487 if (s->inject_active) 488 find_and_inject_target(s, ws.runningApplications, false); 489 490 create_client(s); 491 492 load_crop(s, settings); 493 494 s->allow_transparency = 495 obs_data_get_bool(settings, "allow_transparency"); 496 497 return s; 498} 499 500static void *syphon_create(obs_data_t *settings, obs_source_t *source) 501{ 502 @autoreleasepool { 503 return syphon_create_internal(settings, source); 504 } 505} 506 507static inline void stop_listener(id listener) 508{ 509 if (!listener) 510 return; 511 512 NSNotificationCenter *nc = [NSNotificationCenter defaultCenter]; 513 [nc removeObserver:listener]; 514} 515 516static inline void syphon_destroy_internal(syphon_t s) 517{ 518 stop_listener(s->new_server_listener); 519 stop_listener(s->retire_listener); 520 521 NSWorkspace *ws = [NSWorkspace sharedWorkspace]; 522 [ws removeObserver:s->launch_listener 523 forKeyPath:NSStringFromSelector(@selector 524 (runningApplications))]; 525 526 obs_data_release(s->inject_info); 527 528 obs_enter_graphics(); 529 stop_client(s); 530 531 if (s->sampler) 532 gs_samplerstate_destroy(s->sampler); 533 if (s->vertbuffer) 534 gs_vertexbuffer_destroy(s->vertbuffer); 535 obs_leave_graphics(); 536 537 bfree(s); 538} 539 540static void syphon_destroy(void *data) 541{ 542 @autoreleasepool { 543 syphon_destroy_internal(data); 544 } 545} 546 547static inline NSString *get_string(obs_data_t *settings, const char *name) 548{ 549 if (!settings) 550 return nil; 551 552 return @(obs_data_get_string(settings, name)); 553} 554 555static inline void update_strings_from_context(syphon_t s, obs_data_t *settings, 556 NSString **app, NSString **name, 557 NSString **uuid) 558{ 559 if (!s || !s->uuid_changed) 560 return; 561 562 s->uuid_changed = false; 563 *app = s->app_name; 564 *name = s->name; 565 *uuid = s->uuid; 566 567 obs_data_set_string(settings, "app_name", s->app_name.UTF8String); 568 obs_data_set_string(settings, "name", s->name.UTF8String); 569 obs_data_set_string(settings, "uuid", s->uuid.UTF8String); 570} 571 572static inline void add_servers(syphon_t s, obs_property_t *list, 573 obs_data_t *settings) 574{ 575 bool found_current = settings == NULL; 576 577 NSString *set_app = get_string(settings, "app_name"); 578 NSString *set_name = get_string(settings, "name"); 579 NSString *set_uuid = get_string(settings, "uuid"); 580 581 update_strings_from_context(s, settings, &set_app, &set_name, 582 &set_uuid); 583 584 obs_property_list_add_string(list, "", ""); 585 NSArray *arr = [[SyphonServerDirectory sharedDirectory] servers]; 586 for (NSDictionary *server in arr) { 587 NSString *app = server[SyphonServerDescriptionAppNameKey]; 588 NSString *name = server[SyphonServerDescriptionNameKey]; 589 NSString *uuid = server[SyphonServerDescriptionUUIDKey]; 590 NSString *serv = 591 [NSString stringWithFormat:@"[%@] %@", app, name]; 592 593 obs_property_list_add_string(list, serv.UTF8String, 594 uuid.UTF8String); 595 596 if (!found_current) 597 found_current = [uuid isEqual:set_uuid]; 598 } 599 600 if (found_current || !set_uuid.length || !set_app.length) 601 return; 602 603 NSString *serv = 604 [NSString stringWithFormat:@"[%@] %@", set_app, set_name]; 605 size_t idx = obs_property_list_add_string(list, serv.UTF8String, 606 set_uuid.UTF8String); 607 obs_property_list_item_disable(list, idx, true); 608} 609 610static bool servers_changed(obs_properties_t *props, obs_property_t *list, 611 obs_data_t *settings) 612{ 613 @autoreleasepool { 614 obs_property_list_clear(list); 615 add_servers(obs_properties_get_param(props), list, settings); 616 return true; 617 } 618} 619 620static inline NSString *get_inject_application_path() 621{ 622 static NSString *ident = @"zakk.lol.SyphonInject"; 623 NSWorkspace *ws = [NSWorkspace sharedWorkspace]; 624 return [ws absolutePathForAppBundleWithIdentifier:ident]; 625} 626 627static inline bool is_inject_available_in_lib_dir(NSFileManager *fm, NSURL *url) 628{ 629 if (!url.isFileURL) 630 return false; 631 632 for (NSString *path in [fm contentsOfDirectoryAtPath:url.path 633 error:nil]) { 634 NSURL *bundle_url = [url URLByAppendingPathComponent:path]; 635 NSBundle *bundle = [NSBundle bundleWithURL:bundle_url]; 636 if (!bundle) 637 continue; 638 639 if ([bundle.bundleIdentifier 640 isEqual:@"zakk.lol.SASyphonInjector"]) 641 return true; 642 } 643 644 return false; 645} 646 647static inline bool is_inject_available() 648{ 649 if (get_inject_application_path()) 650 return true; 651 652 NSFileManager *fm = [NSFileManager defaultManager]; 653 for (NSURL *url in [fm URLsForDirectory:NSLibraryDirectory 654 inDomains:NSAllDomainsMask]) { 655 NSURL *scripting = [url 656 URLByAppendingPathComponent:@"ScriptingAdditions" 657 isDirectory:true]; 658 if (is_inject_available_in_lib_dir(fm, scripting)) 659 return true; 660 } 661 662 return false; 663} 664 665static inline void launch_syphon_inject_internal() 666{ 667 NSString *path = get_inject_application_path(); 668 NSWorkspace *ws = [NSWorkspace sharedWorkspace]; 669 if (path) 670 [ws launchApplication:path]; 671} 672 673static bool launch_syphon_inject(obs_properties_t *props, obs_property_t *prop, 674 void *data) 675{ 676 UNUSED_PARAMETER(props); 677 UNUSED_PARAMETER(prop); 678 UNUSED_PARAMETER(data); 679 680 @autoreleasepool { 681 launch_syphon_inject_internal(); 682 return false; 683 } 684} 685 686static int describes_app(obs_data_t *info, NSRunningApplication *app) 687{ 688 int score = 0; 689 if ([app.localizedName isEqual:get_string(info, "name")]) 690 score += 2; 691 692 if ([app.bundleIdentifier isEqual:get_string(info, "bundle")]) 693 score += 2; 694 695 if ([app.executableURL isEqual:get_string(info, "executable")]) 696 score += 2; 697 698 if (score && app.processIdentifier == obs_data_get_int(info, "pid")) 699 score += 1; 700 701 return score; 702} 703 704static inline void app_to_data(NSRunningApplication *app, obs_data_t *app_data) 705{ 706 obs_data_set_string(app_data, "name", app.localizedName.UTF8String); 707 obs_data_set_string(app_data, "bundle", 708 app.bundleIdentifier.UTF8String); 709 // Until we drop 10.8, use path.fileSystemRepsentation 710 obs_data_set_string(app_data, "executable", 711 app.executableURL.path.fileSystemRepresentation); 712 obs_data_set_int(app_data, "pid", app.processIdentifier); 713} 714 715static inline NSDictionary *get_duplicate_names(NSArray *apps) 716{ 717 NSMutableDictionary *result = 718 [NSMutableDictionary dictionaryWithCapacity:apps.count]; 719 for (NSRunningApplication *app in apps) { 720 if (result[app.localizedName]) 721 result[app.localizedName] = @(true); 722 else 723 result[app.localizedName] = @(false); 724 } 725 return result; 726} 727 728static inline size_t add_app(obs_property_t *prop, NSDictionary *duplicates, 729 NSString *name, const char *bundle, 730 const char *json_data, bool is_duplicate, 731 pid_t pid) 732{ 733 if (!is_duplicate) { 734 NSNumber *val = duplicates[name]; 735 is_duplicate = val && val.boolValue; 736 } 737 738 if (is_duplicate) 739 name = [NSString 740 stringWithFormat:@"%@ (%s: %d)", name, bundle, pid]; 741 742 return obs_property_list_add_string(prop, name.UTF8String, json_data); 743} 744 745static void update_inject_list_internal(obs_properties_t *props, 746 obs_property_t *prop, 747 obs_data_t *settings) 748{ 749 UNUSED_PARAMETER(props); 750 751 const char *current_str = obs_data_get_string(settings, "application"); 752 obs_data_t *current = obs_data_create_from_json(current_str); 753 NSString *current_name = @(obs_data_get_string(current, "name")); 754 755 bool current_found = !obs_data_has_user_value(current, "name"); 756 757 obs_property_list_clear(prop); 758 obs_property_list_add_string(prop, "", ""); 759 760 NSWorkspace *ws = [NSWorkspace sharedWorkspace]; 761 NSArray *apps = ws.runningApplications; 762 763 NSDictionary *duplicates = get_duplicate_names(apps); 764 NSMapTable *candidates = [NSMapTable weakToStrongObjectsMapTable]; 765 766 obs_data_t *app_data = obs_data_create(); 767 for (NSRunningApplication *app in apps) { 768 app_to_data(app, app_data); 769 int score = describes_app(current, app); 770 771 NSString *name = app.localizedName; 772 add_app(prop, duplicates, name, app.bundleIdentifier.UTF8String, 773 obs_data_get_json(app_data), 774 [name isEqual:current_name] && score < 4, 775 app.processIdentifier); 776 777 if (score >= 4) { 778 [candidates setObject:@(score) forKey:app]; 779 current_found = true; 780 } 781 } 782 obs_data_release(app_data); 783 784 if (!current_found) { 785 size_t idx = add_app(prop, duplicates, current_name, 786 obs_data_get_string(current, "bundle"), 787 current_str, 788 duplicates[current_name] != nil, 789 obs_data_get_int(current, "pid")); 790 obs_property_list_item_disable(prop, idx, true); 791 792 } else if (candidates.count > 0) { 793 NSRunningApplication *best_match = nil; 794 NSNumber *best_match_score = @(0); 795 796 for (NSRunningApplication *app in candidates.keyEnumerator) { 797 NSNumber *score = [candidates objectForKey:app]; 798 if ([score compare:best_match_score] == 799 NSOrderedDescending) { 800 best_match = app; 801 best_match_score = score; 802 } 803 } 804 805 // Update settings in case of PID/executable updates 806 if (best_match_score.intValue >= 4) { 807 app_to_data(best_match, current); 808 obs_data_set_string(settings, "application", 809 obs_data_get_json(current)); 810 } 811 } 812 813 obs_data_release(current); 814} 815 816static void toggle_inject_internal(obs_properties_t *props, 817 obs_property_t *prop, obs_data_t *settings) 818{ 819 bool enabled = obs_data_get_bool(settings, "inject"); 820 obs_property_t *inject_list = obs_properties_get(props, "application"); 821 822 bool inject_enabled = obs_property_enabled(prop); 823 obs_property_set_enabled(inject_list, enabled && inject_enabled); 824} 825 826static bool toggle_inject(obs_properties_t *props, obs_property_t *prop, 827 obs_data_t *settings) 828{ 829 @autoreleasepool { 830 toggle_inject_internal(props, prop, settings); 831 return true; 832 } 833} 834 835static bool update_inject_list(obs_properties_t *props, obs_property_t *prop, 836 obs_data_t *settings) 837{ 838 @autoreleasepool { 839 update_inject_list_internal(props, prop, settings); 840 return true; 841 } 842} 843 844static bool update_crop(obs_properties_t *props, obs_property_t *prop, 845 obs_data_t *settings) 846{ 847 bool enabled = obs_data_get_bool(settings, "crop"); 848 849#define LOAD_CROP(x) \ 850 prop = obs_properties_get(props, "crop." #x); \ 851 obs_property_set_enabled(prop, enabled); 852 LOAD_CROP(origin.x); 853 LOAD_CROP(origin.y); 854 LOAD_CROP(size.width); 855 LOAD_CROP(size.height); 856#undef LOAD_CROP 857 858 return true; 859} 860 861static void show_syphon_license_internal(void) 862{ 863 char *path = obs_module_file("syphon_license.txt"); 864 if (!path) 865 return; 866 867 NSWorkspace *ws = [NSWorkspace sharedWorkspace]; 868 [ws openFile:@(path)]; 869 bfree(path); 870} 871 872static bool show_syphon_license(obs_properties_t *props, obs_property_t *prop, 873 void *data) 874{ 875 UNUSED_PARAMETER(props); 876 UNUSED_PARAMETER(prop); 877 UNUSED_PARAMETER(data); 878 879 @autoreleasepool { 880 show_syphon_license_internal(); 881 return false; 882 } 883} 884 885static void syphon_release(void *param) 886{ 887 if (!param) 888 return; 889 890 obs_source_release(((syphon_t)param)->source); 891} 892 893static inline obs_properties_t *syphon_properties_internal(syphon_t s) 894{ 895 if (s) 896 obs_source_addref(s->source); 897 898 obs_properties_t *props = 899 obs_properties_create_param(s, syphon_release); 900 901 obs_property_t *list = obs_properties_add_list( 902 props, "uuid", obs_module_text("Source"), OBS_COMBO_TYPE_LIST, 903 OBS_COMBO_FORMAT_STRING); 904 obs_property_set_modified_callback(list, servers_changed); 905 906 obs_properties_add_bool(props, "allow_transparency", 907 obs_module_text("AllowTransparency")); 908 909 obs_property_t *launch = obs_properties_add_button( 910 props, "launch inject", obs_module_text("LaunchSyphonInject"), 911 launch_syphon_inject); 912 913 obs_property_t *inject = obs_properties_add_bool( 914 props, "inject", obs_module_text("Inject")); 915 obs_property_set_modified_callback(inject, toggle_inject); 916 917 obs_property_t *inject_list = obs_properties_add_list( 918 props, "application", obs_module_text("Application"), 919 OBS_COMBO_TYPE_LIST, OBS_COMBO_FORMAT_STRING); 920 obs_property_set_modified_callback(inject_list, update_inject_list); 921 922 if (!get_inject_application_path()) 923 obs_property_set_enabled(launch, false); 924 925 if (!is_inject_available()) { 926 obs_property_set_enabled(inject, false); 927 obs_property_set_enabled(inject_list, false); 928 } 929 930 obs_property_t *crop = 931 obs_properties_add_bool(props, "crop", obs_module_text("Crop")); 932 obs_property_set_modified_callback(crop, update_crop); 933 934#define LOAD_CROP(x) \ 935 obs_properties_add_float(props, "crop." #x, \ 936 obs_module_text("Crop." #x), 0., 4096.f, \ 937 .5f); 938 LOAD_CROP(origin.x); 939 LOAD_CROP(origin.y); 940 LOAD_CROP(size.width); 941 LOAD_CROP(size.height); 942#undef LOAD_CROP 943 944 obs_properties_add_button(props, "syphon license", 945 obs_module_text("SyphonLicense"), 946 show_syphon_license); 947 948 return props; 949} 950 951static obs_properties_t *syphon_properties(void *data) 952{ 953 @autoreleasepool { 954 return syphon_properties_internal(data); 955 } 956} 957 958static inline void syphon_save_internal(syphon_t s, obs_data_t *settings) 959{ 960 if (!s->uuid_changed) 961 return; 962 963 obs_data_set_string(settings, "app_name", s->app_name.UTF8String); 964 obs_data_set_string(settings, "name", s->name.UTF8String); 965 obs_data_set_string(settings, "uuid", s->uuid.UTF8String); 966} 967 968static void syphon_save(void *data, obs_data_t *settings) 969{ 970 @autoreleasepool { 971 syphon_save_internal(data, settings); 972 } 973} 974 975static inline void build_sprite(struct gs_vb_data *data, float fcx, float fcy, 976 float start_u, float end_u, float start_v, 977 float end_v) 978{ 979 struct vec2 *tvarray = data->tvarray[0].array; 980 981 vec3_set(data->points + 1, fcx, 0.0f, 0.0f); 982 vec3_set(data->points + 2, 0.0f, fcy, 0.0f); 983 vec3_set(data->points + 3, fcx, fcy, 0.0f); 984 vec2_set(tvarray, start_u, start_v); 985 vec2_set(tvarray + 1, end_u, start_v); 986 vec2_set(tvarray + 2, start_u, end_v); 987 vec2_set(tvarray + 3, end_u, end_v); 988} 989 990static inline void build_sprite_rect(struct gs_vb_data *data, float origin_x, 991 float origin_y, float end_x, float end_y) 992{ 993 build_sprite(data, fabs(end_x - origin_x), fabs(end_y - origin_y), 994 origin_x, end_x, origin_y, end_y); 995} 996 997static inline void tick_inject_state(syphon_t s, float seconds) 998{ 999 s->inject_wait_time -= seconds; 1000 1001 if (s->inject_wait_time > 0.f) 1002 return; 1003 1004 s->inject_wait_time = 1.f; 1005 NSWorkspace *ws = [NSWorkspace sharedWorkspace]; 1006 find_and_inject_target(s, ws.runningApplications, true); 1007} 1008 1009static void syphon_video_tick(void *data, float seconds) 1010{ 1011 UNUSED_PARAMETER(seconds); 1012 1013 syphon_t s = data; 1014 1015 if (s->inject_active && !s->inject_server_found) 1016 tick_inject_state(s, seconds); 1017 1018 if (!s->tex) 1019 return; 1020 1021 static const CGRect null_crop = {{0.f}}; 1022 const CGRect *crop = &null_crop; 1023 if (s->crop) 1024 crop = &s->crop_rect; 1025 1026 obs_enter_graphics(); 1027 build_sprite_rect(gs_vertexbuffer_get_data(s->vertbuffer), 1028 crop->origin.x, s->height - crop->origin.y, 1029 s->width - crop->size.width, crop->size.height); 1030 obs_leave_graphics(); 1031} 1032 1033static void syphon_video_render(void *data, gs_effect_t *effect) 1034{ 1035 UNUSED_PARAMETER(effect); 1036 1037 syphon_t s = data; 1038 1039 if (!s->tex) 1040 return; 1041 1042 gs_vertexbuffer_flush(s->vertbuffer); 1043 gs_load_vertexbuffer(s->vertbuffer); 1044 gs_load_indexbuffer(NULL); 1045 gs_load_samplerstate(s->sampler, 0); 1046 const char *tech_name = s->allow_transparency ? "Draw" : "DrawOpaque"; 1047 gs_technique_t *tech = gs_effect_get_technique(s->effect, tech_name); 1048 gs_effect_set_texture(gs_effect_get_param_by_name(s->effect, "image"), 1049 s->tex); 1050 gs_technique_begin(tech); 1051 gs_technique_begin_pass(tech, 0); 1052 1053 gs_draw(GS_TRISTRIP, 0, 4); 1054 1055 gs_technique_end_pass(tech); 1056 gs_technique_end(tech); 1057} 1058 1059static uint32_t syphon_get_width(void *data) 1060{ 1061 syphon_t s = (syphon_t)data; 1062 if (!s->crop) 1063 return s->width; 1064 int32_t width = 1065 s->width - s->crop_rect.origin.x - s->crop_rect.size.width; 1066 return MAX(0, width); 1067} 1068 1069static uint32_t syphon_get_height(void *data) 1070{ 1071 syphon_t s = (syphon_t)data; 1072 if (!s->crop) 1073 return s->height; 1074 int32_t height = 1075 s->height - s->crop_rect.origin.y - s->crop_rect.size.height; 1076 return MAX(0, height); 1077} 1078 1079static inline void inject_app(syphon_t s, NSRunningApplication *app, bool retry) 1080{ 1081 SBApplication *sbapp = nil; 1082 if (app.processIdentifier != -1) 1083 sbapp = [SBApplication 1084 applicationWithProcessIdentifier:app.processIdentifier]; 1085 else if (app.bundleIdentifier) 1086 sbapp = [SBApplication 1087 applicationWithBundleIdentifier:app.bundleIdentifier]; 1088 1089 if (!sbapp) 1090 return LOG(LOG_ERROR, "Could not inject %s", 1091 app.localizedName.UTF8String); 1092 1093 sbapp.timeout = 10 * 60; 1094 sbapp.sendMode = kAEWaitReply; 1095 [sbapp sendEvent:'ascr' id:'gdut' parameters:0]; 1096 sbapp.sendMode = kAENoReply; 1097 [sbapp sendEvent:'SASI' id:'injc' parameters:0]; 1098 1099 if (retry) 1100 return; 1101 1102 LOG(LOG_INFO, "Injected '%s' (%d, '%s')", app.localizedName.UTF8String, 1103 app.processIdentifier, app.bundleIdentifier.UTF8String); 1104} 1105 1106static inline void find_and_inject_target(syphon_t s, NSArray *arr, bool retry) 1107{ 1108 NSMutableArray *best_matches = [NSMutableArray arrayWithCapacity:1]; 1109 int best_score = 0; 1110 for (NSRunningApplication *app in arr) { 1111 int score = describes_app(s->inject_info, app); 1112 if (!score) 1113 continue; 1114 1115 if (score > best_score) { 1116 best_score = score; 1117 [best_matches removeAllObjects]; 1118 } 1119 1120 if (score >= best_score) 1121 [best_matches addObject:app]; 1122 } 1123 1124 for (NSRunningApplication *app in best_matches) 1125 inject_app(s, app, retry); 1126} 1127 1128static inline bool inject_info_equal(obs_data_t *prev, obs_data_t *new) 1129{ 1130 if (![get_string(prev, "name") isEqual:get_string(new, "name")]) 1131 return false; 1132 1133 if (![get_string(prev, "bundle") isEqual:get_string(new, "bundle")]) 1134 return false; 1135 1136 if (![get_string(prev, "executable") 1137 isEqual:get_string(new, "executable")]) 1138 return false; 1139 1140 if (![get_string(prev, "pid") isEqual:get_string(new, "pid")]) 1141 return false; 1142 1143 return true; 1144} 1145 1146static inline void update_inject(syphon_t s, obs_data_t *settings) 1147{ 1148 bool try_injecting = s->inject_active; 1149 s->inject_active = obs_data_get_bool(settings, "inject"); 1150 const char *inject_str = obs_data_get_string(settings, "application"); 1151 1152 try_injecting = !try_injecting && s->inject_active; 1153 1154 obs_data_t *prev = s->inject_info; 1155 s->inject_info = obs_data_create_from_json(inject_str); 1156 1157 s->inject_app = @(obs_data_get_string(s->inject_info, "name")); 1158 1159 SyphonServerDirectory *ssd = [SyphonServerDirectory sharedDirectory]; 1160 NSArray *servers = [ssd serversMatchingName:@"InjectedSyphon" 1161 appName:s->inject_app]; 1162 s->inject_server_found = false; 1163 for (NSDictionary *server in servers) 1164 update_inject_state(s, server, true); 1165 1166 if (!try_injecting) 1167 try_injecting = s->inject_active && 1168 !inject_info_equal(prev, s->inject_info); 1169 1170 obs_data_release(prev); 1171 1172 if (!try_injecting) 1173 return; 1174 1175 NSWorkspace *ws = [NSWorkspace sharedWorkspace]; 1176 find_and_inject_target(s, ws.runningApplications, false); 1177} 1178 1179static inline bool update_syphon(syphon_t s, obs_data_t *settings) 1180{ 1181 NSArray *arr = [[SyphonServerDirectory sharedDirectory] servers]; 1182 1183 if (!load_syphon_settings(s, settings)) 1184 return false; 1185 1186 NSDictionary *dict = find_by_uuid(arr, s->uuid); 1187 if (dict) { 1188 NSString *app = dict[SyphonServerDescriptionAppNameKey]; 1189 NSString *name = dict[SyphonServerDescriptionNameKey]; 1190 obs_data_set_string(settings, "app_name", app.UTF8String); 1191 obs_data_set_string(settings, "name", name.UTF8String); 1192 load_syphon_settings(s, settings); 1193 1194 } else if (!dict && !s->uuid.length) { 1195 obs_data_set_string(settings, "app_name", ""); 1196 obs_data_set_string(settings, "name", ""); 1197 load_syphon_settings(s, settings); 1198 } 1199 1200 return true; 1201} 1202 1203static void syphon_update_internal(syphon_t s, obs_data_t *settings) 1204{ 1205 s->allow_transparency = 1206 obs_data_get_bool(settings, "allow_transparency"); 1207 1208 load_crop(s, settings); 1209 update_inject(s, settings); 1210 if (update_syphon(s, settings)) 1211 create_client(s); 1212} 1213 1214static void syphon_update(void *data, obs_data_t *settings) 1215{ 1216 @autoreleasepool { 1217 syphon_update_internal(data, settings); 1218 } 1219} 1220 1221struct obs_source_info syphon_info = { 1222 .id = "syphon-input", 1223 .type = OBS_SOURCE_TYPE_INPUT, 1224 .output_flags = OBS_SOURCE_VIDEO | OBS_SOURCE_CUSTOM_DRAW | 1225 OBS_SOURCE_DO_NOT_DUPLICATE, 1226 .get_name = syphon_get_name, 1227 .create = syphon_create, 1228 .destroy = syphon_destroy, 1229 .video_render = syphon_video_render, 1230 .video_tick = syphon_video_tick, 1231 .get_properties = syphon_properties, 1232 .get_width = syphon_get_width, 1233 .get_height = syphon_get_height, 1234 .update = syphon_update, 1235 .save = syphon_save, 1236 .icon_type = OBS_ICON_TYPE_GAME_CAPTURE, 1237}; 1238