Creating Collaborative AR Experience

2020-10-22

Creating Collaborative AR Experience#

image-20201022223857176

对于 Collaborative Session 的工作流程来说,Network 层是任意的,也就是说,我们可以通过局域网或者蓝牙等任意通信方式进行数据传输。一般来说,我们选择使用 MultipeerConnectivity 作为 Network 层的实现。

MultipeerConnectivity#

MultipeerConnectivity 为所有 Apple Platform 的设备提供了 peer-to-peer 的局域网和蓝牙通信的功能,非常适合作为 Collaborative Session 的 Network 层。

MCPeerID#

每一个连接的设备都会拥有唯一的 MCPeerID 标识,一般通过 MCPeerID(displayName: UIDevice.current.name) 来获取唯一的 MCPeerID。(该函数在 displayName 相同的时候也会返回不同的 MCPeerID)

MCSession#

MCSession 负责管理 MultipeerConnectivity 连接,只需要提供 MCpeerID 和其他选项即可创建:MCSession(peer: myPeerID, securityIdentity: nil, encryptionPreference: .required)

MCSessionDelegate#

MCSession 拥有一个可选的 MCSessionDelegate,可以处理一些 Session 的事件。注意,每一个事件中的 peerID 参数都是指触发该事件的设备的 peerID,例如 session(_:peer:didChange) 是指 peer 改变了它的 MCSessionState。

// Delegate methods for MCSession.
public protocol MCSessionDelegate : NSObjectProtocol {
    
    // Remote peer changed state.
    func session(_ session: MCSession, peer peerID: MCPeerID, didChange state: MCSessionState)

    
    // Received data from remote peer.
    func session(_ session: MCSession, didReceive data: Data, fromPeer peerID: MCPeerID)

    
    // Received a byte stream from remote peer.
    func session(_ session: MCSession, didReceive stream: InputStream, withName streamName: String, fromPeer peerID: MCPeerID)

    
    // Start receiving a resource from remote peer.
    func session(_ session: MCSession, didStartReceivingResourceWithName resourceName: String, fromPeer peerID: MCPeerID, with progress: Progress)

    
    // Finished receiving a resource from remote peer and saved the content
    // in a temporary location - the app is responsible for moving the file
    // to a permanent location within its sandbox.
    func session(_ session: MCSession, didFinishReceivingResourceWithName resourceName: String, fromPeer peerID: MCPeerID, at localURL: URL?, withError error: Error?)

    
    // Made first contact with peer and have identity information about the
    // remote peer (certificate may be nil).
    optional func session(_ session: MCSession, didReceiveCertificate certificate: [Any]?, fromPeer peerID: MCPeerID, certificateHandler: @escaping (Bool) -> Void)
}

对于 Collaborative Session 来说,只有两个事件比较重要:

func session(_ session: MCSession, peer peerID: MCPeerID, didChange state: MCSessionState)
func session(_ session: MCSession, didReceive data: Data, fromPeer peerID: MCPeerID)

具体的实现依赖于我们实现 Collborative Session 的方式。如果是通过 RealityKit 实现,那么这两个事件是可选的。如果是通过 ARKit 实现,我们需要自己控制数据的传输与接收,因此需要 session(_:didReceive:fromPeer:) 来处理接收事件。

对于其他事件来说,我们只需要抛出 fatalError 即可:

func session(_ session: MCSession, didReceive stream: InputStream, withName streamName: String,
             fromPeer peerID: MCPeerID) {
    fatalError("This service does not send/receive streams.")
}

func session(_ session: MCSession, didStartReceivingResourceWithName resourceName: String,
             fromPeer peerID: MCPeerID, with progress: Progress) {
    fatalError("This service does not send/receive resources.")
}

func session(_ session: MCSession, didFinishReceivingResourceWithName resourceName: String,
             fromPeer peerID: MCPeerID, at localURL: URL?, withError error: Error?) {
    fatalError("This service does not send/receive resources.")
}

