NTTドコモR&Dの技術ブログです。

UnityでAgoraのSignaling(RTM)のサンプルを動かしてみた

※ 本記事は 2026/3/31 以前にNTTコノキューにて記載した記事になります

はじめに

こんにちは、NTTコノキューの中矢です。業務ではUnity、C#と格闘しています。

本記事は、Agoraと呼ばれる通話や配信を簡単に実装できるAPI/SDKのうち、Signaling(RTM)におけるUnityでの入門記事です。

この記事が少しでも、Agoraを利用してみたい人の参考になれば幸いです。

AgoraとSignaling(RTM)とは

Agora(アゴラ)とは、「高品質な通話・配信・会話型AIの機能を簡単に実装できる『Agora』超低遅延&高拡張性のAPI/SDK」です。

Agoraの概要:https://jp.vcube.com/sdk

AgoraのSDKをUnityなどに組み込めば、音声通話、ビデオ通話、配信などのオンライン体験やリアルタイムコミュニケーション機能の実装が簡単に作ることができます。

Agoraの特徴はSD-RTN(Software-Defined Real-Time Network)と呼ばれるネットワーク基盤で、この方式により超低遅延かつ高い信頼性の通信を実現しているとのことです。

SD-RTNの概要:https://www.agora.io/en/the-agora-platform-advantage/

本記事ではAgoraで利用できる機能のうち、Signalingと呼ばれる低遅延なリアルタイムのイベント通信の機能を試します。また、SignalingはRTM(Real-Time Messaging)とも呼ばれているようで、検索するとRTMで引っかかることも多いです。

Signalingの概要:https://docs.agora.io/en/signaling/overview/product-overview?platform=unity

サンプルで動かしたアプリ

本記事でサンプルとして動かしたAndroidアプリは、AgoraのSignalingのクイックスタートを参考に作ったものです。

Signalingのクイックスタート:https://docs.agora.io/en/signaling/get-started/sdk-quickstart?platform=unity

このアプリでできることは以下です。

  1. アプリを立ち上げたとき、Message Channelと呼ばれる簡易的なパブリッシュ/サブスクライブ(Pub/sub)形式の通信経路に接続し、一番上の入力欄に入れたテキストを「Send Msg」ボタンを押すことで別端末に送信することができる

  2. 「Create st ch」ボタンを押すとStream Channelと呼ばれる通信部屋を作成する。

  3. 「Join st ch」ボタンを押すと作成したStream Channelに参加する

  4. 「Join Topic」ボタンを押すとTopicと呼ばれる通信メカニズムに参加する

  5. 「Sub Topic Pub」ボタンを押すとTopicをサブスクライブし、送られてきたメッセージを受信できるようになる

  6. 真ん中の入力欄にテキストを入力し、Topicに参加している場合「Send Msg」ボタンを押下することでテキストをTopicに参加している端末に送信することができる

  7. 「Unsub Topic Pub」ボタンを押すとTopicのサブスクライブを解除し、Topicで送られてきたメッセージを受信できなくする

  8. 「Leave Topic」ボタンを押すと参加していたTopicから退出する

  9. 「Leave st ch」ボタンを押すと参加していたStream Channelから退出する

  10. 「Release st ch」ボタンを押すと作成したStream Channelを破棄する

少々複雑ですが、本記事では上記を作って動かして理解を深めていくことを目的とします。

またここで、クイックスタートのコードをそのまま利用し実装すると、Unityエディタがしばしばクラッシュしたので、あまりクラッシュしないように変更を加えています。変更を加えたコード全文は本記事の最後に記載します。

環境

  1. Windows 11
  2. Unity 2022.3.50f1
  3. Signaling SDK Version 2.1.9 (Latest)

Unityの設定

ここからUnityのシーン設定とコードの解説を行います。だいたいはAgoraのクイックスタートと同じですが、本記事では補足をしたり、修正したところを解説したりしていきます。本記事を読んでからAgoraのクイックスタートを読むとより理解が深まるかなと思います。

まず、後でAndroidでビルドするためにAndroidでSwitch Platformします。 そして以下の画像のようにシーンを設定します。

Canvasを配置し、その配下にボタンを10個追加し、それぞれに「Btn〇」(ex. Btn1)と名付け、適切にボタンの配下のテキストを設定します。また、InputFieldも追加し、それぞれに「InputField1」「InputField2」と名付けました。これらは以下のようにオブジェクトの名前で検索をかけてイベントを設定しているため、名前はスクリプトと合うようにします。

