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