
# リアルタイムメッセージング機能

## 概要

リアルタイムメッセージング機能は WebRTC の DataChannel を利用して、データの送受信を行う機能です。

詳細は [Sora ドキュメント リアルタイムメッセージング機能](https://sora-doc.shiguredo.jp/MESSAGING) をご確認ください。
また、[サンプルアプリケーション](https://github.com/shiguredo/sora-ios-sdk-samples/tree/master/) 内に `DataChannel` サンプルを用意していますのでこちらも参考にしてください。

## メッセージを送受信する

### Sora 接続時にリアルタイムメッセージング用 DataChannel を設定する

Sora の接続時に [Configuration.dataChannels](https://sora-ios-sdk.shiguredo.jp/_static/api/docs/Structs/Configuration#/s:4Sora13ConfigurationV12dataChannelsypSgvp) にリアルタイムメッセージング用 DataChannel を指定できます。 `JSONSerialization` で JSON に変換可能な形式で指定します。

```swift
// 接続の設定
var config = Configuration(url: soraURL,
                           channelId: soraChannelId,
                           role: role)

// DataChannel 経由のシグナリングを有効にする
config.dataChannelSignaling = true

// リアルタイムメッセージング機能に利用する DataChannel を指定する
config.dataChannels = [[
          "label": "#spam",
          "direction": "sendrecv",
      ], [
          "label": "#egg",
          "max_retransmits": 0,
          "ordered": false,
          "protocol": "abc",
          "compress": false,
          "direction": "recvonly",
          "header": [
              ["type": "sender_connection_id"]
          ],
]]
```

### DataChannel の利用開始を確認する

`onDataChannel` にて、リアルタイムメッセージング用 DataChannel が利用可能になった通知を受け取ることができます。

`onDataChannel` はメッセージング用ラベル ( `#` 始まりのラベル) が、すべてクライアント側で `OPEN` になったタイミングで呼び出されます。

> **注釈**
>
> - `onDataChannel` と WebSocket シグナリングで受信する `"type": "switched"` の前後関係は保証されません
> - `onDataChannel` の発火時点では、まだ `"type": "switched"` を受信していない場合があります。その場合、 [MediaChannel.sendMessage](https://sora-ios-sdk.shiguredo.jp/_static/api/docs/Classes/MediaChannel#/s:4Sora12MediaChannelC11sendMessage5label4datas5Error_pSgSS_10Foundation4DataVtF) は `DataChannel is not open yet` エラーを返します
> - メッセージング用 DataChannel が存在しない場合、 `onDataChannel` は呼び出されません
> - 呼び出し元のスレッドは保証されないため、必要に応じて main キューに束ねてください

```swift
config.mediaChannelHandlers.onDataChannel = { mediaChannel in
  print("messaging data channels are available")
}
```

### メッセージを送信する

[MediaChannel.sendMessage](https://sora-ios-sdk.shiguredo.jp/_static/api/docs/Classes/MediaChannel#/s:4Sora12MediaChannelC11sendMessage5label4datas5Error_pSgSS_10Foundation4DataVtF) を利用してメッセージの送信を行います。バイナリデータ (Data 型) の送信のみ可能です。文字列を送信したい場合はバイナリデータに変換してください。

[MediaChannel.sendMessage](https://sora-ios-sdk.shiguredo.jp/_static/api/docs/Classes/MediaChannel#/s:4Sora12MediaChannelC11sendMessage5label4datas5Error_pSgSS_10Foundation4DataVtF) は、対象ラベルの DataChannel が `OPEN` になった後に利用できます。たとえば `#spam` に送信したい場合は、 `onDataChannelOpened` で `#spam` の `OPEN` を確認してから [MediaChannel.sendMessage](https://sora-ios-sdk.shiguredo.jp/_static/api/docs/Classes/MediaChannel#/s:4Sora12MediaChannelC11sendMessage5label4datas5Error_pSgSS_10Foundation4DataVtF) を呼び出します。
メッセージング用ラベル (`#` 始まり) がすべて送受信可能になったタイミングをまとめて確認したい場合は `onDataChannel` を利用できます。

```swift
// メッセージを送信する
// 対象ラベルの onDataChannelOpened を受信した後から送信できる
let error = mediaChannel.sendMessage(label: "#spam", data: "egg".data(using: .utf8)!)
guard error == nil else {
    // エラー処理
    ...
    return
}
```

### メッセージを受信する

[Configuration.mediaChannelHandlers.onDataChannelMessage](https://sora-ios-sdk.shiguredo.jp/_static/api/docs/Classes/MediaChannelHandlers#/s:4Sora20MediaChannelHandlersC06onDataC7MessageyAA0bC0C_SS10Foundation0F0VtcSgvp) を利用してメッセージの受信を行います。バイナリデータ (Data 型) での受信になりますので、適宜必要な型に変換してください。

```swift
// メッセージ受信時の処理をハンドラーに定義する
config.mediaChannelHandlers.onDataChannelMessage = { (mediaChannel, label, data) in
  // label をチェックする
  guard label.starts(with: "#spam") else {
    return
  }
  ...
}
```

たとえば、 UTF-8 の文字列として受信したい場合は `String(data:encoding:)` を利用します。

```swift
config.mediaChannelHandlers.onDataChannelMessage = { (_, label, data) in
  guard label == "#spam" else {
    return
  }

  // UTF-8 文字列として受信する
  if let message = String(data: data, encoding: .utf8) {
    print("string message: \(message)")
    return
  }
}
```

#### ヘッダーとメッセージを分割する

リアルタイムメッセージングにヘッダーが追加されている場合、 `onDataChannelMessage` で受け取るデータにはヘッダーとメッセージが結合された状態で入っています。これを分離するには、 `Configuration.mediaChannelHandlers` の `onReceiveSignaling` と `onDataChannelMessage` を利用して以下のように処理します。

- [Configuration.mediaChannelHandlers.onReceiveSignaling](https://sora-ios-sdk.shiguredo.jp/_static/api/docs/Classes/MediaChannelHandlers#/s:4Sora20MediaChannelHandlersC18onReceiveSignalingyAA0G0OcSgvp) でヘッダーの長さ (バイト数) を取得する
- [Configuration.mediaChannelHandlers.onDataChannelMessage](https://sora-ios-sdk.shiguredo.jp/_static/api/docs/Classes/MediaChannelHandlers#/s:4Sora20MediaChannelHandlersC06onDataC7MessageyAA0bC0C_SS10Foundation0F0VtcSgvp) で受け取ったメッセージを先頭からヘッダーの長さ分とそれ以降に分割する

```swift
// dataChannels の label ごとにヘッダーの長さを保持するための変数
var headerLengths: [String: Int] = [:]

// offer で入ってくる data channel header の長さを取得する
config.mediaChannelHandlers.onReceiveSignaling = { [weak self] signaling in
    guard let self else {
        return
    }
    switch signaling {
    case let .offer(offer):
        guard let dataChannels = offer.dataChannels else {
            return
        }
        for dataChannel in dataChannels {
            // ラベルが "#" で始まる場合のみ処理する
            let label: String = dataChannel["label"] as! String
            guard label.starts(with: "#") else {
                continue
            }

            // dataChannel["header"] が nil ではない場合のみ後続処理を行う
            guard let headers = dataChannel["header"] as? [[String: Any]] else {
                continue
            }
            for header in headers {
                if header["type"] as! String == "sender_connection_id" {
                    let length = header["length"] as! Double
                    headerLengths[label] = Int(length)
                    print("kensaku: \(label) \(Int(length))")
                }
            }
        }
    default:
        break
    }
}

// メッセージ受信時の処理を定義する
config.mediaChannelHandlers.onDataChannelMessage = { [weak self] _, label, data in
    guard let self = self else {
        return
    }

    // "#" で始まるラベル以外は無視する
    guard label.starts(with: "#") else {
        return
    }

    // ヘッダーの長さを取得し、ヘッダーとメッセージを分離する
    let headerLength = self.headerLengths[label] ?? 0
    if headerLength == 0 {
        print(String(data: data, encoding: .utf8) ?? data.map { String($0) }.joined(separator: ", "))
        return
    }
    let header = data.prefix(headerLength)
    let message = data.suffix(from: headerLength)
    ...
}
```

### リアルタイムメッセージングのみで接続する

音声と映像を送受信せずにメッセージのみの送受信を行うことができます。

詳細については Sora のドキュメント [音声と映像を送受信せずにメッセージのみで接続する](https://sora-doc.shiguredo.jp/MESSAGING#3a3616) をご確認ください。

```swift
// 接続の設定
var config = Configuration(url: soraURL,
                           channelId: soraChannelId,
                           role: .sendrecv)

// 音声と映像を送受信しない
config.audioEnabled = false
config.videoEnabled = false

// DataChannel 経由のシグナリングを有効にする
config.dataChannelSignaling = true

// リアルタイムメッセージング機能に利用する DataChannel を指定する
config.dataChannels = [[
          "label": "#spam",
          "direction": "sendrecv",
]]
```





