aebb2033935ad7b620a90b34143b297fd880e464
[vlc.git] / modules / services_discovery / bonjour.m
1 /*****************************************************************************
2  * bonjour.m: mDNS services discovery module based on Bonjour
3  *****************************************************************************
4  * Copyright (C) 2016 VLC authors, VideoLAN and VideoLabs
5  *
6  * Authors: Felix Paul K├╝hne <fkuehne@videolan.org>
7  *          Marvin Scholz <epirat07@gmail.com>
8  *
9  * This program is free software; you can redistribute it and/or modify
10  * it under the terms of the GNU Lesser General Public License as published by
11  * the Free Software Foundation; either version 2.1 of the License, or
12  * (at your option) any later version.
13  *
14  * This program is distributed in the hope that it will be useful,
15  * but WITHOUT ANY WARRANTY; without even the implied warranty of
16  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
17  * GNU Lesser General Public License for more details.
18  *
19  * You should have received a copy of the GNU Lesser General Public License
20  * along with this program; if not, write to the Free Software Foundation,
21  * Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA.
22  *****************************************************************************/
23
24 #ifdef HAVE_CONFIG_H
25 # include <config.h>
26 #endif
27
28 #include <vlc_common.h>
29 #include <vlc_plugin.h>
30 #include <vlc_modules.h>
31 #include <vlc_services_discovery.h>
32 #include <vlc_renderer_discovery.h>
33
34 #import <Foundation/Foundation.h>
35
36 #pragma mark Function declarations
37
38 static int OpenSD( vlc_object_t * );
39 static void CloseSD( vlc_object_t * );
40
41 static int OpenRD( vlc_object_t * );
42 static void CloseRD( vlc_object_t * );
43
44 VLC_SD_PROBE_HELPER( "Bonjour", N_("Bonjour Network Discovery"), SD_CAT_LAN )
45 VLC_RD_PROBE_HELPER( "Bonjour_renderer", "Bonjour Renderer Discovery" )
46
47 typedef struct services_discovery_sys_t
48 {
49     CFTypeRef _Nullable discoveryController;
50 } services_discovery_sys_t;
51
52 struct vlc_renderer_discovery_sys
53 {
54     CFTypeRef _Nullable discoveryController;
55 };
56
57 /*
58  * Module descriptor
59  */
60 vlc_module_begin()
61     set_shortname( "Bonjour" )
62     set_description( N_( "Bonjour Network Discovery" ) )
63     set_category( CAT_PLAYLIST )
64     set_subcategory( SUBCAT_PLAYLIST_SD )
65     set_capability( "services_discovery", 0 )
66     set_callbacks( OpenSD, CloseSD )
67     add_shortcut( "mdns", "bonjour" )
68     VLC_SD_PROBE_SUBMODULE
69     add_submodule() \
70         set_description( N_( "Bonjour Renderer Discovery" ) )
71         set_category( CAT_SOUT )
72         set_subcategory( SUBCAT_SOUT_RENDERER )
73         set_capability( "renderer_discovery", 0 )
74         set_callbacks( OpenRD, CloseRD )
75         add_shortcut( "mdns_renderer", "bonjour_renderer" )
76         VLC_RD_PROBE_SUBMODULE
77 vlc_module_end()
78
79 NSString *const VLCBonjourProtocolName          = @"VLCBonjourProtocolName";
80 NSString *const VLCBonjourProtocolServiceName   = @"VLCBonjourProtocolServiceName";
81 NSString *const VLCBonjourIsRenderer            = @"VLCBonjourIsRenderer";
82 NSString *const VLCBonjourRendererFlags         = @"VLCBonjourRendererFlags";
83 NSString *const VLCBonjourRendererDemux         = @"VLCBonjourRendererDemux";
84
85 /*
86  * For chromecast, the `ca=` is composed from (at least)
87  * 0x01 to indicate video support
88  * 0x04 to indivate audio support
89  */
90 #define CHROMECAST_FLAG_VIDEO 0x01
91 #define CHROMECAST_FLAG_AUDIO 0x04
92
93 #pragma mark -
94 #pragma mark Interface definition
95 @interface VLCNetServiceDiscoveryController : NSObject <NSNetServiceBrowserDelegate, NSNetServiceDelegate>
96 {
97     /* Stores all used service browsers, one for each protocol, usually */
98     NSArray *_serviceBrowsers;
99
100     /* Holds a required reference to all NSNetServices */
101     NSMutableArray *_rawNetServices;
102
103     /* Holds all successfully resolved NSNetServices */
104     NSMutableArray *_resolvedNetServices;
105
106     /* Holds the respective pointers to a vlc_object for each resolved and added NSNetService */
107     NSMutableArray *_inputItemsForNetServices;
108
109     /* Stores all protocols that are currently discovered */
110     NSArray *_activeProtocols;
111 }
112
113 @property (readonly) BOOL isRendererDiscovery;
114 @property (readonly, nonatomic) vlc_object_t *p_this;
115
116 - (instancetype)initWithRendererDiscoveryObject:(vlc_renderer_discovery_t *)p_rd;
117 - (instancetype)initWithServicesDiscoveryObject:(services_discovery_t *)p_sd;
118
119 - (void)startDiscovery;
120 - (void)stopDiscovery;
121
122 @end
123
124 @implementation VLCNetServiceDiscoveryController
125
126 - (instancetype)initWithRendererDiscoveryObject:(vlc_renderer_discovery_t *)p_rd
127 {
128     self = [super init];
129     if (self) {
130         _p_this = VLC_OBJECT( p_rd );
131         _isRendererDiscovery = YES;
132     }
133
134     return self;
135 }
136
137 - (instancetype)initWithServicesDiscoveryObject:(services_discovery_t *)p_sd
138 {
139     self = [super init];
140     if (self) {
141         _p_this = VLC_OBJECT( p_sd );
142         _isRendererDiscovery = NO;
143     }
144
145     return self;
146 }
147
148 - (void)startDiscovery
149 {
150     NSDictionary *VLCFtpProtocol = @{ VLCBonjourProtocolName        : @"ftp",
151                                       VLCBonjourProtocolServiceName : @"_ftp._tcp.",
152                                       VLCBonjourIsRenderer          : @(NO)
153                                       };
154     NSDictionary *VLCSmbProtocol = @{ VLCBonjourProtocolName        : @"smb",
155                                       VLCBonjourProtocolServiceName : @"_smb._tcp.",
156                                       VLCBonjourIsRenderer          : @(NO)
157                                       };
158     NSDictionary *VLCNfsProtocol = @{ VLCBonjourProtocolName        : @"nfs",
159                                       VLCBonjourProtocolServiceName : @"_nfs._tcp.",
160                                       VLCBonjourIsRenderer          : @(NO)
161                                       };
162     NSDictionary *VLCSftpProtocol = @{ VLCBonjourProtocolName       : @"sftp",
163                                        VLCBonjourProtocolServiceName: @"_sftp-ssh._tcp.",
164                                        VLCBonjourIsRenderer         : @(NO)
165                                        };
166     NSDictionary *VLCCastProtocol = @{ VLCBonjourProtocolName       : @"chromecast",
167                                        VLCBonjourProtocolServiceName: @"_googlecast._tcp.",
168                                        VLCBonjourIsRenderer         : @(YES),
169                                        VLCBonjourRendererFlags      : @(VLC_RENDERER_CAN_AUDIO),
170                                        VLCBonjourRendererDemux      : @"cc_demux"
171                                        };
172
173     NSArray *VLCSupportedProtocols = @[VLCFtpProtocol,
174                                       VLCSmbProtocol,
175                                       VLCNfsProtocol,
176                                       VLCSftpProtocol,
177                                       VLCCastProtocol];
178
179     _rawNetServices = [[NSMutableArray alloc] init];
180     _resolvedNetServices = [[NSMutableArray alloc] init];
181     _inputItemsForNetServices = [[NSMutableArray alloc] init];
182
183     NSMutableArray *discoverers = [[NSMutableArray alloc] init];
184     NSMutableArray *protocols = [[NSMutableArray alloc] init];
185
186     msg_Info(_p_this, "starting discovery");
187     for (NSDictionary *protocol in VLCSupportedProtocols) {
188         /* Only discover services if we actually have a module that can handle those */
189         if (!module_exists([[protocol objectForKey: VLCBonjourProtocolName] UTF8String]) && !_isRendererDiscovery) {
190             msg_Dbg(_p_this, "no module for %s, skipping", [[protocol objectForKey: VLCBonjourProtocolName] UTF8String]);
191             continue;
192         }
193
194         /* Only discover hosts it they match the current mode (renderer or service) */
195         if ([[protocol objectForKey: VLCBonjourIsRenderer] boolValue] != _isRendererDiscovery) {
196             msg_Dbg(_p_this, "%s does not match current discovery mode, skipping", [[protocol objectForKey: VLCBonjourProtocolName] UTF8String]);
197             continue;
198         }
199
200         NSNetServiceBrowser *serviceBrowser = [[NSNetServiceBrowser alloc] init];
201         [serviceBrowser setDelegate:self];
202         msg_Dbg(_p_this, "starting discovery for type %s", [[protocol objectForKey: VLCBonjourProtocolServiceName] UTF8String]);
203         [serviceBrowser searchForServicesOfType:[protocol objectForKey: VLCBonjourProtocolServiceName] inDomain:@"local."];
204         [discoverers addObject:serviceBrowser];
205         [protocols addObject:protocol];
206     }
207
208     _serviceBrowsers = [discoverers copy];
209     _activeProtocols = [protocols copy];
210 }
211
212 - (void)stopDiscovery
213 {
214     [_serviceBrowsers makeObjectsPerformSelector:@selector(stop)];
215
216     /* Work around a macOS 10.12 bug, see https://openradar.appspot.com/28943305 */
217     [_serviceBrowsers makeObjectsPerformSelector:@selector(setDelegate:) withObject:nil];
218     [_resolvedNetServices makeObjectsPerformSelector:@selector(setDelegate:) withObject:nil];
219
220     for (NSValue *item in _inputItemsForNetServices) {
221         if (_isRendererDiscovery) {
222             [self removeRawRendererItem:item];
223         } else {
224             [self removeRawInputItem:item];
225         }
226     }
227
228     [_inputItemsForNetServices removeAllObjects];
229     [_resolvedNetServices removeAllObjects];
230     msg_Info(_p_this, "stopped discovery");
231 }
232
233 #pragma mark - 
234 #pragma mark Delegate methods
235
236 - (void)netServiceBrowser:(NSNetServiceBrowser *)aNetServiceBrowser didFindService:(NSNetService *)aNetService moreComing:(BOOL)moreComing
237 {
238     msg_Dbg(_p_this, "service found: %s (%s), resolving", [aNetService.name UTF8String], [aNetService.type UTF8String]);
239     [_rawNetServices addObject:aNetService];
240     aNetService.delegate = self;
241     [aNetService resolveWithTimeout:5.];
242 }
243
244 - (void)netServiceBrowser:(NSNetServiceBrowser *)aNetServiceBrowser didRemoveService:(NSNetService *)aNetService moreComing:(BOOL)moreComing
245 {
246     msg_Dbg(self.p_this, "service disappeared: %s (%s), removing", [aNetService.name UTF8String], [aNetService.type UTF8String]);
247
248     /* If the item was not looked-up yet, just remove it */
249     if ([_rawNetServices containsObject:aNetService])
250         [_rawNetServices removeObject:aNetService];
251
252     /* If the item was already resolved, the associated input or renderer items needs to be removed as well */
253     if ([_resolvedNetServices containsObject:aNetService]) {
254         NSInteger index = [_resolvedNetServices indexOfObject:aNetService];
255         if (index == NSNotFound) {
256             return;
257         }
258
259         [_resolvedNetServices removeObjectAtIndex:index];
260
261         if (_isRendererDiscovery) {
262             [self removeRawRendererItem:[_inputItemsForNetServices objectAtIndex:index]];
263         } else {
264             [self removeRawInputItem:[_inputItemsForNetServices objectAtIndex:index]];
265         }
266
267         /* Remove item pointer from our lookup array */
268         [_inputItemsForNetServices removeObjectAtIndex:index];
269     }
270 }
271
272 - (void)netServiceDidResolveAddress:(NSNetService *)aNetService
273 {
274     msg_Dbg(_p_this, "service resolved: %s", [aNetService.name UTF8String]);
275     if (![_resolvedNetServices containsObject:aNetService]) {
276         NSString *serviceType = aNetService.type;
277         NSString *protocol = nil;
278         for (NSDictionary *protocolDefinition in _activeProtocols) {
279             if ([serviceType isEqualToString:[protocolDefinition objectForKey:VLCBonjourProtocolServiceName]]) {
280                 protocol = [protocolDefinition objectForKey:VLCBonjourProtocolName];
281             }
282         }
283
284         if (_isRendererDiscovery) {
285             [self addResolvedRendererItem:aNetService withProtocol:protocol];
286         } else {
287             [self addResolvedInputItem:aNetService withProtocol:protocol];
288         }
289     }
290
291     [_rawNetServices removeObject:aNetService];
292 }
293
294 - (void)netService:(NSNetService *)aNetService didNotResolve:(NSDictionary *)errorDict
295 {
296     msg_Warn(_p_this, "service resolution failed: %s, removing", [aNetService.name UTF8String]);
297     [_rawNetServices removeObject:aNetService];
298 }
299
300 #pragma mark -
301 #pragma mark Helper methods
302
303 - (void)addResolvedRendererItem:(NSNetService *)netService withProtocol:(NSString *)protocol
304 {
305     vlc_renderer_discovery_t *p_rd = (vlc_renderer_discovery_t *)_p_this;
306
307     NSString *uri = [NSString stringWithFormat:@"%@://%@:%ld", protocol, netService.hostName, netService.port];
308     NSDictionary *txtDict = [NSNetService dictionaryFromTXTRecordData:[netService TXTRecordData]];
309     NSString *displayName = netService.name;
310     int rendererFlags = 0;
311
312     if ([netService.type isEqualToString:@"_googlecast._tcp."]) {
313         NSData *modelData = [txtDict objectForKey:@"md"];
314         NSData *nameData = [txtDict objectForKey:@"fn"];
315         NSData *flagsData = [txtDict objectForKey:@"ca"];
316
317         // Get CC capability flags from TXT data
318         if (flagsData) {
319             NSString *flagsString = [[NSString alloc] initWithData:flagsData encoding:NSUTF8StringEncoding];
320             NSInteger flags = [flagsString intValue];
321
322             if ((flags & CHROMECAST_FLAG_VIDEO) != 0) {
323                 rendererFlags |= VLC_RENDERER_CAN_VIDEO;
324             }
325             if ((flags & CHROMECAST_FLAG_AUDIO) != 0) {
326                 rendererFlags |= VLC_RENDERER_CAN_AUDIO;
327             }
328         }
329
330         // Get CC model and name from TXT data
331         if (modelData && nameData) {
332             NSString *model = [[NSString alloc] initWithData:modelData encoding:NSUTF8StringEncoding];
333             NSString *name = [[NSString alloc] initWithData:nameData encoding:NSUTF8StringEncoding];
334             displayName = [NSString stringWithFormat:@"%@ (%@)", name, model];
335         }
336     }
337
338     const char *extra_uri = rendererFlags & VLC_RENDERER_CAN_VIDEO ? NULL : "no-video";
339
340     // TODO: Adapt to work with not just chromecast!
341     vlc_renderer_item_t *p_renderer_item = vlc_renderer_item_new("chromecast", [displayName UTF8String],
342                                                                  [uri UTF8String], extra_uri, "cc_demux",
343                                                                  "", rendererFlags );
344     if (p_renderer_item != NULL) {
345         vlc_rd_add_item( p_rd, p_renderer_item );
346         [_inputItemsForNetServices addObject:[NSValue valueWithPointer:p_renderer_item]];
347         [_resolvedNetServices addObject:netService];
348     }
349 }
350
351 - (void)removeRawRendererItem:(NSValue *)item
352 {
353     vlc_renderer_discovery_t *p_rd = (vlc_renderer_discovery_t *)_p_this;
354     vlc_renderer_item_t *input_item = [item pointerValue];
355
356     if (input_item != NULL) {
357         vlc_rd_remove_item( p_rd, input_item );
358         vlc_renderer_item_release( input_item );
359     }
360 }
361
362 - (void)addResolvedInputItem:(NSNetService *)netService withProtocol:(NSString *)protocol
363 {
364     services_discovery_t *p_sd = (services_discovery_t *)_p_this;
365
366     NSString *uri = [NSString stringWithFormat:@"%@://%@:%ld", protocol, netService.hostName, netService.port];
367     input_item_t *p_input_item = input_item_NewDirectory([uri UTF8String], [netService.name UTF8String], ITEM_NET );
368     if (p_input_item != NULL) {
369         services_discovery_AddItem(p_sd, p_input_item);
370         [_inputItemsForNetServices addObject:[NSValue valueWithPointer:p_input_item]];
371         [_resolvedNetServices addObject:netService];
372     }
373 }
374
375 - (void)removeRawInputItem:(NSValue *)item
376 {
377     services_discovery_t *p_sd = (services_discovery_t *)_p_this;
378     input_item_t *input_item = [item pointerValue];
379
380     if (input_item != NULL) {
381         services_discovery_RemoveItem( p_sd, input_item );
382         input_item_Release( input_item );
383     }
384 }
385
386 @end
387
388 static int OpenSD(vlc_object_t *p_this)
389 {
390     services_discovery_t *p_sd = (services_discovery_t *)p_this;
391     services_discovery_sys_t *p_sys = NULL;
392
393     p_sd->p_sys = p_sys = calloc(1, sizeof(services_discovery_sys_t));
394     if (!p_sys) {
395         return VLC_ENOMEM;
396     }
397
398     p_sd->description = _("Bonjour Network Discovery");
399
400     VLCNetServiceDiscoveryController *discoveryController = [[VLCNetServiceDiscoveryController alloc] initWithServicesDiscoveryObject:p_sd];
401
402     p_sys->discoveryController = CFBridgingRetain(discoveryController);
403
404     [discoveryController startDiscovery];
405
406     return VLC_SUCCESS;
407 }
408
409 static void CloseSD(vlc_object_t *p_this)
410 {
411     services_discovery_t *p_sd = (services_discovery_t *)p_this;
412     services_discovery_sys_t *p_sys = p_sd->p_sys;
413
414     VLCNetServiceDiscoveryController *discoveryController = (__bridge VLCNetServiceDiscoveryController *)(p_sys->discoveryController);
415     [discoveryController stopDiscovery];
416
417     CFBridgingRelease(p_sys->discoveryController);
418     discoveryController = nil;
419
420     free(p_sys);
421 }
422
423 static int OpenRD(vlc_object_t *p_this)
424 {
425     vlc_renderer_discovery_t *p_rd = (vlc_renderer_discovery_t *)p_this;
426     struct vlc_renderer_discovery_sys *p_sys = NULL;
427
428     p_rd->p_sys = p_sys = calloc(1, sizeof(struct vlc_renderer_discovery_sys));
429     if (!p_sys) {
430         return VLC_ENOMEM;
431     }
432
433     VLCNetServiceDiscoveryController *discoveryController = [[VLCNetServiceDiscoveryController alloc] initWithRendererDiscoveryObject:p_rd];
434
435     p_sys->discoveryController = CFBridgingRetain(discoveryController);
436
437     [discoveryController startDiscovery];
438
439     return VLC_SUCCESS;
440 }
441
442 static void CloseRD(vlc_object_t *p_this)
443 {
444     vlc_renderer_discovery_t *p_rd = (vlc_renderer_discovery_t *)p_this;
445     struct vlc_renderer_discovery_sys *p_sys = p_rd->p_sys;
446
447     VLCNetServiceDiscoveryController *discoveryController = (__bridge VLCNetServiceDiscoveryController *)(p_sys->discoveryController);
448     [discoveryController stopDiscovery];
449
450     CFBridgingRelease(p_sys->discoveryController);
451     discoveryController = nil;
452
453     free(p_sys);
454 }