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