rd/bonjour: Use Chromecast name and model from TXT record
[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 struct services_discovery_sys_t
48 {
49     CFTypeRef _Nullable discoveryController;
50 };
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 #pragma mark -
86 #pragma mark Interface definition
87 @interface VLCNetServiceDiscoveryController : NSObject <NSNetServiceBrowserDelegate, NSNetServiceDelegate>
88 {
89     /* Stores all used service browsers, one for each protocol, usually */
90     NSArray *_serviceBrowsers;
91
92     /* Holds a required reference to all NSNetServices */
93     NSMutableArray *_rawNetServices;
94
95     /* Holds all successfully resolved NSNetServices */
96     NSMutableArray *_resolvedNetServices;
97
98     /* Holds the respective pointers to a vlc_object for each resolved and added NSNetService */
99     NSMutableArray *_inputItemsForNetServices;
100
101     /* Stores all protocols that are currently discovered */
102     NSArray *_activeProtocols;
103 }
104
105 @property (readonly) BOOL isRendererDiscovery;
106 @property (readonly, nonatomic) vlc_object_t *p_this;
107
108 - (instancetype)initWithRendererDiscoveryObject:(vlc_renderer_discovery_t *)p_rd;
109 - (instancetype)initWithServicesDiscoveryObject:(services_discovery_t *)p_sd;
110
111 - (void)startDiscovery;
112 - (void)stopDiscovery;
113
114 @end
115
116 @implementation VLCNetServiceDiscoveryController
117
118 - (instancetype)initWithRendererDiscoveryObject:(vlc_renderer_discovery_t *)p_rd
119 {
120     self = [super init];
121     if (self) {
122         _p_this = VLC_OBJECT( p_rd );
123         _isRendererDiscovery = YES;
124     }
125
126     return self;
127 }
128
129 - (instancetype)initWithServicesDiscoveryObject:(services_discovery_t *)p_sd
130 {
131     self = [super init];
132     if (self) {
133         _p_this = VLC_OBJECT( p_sd );
134         _isRendererDiscovery = NO;
135     }
136
137     return self;
138 }
139
140 - (void)startDiscovery
141 {
142     NSDictionary *VLCFtpProtocol = @{ VLCBonjourProtocolName        : @"ftp",
143                                       VLCBonjourProtocolServiceName : @"_ftp._tcp.",
144                                       VLCBonjourIsRenderer          : @(NO)
145                                       };
146     NSDictionary *VLCSmbProtocol = @{ VLCBonjourProtocolName        : @"smb",
147                                       VLCBonjourProtocolServiceName : @"_smb._tcp.",
148                                       VLCBonjourIsRenderer          : @(NO)
149                                       };
150     NSDictionary *VLCNfsProtocol = @{ VLCBonjourProtocolName        : @"nfs",
151                                       VLCBonjourProtocolServiceName : @"_nfs._tcp.",
152                                       VLCBonjourIsRenderer          : @(NO)
153                                       };
154     NSDictionary *VLCSftpProtocol = @{ VLCBonjourProtocolName       : @"sftp",
155                                        VLCBonjourProtocolServiceName: @"_sftp-ssh._tcp.",
156                                        VLCBonjourIsRenderer         : @(NO)
157                                        };
158     NSDictionary *VLCCastProtocol = @{ VLCBonjourProtocolName       : @"chromecast",
159                                        VLCBonjourProtocolServiceName: @"_googlecast._tcp.",
160                                        VLCBonjourIsRenderer         : @(YES),
161                                        VLCBonjourRendererFlags      : @(VLC_RENDERER_CAN_AUDIO),
162                                        VLCBonjourRendererDemux      : @"cc_demux"
163                                        };
164
165     NSArray *VLCSupportedProtocols = @[VLCFtpProtocol,
166                                       VLCSmbProtocol,
167                                       VLCNfsProtocol,
168                                       VLCSftpProtocol,
169                                       VLCCastProtocol];
170
171     _rawNetServices = [[NSMutableArray alloc] init];
172     _resolvedNetServices = [[NSMutableArray alloc] init];
173     _inputItemsForNetServices = [[NSMutableArray alloc] init];
174
175     NSMutableArray *discoverers = [[NSMutableArray alloc] init];
176     NSMutableArray *protocols = [[NSMutableArray alloc] init];
177
178     msg_Info(_p_this, "starting discovery");
179     for (NSDictionary *protocol in VLCSupportedProtocols) {
180         msg_Info(_p_this, "looking up %s", [[protocol objectForKey: VLCBonjourProtocolName] UTF8String]);
181
182         /* Only discover services if we actually have a module that can handle those */
183         if (!module_exists([[protocol objectForKey: VLCBonjourProtocolName] UTF8String]) && !_isRendererDiscovery) {
184             msg_Info(_p_this, "no module for %s, skipping", [[protocol objectForKey: VLCBonjourProtocolName] UTF8String]);
185             continue;
186         }
187
188         /* Only discover hosts it they match the current mode (renderer or service) */
189         if ([[protocol objectForKey: VLCBonjourIsRenderer] boolValue] != _isRendererDiscovery) {
190             msg_Info(_p_this, "%s does not match current discovery mode, skipping", [[protocol objectForKey: VLCBonjourProtocolName] UTF8String]);
191             continue;
192         }
193
194         NSNetServiceBrowser *serviceBrowser = [[NSNetServiceBrowser alloc] init];
195         [serviceBrowser setDelegate:self];
196         msg_Info(_p_this, "starting discovery for type %s", [[protocol objectForKey: VLCBonjourProtocolServiceName] UTF8String]);
197         [serviceBrowser searchForServicesOfType:[protocol objectForKey: VLCBonjourProtocolServiceName] inDomain:@"local."];
198         [discoverers addObject:serviceBrowser];
199         [protocols addObject:protocol];
200     }
201
202     _serviceBrowsers = [discoverers copy];
203     _activeProtocols = [protocols copy];
204 }
205
206 - (void)stopDiscovery
207 {
208     [_serviceBrowsers makeObjectsPerformSelector:@selector(stop)];
209
210     /* Work around a macOS 10.12 bug, see https://openradar.appspot.com/28943305 */
211     [_serviceBrowsers makeObjectsPerformSelector:@selector(setDelegate:) withObject:nil];
212     [_resolvedNetServices makeObjectsPerformSelector:@selector(setDelegate:) withObject:nil];
213
214     for (NSValue *item in _inputItemsForNetServices) {
215         if (_isRendererDiscovery) {
216             [self removeRawRendererItem:item];
217         } else {
218             [self removeRawInputItem:item];
219         }
220     }
221
222     [_inputItemsForNetServices removeAllObjects];
223     [_resolvedNetServices removeAllObjects];
224     msg_Info(_p_this, "stopped discovery");
225 }
226
227 #pragma mark - 
228 #pragma mark Delegate methods
229
230 - (void)netServiceBrowser:(NSNetServiceBrowser *)aNetServiceBrowser didFindService:(NSNetService *)aNetService moreComing:(BOOL)moreComing
231 {
232     msg_Info(_p_this, "found something, looking up");
233     msg_Dbg(self.p_this, "found bonjour service: %s (%s)", [aNetService.name UTF8String], [aNetService.type UTF8String]);
234     [_rawNetServices addObject:aNetService];
235     aNetService.delegate = self;
236     [aNetService resolveWithTimeout:5.];
237 }
238
239 - (void)netServiceBrowser:(NSNetServiceBrowser *)aNetServiceBrowser didRemoveService:(NSNetService *)aNetService moreComing:(BOOL)moreComing
240 {
241     msg_Dbg(self.p_this, "bonjour service disappeared: %s", [aNetService.name UTF8String]);
242
243     /* If the item was not looked-up yet, just remove it */
244     if ([_rawNetServices containsObject:aNetService])
245         [_rawNetServices removeObject:aNetService];
246
247     /* If the item was already resolved, the associated input or renderer items needs to be removed as well */
248     if ([_resolvedNetServices containsObject:aNetService]) {
249         NSInteger index = [_resolvedNetServices indexOfObject:aNetService];
250         if (index == NSNotFound) {
251             return;
252         }
253
254         [_resolvedNetServices removeObjectAtIndex:index];
255
256         if (_isRendererDiscovery) {
257             [self removeRawRendererItem:[_inputItemsForNetServices objectAtIndex:index]];
258         } else {
259             [self removeRawInputItem:[_inputItemsForNetServices objectAtIndex:index]];
260         }
261
262         /* Remove item pointer from our lookup array */
263         [_inputItemsForNetServices removeObjectAtIndex:index];
264     }
265 }
266
267 - (void)netServiceDidResolveAddress:(NSNetService *)aNetService
268 {
269     msg_Info(_p_this, "resolved something");
270     if (![_resolvedNetServices containsObject:aNetService]) {
271         NSString *serviceType = aNetService.type;
272         NSString *protocol = nil;
273         for (NSDictionary *protocolDefinition in _activeProtocols) {
274             if ([serviceType isEqualToString:[protocolDefinition objectForKey:VLCBonjourProtocolServiceName]]) {
275                 protocol = [protocolDefinition objectForKey:VLCBonjourProtocolName];
276             }
277         }
278
279         if (_isRendererDiscovery) {
280             [self addResolvedRendererItem:aNetService withProtocol:protocol];
281         } else {
282             [self addResolvedInputItem:aNetService withProtocol:protocol];
283         }
284     }
285
286     [_rawNetServices removeObject:aNetService];
287 }
288
289 - (void)netService:(NSNetService *)aNetService didNotResolve:(NSDictionary *)errorDict
290 {
291     msg_Dbg(_p_this, "failed to resolve: %s", [aNetService.name UTF8String]);
292     [_rawNetServices removeObject:aNetService];
293 }
294
295 #pragma mark -
296 #pragma mark Helper methods
297
298 - (void)addResolvedRendererItem:(NSNetService *)netService withProtocol:(NSString *)protocol
299 {
300     vlc_renderer_discovery_t *p_rd = (vlc_renderer_discovery_t *)_p_this;
301
302     NSString *uri = [NSString stringWithFormat:@"%@://%@:%ld", protocol, netService.hostName, netService.port];
303     NSDictionary *txtDict = [NSNetService dictionaryFromTXTRecordData:[netService TXTRecordData]];
304     NSString *displayName = netService.name;
305
306     if ([netService.type isEqualToString:@"_googlecast._tcp."]) {
307         NSData *modelData = [txtDict objectForKey:@"md"];
308         NSData *nameData = [txtDict objectForKey:@"fn"];
309         if (modelData && nameData) {
310             NSString *model = [[NSString alloc] initWithData:modelData encoding:NSUTF8StringEncoding];
311             NSString *name = [[NSString alloc] initWithData:nameData encoding:NSUTF8StringEncoding];
312             displayName = [NSString stringWithFormat:@"%@ (%@)", name, model];
313         }
314     }
315     // TODO: Detect rendered capabilities and adapt to work with not just chromecast
316     vlc_renderer_item_t *p_renderer_item = vlc_renderer_item_new( "chromecast", [displayName UTF8String],
317                                                                  [uri UTF8String], NULL, "cc_demux",
318                                                                  "", VLC_RENDERER_CAN_VIDEO );
319     if (p_renderer_item != NULL) {
320         vlc_rd_add_item( p_rd, p_renderer_item );
321         [_inputItemsForNetServices addObject:[NSValue valueWithPointer:p_renderer_item]];
322         [_resolvedNetServices addObject:netService];
323     }
324 }
325
326 - (void)removeRawRendererItem:(NSValue *)item
327 {
328     vlc_renderer_discovery_t *p_rd = (vlc_renderer_discovery_t *)_p_this;
329     vlc_renderer_item_t *input_item = [item pointerValue];
330
331     if (input_item != NULL) {
332         vlc_rd_remove_item( p_rd, input_item );
333         vlc_renderer_item_release( input_item );
334     }
335 }
336
337 - (void)addResolvedInputItem:(NSNetService *)netService withProtocol:(NSString *)protocol
338 {
339     services_discovery_t *p_sd = (services_discovery_t *)_p_this;
340
341     NSString *uri = [NSString stringWithFormat:@"%@://%@:%ld", protocol, netService.hostName, netService.port];
342     input_item_t *p_input_item = input_item_NewDirectory([uri UTF8String], [netService.name UTF8String], ITEM_NET );
343     if (p_input_item != NULL) {
344         services_discovery_AddItem(p_sd, p_input_item);
345         [_inputItemsForNetServices addObject:[NSValue valueWithPointer:p_input_item]];
346         [_resolvedNetServices addObject:netService];
347     }
348 }
349
350 - (void)removeRawInputItem:(NSValue *)item
351 {
352     services_discovery_t *p_sd = (services_discovery_t *)_p_this;
353     input_item_t *input_item = [item pointerValue];
354
355     if (input_item != NULL) {
356         services_discovery_RemoveItem( p_sd, input_item );
357         input_item_Release( input_item );
358     }
359 }
360
361 @end
362
363 static int OpenSD(vlc_object_t *p_this)
364 {
365     services_discovery_t *p_sd = (services_discovery_t *)p_this;
366     services_discovery_sys_t *p_sys = NULL;
367
368     p_sd->p_sys = p_sys = calloc(1, sizeof(services_discovery_sys_t));
369     if (!p_sys) {
370         return VLC_ENOMEM;
371     }
372
373     p_sd->description = _("Bonjour Network Discovery");
374
375     VLCNetServiceDiscoveryController *discoveryController = [[VLCNetServiceDiscoveryController alloc] initWithServicesDiscoveryObject:p_sd];
376
377     p_sys->discoveryController = CFBridgingRetain(discoveryController);
378
379     [discoveryController startDiscovery];
380
381     return VLC_SUCCESS;
382 }
383
384 static void CloseSD(vlc_object_t *p_this)
385 {
386     services_discovery_t *p_sd = (services_discovery_t *)p_this;
387     services_discovery_sys_t *p_sys = p_sd->p_sys;
388
389     VLCNetServiceDiscoveryController *discoveryController = (__bridge VLCNetServiceDiscoveryController *)(p_sys->discoveryController);
390     [discoveryController stopDiscovery];
391
392     CFBridgingRelease(p_sys->discoveryController);
393     discoveryController = nil;
394
395     free(p_sys);
396 }
397
398 static int OpenRD(vlc_object_t *p_this)
399 {
400     vlc_renderer_discovery_t *p_rd = (vlc_renderer_discovery_t *)p_this;
401     vlc_renderer_discovery_sys *p_sys = NULL;
402
403     p_rd->p_sys = p_sys = calloc(1, sizeof(vlc_renderer_discovery_sys));
404     if (!p_sys) {
405         return VLC_ENOMEM;
406     }
407
408     VLCNetServiceDiscoveryController *discoveryController = [[VLCNetServiceDiscoveryController alloc] initWithRendererDiscoveryObject:p_rd];
409
410     p_sys->discoveryController = CFBridgingRetain(discoveryController);
411
412     [discoveryController startDiscovery];
413
414     return VLC_SUCCESS;
415 }
416
417 static void CloseRD(vlc_object_t *p_this)
418 {
419     vlc_renderer_discovery_t *p_rd = (vlc_renderer_discovery_t *)p_this;
420     vlc_renderer_discovery_sys *p_sys = p_rd->p_sys;
421
422     VLCNetServiceDiscoveryController *discoveryController = (__bridge VLCNetServiceDiscoveryController *)(p_sys->discoveryController);
423     [discoveryController stopDiscovery];
424
425     CFBridgingRelease(p_sys->discoveryController);
426     discoveryController = nil;
427
428     free(p_sys);
429 }