MultipeerConnectivity browsing issue on CLI

Hello everyone,


I am trying to run a simple CLI application using MultipeerConnectivity.

My concern is that startBrowsingForPeer seems to work only on a graphical application (NSApplication) when startAdvertisingPeerseems to work properly on both graphical and CLI apps.


Indeed, by executing the same code on the CLI and UI versions, I get this result:
- UI browsing / UI advertising = working

- UI browsing / CLI advertising = working (startAdvertisingPeer works on CLI)

- UI advertising / CLI browsing = not working (startBrowsingForPeer doesn'twork on CLI)
I can't figure out if it's a permissions issue (signature, entitlements, etc...) or if it's just that the browsing API can only work in the context of an NSApplication.
Steps to reproduce:
(Setup)

1. git clone this repo (UI app): https://github.com/krugazor/MCChat

2. git clone this repo (CLI app): https://github.com/aeddi/test-cli-multipeerconnectivy

(Working case)

3. Open the UI app project in XCode and run the macOS or iOS app
4. Click on the join button
5. In a terminal, go to the CLI app directory and enter: make host


(Not working case)
6. Open the UI app project in XCode and run the macOS or iOS app

7. Click on the host button

8. In a terminal, go to the CLI app directory and enter: make guest

Details:
The command line testing tool code looks like this:

main.m

#import <Foundation/Foundation.h>
#import "MPCManager.h"

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        if (argc > 1 && !strcmp(argv[1], "host")) {
            [[MPCManager alloc] hostWithDisplayName:@"cli-host"];
        } else {
            [[MPCManager alloc] joinWithDisplayName:@"cli-guest"];
        }

        dispatch_main();
    }
    return 0;
}


MPCManager.h

#ifndef MPCManager_h
#define MPCManager_h

#import <Foundation/Foundation.h>
#import <MultipeerConnectivity/MultipeerConnectivity.h>

@interface MPCManager : NSObject

- (instancetype) hostWithDisplayName:(NSString*)n;
- (instancetype) joinWithDisplayName:(NSString*)n;

@end

#endif /* MPCManager_h */

MPCManager.m

#import "MPCManager.h"

static NSString * const AppServiceType = @"chat-service";

@interface MPCManager () <mcnearbyserviceadvertiserdelegate, mcnearbyservicebrowserdelegate,="" mcsessiondelegate="">
@property(nonatomic, copy) NSString *displayName;
@property(nonatomic, strong) MCPeerID *localPeerID;
@property(nonatomic, strong) MCNearbyServiceAdvertiser *advertiser;
@property(nonatomic, strong) MCNearbyServiceBrowser *browser;
@property(nonatomic) MCSession *session;
@end

@implementation MPCManager

- (MCSession*) createOrGetSession {
    NSLog(@"createOrGetSession");

    if (self.session == nil) {
        self.session = [[MCSession alloc] initWithPeer:self.localPeerID securityIdentity:nil encryptionPreference:MCEncryptionNone];
        self.session.delegate = self;
    }
    return self.session;
}

- (instancetype)hostWithDisplayName:(NSString *)n {
    NSLog(@"initAsHostWithDisplayName: %@", n);

    self.displayName = n;
    self.localPeerID = [[MCPeerID alloc] initWithDisplayName:n];

    self.advertiser = [[MCNearbyServiceAdvertiser alloc] initWithPeer:self.localPeerID discoveryInfo:nil serviceType:AppServiceType];
    self.advertiser.delegate = self;
    [self.advertiser startAdvertisingPeer];

    return self;
}

- (instancetype)joinWithDisplayName:(NSString *)n {
    NSLog(@"joinWithDisplayName: %@", n);

    self.localPeerID = [[MCPeerID alloc] initWithDisplayName:n];

    self.browser = [[MCNearbyServiceBrowser alloc] initWithPeer:self.localPeerID serviceType:AppServiceType];
    self.browser.delegate = self;
    [self.browser startBrowsingForPeers];

    return self;
}

- (void) advertiser:(MCNearbyServiceAdvertiser *)advertiser didReceiveInvitationFromPeer:(MCPeerID *)peerID withContext:(NSData *)context invitationHandler: (void(^)(BOOL accept, MCSession *session))invitationHandler {
    NSLog(@"didReceiveInvitationFromPeer: %@", [peerID displayName]);
    invitationHandler(YES, [self createOrGetSession]);
}

- (void) browser:(MCNearbyServiceBrowser *)browser foundPeer:(MCPeerID *)peerID withDiscoveryInfo:(nullable NSDictionary *)info {
    NSLog(@"foundPeer: %@", [peerID displayName]);
    [browser invitePeer:peerID toSession:[self createOrGetSession] withContext:nil timeout:30.0]; // 30s
}

- (void)browser:(MCNearbyServiceBrowser *)browser lostPeer:(MCPeerID *)peerID {
    NSLog(@"lostPeer: %@", [peerID displayName]);
}

