syphon.m 31 KB

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