// OnClick Event Handler for Btn1
public async void PublishStringMessage()
{
   // Publish string messages to a message channel
   var message = GameObject.Find("InputField1").GetComponent<TMP_InputField>().text;
  
   if (rtmClient != null)
   {

また、クイックスタートではレガシーなInputFieldを利用していたため、本記事ではTextMeshProを使ったInputFieldに変更しています。もしクイックスタートをそのまま使う場合は、レガシーなInputFieldを使う必要があります。

続いて空のGameObjectに「Agora」と名付け、それに「RtmEngine.cs」と名付けたクイックスタートのサンプルコードを改変したコードをアタッチします。ここで、RtmEngine.csのサンプルコードは長いため、本記事の最後に全文を記載します。

次に、RtmEngineクラスのメソッドをボタンにイベントを設定します。以下のように対応させます。

Btn1:PublishStringMessageメソッド Btn2:CreateStChannelメソッド Btn3:JoinStChannelメソッド Btn4:JoinTopicメソッド Btn5:SubscribeTopicメソッド Btn6:PublishBinaryMessageメソッド Btn7:ReleaseStChannelメソッド Btn8:LeaveStChannelメソッド Btn9:LeaveTopicメソッド Btn10:UnsubscribeTopicメソッド

Agoraの設定

ここからAgoraのコンソール画面での設定を行っていきます。Voice Calling(RTC)の設定をしたことある場合は、途中までは同じです。

SignalingのAgoraの設定方法:https://docs.agora.io/en/signaling/get-started/manage-agora-account?platform=android

まずAgoraのログイン画面でログインします。

Agoraコンソールのログイン画面:https://sso2.agora.io/en/login

ログインした最初の画面で白かった場合、画面上部に出てくる「Switch to the new version」を押下し、コンソール画面のバージョンを上げます。

すると真っ黒なコンソール画面が表示されます。このバージョンの画面が、Agoraのドキュメントで解説されているバージョンの画面になります。以降、この画面で解説していきます。

まずAgoraのプロジェクトを新規作成します。「New Project」を押下します。

Project Nameに任意のプロジェクト名を入力し、Use Caseはそれっぽいものを入れ(ここではEducation > Tutoring)、推奨されているApp ID + Tokenで認証モードを設定します。

ここまでは、AgoraのVoice Calling(RTC)のコンソール上の設定と同じで、この先からSignaling(RTM)の機能をアクティブにしていきます。

まず作成したAgoraのプロジェクトを選択し編集画面へ遷移します。

ALL FEATURESのSignalingタブのBasic Informationを開き、Data Centerを選択します。日本であれば「Asia-Pacific (APAC)」で良いと思います。稀にここが設定できない場合がありますが、その場合は1日寝てみてみるといいと思います。(私は最初設定できず、数日後みたら設定できるようになりました。障害か何かだったのかなと思っています)

続いてPresence ConfigurationとStorage Configurationはデフォルトのままで、Stream Channel ConfigurationをEnableにします。ちなみになぜかここの手順がクイックスタートのドキュメントになく、この手順をやらないとSignalingが機能しないので気を付ける必要があります。

これでSignalingのAgoraコンソール上の設定が完了です。

サンプルアプリの動かし方

ここから、今まで組み上げたサンプルを動かします。 AgoraオブジェクトのRtmEngineコンポーネントにパラメータを設定します。

まずUser Idには0を入れ、App IdはAgoraコンソールから取得し、TokenはAgoraが用意しているサンプル用Token Builderを利用して取得します。

Token Builder:https://agora-token-generator-demo.vercel.app/

利用するSDKをSignalingに設定し、AgoraコンソールからApp IDとApp Certificateをコピペし、Generate Tokenでトークンを発行することで、RtmEngineのTokenが取得できます。

ここで、App IDとAppCertificateはAgoraコンソールの以下画面から取得できます。

続いて、Token BuilderのAdd the User IDに先ほど入力したUser IDの0を入力します。そして、Add the channel nameにRtmEngineコンポーネントのSt Channel Nameに設定した値を入力し、Generate Tokenします。そこで取得した値をRtmEngineコンポーネントのRtc Tokenに設定することで、サンプルが動きます。

1つの端末でサンプルが動いたら、2つ目の端末でUser IDを1にして、同様の手順でサンプルプロジェクトを動かします。ここで、User IDは必ず異なる値にしないといけません。

以上でサンプルが動き、端末間でメッセージの送受信ができるようになります。

コード解説

続いてコード解説です。サンプルのボタンの処理ごとに解説します。

クイックスタートのサンプルコードをそのまま使うとUnityがクラッシュすることがあるため、少し書きかえます。

まず、AwakeメソッドでInitializeメソッドを動かします。

public async void Awake()
{
   await Initialize();
}

Initializeメソッドでは、RTMエンジンのインスタンス化と、Agoraへのログイン、MessageChannelのサブスクライブをしています。

RTMエンジンのインスタンス化は以下です。パラメータを設定してインスタンスを作成しているだけです。

private async UniTask Initialize()
{
   if (string.IsNullOrEmpty(userId) || string.IsNullOrEmpty(appId))
   {
       ShowMessage("We need a userId and appId to initialize!");
       return;
   }
   RtmLogConfig logConfig = new RtmLogConfig();
   // Set log file path
   logConfig.filePath = "myFilePath";
   // Set the file size of the agore.log file
   logConfig.fileSizeInKB = 512;
   // Set the log report level
   logConfig.level = RTM_LOG_LEVEL.INFO;
   RtmConfig config = new RtmConfig();
   config.appId = appId;
   config.userId = userId;
   config.logConfig = logConfig;
   // Create the RTM Client
   try
   {
       rtmClient = RtmClient.CreateAgoraRtmClient(config);
       ShowMessage("RTM Client Initialize Successful");

続いてログインは以下です。 ここでは、ログインの前にイベントを設定しています。 OnMessageEventはメッセージを受信したときの処理です。 OnPresenceEventはユーザ状態のイベントを取得したときの処理です。 OnConnectionStateChangedは通信状態のイベントを取得したときの処理です。 その後、RTMへログインしています。

// Add listener
if (rtmClient != null)
{
   // Add the message event listener
   rtmClient.OnMessageEvent += OnMessageEvent;
   // Add the presence event listener
   rtmClient.OnPresenceEvent += OnPresenceEvent;
   // Add the connection state change listener
   rtmClient.OnConnectionStateChanged += OnConnectionStateChanged;
   // Log in to the RTM server
   var result = await rtmClient.LoginAsync(token);
   var status = result.Status;
   var response = result.Response;
   if (status.Error)
   {
       ShowMessage(string.Format("{0} is failed", status.Operation));
       ShowMessage(string.Format("The error code is {0}, due to: {1}", status.ErrorCode, status.Reason));
       Debug.Log(string.Format("Login failed"));
   }

最後にMessageChannelをサブスクライブして、サンプルアプリの画面上部のボタンでの簡易的なメッセージ受信ができるようになります。これでInitializeの処理は終了です。

//Subscribe to a Message Channel
SubscribeOptions options = new SubscribeOptions()
{
   withMessage = true,
   withPresence = true
};
var result2 = await rtmClient.SubscribeAsync(msChannelName, options);
var status2 = result2.Status;
var response2 = result2.Response;

続いて画面上部の「Send Msg」ボタンの処理です。これはInitializeメソッドで準備されたMessage Channnelで利用できるメッセージ送信です。

TextMeshProのInputFieldに入力されたテキストをAgoraのRTMへ送信しています。以下はテキストそのままで送信しています。

// OnClick Event Handler for Btn1
public async void PublishStringMessage()
{
   // Publish string messages to a message channel
   var message = GameObject.Find("InputField1").GetComponent<TMP_InputField>().text;
  
   if (rtmClient != null)
   {
       PublishOptions options = new PublishOptions()
       {
           channelType = RTM_CHANNEL_TYPE.MESSAGE,
           customType = "PlainText"
       };
       var result = await rtmClient.PublishAsync(msChannelName, message, options);
       var status = result.Status;
       var response = result.Response;

続いて「Create st ch」ボタンです。このボタンではStream Channelと呼ばれる通信部屋を作成します。処理内容は単純で、CreateStreamChannelメソッドで、引数の名前のチャンネルを作成するだけです。

// Onclick event handler for Btn2
public void CreateStChannel()
{
   string[] activeComponentsList = { "Btn3", "Btn7" };
   string[] inactiveComponentList = { "Btn2", "Btn4", "Btn5", "Btn6", "Btn8", "Btn9", "Btn10" };
   // Create a Stream Channel
   if (rtmClient != null)
   {
       streamChannel = rtmClient.CreateStreamChannel(stChannelName);
       ShowMessage("Create Stream Channel:" + stChannelName + "Success !");

続いて「Join st ch」ボタンです。トークンを設定して作成したStream Channelに参加するだけです。

// Onclick event handler for Btn3
public async void JoinStChannel()
{
   string[] activeComponentsList = { "Btn4", "Btn5", "Btn8" };
   string[] inactiveComponentList = { "Btn2", "Btn3", "Btn6", "Btn9", "Btn10", "Btn7" };
   // Join a Stream Channel
   if (streamChannel != null)
   {
       JoinChannelOptions options = new JoinChannelOptions();
       options.withPresence = true;
       options.token = rtcToken;
       var result = await streamChannel.JoinAsync(options);
       var status = result.Status;
       var response = result.Response;

続いて「Join Topic」です。Topicと呼ばれる通信メカニズムにパラメータとトピック名を設定して参加します。

// Onclick event handler for Btn4
public async void JoinTopic()
{
   string[] activeComponentsList = { "Btn6", "Btn8", "Btn9" };
   string[] inactiveComponentList = { "Btn2", "Btn3", "Btn4", "Btn7" };
   // Join a topic
   if (streamChannel != null)
   {
       JoinTopicOptions options = new JoinTopicOptions();
       options.qos = RTM_MESSAGE_QOS.ORDERED;
       options.syncWithMedia = false;
       var result = await streamChannel.JoinTopicAsync(topicName, options);

「Sub Topic Pub」ボタンでは以下の処理が実装されています。以下の処理でTopicで送信されるメッセージを受信できるようになります。

// Onclick event handler for Btn5
public async void SubscribeTopic()
{
   string[] activeComponentsList = { "Btn8", "Btn10" };
   string[] inactiveComponentList = { "Btn2", "Btn3", "Btn5", "Btn7" };
   // Subscribe a publisher from a topic
   TopicOptions options = new TopicOptions();
   var result = await streamChannel.SubscribeTopicAsync(topicName, options);

次に画面真ん中の「Send Msg」ボタンの処理を解説します。処理は以下です。

先ほどの「Send Msg」ボタンと同様、TextMeshProのInputFieldのテキストをAgoraのRTMで送信します。このとき、本処理はバイナリでデータ送信しています。そのため、string型のメッセージをUTF8でバイナリにエンコードしています。

// OnClick Event Handler for Btn6
public async void PublishBinaryMessage()
{
   // Publish Binary Message to Stream Channel
   var message = GameObject.Find("InputField2").GetComponent<TMP_InputField>().text;
   byte[] byteMsg = System.Text.Encoding.UTF8.GetBytes(message);
   if (streamChannel != null)
   {
       TopicMessageOptions options = new TopicMessageOptions();
       options.customType = "PlainBinary";
       options.sendTs = 0;
       var result = await streamChannel.PublishTopicMessageAsync(topicName, byteMsg, options);

メッセージ受信はOnMessageEventで行っており、処理は以下です。 メッセージがstring型かバイナリかで処理を分けています。バイナリの時はUTF8でデコードしています。

// Implement the message event handler
private void OnMessageEvent(MessageEvent eve)
{
   var channelName = eve.channelName;
   var channelType = eve.channelType;
   var topic = eve.channelTopic;
   var publisher = eve.publisher;
   var messageType = eve.messageType;
   var customType = eve.customType;
   var message = eve.message;
   if (messageType == RTM_MESSAGE_TYPE.STRING)
   {
       var stMessage = message.GetData<string>();
       ShowMessage(string.Format("You have received a string type message: {0} from: {1} in channel:{2}", stMessage, publisher, channelName));
       ShowMessage(string.Format("The channel type is {0}", channelType));
   }
   else
   {
       Debug.Log($"Message type is not string, it is {messageType}");
       var biMessage = message.GetData<byte[]>();
       ShowMessage(string.Format("You have received a binary type message: {0},from: {1} in channel:{2}", System.Text.Encoding.UTF8.GetString(biMessage), publisher, channelName));
       ShowMessage(string.Format("The channel type is {0}", channelType));
   }
}

最後にチャンネルからの退出およびチャンネル削除についてさくっと見ていきます。 チャンネル作成や参加の逆でありコード自体は簡単です。

「Unsub Topic Pub」ボタンでのトピックのSubscribeの解除は以下です。

// Onclick event handler for Btn10
public async void UnsubscribeTopic()
{
   string[] activeComponentsList = { "Btn5", "Btn8" };
   string[] inactiveComponentList = { "Btn2", "Btn3", "Btn7", "Btn10" };
   // Unsubscribe a publisher from a topic
   List<string> userList = new List<string>();
   userList.Add("Tony");
   TopicOptions options = new TopicOptions();
   options.users = userList.ToArray();
   var result = await streamChannel.UnsubscribeTopicAsync(topicName, options);

「Leave Topic」ボタンでトピックから退出します。コードは以下です。

// Onclick event handler for Btn9
public async void LeaveTopic()
{
   string[] activeComponentsList = { "Btn4", "Btn8" };
   string[] inactiveComponentList = { "Btn2", "Btn3", "Btn6", "Btn7", "Btn9" };
   // Leave a topic
   if (streamChannel != null)
   {
       var result = await streamChannel.LeaveTopicAsync(topicName);

続いて「Leave st ch」ボタンでStream Channelの退出処理は以下です。

// Onclick event handler for Btn8
public async void LeaveStChannel()
{
   string[] activeComponentsList = { "Btn3", "Btn7" };
   string[] inactiveComponentList = { "Btn2", "Btn4", "Btn5", "Btn6", "Btn8", "Btn9", "Btn10" };
   // Leave a Stream Channel
   if (streamChannel != null)
   {
       var result = await streamChannel.LeaveAsync();

最後に「Release st ch」のStream Channelの破棄の処理は以下です。

// Onclick event handler for Btn7
public void ReleaseStChannel()
{
   string[] activeComponentsList = { "Btn2" };
   string[] inactiveComponentList = { "Btn3", "Btn4", "Btn5", "Btn6", "Btn7", "Btn8", "Btn9", "Btn10" };
   // Release a Stream Channel
   if (rtmClient != null && streamChannel != null)
   {
       var status = streamChannel.Dispose();

以上がAgoraの退出処理になります。 また、OnDestroyは以下であり、少しクイックスタートから書きかえています。

private void OnDestroy()
{
   // Dispose a Stream Channel
   if (rtmClient != null && streamChannel != null)
   {
       var status = streamChannel.Dispose();
       if (status.Error)
       {
           ShowMessage(string.Format("{0} is failed", status.Operation));
           ShowMessage(string.Format("The error code is {0}, due to: {1}", status.ErrorCode, status.Reason));
       }
       else
       {
           ShowMessage("Dispose Channel Success!");
       }
   }
   if (rtmClient != null)
   {
       var status = rtmClient.Dispose();
       if (status.Error)
       {
           ShowMessage(string.Format("{0} is failed", status.Operation));
           ShowMessage(string.Format("The error code is {0}, due to: {1}", status.ErrorCode, status.Reason));
       }
       else
       {
           ShowMessage("Dispose rtmClient Success!");
           rtmClient = null;
       }
   }
}

まとめ

本記事ではAgoraのSignalingのクイックスタートのコードを参考にサンプルアプリを作成、解説しました。

元のクイックスタートのドキュメントにはシーケンスや、トークン発行のサーバの作成方法も記載されているため、Agoraを使って何かしら通信するサービスを作りたい場合、本記事を足掛かりにAgoraのドキュメントを読むと幸せになるかもしれません。

Agoraドキュメント:https://docs.agora.io/en/signaling/get-started/sdk-quickstart?platform=unity

サンプルコード全文

ここではサンプルコード全文を載せます。

ほとんどクイックスタートのコードと変わらないですが、クイックスタートのコードをそのまま使うと、Unityエディタがクラッシュするなどで少し不便だったため、少し改造しています。参考になれば幸いです。

using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using Cysharp.Threading.Tasks;
using Agora.Rtm;
using TMPro;


public class RtmEngine : MonoBehaviour
{
   [SerializeField]
   private string appId = "your_appId"; // Get an App ID from Agora console
   [SerializeField]
   private string userId = "your_userId"; // Change the user ID to a numeric string
   [SerializeField]
   private string token = "your_token"; // Login token
   [SerializeField]
   private string rtcToken = "your_RTC_token"; // Token to join a stream channel
   [SerializeField]
   private string msChannelName = "SeeYou";
   [SerializeField]
   private string stChannelName = "Greeting";
   [SerializeField]
   private string topicName = "Hello_world";
  
   private IRtmClient rtmClient;
   private IStreamChannel streamChannel;
   public async void Awake()
   {
       await Initialize();
   }
   // Initialize RTM
   private async UniTask Initialize()
   {
       if (string.IsNullOrEmpty(userId) || string.IsNullOrEmpty(appId))
       {
           ShowMessage("We need a userId and appId to initialize!");
           return;
       }
       RtmLogConfig logConfig = new RtmLogConfig();
       // Set log file path
       logConfig.filePath = "myFilePath";
       // Set the file size of the agore.log file
       logConfig.fileSizeInKB = 512;
       // Set the log report level
       logConfig.level = RTM_LOG_LEVEL.INFO;
       RtmConfig config = new RtmConfig();
       config.appId = appId;
       config.userId = userId;
       config.logConfig = logConfig;
       // Create the RTM Client
       try
       {
           rtmClient = RtmClient.CreateAgoraRtmClient(config);
           ShowMessage("RTM Client Initialize Successful");
           // Inactive unnecessary components
           string[] activeComponentsList = { "Btn2" };
           string[] inactiveComponentList = { "Btn3", "Btn4", "Btn5", "Btn6", "Btn7", "Btn8", "Btn9", "Btn10" };
           ChangeComponentView(activeComponentsList, "ACTIVATE");
           ChangeComponentView(inactiveComponentList, "INACTIVATE");
       }
       catch (RTMException e)
       {
           ShowMessage(string.Format("{0} is failed", e.Status.Operation));
           ShowMessage(string.Format("The error code is {0}, due to: {1}", e.Status.ErrorCode, e.Status.Reason));
       }
       // Add listener
       if (rtmClient != null)
       {
           // Add the message event listener
           rtmClient.OnMessageEvent += OnMessageEvent;
           // Add the presence event listener
           rtmClient.OnPresenceEvent += OnPresenceEvent;
           // Add the connection state change listener
           rtmClient.OnConnectionStateChanged += OnConnectionStateChanged;
           // Log in to the RTM server
           var result = await rtmClient.LoginAsync(token);
           var status = result.Status;
           var response = result.Response;
           if (status.Error)
           {
               ShowMessage(string.Format("{0} is failed", status.Operation));
               ShowMessage(string.Format("The error code is {0}, due to: {1}", status.ErrorCode, status.Reason));
               Debug.Log(string.Format("Login failed"));
           }
           else
           {
               ShowMessage("Login Successfully");
           }
           //Subscribe to a Message Channel
           SubscribeOptions options = new SubscribeOptions()
           {
               withMessage = true,
               withPresence = true
           };
           var result2 = await rtmClient.SubscribeAsync(msChannelName, options);
           var status2 = result2.Status;
           var response2 = result2.Response;
           if (status2.Error)
           {
               ShowMessage(string.Format("{0} is failed", status2.Operation));
               ShowMessage(string.Format("The error code is {0}, due to: {1}", status2.ErrorCode, status2.Reason));
           }
           else
           {
               ShowMessage(string.Format("Subscribe channel success! at Channel:{0}", response2.ChannelName));
           }
       }
   }
   private void ChangeComponentView(string[] componentsList, string viewType)
   {
       for (int i = 0; i < componentsList.Length; i++)
       {
           if (viewType == "ACTIVATE")
               GameObject.Find(componentsList[i]).GetComponent<Button>().interactable = true;
           else if (viewType == "INACTIVATE")
               GameObject.Find(componentsList[i]).GetComponent<Button>().interactable = false;
       }
   }
   private void ShowMessage(string msg)
   {
       Debug.Log("ShowMessage: " + msg);
   }
   // Implement the event listener handler
   // Implement the connection state change event handler
   private void OnConnectionStateChanged(string channelName, RTM_CONNECTION_STATE state, RTM_CONNECTION_CHANGE_REASON reason)
   {
       ShowMessage(string.Format("Channel:{0} connection state have changed to:{1} because of {2}", channelName, state, reason));
   }
   // Implement the presence event handler
   private void OnPresenceEvent(PresenceEvent eve)
   {
       var channelName = eve.channelName;
       var channelType = eve.channelType;
       var eventType = eve.type;
       var publisher = eve.publisher;
       var stateItems = eve.stateItems;
       var interval = eve.interval;
       var snapshot = eve.snapshot;
       Debug.Log(string.Format("The Event : {0} Happend", eventType));
       switch (eventType)
       {
           case RTM_PRESENCE_EVENT_TYPE.SNAPSHOT:
               ShowMessage(string.Format("You have sub or join channel:{0} channe type is:{1}", channelName, channelType));
               break;
           case RTM_PRESENCE_EVENT_TYPE.INTERVAL:
               ShowMessage(string.Format("The channel:{0} channe type is:{1} is now in interval mode!", channelName, channelType));
               break;
           case RTM_PRESENCE_EVENT_TYPE.REMOTE_JOIN:
               ShowMessage(string.Format("User:{0} sub or join channel:{1} channe type is:{2}", publisher, channelName, channelType));
               break;
           case RTM_PRESENCE_EVENT_TYPE.REMOTE_LEAVE:
               ShowMessage(string.Format("User:{0} unsub or leave channel:{1} channe type is:{2}", publisher, channelName, channelType));
               break;
           case RTM_PRESENCE_EVENT_TYPE.REMOTE_TIMEOUT:
               ShowMessage(string.Format("User:{0} timeout from channel:{1} channe type is:{2}", publisher, channelName, channelType));
               break;
           case RTM_PRESENCE_EVENT_TYPE.REMOTE_STATE_CHANGED:
               ShowMessage(string.Format("User:{0} state change in channel:{1} channe type is:{2}", publisher, channelName, channelType));
               break;
           case RTM_PRESENCE_EVENT_TYPE.ERROR_OUT_OF_SERVICE:
               ShowMessage(string.Format("User:{0} joined channel without presence service:{1} channe type is:{2}", publisher, channelName, channelType));
               break;
       }
   }
   // Implement the message event handler
   private void OnMessageEvent(MessageEvent eve)
   {
       var channelName = eve.channelName;
       var channelType = eve.channelType;
       var topic = eve.channelTopic;
       var publisher = eve.publisher;
       var messageType = eve.messageType;
       var customType = eve.customType;
       var message = eve.message;
       if (messageType == RTM_MESSAGE_TYPE.STRING)
       {
           var stMessage = message.GetData<string>();
           ShowMessage(string.Format("You have received a string type message: {0} from: {1} in channel:{2}", stMessage, publisher, channelName));
           ShowMessage(string.Format("The channel type is {0}", channelType));
       }
       else
       {
           Debug.Log($"Message type is not string, it is {messageType}");
           var biMessage = message.GetData<byte[]>();
           ShowMessage(string.Format("You have received a binary type message: {0},from: {1} in channel:{2}", System.Text.Encoding.UTF8.GetString(biMessage), publisher, channelName));
           ShowMessage(string.Format("The channel type is {0}", channelType));
       }
   }
   // OnClick Event Handler for Btn1
   public async void PublishStringMessage()
   {
       // Publish string messages to a message channel
       var message = GameObject.Find("InputField1").GetComponent<TMP_InputField>().text;
      
       if (rtmClient != null)
       {
           PublishOptions options = new PublishOptions()
           {
               channelType = RTM_CHANNEL_TYPE.MESSAGE,
               customType = "PlainText"
           };
           var result = await rtmClient.PublishAsync(msChannelName, message, options);
           var status = result.Status;
           var response = result.Response;
           if (status.Error)
           {
               ShowMessage(string.Format("{0} is failed, The error code is {1}", status.Operation, status.ErrorCode));
           }
           else
           {
               ShowMessage("Publish Message Success!");
           }
       }
   }
   // OnClick Event Handler for Btn6
   public async void PublishBinaryMessage()
   {
       // Publish Binary Message to Stream Channel
       var message = GameObject.Find("InputField2").GetComponent<TMP_InputField>().text;
       byte[] byteMsg = System.Text.Encoding.UTF8.GetBytes(message);
       if (streamChannel != null)
       {
           TopicMessageOptions options = new TopicMessageOptions();
           options.customType = "PlainBinary";
           options.sendTs = 0;
           var result = await streamChannel.PublishTopicMessageAsync(topicName, byteMsg, options);
           var status = result.Status;
           var response = result.Response;
           if (status.Error)
           {
               ShowMessage(string.Format("{0} is failed", status.Operation));
               ShowMessage(string.Format("The error code is {0}, due to: {1}", status.ErrorCode, status.Reason));
           }
           else
           {
               ShowMessage("Publish Binary Message Success!");
           }
       }
   }
   // Onclick event handler for Btn2
   public void CreateStChannel()
   {
       string[] activeComponentsList = { "Btn3", "Btn7" };
       string[] inactiveComponentList = { "Btn2", "Btn4", "Btn5", "Btn6", "Btn8", "Btn9", "Btn10" };
       // Create a Stream Channel
       if (rtmClient != null)
       {
           streamChannel = rtmClient.CreateStreamChannel(stChannelName);
           ShowMessage("Create Stream Channel:" + stChannelName + "Success !");
           // Disable unnecessary components
           ChangeComponentView(activeComponentsList, "ACTIVATE");
           ChangeComponentView(inactiveComponentList, "INACTIVATE");
       }
   }
   // Onclick event handler for Btn3
   public async void JoinStChannel()
   {
       string[] activeComponentsList = { "Btn4", "Btn5", "Btn8" };
       string[] inactiveComponentList = { "Btn2", "Btn3", "Btn6", "Btn9", "Btn10", "Btn7" };
       // Join a Stream Channel
       if (streamChannel != null)
       {
           JoinChannelOptions options = new JoinChannelOptions();
           options.withPresence = true;
           options.token = rtcToken;
           var result = await streamChannel.JoinAsync(options);
           var status = result.Status;
           var response = result.Response;
           if (status.Error)
           {
               ShowMessage(string.Format("{0} is failed", status.Operation));
               ShowMessage(string.Format("The error code is {0}, due to: {1}", status.ErrorCode, status.Reason));
           }
           else
           {
               ShowMessage(string.Format("User:{0} Join stream channel success! at Channel:{1}", response.UserId, response.ChannelName));
               ChangeComponentView(activeComponentsList, "ACTIVATE");
               ChangeComponentView(inactiveComponentList, "INACTIVATE");
           }
       }
   }
   // Onclick event handler for Btn4
   public async void JoinTopic()
   {
       string[] activeComponentsList = { "Btn6", "Btn8", "Btn9" };
       string[] inactiveComponentList = { "Btn2", "Btn3", "Btn4", "Btn7" };
       // Join a topic
       if (streamChannel != null)
       {
           JoinTopicOptions options = new JoinTopicOptions();
           options.qos = RTM_MESSAGE_QOS.ORDERED;
           options.syncWithMedia = false;
           var result = await streamChannel.JoinTopicAsync(topicName, options);
           var status = result.Status;
           var response = result.Response;
           if (status.Error)
           {
               ShowMessage(string.Format("{0} is failed", status.Operation));
               ShowMessage(string.Format("The error code is {0}, due to: {1}", status.ErrorCode, status.Reason));
           }
           else
           {
               ShowMessage(string.Format("User:{0} Join Topic:{1} success! at Channel:{2}", response.UserId, response.Topic, response.ChannelName));
               ChangeComponentView(activeComponentsList, "ACTIVATE");
               ChangeComponentView(inactiveComponentList, "INACTIVATE");
           }
       }
   }
   // Onclick event handler for Btn5
   public async void SubscribeTopic()
   {
       string[] activeComponentsList = { "Btn8", "Btn10" };
       string[] inactiveComponentList = { "Btn2", "Btn3", "Btn5", "Btn7" };
       // Subscribe a publisher from a topic
       TopicOptions options = new TopicOptions();
       var result = await streamChannel.SubscribeTopicAsync(topicName, options);
       var status = result.Status;
       var response = result.Response;
       if (status.Error)
       {
           ShowMessage(string.Format("{0} is failed", status.Operation));
           ShowMessage(string.Format("The error code is {0}, due to: {1}", status.ErrorCode, status.Reason));
       }
       else
       {
           ShowMessage(string.Format("User:{0} Subscribe Topic:{1} success! at Channel:{2}", response.UserId, response.Topic, response.ChannelName));
           ChangeComponentView(activeComponentsList, "ACTIVATE");
           ChangeComponentView(inactiveComponentList, "INACTIVATE");
       }
   }
  
   // Onclick event handler for Btn7
   public void ReleaseStChannel()
   {
       string[] activeComponentsList = { "Btn2" };
       string[] inactiveComponentList = { "Btn3", "Btn4", "Btn5", "Btn6", "Btn7", "Btn8", "Btn9", "Btn10" };
       // Release a Stream Channel
       if (rtmClient != null && streamChannel != null)
       {
           var status = streamChannel.Dispose();
           if (status.Error)
           {
               ShowMessage(string.Format("{0} is failed", status.Operation));
               ShowMessage(string.Format("The error code is {0}, due to: {1}", status.ErrorCode, status.Reason));
           }
           else
           {
               streamChannel = null;
               ShowMessage("Dispose Channel Success!");
               ChangeComponentView(activeComponentsList, "ACTIVATE");
               ChangeComponentView(inactiveComponentList, "INACTIVATE");
           }
       }
   }
   // Onclick event handler for Btn8
   public async void LeaveStChannel()
   {
       string[] activeComponentsList = { "Btn3", "Btn7" };
       string[] inactiveComponentList = { "Btn2", "Btn4", "Btn5", "Btn6", "Btn8", "Btn9", "Btn10" };
       // Leave a Stream Channel
       if (streamChannel != null)
       {
           var result = await streamChannel.LeaveAsync();
           var status = result.Status;
           var response = result.Response;
           if (status.Error)
           {
               ShowMessage(string.Format("{0} is failed", status.Operation));
               ShowMessage(string.Format("The error code is {0}, due to: {1}", status.ErrorCode, status.Reason));
           }
           else
           {
               ShowMessage(string.Format("User:{0} Leave stream channel success! at Channel:{1}", response.UserId, response.ChannelName));
               ChangeComponentView(activeComponentsList, "ACTIVATE");
               ChangeComponentView(inactiveComponentList, "INACTIVATE");
           }
       }
   }
   // Onclick event handler for Btn9
   public async void LeaveTopic()
   {
       string[] activeComponentsList = { "Btn4", "Btn8" };
       string[] inactiveComponentList = { "Btn2", "Btn3", "Btn6", "Btn7", "Btn9" };
       // Leave a topic
       if (streamChannel != null)
       {
           var result = await streamChannel.LeaveTopicAsync(topicName);
           var status = result.Status;
           var response = result.Response;
           if (status.Error)
           {
               ShowMessage(string.Format("{0} is failed", status.Operation));
               ShowMessage(string.Format("The error code is {0}, due to: {1}", status.ErrorCode, status.Reason));
           }
           else
           {
               ShowMessage(string.Format("User:{0} Leave Topic:{1} success! at Channel:{2}", response.UserId, response.Topic, response.ChannelName));
               ChangeComponentView(activeComponentsList, "ACTIVATE");
               ChangeComponentView(inactiveComponentList, "INACTIVATE");
           }
       }
   }
   // Onclick event handler for Btn10
   public async void UnsubscribeTopic()
   {
       string[] activeComponentsList = { "Btn5", "Btn8" };
       string[] inactiveComponentList = { "Btn2", "Btn3", "Btn7", "Btn10" };
       // Unsubscribe a publisher from a topic
       List<string> userList = new List<string>();
       userList.Add("Tony");
       TopicOptions options = new TopicOptions();
       options.users = userList.ToArray();
       var result = await streamChannel.UnsubscribeTopicAsync(topicName, options);
       var status = result.Status;
       var response = result.Response;
       if (status.Error)
       {
           ShowMessage(string.Format("{0} is failed", status.Operation));
           ShowMessage(string.Format("The error code is {0}, due to: {1}", status.ErrorCode, status.Reason));
       }
       else
       {
           ShowMessage(string.Format("Unsubscribe Topic Success!"));
           ChangeComponentView(activeComponentsList, "ACTIVATE");
           ChangeComponentView(inactiveComponentList, "INACTIVATE");
       }
   }
   private void OnDestroy()
   {
       // Dispose a Stream Channel
       if (rtmClient != null && streamChannel != null)
       {
           var status = streamChannel.Dispose();
           if (status.Error)
           {
               ShowMessage(string.Format("{0} is failed", status.Operation));
               ShowMessage(string.Format("The error code is {0}, due to: {1}", status.ErrorCode, status.Reason));
           }
           else
           {
               ShowMessage("Dispose Channel Success!");
           }
       }
       if (rtmClient != null)
       {
           var status = rtmClient.Dispose();
           if (status.Error)
           {
               ShowMessage(string.Format("{0} is failed", status.Operation));
               ShowMessage(string.Format("The error code is {0}, due to: {1}", status.ErrorCode, status.Reason));
           }
           else
           {
               ShowMessage("Dispose rtmClient Success!");
               rtmClient = null;
           }
       }
   }
}