MultipeerConnectivity Determining declined peer MCSession invitation vs error handling?

So I noticed when sending data using MultipeerConnectivity I'm not sure exactly how I'm supposed to distinguish between an error or a declined invitation. For example I have Device Sender & Device Receiver.


1) Device Sender sends device Receiveran invitation using MCNearbyServiceBrowser's method -invitePeer:toSession:withContext:timeout:

2) Device Sender hasthe delegate set on the session used to send the invitation.

3) The delegate for the MCSession invitation waits for -session:peer:didChangeState: before sending the real meat.


-(void)session:(MCSession*)session peer:(MCPeerID*)peerID didChangeState:(MCSessionState)state
{
   if (state == MCSessionStateConnecting)
   {
   }
   else if (state == MCSessionStateNotConnected)
   {  
      if (self.waitingOnReceiverToAcceptInvite)
      {
         NSLog(@"uh o, never got started abort...invitation was declined?");
         [session disconnect];
      }
      else
      {
          //Not connected. Maybe we finished all our work already, maybe not...
          //handle error in the completion handler of -sendResourceAtURL:withName:toPeer:withCompletionHandler: 
         //if we have one
          
      }
   }
   else if (state == MCSessionStateConnected)
   {
      if (self.waitingOnReceiverToAcceptInvite)
      {
         self.waitingOnReceiverToAcceptInvite = NO;
         [self beingSendingStuffToReciever];
      }
   }
}


So... the -beingSendingStuffToReciever method sends a resource using the session's sendResourceAtURL:withName:toPeer:withCompletionHandler:


So during testing, if the Receiver declines the invitation the Sender's session gets -session:peer:didChangeState: with a not connected state. If the waitingOnReceiverToAcceptInvite property is YES, I bail on the session and assume the invitation was declined. Why? Because I don't see any other way to detect if the Sender declined the invitation. But, during testing, I noticed that other reasons (like errors which seem to be abstracted away and inaccessible from the MultipeerConnectivity API) can cause the session delegate to get a -session:peer:didChangeState: message with a MCSessionStateNotConnected state while my waitingOnReceiverToAcceptInvite is YES.


So the peer is not connected to the session and I don't see a way to determine why. Was there some sort of network error and can I retry or did the Receiver just decline the invitation? How am I supposed to know? Is there some other API in this framework that I'm missing?


Thanks a lot

Replies

So I guess the way to do this would be to change the code in the previous post to set the isWaitingOnReceiverToAcceptInvite flag to NO in the MCSessionStateConnecting state. I guess if the other device declines the invitation the delegate method will transition to MCSessionStateNotConnected and won't enter the MCSessionStateConnecting state.


