15

I wonder if this Multipeer Connectivity framework is ready for use in the real world, given all the bugs that have been encountered by the community. I think I'm setting it up right, but all the other sample projects I've tried encounter similar issues.

The problem I'm having may be tied to some issue inherent to Bonjour or something, I can't figure it out, but basically the problem is as follows:

  • I have an active MCSession with a number of peers.
  • Now, if a device is in a session, and then force quits out, that "Peer" stays connected for an indefinite amount of time.
  • There's nothing I can do to force that user out, even though the browser:lostPeer: method is called for that peer and is no longer even showing up in the browser as "Nearby".
  • The session:peer:didChangeState: method is not called for that peer.
  • When that peer that force quitted comes back to the app, they are "Found" again by the browser:foundPeer:withDiscoveryInfo: but still also exist in the session.connectedPeers NSArray. Obviously they don't receive any data or updates about the session still and are not actually connected.
  • The only thing that seems to work to register that original peer as MCSessionStateNotConnected to the session is by reconnecting that peer to the original session. Then there is a duplicate call to session:peer:didChangeState: where the new instance of the peerID is MCSessionStateConnected and shortly after the old instance of the peerID calls with MCSessionStateNotConnected.

The sample chat application demonstrates this issue well: https://developer.apple.com/library/ios/samplecode/MultipeerGroupChat/Introduction/Intro.html

Since there doesn't seem to be any way to manually force remove a peer from the session, what should I do? Should I try and rebuild the session somehow?

This Framework seems like a bit of a mess, but I'm trying to reserve judgement!

