Multipeer Connectivity.Framework を使って AppleTVで近距離データ通信を試してみる



# はじめに

tvOS10で`Multipeer Connectivity.Framework`がサポートされていたので、複数のAppleTV同士、またはiPhoneとの近距離データ通信で何かできることあるかなと思い、近距離通信を試してみました。

<del>この記事は当初、[tvOS Advent Calendar 2017 \- Qiita](https://qiita.com/advent-calendar/2017/tvos)に参加しようと調査していたのですが、tvOS特に関係なくなってしまったためこちらで吐き出します。</del>

20日が、空いていたので[tvOS Advent Calendar 2017 \- Qiita](https://qiita.com/advent-calendar/2017/tvos)に投稿しました。番外編ってことでご覧いただけたらと思います。

# Multipeer Connectivity.Framework とは？

`Multipeer Connectivity.Framework`は、WifiやBluetoothを利用したピアツーピア接続やデバイスの検出を手軽に実現することのできるフレームワークです。
同一LAN内のWifiやピア同士のWifi/Bluetoothによる通信が可能です。

iOS7.0からサポートされているのでご存じの方もたくさんいらっしゃるかと思います。
ちなみにmacOS10.10以上でもサポートされています。

[MultipeerConnectivity \| Apple Developer Documentation](https://developer.apple.com/documentation/multipeerconnectivity)

# Multipeer Connectivity.Framework の主なClass

- MCPeerID
    - ピア識別子

- MCSession 
    - MultipeerConnectivity における通信・データの送受信を管理するクラス

- MCAdvertiserAssistant
    - 周辺の機種に自身の告知（MCNearbyServiceAdvertiser）
    - 招待状の表示

- MCBrowserViewController
    - 接続可能なピアの検出
    - 接続の招待から確率

※`MCBrowserViewController`はUIも実装されているので、カスタムしたい場合は`MCNearbyServiceBrowser`を使う必要があります。

- MCNearbyServiceAdvertiser
    - 周辺の機種に自身の告知をする

# 1. 標準UIを使い接続する

- 簡易Chatを実装してみる
    - `MCBrowserViewController`と`MCAdvertiserAssistant`を使用

### 1-1. 下準備

**MCSessionを実装**

```
// ピアの表示名
let displayName = UIDevice.current.name
// 告知用の文字列
let serviceType = "p2p-test"
let peerID = MCPeerID(displayName: displayName)
let session = MCSession(peer: peerID)
```

### 1-2. 接続

**周辺の機種に自身の告知**

```
let advertiserAssistant = MCAdvertiserAssistant(serviceType: serviceType, discoveryInfo: nil, session: session)
advertiserAssistant.start()
```

**MCBrowserViewControllerを開く**

```
let browserViewController: MCBrowserViewController = .init(serviceType: Const.serviceType, session: session)
browserViewController.delegate = self
present(browserViewController, animated: true, completion: nil)
```

↓MCBrowserViewController内のイベントに関してはdelegateが用意されています。

```
// MARK: - MCBrowserViewControllerDelegate

func browserViewControllerDidFinish(_ browserViewController: MCBrowserViewController) {
    browserViewController.dismiss(animated: true, completion: nil)
}
func browserViewControllerWasCancelled(_ browserViewController: MCBrowserViewController) {
    browserViewController.dismiss(animated: true, completion: nil)
}

// 検出したピアを表示する場合はtrue 
func browserViewController(_ browserViewController: MCBrowserViewController, shouldPresentNearbyPeer peerID: MCPeerID, withDiscoveryInfo info: [String : String]?) -> Bool {
    return true
}
```

UIはこんな感じです。

<img src="https://user-images.githubusercontent.com/2265513/33761035-22f7ba38-dc4b-11e7-9214-144147c69e9f.png" height="406">　　　　<img src="https://user-images.githubusercontent.com/2265513/33761041-255fa358-dc4b-11e7-9a66-11cee7c2f084.png" width="45%">


### 1-3. データの送受信

**送信**

```
let data = "メッセージ".data(using:.utf8)
try? session.send(data, toPeers: session.connectedPeers, with: .reliable)
```

**受信**

```
session.delegate = self
```

↓MCSessionに関するイベントを処理するdelegateメソッドが用意されています。

```
// MARK: - MCSessionDelegate

// Sessionの状態が変わった時
func session(_ session: MCSession, peer peerID: MCPeerID, didChange state: MCSessionState)

// 接続中のピアから受信したData
func session(_ session: MCSession, didReceive data: Data, fromPeer peerID: MCPeerID)　{
    let message = String(data: data, encoding: .utf8)
    // UIに反映
}

// 接続中のピアから受信したバイトストリーム
func session(_ session: MCSession, didReceive stream: InputStream, withName streamName: String, fromPeer peerID: MCPeerID)

// Resourceの受信開始
func session(_ session: MCSession, didStartReceivingResourceWithName resourceName: String, fromPeer peerID: MCPeerID, with progress: Progress)

// Resourceの受信完了
func session(_ session: MCSession, didFinishReceivingResourceWithName resourceName: String, fromPeer peerID: MCPeerID, at localURL: URL?, withError error: Error?)
```

あとは、delegateメソッドで受け取った値をUIに反映させるだけです。

<img src="https://user-images.githubusercontent.com/2265513/33759207-d5272bbe-dc44-11e7-9a1e-528e75d3713f.gif">

拍子抜けするくらい簡単に実装できました :clap: 

# 2. 標準UIを使わず接続する

- AppleTVのスクリーンキャプチャをiPhoneで表示する
    - 接続元をAppleTV、
接続先をiPhone
    - `MCNearbyServiceBrowser`と`MCNearbyServiceAdvertiser`を使用
    - AppleTVは接続可能なピアを検出したら招待を送る
    - iPhoneは招待を受け取ったら接続を有効にする

### 2-1. 下準備

**MCSessionを実装**

```
// ピアの表示名
let displayName = UIDevice.current.name
// 告知用の文字列
let serviceType = "p2p-test"
let peerID = MCPeerID(displayName: displayName)
let session = MCSession(peer: peerID)
```

### 2-2. 接続

**AppleTV(送信)**

```
let nearbyServiceBrowser = MCNearbyServiceBrowser(peer: peerID, serviceType: serviceType)
nearbyServiceBrowser.delegate = self
nearbyServiceBrowser.startBrowsingForPeers()
```

```
// MARK: - MCNearbyServiceBrowserDelegate

// 接続可能なピアを検出
func browser(_ browser: MCNearbyServiceBrowser, foundPeer peerID: MCPeerID, withDiscoveryInfo info: [String : String]?)　{
    browser.invitePeer(peerID, to: session, withContext: nil, timeout: 0) //timeoutは0指定でdefault値の30が採用される
}
```

**iPhone(受信)**

```
let nearbyServiceAdvertiser = MCNearbyServiceAdvertiser(peer: peerID, discoveryInfo: nil, serviceType: serviceType)
nearbyServiceAdvertiser.delegate = self
nearbyServiceAdvertiser.startAdvertisingPeer()
```

```
// MARK: - MCNearbyServiceAdvertiserDelegate

// 招待受信
// invitationHandlerブロックをtrueで呼び出すと接続を有効
func advertiser(_ advertiser: MCNearbyServiceAdvertiser, didReceiveInvitationFromPeer peerID: MCPeerID, withContext context: Data?, invitationHandler: @escaping (Bool, MCSession?) -> Swift.Void) {
    invitationHandler(true, session)
}
```

### 2-3. データの送受信

基本的には[1-3-データの送受信](#1-3-データの送受信)と同じなので省略します。

今回は簡単ですが、`ReplayKit`を使って画面のキャプチャを取得して、イメージに変更したものを使用しました。

<img src="https://user-images.githubusercontent.com/2265513/33830300-e75de038-deb6-11e7-8c5d-1d6c69877fe5.gif" width="45%">

`ReplayKit`使ったので同時にキャプチャとれなかったのですがAppleTV側はこんな画面です。

<img src="https://user-images.githubusercontent.com/2265513/33831761-b4cfe9ee-debc-11e7-97ae-a1036efdb285.gif" width="45%">

あまり、実用的ではないですがとりあえず出来たのでここで終了します。

# ちょっぴりハマったところ

`MCSession`と`MCAdvertiserAssistant`(`MCNearbyServiceBrowser`もしくは`MCNearbyServiceAdvertiser`)を生成する際の`MCPeerID`は共通のインスタンスを使用する。（同じ`displayName`でも別インスタンスだと接続できない）

# さいごに

今回はtvOSをメインで実装しましたが、iOSでも方法は特に変わりません。
framework自体は、シンプルなので今後iOSとtvOS、複数のtvOSでの連携などでおもしろそうな連携などがあれば是非採用してみたいです。

今回作成したプロジェクトは下記になります。興味がございましたら是非ご覧ください。
[hryk224/MultipeerConnectivityExample](https://github.com/hryk224/MultipeerConnectivityExample)

# 参考

- [MultipeerConnectivity \| Apple Developer Documentation](https://developer.apple.com/documentation/multipeerconnectivity)
- [MultipeerGroupChat](https://developer.apple.com/library/content/samplecode/MultipeerGroupChat/Introduction/Intro.html)