- (void)session:(MCSession *)session peer:(MCPeerID *)peerID didChangeState:(MCSessionState)state {
    NSLog(@"didChangeState: %ld", state);
}

- (void)session:(MCSession *)session didReceiveData:(NSData *)data fromPeer:(MCPeerID *)peerID {
    NSLog(@"didReceiveData: %@", [peerID displayName]);
}

- (void)session:(MCSession *)session didReceiveStream:(NSInputStream *)stream withName:(NSString *)streamName fromPeer:(MCPeerID *)peerID {
    NSLog(@"didReceiveStream: %@", [peerID displayName]);
}

- (void)session:(MCSession *)session didStartReceivingResourceWithName:(NSString *)resourceName fromPeer:(MCPeerID *)peerID withProgress:(NSProgress *)progress {
    NSLog(@"didStartReceivingResourceWithName: %@", [peerID displayName]);
}

- (void)session:(MCSession *)session didFinishReceivingResourceWithName:(NSString *)resourceName fromPeer:(MCPeerID *)peerID atURL:(NSURL *)localURL withError:(NSError *)error {
    NSLog(@"didFinishReceivingResourceWithName: %@", [peerID displayName]);
}

@end


And I compile this code using:
- Compiler: Apple clang version 11.0.0 (clang-1100.0.33.17)
- OS: macOS 10.5.3
- Command: clang *.m -framework Foundation -framework MultipeerConnectivity

I don't sign it in any way.
Note: I also tried to create a Command Line Tool project in XCode containing this code, but when I run it, not only the browing still not work, but advertising doesn't work either, regardless of the many signature and capability settings I tried.

Accepted Reply

I have one quick suggestion for you: Try using

NSRunLoop
rather than
dispatch_main
. The latter won’t work if the frameworks you use require a run loop. I’m not sure whether that’s the case for MultipeerConnectivity [1], but it’s an easy thing to test.

Share and Enjoy

Quinn “The Eskimo!”
Apple Developer Relations, Developer Technical Support, Core OS/Hardware

let myEmail = "eskimo" + "1" + "@apple.com"

[1] Last I checked MultipeerConnectivity used

NSNetService
for its Bonjour integration, and
NSNetService
does require a run loop.

Replies

I have one quick suggestion for you: Try using

NSRunLoop
rather than
dispatch_main
. The latter won’t work if the frameworks you use require a run loop. I’m not sure whether that’s the case for MultipeerConnectivity [1], but it’s an easy thing to test.

Share and Enjoy

Quinn “The Eskimo!”
Apple Developer Relations, Developer Technical Support, Core OS/Hardware

let myEmail = "eskimo" + "1" + "@apple.com"

[1] Last I checked MultipeerConnectivity used

NSNetService
for its Bonjour integration, and
NSNetService
does require a run loop.

Indeed, it worked!
Thanks

@eskimo I have some follow-up questions.


From what I understood by digging a bit, it is that MCNearbyServiceBrowser must instantiate a NSNetService scheduled on the mainRunLoop.


This implies (unless I'm mistaken) that my only choice is to start an NSRunLoop on the main thread if I want MCNearbyServiceBrowser to work.


In my case, the main thread is already used and dedicated to something else than running an NSRunLoop.


So my questions are:

- Can I override the NSRunLoop of the NSNetService of MCNearbyServiceBrowser?

- Is there another way to make MCNearbyServiceBrowser work on another thread?

If none of this is possible, I read in the MCSession documentation that it is possible to implement and use a custom browser.


Is the source of MCNearbyServiceBrowser available somewhere so that I can implement a fully compatible browser with the original one running only on another thread?

Can I override the

NSRunLoop
of the
NSNetService
of
MCNearbyServiceBrowser
?

Not explicitly.

NSNetService
isn’t tied to the main thread’s run loop, but rather uses the run loop of the thread that calls the ‘start’ method. I’m not sure whether MultipeerConnectivity follows that convention or explicitly targets the main thread.

Is there another way to make

MCNearbyServiceBrowser
work on another thread?

No.

In my case, the main thread is already used and dedicated to something else than running an

NSRunLoop
.

The obvious alternative is to turn this around and use a secondary thread for that other task. That should be feasible as long as the task is under your control.

Is the source of

MCNearbyServiceBrowser
available … ?

No.

Personally I’d avoid MultipeerConnectivity in situations like this. The latest version of Network framework covers all the things that I typically need (Bonjour registration, browsing and connecting; peer-to-peer Wi-Fi; ad hoc TLS via PSK; framing and unframing). And these are all based on industry-standard protocols [1], so interoperability isn’t a problem.

Share and Enjoy

Quinn “The Eskimo!”
Apple Developer Relations, Developer Technical Support, Core OS/Hardware

let myEmail = "eskimo" + "1" + "@apple.com"

[1] Except for peer-to-peer Wi-Fi, which uses an Apple-designed protocol that’s not documented for third-party use. However, all of Apple’s OSes use the same peer-to-peer Wi-Fi protocol, so this is only a problem if you need to support other platforms.