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

概要

リアルタイムメッセージング機能は WebRTC の DataChannel を利用して、データの送受信を行う機能です。 詳細は Sora のドキュメント を参照してください。 また、サンプルアプリケーションに リアルタイムメッセージング を用意していますのでこちらも参考にしてください。

Sora Android SDK では SoraMediaChannel の初期化時に dataChannels に compress = true を指定した場合、メッセージの圧縮と解凍は SDK 内部で自動的に行われます。アプリケーション側で意識する必要はありません。

メッセージを送受信する

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

Sora の 接続時に SoraMediaChannel にリアルタイムメッセージング用の DataChannel を指定できます。 List<Map<String, Any>> の形式で指定します。

// リアルタイムメッセージング機能に利用する DataChannel を指定する
val dataChannels = listOf(
  mapOf(
      "label" to "#spam",
      "direction" to "sendrecv"
  ),
  mapOf(
      "label" to "#egg",
      "max_retransmits" to 0,
      "ordered" to false,
      "protocol" to "abc",
      "compress" to false,
      "direction" to "recvonly",
      "header" to listOf(
         mapOf(
            "type" to "sender_connection_id"
         )
      )
  ),
)

// オプションを設定する。通常の接続と設定内容は変わりません。
val mediaOption = SoraMediaOption()
mediaOption.enableMultistream()
mediaOption.enableVideoDownstream(null)

// Sora に接続する。SoraMediaChannel 生成時に dataChannels を設定する。
val mediaChannel = SoraMediaChannel(
              context = this,
              signalingEndpointCandidates = [
                  "wss://sora.example.com/signaling",
              ],
              channelId = "sora",
              mediaOption = option,
              listener = channelListener,
              // DataChannel シグナリングを有効化する
              dataChannelSignaling = true,
              dataChannels = dataChannels)
mediaChannel.connect()

メッセージを受信する

val channelListener = object : SoraMediaChannel.Listener {

  // DataChannel が利用可能になったタイミングで実行されるコールバック
  override fun onDataChannel(mediaChannel: SoraMediaChannel, dataChannels: List<Map<String, Any>>?) {
      super.onDataChannel(mediaChannel, dataChannels)
      // リアルタイムメッセージングで利用できる DataChannel 一覧を出力する
      Log.d("onDataChannel", "$dataChannels")
    }
  }

  // DataChannel メッセージ受信時のコールバック
  override fun onDataChannelMessage(mediaChannel: SoraMediaChannel, label: String, data: ByteBuffer) {
    // ByteBuffer を String に変換して出力する
    val message = mediaChannel.dataToString(data)
    Log.d("onDataChannelMessage", message)
  }

}

メッセージを送信する

// メッセージ送信する
mediaChannel.sendDataChannelMessage("#spam", "Hello World")

DataChannel を利用したリアルタイムメッセージングのみで接続する

音声と映像を送受信せずにメッセージのみの送受信を行う方法の詳細については、 Sora のドキュメント 音声と映像を送受信せずにメッセージのみで接続する をご確認ください。 Sora Android SDK では、 SoraMediaOption.roleSoraChannelRole.SENDRECV を指定して接続を行います。

// オプションを設定する
val mediaOption = SoraMediaOption()
mediaOption.role = SoraChannelRole.SENDRECV
mediaOption.enableMultistream()

// Sora に接続する
val mediaChannel =
   SoraMediaChannel(
      context = this,
      signalingEndpointCandidates =
            [
               "wss://sora.example.com/signaling",
            ],
      channelId = "sora",
      mediaOption = option,
      listener = channelListener,
      // DataChannel シグナリングを有効化する
      dataChannelSignaling = true,
      dataChannels = dataChannels
   )

mediaChannel.connect()

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

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

以下はヘッダーに sender_connection_id が指定されている場合の例です

// label ごとにヘッダーの長さを保持するための変数
private var headerLengthMap: MutableMap<String, Int> = mutableMapOf()

private val channelListener = object : SoraMediaChannel.Listener {
   override fun onOfferMessage(mediaChannel: SoraMediaChannel, offer: OfferMessage) {
      val dataChannels = offer.dataChannels
      // dataChannels の null チェック
      if (dataChannels != null) {
         for (dataChannel in dataChannels) {
            // label の 先頭に # がついているものがリアルタイムメッセージング用
            // # がついていないものは処理しない
            val label = dataChannel["label"] as String
            if (!label.startsWith("#")) {
               continue
            }

            // dataChannel に header (List<Map<String, Any>>) がある場合は
            // header の中から type: sender_connection_id の Map を取得し
            // length の値を取得する
            val header = dataChannel["header"] as? List<*>?
            if (header != null) {
               for (headerMap in header) {
                  if (headerMap is Map<*, *>) {
                     if (headerMap["type"] == "sender_connection_id") {
                         // Double で取得されるため Int に変換する
                         val length = headerMap["length"] as Double
                         headerLengthMap[label] = length.toInt()
                     }
                  }
               }
            }
         }
      }
   }

   override fun onDataChannelMessage(
      mediaChannel: SoraMediaChannel,
      label: String,
      data: ByteBuffer
   ) {
      // headerLengthMap に label が存在するか確認する
      // 存在する場合はその length を取得し、以下のように分割処理する
      // 1. 先頭から length バイトが sender_connection_id
      // 2. 残りがメッセージ本体
      if (headerLengthMap.containsKey(label)) {
         val length = headerLengthMap[label]!!
         val senderConnectionId = ByteArray(length)
         data.get(senderConnectionId)
         val message = ByteArray(data.remaining())
         data.get(message)
         // String() で utf-8 文字列に変換する
         Log.d(
            "tag",
            "received data: label=$label," +
            "sender_connection_id=${String(senderConnectionId)}," +
            "message=${String(message)}"
         )
      } else {
         Log.d("tag", "received data: label=$label, message=${mediaChannel.dataToString(data)}")
      }
   }
}
© Copyright 2018-2023, Shiguredo Inc. Created using Sphinx 8.1.3