syphon.m 32 KB

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