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
38static int OpenSD( vlc_object_t * );
39static void CloseSD( vlc_object_t * );
40
41static int OpenRD( vlc_object_t * );
42static void CloseRD( vlc_object_t * );
43
44VLC_SD_PROBE_HELPER( "Bonjour", N_("Bonjour Network Discovery"), SD_CAT_LAN )
45VLC_RD_PROBE_HELPER( "Bonjour_renderer", "Bonjour Renderer Discovery" )
46
47struct services_discovery_sys_t
48{
49    CFTypeRef _Nullable discoveryController;
50};
51
52struct vlc_renderer_discovery_sys
53{
54    CFTypeRef _Nullable discoveryController;
55};
56
57/*
58 * Module descriptor
59 */
60vlc_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
77vlc_module_end()
78
79NSString *const VLCBonjourProtocolName          = @"VLCBonjourProtocolName";
80NSString *const VLCBonjourProtocolServiceName   = @"VLCBonjourProtocolServiceName";
81NSString *const VLCBonjourIsRenderer            = @"VLCBonjourIsRenderer";
82NSString *const VLCBonjourRendererFlags         = @"VLCBonjourRendererFlags";
83NSString *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
388static 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
409static 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
423static int OpenRD(vlc_object_t *p_this)
424{
425    vlc_renderer_discovery_t *p_rd = (vlc_renderer_discovery_t *)p_this;
426    vlc_renderer_discovery_sys *p_sys = NULL;
427
428    p_rd->p_sys = p_sys = calloc(1, sizeof(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
442static void CloseRD(vlc_object_t *p_this)
443{
444    vlc_renderer_discovery_t *p_rd = (vlc_renderer_discovery_t *)p_this;
445    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}
455