Arjun Mehta
  • 2,500
  • 1
  • 24
  • 40
  • I had a working application but needed to extend it past 8 peers, now its broke :(. I have found one issue thus far, don't inadvertently have strong references to MC objects when going into background (I know, applies more broadly than just MC.. but a reminder helps!) – 300baud Apr 22 '14 at 06:38
  • I have the same problem. Sometimes MCSession's session:peer:didChangeState: won't get called with MCSessionStateNotConnected for a peer that has disconnect. When multiple peers are connected some peers will get notified other won't. Sometimes everyone gets notified correctly. I have been able to track down the root cause of it. It happens even when a peer is calling its disconnect method. – Tod Cunningham May 24 '14 at 00:59

5 Answers5

8

My only workaround to this type of issue has been to have a 1-1 relationship between sessions and peers. It complicates the sending of broadcasts, but at least allows for peer-level disconnects and cleanup through disconnecting/removing the session itself.

Update

To elaborate on my original answer, in order to be able to send data to connected peers it's necessary to maintain a reference to the session that was created for each peer. I've been using a mutable dictionary for this.

Once the invitation has been sent/accepted with a new session, use the MCSession delegate method to update the dictionary:

- (void)session:(MCSession *)session peer:(MCPeerID *)peerID didChangeState:(MCSessionState)state {

    if (state==MCSessionStateConnected){

        _myPeerSessions[peerID.displayName] = session;

    }
    else if (state==MCSessionStateNotConnected){

        //This is where the session can be disconnected without
        //affecting other peers
        [session disconnect];            

        [_myPeerSessions removeObjectForKey:peerID.displayName];
    }
}

All peers can be accessed with a method that returns all values of the dictionary, and in turn all connectedPeers (in this case one) for each MCSession:

- (NSArray *)allConnectedPeers {

   return [[_myPeerSessions allValues] valueForKey:@"connectedPeers"];

}

Sending data to a particular peer or via broadcast can be done with a method like this:

- (void)sendData:(NSData *)data toPeerIDs:(NSArray *)remotePeers reliable:(BOOL)reliable error:(NSError *__autoreleasing *)error {

    MCSessionSendDataMode mode = (reliable) ? MCSessionSendDataReliable : MCSessionSendDataUnreliable;

    for (MCPeerID *peer in remotePeers){

       NSError __autoreleasing *currentError = nil;

       MCSession *session = _myPeerSessions[peer.displayName];
       [session sendData:data toPeers:session.connectedPeers withMode:mode error:currentError];

       if (currentError && !error)
        *error = *currentError;
    }
}
ChrisH
  • 4,468
  • 2
  • 33
  • 42
  • As for the framework itself, I agree with you @ArjunMehta. You can't rely on it for anything other than simple use cases. It didn't work at all in the first Xcode 5 beta, and Apple has done almost nothing with it since then. – ChrisH Apr 11 '14 at 16:04
  • 1
    I'd add my voice to that call, it's got such promise but we had to wait a frustratingly long amount of time for it to work at all, and then it's not been given much love since then. – Cocoadelica Apr 11 '14 at 16:26
  • It's such an exciting idea! I wish it worked as easy as it is made out to, but it seems to be a finicky thing. I have noticed that if I disable bluetooth on my devices it's only slightly more reliable. But it seems to bug out when one of the clients closes the app (not even a force quit). – Arjun Mehta Apr 11 '14 at 21:55
  • @ChrisH is there a pattern you use to bridge all those single 1-1 connections to feel like one session? – Arjun Mehta Apr 12 '14 at 10:25
  • @ChrisH Thanks! I think I see what you're doing. So in this case, when browser:lostPeer: gets called I can check _myPeerSessions for the peer name that dropped, and close all sessions with that peer. Thanks again! – Arjun Mehta Apr 15 '14 at 19:18
  • I had trouble getting this to work. Essentially i could never get the session to actually connect. My delegate method : `-(void)advertiser:(MCNearbyServiceAdvertiser *)advertiser didReceiveInvitationFromPeer:(MCPeerID *)peerID withContext:(NSData *)context invitationHandler:(void(^)(BOOL accept, MCSession *session))invitationHandler{ MCSession *newSession = [[MCSession alloc] initWithPeer:_peerID]; newSession.delegate = self; invitationHandler(YES, newSession); }` never connected for some reason. I never got any updated to didChangeState. – Juan Carlos Ospina Gonzalez May 01 '14 at 12:24
  • @Piterwilson are both peers sending invitations? You might want to take a look at this: http://stackoverflow.com/questions/19469984/reconnecting-to-disconnected-peers/19529933#19529933 – ChrisH May 01 '14 at 15:38
  • No, they are not. What i do is that i save the start session time and send this information in the discovery options dictionary. Whoever has the earliest start session time gets to be 'host' and sends the invite. The other just waits. – Juan Carlos Ospina Gonzalez May 05 '14 at 12:12
  • 1
    could anyone upload a demo of a project with this form of mc connection? – Manesh Nov 12 '15 at 13:49
2

Have you tried disconnecting the session before the application closes? This should remove the peer from the session properly and cleanup any resources allocated for the peer.

Specifically I mean something like [self.peer disconnect] in applicationWillTerminate:

Sam
  • 2,579
  • 17
  • 27
  • I have tried this, yes. My application would ideally support backgrounding, so applicationWillTerminate does not even get called. didEnterBackgroundDoes get called and I'm trying to do something there. But still, no luck, even if I try disconnecting the peer from the session. I think you might mean [self.session disconnect]? – Arjun Mehta Apr 14 '14 at 12:12
  • I have found that most of the time it does get the MCSessionStateNotConnected. However, sometimes some peers don't get notified. I have seen this even when disconnect is being called on the client. – Tod Cunningham May 24 '14 at 00:51
  • That is a different test case. Hard-quiting the app should work as well (which would be similar to moving out of range, for example). – fishinear Aug 02 '17 at 21:21
1

I've been having similar problems. It seems though that if I have run my app on one iOS device, and connected to another, then quit and relaunch (say when I rerun from Xcode), then I am in a situation where I get a Connected message and then a Not Connected message a little later. This was throwing me off. But looking more carefully, I can see that the Not Connected message is actually meant for a different peerId than the one that has connected.

I think the problem here is that most samples I've seen just care about the displayName of the peerID, and neglect the fact that you can get multiple peerIDs for the same device/displayName.

I am now checking the displayName first and then verifying that the peerID is the same, by doing a compare of the pointers.

- (void)session:(MCSession *)session peer:(MCPeerID *)peerID didChangeState:(MCSessionState)state {

    MyPlayer *player = _players[peerID.displayName];

    if ((state == MCSessionStateNotConnected) &&
        (peerID != player.peerID)) {
        NSLog(@"remnant connection drop");
        return; // note that I don't care if player is nil, since I don't want to
                // add a dictionary object for a Not Connecting peer.
    }
    if (player == nil) {
        player = [MyPlayer init];
        player.peerID = peerID;
        _players[peerID.displayName] = player;
    }
    player.state = state;

...
mahboudz
  • 39,196
  • 16
  • 97
  • 124
  • displayName can be identical for different peers, so you should do nothing with the displayName other than displaying it. Your _players dictionary should be keyed by peerID, not displayName. – fishinear Aug 02 '17 at 21:18
0

I couldn't get the accepted answer to ever work, so what i did instead is have a timer that would fire to reset the connection when the browser would report not connected and there were no other connected peers.

-(void)session:(MCSession *)session peer:(MCPeerID *)peerID didChangeState:(MCSessionState)state{

//DebugLog(@"session didChangeState: %ld",state);

if(resetTimer != nil){
    [resetTimer invalidate];
    resetTimer = nil;
}

if(state == MCSessionStateNotConnected){

    [session disconnect];
    [peerSessions removeObjectForKey:peerID.displayName];
    [self removeGuidyPeerWithPeerID:peerID];
    //DebugLog(@"removing all guides from peer %@",peerID);

    if([localSession connectedPeers].count == 0){

        DebugLog(@"nothing found... maybe restart in 3 seconds");
        dispatch_async(dispatch_get_main_queue(), ^{
            resetTimer = [NSTimer
                      scheduledTimerWithTimeInterval:3.0
                      target:self selector:@selector(onResetTimer:)
                      userInfo:nil
                      repeats:NO];
            }
        );
    }
}
...

}

0

You can delete the peer from the MCBrowserViewController with following code in Swift 3:

self.mySession.cancelConnectPeer(self.myPeerID)