In the case of an error, usually it seems the state goes from MCSessionStateConnecting -> MCSessionStateNotConnected (though I'm not sure if this is true 100% of the time, or if there could be an immediate error apart from an invitation being declined).


Not sure if this is documented somewhere or not but this is my best guess. Still there doesn't really seem to be a way to access any sort of error that occurs during invite/connecting phase to determine whether or not it's appropriate to retry connecting or just give up and present an error.

Can you tell me a little bit more about this workflow? For example, are you wanting to use peer-to-peer to communicate with another device on a network? If so, have your looked into Network framework's implementation of using NWListener, NWBrowser, and NWConnection for a workflow like this?


As an brief example, your NWListener you be setup to broadcast a service:

/// NWListener

let udpOption = NWProtocolUDP.Options()
let params = NWParameters(dtls: nil, udp: udpOption)
params.includePeerToPeer = true

let listener = try NWListener(using: params)

listener.service = NWListener.Service(name: "YourService",
                                      type: "_service._udp")


listener.stateUpdateHandler = { newState in
    switch newState {
    case .ready:
        if let port = listener.port {
            // Listener setup on a port.  Active browsing for this service.
        }
    case .failed(let error):
        
        listener.cancel()
        os_log("Listener - failed with %{public}@, restarting", error.localizedDescription)
        // Handle restarting listener
    default:
        break
    }
}

// Used for receiving a new connection for a service.
// This is how the connection gets created and ultimately receives data from the browsing device.
listener.newConnectionHandler = { newConnection in

    // Send newConnection (NWConnection) back on a delegate to set it up for sending/receiving data
}

// Start listening, and request updates on the main queue.
listener.start(queue: .main)


Next, a browsing device could browse the network for the service your NWListener is providing. When the service is found the browsing device can react by taking the endpoint and connecting to it using NWConnection.

/// NWBrowser

let params = NWParameters()
params.includePeerToPeer = true


let browser = NWBrowser(for: .bonjour(type: "_service._udp",
                                      domain: "local"), using: params)

browser.stateUpdateHandler = { newState in
    switch newState {
    case .failed(let error):
        browser.cancel()
        // Handle restarting browser
        os_log("Browser - failed with %{public}@, restarting", error.localizedDescription)
    case .ready:
        os_log("Browser - ready")
    case .setup:
        os_log("Browser - setup")
    default:
        break
    }
}

// Used to browse for discovered endpoints. 
browser.browseResultsChangedHandler = { results, changes in
    for result in results {
        os_log("Browser - found matching endpoint with %{public}@", result.endpoint.debugDescription)
        
        // Send endPoint back on a delegate to set up a NWConnection for sending/receiving data
        break
    }
}

// Start browsing and ask for updates on the main queue.
browser.start(queue: .main)


Take the incoming endpoint from browseResultsChangedHandler and create a NWConnection to connect to the service.

Here is where the benefits of using Network Framework come in; in the stateUpdateHandler the state of the connection is passed in and the connecting device can then react to the state the connection is in as it connects to the service.

/// NWConnection

var p2p2Connection: NWConnection
...

let udpOption = NWProtocolUDP.Options()
let params = NWParameters(dtls: nil, udp: udpOption)
params.includePeerToPeer = true
p2p2Connection = NWConnection(to: nwEndpoint, using: params) // Discovered browsing endPoint

...

// Here is where you could potentially react to failures in connecting to the endpoint discovered by your browser
p2p2Connection.stateUpdateHandler = { newState in
            
  switch newState {
  case .ready:
     os_log("Connection established")
     
  case .preparing:
     os_log("Connection preparing")
  case .setup:
     os_log("Connection setup")
  case .waiting(let error):
     os_log("Connection waiting: %{public}@", error.localizedDescription)
  case .failed(let error):
     os_log("Connection failed: %{public}@", error.localizedDescription)
     
     // Cancel the connection upon a failure.
     self.p2p2Connection.cancel()
     // Notify your delegate that the connection failed with an error message.
  default:
     break
  }
}

// Start the connection and send receive responses on the main queue.
p2p2Connection.start(queue: .main)

// Setup the receive method to ensure data is captured on the incoming connection.
receiveIncomingDataOnConnection()


Matt Eaton

DTS Engineering, CoreOS

meaton3 at apple.com

I may end up having to look into the Network framework if I want to pursue this more which is a shame because I spent a good amount of time digging into the Multipeer Connectivity framework. Thanks a lot for taking the time to reply with that.


>Can you tell me a little bit more about this workflow? For example, are you wanting to use peer-to-peer to communicate with another device on a network?


I want to be able to send data to nearby devices. MultipeerConnectivity seemed like a good fit because it's high-level and easy and I don't really care if the nearby device is on wifi, bluetooth, or ethernet, I only care that the device is nearby. The problem is MultipeerConnectivity might just be a little too high level.


As it relates to my concern in my original post (detecting a declined invite) I did in fact run into a situation (only once out of like 99 tests, but enough to make me uncomfortable enough to maybe ditch MPC) where an attempted session transitioned directly to the NotConnecting state even though the testing device on the other end doesn't ever decline invitations. So it does seem like it's possible to enter the NotConnecting stage immediately due to an error while skipping the 'Connecting' stage. So in a real app, if a Peer declines an invite I'd want to alert the user "DeviceName doesn't want to talk to you right now. Goodbye." But if the invite is not declined and there is an error, and the MCNearbyServiceBrowser still has that peer, I can retry the operation. But I don't want to show an alert saying that an invitation was declined to the user when really the connection just dropped.


Just a little bit more context as to why the peer's state transitioned to not connecting and multipeerconnectivity framework might be fine for my needs.

Thank you for providing more information on this workflow. If the devices your want to send data to are all connecting to a listener that is setup on your device to broadcast a service, then Network Framework is what you are looking for. If you are wanting all of the devices to send data to each other at all times then the Multipeer Connectivity framework is most likely what you are looking for.



As to your original question; yes, detecting declined invites or dropped invites is something that is not well supported in these APIs. This was part of my reasoning for suggesting Network Framework. A lot of this, as you are finding, ends up having to be handled with manual logic that defines a set of behavior for when it makes sense for you app to consider the invitation declined or that it needs to move on.



Matt Eaton

DTS Engineering, CoreOS

meaton3 at apple.com

>As to your original question; yes, detecting declined invites or dropped invites is something that is not well supported in these APIs. This was part of my reasoning for suggesting Network Framework. A lot of this, as you are finding, ends up having to be handled with manual logic that defines a set of behavior for when it makes sense for you app to consider the invitation declined or that it needs to move on.


Yes seemd to be the case. Thanks again for the reply and confirming. Noticed when the other peer in the session transitions to NotConnecting without ever transitioning to connecting *and* the peer doesn't decline the invite (which causes my logic that checks for a declined invitation to fail) this seems to always log out in my console:


[MCNearbyServiceBrowser] Received an invitation response from [thedevicename,33485893], but we never sent it an invitation. Aborting!


Log is definitely bogus (invitation was sent) so there might be a bug in MCNearbyServiceBrowser. I'm not sure if I could potentially "resolve" my declined invitatiion detection "problem" by using the custom discovery API.


@interface MCSession (MCSessionCustomDiscovery)

// Gets the connection data for a remote peer.
- (void)nearbyConnectionDataForPeer:(MCPeerID *)peerID
              withCompletionHandler:(void (^)(NSData * __nullable connectionData, NSError * __nullable error))completionHandler;

// Connect a peer to the session once connection data is received.
- (void)connectPeer:(MCPeerID *)peerID withNearbyConnectionData:(NSData *)data;

// Cancel connection attempt with a peer.
- (void)cancelConnectPeer:(MCPeerID *)peerID;

@end