MCNearbyServiceBrowser#

MCSession 中的设备通过 MCNearbyServiceBrowser 来负责寻找并邀请本地网络中的设备(或者附近的蓝牙设备),只需要提供 MCPeerID 和 serviceType 即可:serviceBrowser = MCNearbyServiceBrowser(peer: myPeerID, serviceType: "ar-collab")。其中 serviceType 用来确定搜索的范围,它将与之后提到的 MCNearbyServiceAdvertiser 配合工作。

MCNearbyServiceBrowserDelegate#

MCNearbyServiceBrowser 通过 MCNearbyServiceBrowserDelegate 来处理发现目标设备等事件。

public protocol MCNearbyServiceBrowserDelegate : NSObjectProtocol {

    // Found a nearby advertising peer.
    func browser(_ browser: MCNearbyServiceBrowser, foundPeer peerID: MCPeerID, withDiscoveryInfo info: [String : String]?)

    
    // A nearby peer has stopped advertising.
    func browser(_ browser: MCNearbyServiceBrowser, lostPeer peerID: MCPeerID)

    
    // Browsing did not start due to an error.
    optional func browser(_ browser: MCNearbyServiceBrowser, didNotStartBrowsingForPeers error: Error)
}

一个可能的示例如下:

extension ViewController: MCNearbyServiceBrowserDelegate {
    
    public func browser(_ browser: MCNearbyServiceBrowser, foundPeer peerID: MCPeerID, withDiscoveryInfo info: [String: String]?) {
        // Ask whether we should invite this peer or not
          let accepted = true // ...
        if accepted {
            browser.invitePeer(peerID, to: session, withContext: nil, timeout: 10)
        }
    }

    public func browser(_ browser: MCNearbyServiceBrowser, lostPeer peerID: MCPeerID) {
        // We usually doesn't do anything with non-invited peers, so there's nothing to do here.
    }
}

MCNearbyServiceAdvertiser#

MCSession 中的设备通过 MCNearbyServiceAdvertiser 来负责处理本地网络中的设备发出的邀请(或者附近的蓝牙设备),同样地,只需要提供 MCPeerID 和 serviceType 即可:serviceAdvertiser = MCNearbyServiceAdvertiser(peer: myPeerID, discoveryInfo: nil, serviceType: "ar-collab")。其中 serviceType 用来确定能够被 MCNearbyServiceBrowser 搜索到的范围。

MCNearbyServiceAdvertiserDelegate#

MCNearbyServiceAdvertiser 通过 MCNearbyServiceAdvertiserDelegate 来处理其他设备的邀请。其中处理的事件是:advertiser(_:didReceiveInvitationFromPeer:withContext:invitationHandler)

public protocol MCNearbyServiceAdvertiserDelegate : NSObjectProtocol {

    // Incoming invitation request.  Call the invitationHandler block with YES
    // and a valid session to connect the inviting peer to the session.
    func advertiser(_ advertiser: MCNearbyServiceAdvertiser, didReceiveInvitationFromPeer peerID: MCPeerID, withContext context: Data?, invitationHandler: @escaping (Bool, MCSession?) -> Void)

    
    // Advertising did not start due to an error.
    optional func advertiser(_ advertiser: MCNearbyServiceAdvertiser, didNotStartAdvertisingPeer error: Error)
}

一个可能的示例如下(对于所有的邀请都返回 true):

extension ViewController: MCNearbyServiceAdvertiserDelegate {
    func advertiser(_ advertiser: MCNearbyServiceAdvertiser, didReceiveInvitationFromPeer peerID: MCPeerID,
                    withContext context: Data?, invitationHandler: @escaping (Bool, MCSession?) -> Void) {
        // Call the handler to accept the peer's invitation to join.
        invitationHandler(true, self.session)
    }
}

手动数据传输#

首先介绍如何进行手动的数据传输。

自动数据传输#