Vuo  2.3.2
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 
156  // Now that we've got the service's IP address and port, we can cancel the resolver.
157 
158  CFNetServiceUnscheduleFromRunLoop(service, CFRunLoopGetMain(), kCFRunLoopDefaultMode);
159 
160  CFNetServiceClientContext context;
161  context.version = 0;
162  context.info = NULL;
163  context.retain = NULL;
164  context.release = NULL;
165  context.copyDescription = NULL;
166  if (!CFNetServiceSetClient(service, NULL, &context))
167  VUserLog("Error: Failed to clear the client callback.");
168 
169  CFNetServiceCancel(service);
170 }
171 
175 void VuoOsc_deviceCallback(CFNetServiceBrowserRef browser, CFOptionFlags flags, CFTypeRef domainOrService, CFStreamError *error, void *info)
176 {
177  if (error->domain || error->error)
178  {
179  if (error->domain == kCFStreamErrorDomainCustom || error->error == 0)
180  {
181  // Happens occasionally for unknown reasons and seems to be harmless.
182  }
183  else if (error->domain == kCFStreamErrorDomainMacOSStatus)
184  {
185  char *errorText = VuoOsStatus_getText(error->error);
186  VUserLog("Error: %s", errorText);
187  free(errorText);
188  }
189  else
190  VUserLog("Error: %ld:%d", error->domain, error->error);
191  return;
192  }
193 
194  // We only care about services.
195  if (flags & kCFNetServiceFlagIsDomain)
196  return;
197 
198  bool deviceAdded = !(flags & kCFNetServiceFlagRemove);
199 
200  CFNetServiceRef service = (CFNetServiceRef)domainOrService;
201  if (!service)
202  return;
203 
204  if (deviceAdded)
205  {
206  CFNetServiceClientContext context;
207  context.version = 0;
208  context.info = NULL;
209  context.retain = NULL;
210  context.release = NULL;
211  context.copyDescription = NULL;
212  if (!CFNetServiceSetClient(service, VuoOsc_clientCallback, &context))
213  VUserLog("Error: Failed to set the client callback.");
214 
215  CFNetServiceScheduleWithRunLoop(service, CFRunLoopGetMain(), kCFRunLoopDefaultMode);
216 
217  CFStreamError error;
218  if (!CFNetServiceResolveWithTimeout(service, 0, &error))
219  VUserLog("Error: Failed to schedule service resolution (%ld:%d).",error.domain,error.error);
220  }
221  else
222  {
223  CFStringRef nameCF = CFNetServiceGetName(service);
224  VuoText name = VuoText_makeFromCFString(nameCF);
225  VuoRetain(name);
226 
227  dispatch_sync(VuoOsc_deviceQueue, ^{
229  bool removedAnyInputs = false;
230  for (VuoInteger i = inputCount; i > 0; --i)
231  {
233  if (VuoText_areEqual(d.name, name))
234  {
236  removedAnyInputs = true;
237  }
238  }
239  if (removedAnyInputs)
241 
242 
244  bool removedAnyOutputs = false;
245  for (VuoInteger i = outputCount; i > 0; --i)
246  {
248  if (VuoText_areEqual(d.name, name))
249  {
251  removedAnyOutputs = true;
252  }
253  }
254  if (removedAnyOutputs)
256  });
257 
258  VuoRelease(name);
259  }
260 }
261 
262 unsigned int VuoOsc_useCount = 0;
263 CFNetServiceBrowserRef VuoOsc_browser;
264 
270 void VuoOsc_use(void)
271 {
272  if (__sync_add_and_fetch(&VuoOsc_useCount, 1) == 1)
273  {
274  VuoOsc_deviceQueue = dispatch_queue_create("org.vuo.osc.device", NULL);
275  dispatch_sync(VuoOsc_deviceQueue, ^{
276 
279 
282 
283  CFNetServiceClientContext context;
284  context.version = 0;
285  context.info = NULL;
286  context.retain = NULL;
287  context.release = NULL;
288  context.copyDescription = NULL;
289 
290  VuoOsc_browser = CFNetServiceBrowserCreate(NULL, VuoOsc_deviceCallback, &context);
291  if (!VuoOsc_browser)
292  {
293  VUserLog("Error: Failed to create browser.");
294  return;
295  }
296 
297  CFNetServiceBrowserScheduleWithRunLoop(VuoOsc_browser, CFRunLoopGetMain(), kCFRunLoopDefaultMode);
298 
299  CFStreamError error;
300  if (!CFNetServiceBrowserSearchForServices(VuoOsc_browser, CFSTR(""), CFSTR("_osc._udp"), &error))
301  VUserLog("Error: Failed to activate browser (%ld:%d).", error.domain, error.error);
302 
303  });
304  }
305 }
306 
312 void VuoOsc_disuse(void)
313 {
314  if (VuoOsc_useCount <= 0)
315  {
316  VUserLog("Error: Unbalanced VuoOsc_use() / _disuse() calls.");
317  return;
318  }
319 
320  if (__sync_sub_and_fetch(&VuoOsc_useCount, 1) == 0)
321  {
322  CFStreamError error;
323  error.domain = kCFStreamErrorDomainCustom;
324  error.error = 0;
325  CFNetServiceBrowserStopSearch(VuoOsc_browser, &error);
326 
327  CFNetServiceBrowserUnscheduleFromRunLoop(VuoOsc_browser, CFRunLoopGetMain(), kCFRunLoopDefaultMode);
328 
329  CFNetServiceBrowserInvalidate(VuoOsc_browser);
330 
331  CFRelease(VuoOsc_browser);
332 
333  dispatch_sync(VuoOsc_deviceQueue, ^{});
334  dispatch_release(VuoOsc_deviceQueue);
335 
338  }
339 }
340 
351 )
352 {
353  if (inputDevices)
354  {
355  VuoOsc_inputDeviceCallbacks.addTrigger(inputDevices);
356  inputDevices(VuoOsc_getInputDeviceList());
357  }
358 
359  if (outputDevices)
360  {
361  VuoOsc_outputDeviceCallbacks.addTrigger(outputDevices);
362  outputDevices(VuoOsc_getOutputDeviceList());
363  }
364 }
365 
372 (
375 )
376 {
377  if (inputDevices)
378  VuoOsc_inputDeviceCallbacks.removeTrigger(inputDevices);
379 
380  if (outputDevices)
381  VuoOsc_outputDeviceCallbacks.removeTrigger(outputDevices);
382 }
383 
385 #define setRealizedDevice(newDevice) \
386  realizedDevice->name = VuoText_make(newDevice.name); \
387  realizedDevice->ipAddress = VuoText_make(newDevice.ipAddress); \
388  realizedDevice->port = newDevice.port;
389 
404 {
405  // Already have all properties; nothing to do.
406  if (!VuoText_isEmpty(device.name) && !VuoText_isEmpty(device.ipAddress) && device.port)
407  {
408  setRealizedDevice(device);
409  return true;
410  }
411 
412  // Otherwise, try to find a matching device.
413 
414  VDebugLog("Requested device: %s", json_object_to_json_string(VuoOscInputDevice_getJson(device)));
416  VuoLocal(devices);
417  __block bool found = false;
418 
419  // If a port was specified, it must match.
420  if (device.port)
422  if (device.port == item.port)
423  {
424  VDebugLog("Matched by port: %s",json_object_to_json_string(VuoOscInputDevice_getJson(item)));
425  setRealizedDevice(item);
426  found = true;
427  return false;
428  }
429  return true;
430  });
431 
432  // If no port was specified, match by name.
433  else
435  if (!VuoText_isEmpty(device.name) && VuoText_compare(item.name, (VuoTextComparison){VuoTextComparison_Contains, true}, device.name))
436  {
437  VDebugLog("Matched by name: %s",json_object_to_json_string(VuoOscInputDevice_getJson(item)));
438  setRealizedDevice(item);
439  found = true;
440  return false;
441  }
442  return true;
443  });
444 
445  if (!found)
446  VDebugLog("No matching device found.");
447 
448  return found;
449 }
450 
465 {
466  // Already have all properties; nothing to do.
467  if (!VuoText_isEmpty(device.name) && !VuoText_isEmpty(device.ipAddress) && device.port)
468  {
469  setRealizedDevice(device);
470  return true;
471  }
472 
473  // Otherwise, try to find a matching device.
474 
475  VDebugLog("Requested device: %s", json_object_to_json_string(VuoOscOutputDevice_getJson(device)));
477  VuoLocal(devices);
478  __block bool found = false;
479 
480  // If a port was specified, it must match.
481  if (device.port)
483  if (device.port == item.port)
484  {
485  VDebugLog("Matched by port: %s",json_object_to_json_string(VuoOscOutputDevice_getJson(item)));
486  setRealizedDevice(item);
487  found = true;
488  return false;
489  }
490  return true;
491  });
492 
493  // If no port was specified, match by name.
494  else
496  if (!VuoText_isEmpty(device.name) && VuoText_compare(item.name, (VuoTextComparison){VuoTextComparison_Contains, true}, device.name))
497  {
498  VDebugLog("Matched by name: %s",json_object_to_json_string(VuoOscOutputDevice_getJson(item)));
499  setRealizedDevice(item);
500  found = true;
501  return false;
502  }
503  return true;
504  });
505 
506  if (!found)
507  VDebugLog("No matching device found.");
508 
509  return found;
510 }