Vuo  2.0.0
VuoOscDevices.cc
Go to the documentation of this file.
1 
10 #include "VuoOsc.h"
11 #include "VuoTriggerSet.hh"
12 #include "VuoOsStatus.h"
13 
14 #include <CoreServices/CoreServices.h>
15 #include <netinet/in.h>
16 #include <arpa/inet.h>
17 
18 extern "C"
19 {
20 
21 #ifdef VUO_COMPILER
23  "title" : "VuoOscDevices",
24  "dependencies" : [
25  "CoreServices.framework",
26  "VuoOscInputDevice",
27  "VuoOscOutputDevice",
28  "VuoOsStatus",
29  "VuoList_VuoOscInputDevice",
30  "VuoList_VuoOscOutputDevice"
31  ]
32  });
33 #endif
34 }
35 
36 static dispatch_queue_t VuoOsc_deviceQueue;
41 
46 {
47  __block VuoList_VuoOscInputDevice devices;
48  dispatch_sync(VuoOsc_deviceQueue, ^{
50  });
51  return devices;
52 }
53 
58 {
59  __block VuoList_VuoOscOutputDevice devices;
60  dispatch_sync(VuoOsc_deviceQueue, ^{
62  });
63  return devices;
64 }
65 
69 void VuoOsc_clientCallback(CFNetServiceRef service, CFStreamError *error, void *info)
70 {
71  if (error->domain || error->error)
72  {
73  VUserLog("Error: CFStreamError (%ld:%d)", error->domain, error->error);
74  return;
75  }
76 
77  CFStringRef nameCF = CFNetServiceGetName(service);
78  VuoText name = VuoText_makeFromCFString(nameCF);
79  VuoRetain(name);
80 
81  CFDataRef txt = CFNetServiceGetTXTData(service);
82  CFDictionaryRef txtDict = CFNetServiceCreateDictionaryWithTXTData(NULL, txt);
83  CFDataRef typeData = (CFDataRef)CFDictionaryGetValue(txtDict, CFSTR("type"));
84  bool isInput = true;
85  if (typeData)
86  {
87  VuoText type = VuoText_makeFromData(CFDataGetBytePtr(typeData), CFDataGetLength(typeData));
88  VuoRetain(type);
89  if (VuoText_areEqual(type, "server"))
90  isInput = false;
91  VuoRelease(type);
92  }
93  CFRelease(txtDict);
94 
95 
96  SInt32 port = CFNetServiceGetPortNumber(service);
97 
98  CFArrayRef addresses = CFNetServiceGetAddressing(service);
99  CFIndex addressCount = CFArrayGetCount(addresses);
100 
101  for (CFIndex i = 0; i < addressCount; ++i)
102  {
103  struct sockaddr *socketAddress = (struct sockaddr *)CFDataGetBytePtr((CFDataRef)CFArrayGetValueAtIndex(addresses, i));
104 
105  // Ignore IPv6 for now.
106  if (!socketAddress || socketAddress->sa_family != AF_INET)
107  continue;
108 
109  char ip[256];
110  if (!inet_ntop(AF_INET, &((struct sockaddr_in *)socketAddress)->sin_addr, ip, sizeof(ip)))
111  {
112  VUserLog("Error: Couldn't get IP address for '%s': %s", name, strerror(errno));
113  continue;
114  }
115 
116  VuoText ipAddress = VuoText_make(ip);
117  VuoRetain(ipAddress);
118 
119  if (isInput)
120  {
121  VuoOscInputDevice d = VuoOscInputDevice_make(name, ipAddress, port);
122  dispatch_sync(VuoOsc_deviceQueue, ^{
123  // Sometimes Bonjour sends multiple notifications for the same device.
124  // https://b33p.net/kosada/node/11397
125  unsigned long deviceCount = VuoListGetCount_VuoOscInputDevice(VuoOsc_inputDevices);
126  for (int i = 1; i <= deviceCount; ++i)
128  return;
129 
132  });
133  }
134  else
135  {
136  VuoOscOutputDevice d = VuoOscOutputDevice_makeUnicast(name, ipAddress, port);
137  dispatch_sync(VuoOsc_deviceQueue, ^{
138  // Sometimes Bonjour sends multiple notifications for the same device.
139  // https://b33p.net/kosada/node/11397
140  unsigned long deviceCount = VuoListGetCount_VuoOscOutputDevice(VuoOsc_outputDevices);
141  for (int i = 1; i <= deviceCount; ++i)
143  return;
144 
147  });
148  }
149 
150  VuoRelease(ipAddress);
151  }
152 
153  VuoRelease(name);
154 }
155 
159 void VuoOsc_deviceCallback(CFNetServiceBrowserRef browser, CFOptionFlags flags, CFTypeRef domainOrService, CFStreamError *error, void *info)
160 {
161  if (error->domain || error->error)
162  {
163  if (error->domain == kCFStreamErrorDomainCustom || error->error == 0)
164  {
165  // Happens occasionally for unknown reasons and seems to be harmless.
166  }
167  else if (error->domain == kCFStreamErrorDomainMacOSStatus)
168  {
169  char *errorText = VuoOsStatus_getText(error->error);
170  VUserLog("Error: %s", errorText);
171  free(errorText);
172  }
173  else
174  VUserLog("Error: %ld:%d", error->domain, error->error);
175  return;
176  }
177 
178  // We only care about services.
179  if (flags & kCFNetServiceFlagIsDomain)
180  return;
181 
182  bool deviceAdded = !(flags & kCFNetServiceFlagRemove);
183 
184  CFNetServiceRef service = (CFNetServiceRef)domainOrService;
185  if (!service)
186  return;
187 
188  if (deviceAdded)
189  {
190  CFNetServiceClientContext context;
191  context.version = 0;
192  context.info = NULL;
193  context.retain = NULL;
194  context.release = NULL;
195  context.copyDescription = NULL;
196  if (!CFNetServiceSetClient(service, VuoOsc_clientCallback, &context))
197  VUserLog("Error: Failed to set the client callback.");
198 
199  CFNetServiceScheduleWithRunLoop(service, CFRunLoopGetMain(), kCFRunLoopDefaultMode);
200 
201  CFStreamError error;
202  if (!CFNetServiceResolveWithTimeout(service, 0, &error))
203  VUserLog("Error: Failed to schedule service resolution (%ld:%d).",error.domain,error.error);
204  }
205  else
206  {
207  CFStringRef nameCF = CFNetServiceGetName(service);
208  VuoText name = VuoText_makeFromCFString(nameCF);
209  VuoRetain(name);
210 
211  dispatch_sync(VuoOsc_deviceQueue, ^{
213  bool removedAnyInputs = false;
214  for (VuoInteger i = inputCount; i > 0; --i)
215  {
217  if (VuoText_areEqual(d.name, name))
218  {
220  removedAnyInputs = true;
221  }
222  }
223  if (removedAnyInputs)
225 
226 
228  bool removedAnyOutputs = false;
229  for (VuoInteger i = outputCount; i > 0; --i)
230  {
232  if (VuoText_areEqual(d.name, name))
233  {
235  removedAnyOutputs = true;
236  }
237  }
238  if (removedAnyOutputs)
240  });
241 
242  VuoRelease(name);
243 
244  CFNetServiceUnscheduleFromRunLoop(service, CFRunLoopGetMain(), kCFRunLoopDefaultMode);
245 
246  CFNetServiceClientContext context;
247  context.version = 0;
248  context.info = NULL;
249  context.retain = NULL;
250  context.release = NULL;
251  context.copyDescription = NULL;
252  if (!CFNetServiceSetClient(service, NULL, &context))
253  VUserLog("Error: Failed to clear the client callback.");
254 
255  CFNetServiceCancel(service);
256  }
257 }
258 
259 unsigned int VuoOsc_useCount = 0;
260 CFNetServiceBrowserRef VuoOsc_browser;
261 
267 void VuoOsc_use(void)
268 {
269  if (__sync_add_and_fetch(&VuoOsc_useCount, 1) == 1)
270  {
271  VuoOsc_deviceQueue = dispatch_queue_create("org.vuo.osc.device", NULL);
272  dispatch_sync(VuoOsc_deviceQueue, ^{
273 
276 
279 
280  CFNetServiceClientContext context;
281  context.version = 0;
282  context.info = NULL;
283  context.retain = NULL;
284  context.release = NULL;
285  context.copyDescription = NULL;
286 
287  VuoOsc_browser = CFNetServiceBrowserCreate(NULL, VuoOsc_deviceCallback, &context);
288  if (!VuoOsc_browser)
289  {
290  VUserLog("Error: Failed to create browser.");
291  return;
292  }
293 
294  CFNetServiceBrowserScheduleWithRunLoop(VuoOsc_browser, CFRunLoopGetMain(), kCFRunLoopDefaultMode);
295 
296  CFStreamError error;
297  if (!CFNetServiceBrowserSearchForServices(VuoOsc_browser, CFSTR(""), CFSTR("_osc._udp"), &error))
298  VUserLog("Error: Failed to activate browser (%ld:%d).", error.domain, error.error);
299 
300  });
301  }
302 }
303 
309 void VuoOsc_disuse(void)
310 {
311  if (VuoOsc_useCount <= 0)
312  {
313  VUserLog("Error: Unbalanced VuoOsc_use() / _disuse() calls.");
314  return;
315  }
316 
317  if (__sync_sub_and_fetch(&VuoOsc_useCount, 1) == 0)
318  {
319  CFStreamError error;
320  error.domain = kCFStreamErrorDomainCustom;
321  error.error = 0;
322  CFNetServiceBrowserStopSearch(VuoOsc_browser, &error);
323 
324  CFNetServiceBrowserUnscheduleFromRunLoop(VuoOsc_browser, CFRunLoopGetMain(), kCFRunLoopDefaultMode);
325 
326  CFNetServiceBrowserInvalidate(VuoOsc_browser);
327 
328  CFRelease(VuoOsc_browser);
329 
330  dispatch_sync(VuoOsc_deviceQueue, ^{});
331  dispatch_release(VuoOsc_deviceQueue);
332 
335  }
336 }
337 
348 )
349 {
350  if (inputDevices)
351  {
352  VuoOsc_inputDeviceCallbacks.addTrigger(inputDevices);
353  inputDevices(VuoOsc_getInputDeviceList());
354  }
355 
356  if (outputDevices)
357  {
358  VuoOsc_outputDeviceCallbacks.addTrigger(outputDevices);
359  outputDevices(VuoOsc_getOutputDeviceList());
360  }
361 }
362 
369 (
372 )
373 {
374  if (inputDevices)
375  VuoOsc_inputDeviceCallbacks.removeTrigger(inputDevices);
376 
377  if (outputDevices)
378  VuoOsc_outputDeviceCallbacks.removeTrigger(outputDevices);
379 }
380 
382 #define setRealizedDevice(newDevice) \
383  realizedDevice->name = VuoText_make(newDevice.name); \
384  realizedDevice->ipAddress = VuoText_make(newDevice.ipAddress); \
385  realizedDevice->port = newDevice.port;
386 
401 {
402  // Already have all properties; nothing to do.
403  if (!VuoText_isEmpty(device.name) && !VuoText_isEmpty(device.ipAddress) && device.port)
404  {
405  setRealizedDevice(device);
406  return true;
407  }
408 
409  // Otherwise, try to find a matching device.
410 
411  VDebugLog("Requested device: %s", json_object_to_json_string(VuoOscInputDevice_getJson(device)));
413  VuoLocal(devices);
414  __block bool found = false;
415 
416  // If a port was specified, it must match.
417  if (device.port)
419  if (device.port == item.port)
420  {
421  VDebugLog("Matched by port: %s",json_object_to_json_string(VuoOscInputDevice_getJson(item)));
422  setRealizedDevice(item);
423  found = true;
424  return false;
425  }
426  return true;
427  });
428 
429  // If no port was specified, match by name.
430  else
432  if (!VuoText_isEmpty(device.name) && VuoText_compare(item.name, (VuoTextComparison){VuoTextComparison_Contains, true, ""}, device.name))
433  {
434  VDebugLog("Matched by name: %s",json_object_to_json_string(VuoOscInputDevice_getJson(item)));
435  setRealizedDevice(item);
436  found = true;
437  return false;
438  }
439  return true;
440  });
441 
442  if (!found)
443  VDebugLog("No matching device found.");
444 
445  return found;
446 }
447 
462 {
463  // Already have all properties; nothing to do.
464  if (!VuoText_isEmpty(device.name) && !VuoText_isEmpty(device.ipAddress) && device.port)
465  {
466  setRealizedDevice(device);
467  return true;
468  }
469 
470  // Otherwise, try to find a matching device.
471 
472  VDebugLog("Requested device: %s", json_object_to_json_string(VuoOscOutputDevice_getJson(device)));
474  VuoLocal(devices);
475  __block bool found = false;
476 
477  // If a port was specified, it must match.
478  if (device.port)
480  if (device.port == item.port)
481  {
482  VDebugLog("Matched by port: %s",json_object_to_json_string(VuoOscOutputDevice_getJson(item)));
483  setRealizedDevice(item);
484  found = true;
485  return false;
486  }
487  return true;
488  });
489 
490  // If no port was specified, match by name.
491  else
493  if (!VuoText_isEmpty(device.name) && VuoText_compare(item.name, (VuoTextComparison){VuoTextComparison_Contains, true, ""}, device.name))
494  {
495  VDebugLog("Matched by name: %s",json_object_to_json_string(VuoOscOutputDevice_getJson(item)));
496  setRealizedDevice(item);
497  found = true;
498  return false;
499  }
500  return true;
501  });
502 
503  if (!found)
504  VDebugLog("No matching device found.");
505 
506  return found;
507 }