# Signaling Quickstart (/en/realtime-media/rtm/quickstart)

> For AI agents: see the complete documentation index at [llms.txt](/llms.txt).

Use Signaling SDK to add low-latency, high-concurrency signaling and synchronization capabilities to your app.

Signaling also helps you enhance the user experience in Video Calling, Voice Calling, Interactive Live Streaming, and Broadcast Streaming applications.

This page shows you how to use the Signaling SDK to rapidly build a simple application that sends and receives messages. It shows you how to integrate the Signaling SDK in your project and implement pub/sub messaging through [Message channels](/en/realtime-media/rtm/build/work-with-channels/message-channel). To get started with stream channels, follow this guide to create a basic Signaling app and then refer to the [Stream channels](/en/realtime-media/rtm/build/work-with-channels/stream-channel) guide.

<_PlatformTabsGroup groupMode="structured" canonicalPlatform="web" platforms="[&#x22;android&#x22;,&#x22;ios&#x22;,&#x22;web&#x22;,&#x22;flutter&#x22;,&#x22;linux-cpp&#x22;,&#x22;unity&#x22;]" showTabs="true">
  <_PlatformPanel platform="android">
    <_PlatformProcessedMarker groupMode="structured" canonicalPlatform="web" platform="android" />

    ## Understand the tech [#understand-the-tech]

    To use Signaling features in your app, you initialize a Signaling client instance and add event listeners. To connect to Signaling, you login using an authentication token. To send a message to a message channel, you publish the message. Signaling creates a channel when a user subscribes to it. To receive messages other users publish to a channel, your app listens for events.

    To create a pub/sub session for Signaling, implement the following steps in your app:

    <Accordions>
      <Accordion title="Signaling workflow">
        ![Signaling workflow for Android](https://assets-docs.agora.io/images/signaling/get-started-workflow-android.svg)
      </Accordion>
    </Accordions>

    ## Prerequisites [#prerequisites]

    To implement the code presented on this page you need to have:

    * An Agora [account](/en/realtime-media/rtm/manage-agora-account) and [project](/en/realtime-media/rtm/manage-agora-account).

    * [Enabled Signaling](/en/realtime-media/rtm/manage-agora-account) in Agora Console

    * [Android Studio](https://developer.android.com/studio) 4.1 or higher.

    * Android SDK API Level 24 or higher.

    * A mobile device that runs Android 4.1 or higher.

    * Ensure that a firewall is not blocking your network communication.

    <CalloutContainer type="info">
      <CalloutDescription>
        Signaling 2.x is an enhanced version compared to 1.x with a wide range of new features. It follows a new pricing structure. See [Pricing](/en/realtime-media/rtm/reference/pricing) for details.
      </CalloutDescription>
    </CalloutContainer>

    ## Project setup [#project-setup]

    ### Create a project [#create-a-project]

    1. **Create a [new project](https://developer.android.com/studio/projects/create-project)**.

       1. Open Android Studio and select &#x2A;*File > New > New Project...**.
       2. Select **Phone and Tablet** > **Empty Activity** and click **Next**.
       3. Set the project name and storage path.
       4. Select **Java** or **Kotlin** as the language, and click **Finish** to create the project.

          <CalloutContainer type="info">
            <CalloutDescription>
              After you create a project, Android Studio automatically starts gradle sync. Ensure that the synchronization is successful before proceeding to the next step.
            </CalloutDescription>
          </CalloutContainer>

    2. **Add network permissions**

       Open the `/app/src/main/AndroidManifest.xml` file and add the following permissions before `<application>`:

       ```xml
       <uses-permission android:name="android.permission.INTERNET" />
       <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
       ```

    3. **Prevent code obfuscation**

       Open the `/app/proguard-rules.pro` file and add the following line to prevent code obfuscation:

       ```java
       -keep class io.agora.**{*;}
       ```

    ### Integrate the SDK [#integrate-the-sdk]

    Use either of the following methods to integrate Signaling SDK into your project.

    **Using Maven Central**

    1. Open the `settings.gradle` file in the project's root directory and add the Maven Central dependency, if it doesn't already exist:

       ```java
       repositories {
            mavenCentral()
          }
       ```

       <CalloutContainer type="info">
         <CalloutDescription>
           If your Android project uses [`dependencyResolutionManagement`](https://docs.gradle.org/current/userguide/declaring_repositories.html#sub\:centralized-repository-declaration), there may be differences in how you add Maven Central dependencies.
         </CalloutDescription>
       </CalloutContainer>

    2. Add the following to the `/Gradle Scripts/build.gradle(Module: <projectname>.app)` file under `dependencies` to integrate the SDK into your Android project:

       * Groovy

         ```text
         implementation 'io.agora.rtm:rtm-sdk:x.y.z'
         ```

         To resolve integration issues when co-integrating with the RTC SDK use:

         ```text
         implementation 'io.agora.rtm:rtm-sdk-lite:x.y.z'
         ```

       * Kotlin

         ```kotlin
         implementation("io.agora.rtm:rtm-sdk:x.y.z")
         ```

         To resolve integration issues when co-integrating with the RTC SDK use:

         ```kotlin
         implementation("io.agora.rtm:rtm-sdk-lite:x.y.z")
         ```

       Replace `x.y.z` with the specific SDK version number, such as `2.2.8`. To get the latest version number, check the [Release notes](/en/realtime-media/rtm/reference/release-notes).

    **Using CDN**

    1. [Download](/en/api-reference/sdks?product=signaling\&platform=android) the latest version of Signaling SDK for Android.
    2. Copy all files in the `sdk` folder of the package to the `/app/libs` folder of the project.
    3. To add the SDK reference, open the project file `/Gradle Scripts/build.gradle(Module: <projectname>.app)` and add the following code:

       1. Add a `ndk` node under the default `Config` node, to specify the supported architectures:

          ```text
          Config {
                      // ...
                      ndk{
                          abiFilters 'armeabi-v7a', 'arm64-v8a', 'x86', 'x86_64'
                      }
                  }
          ```

          <CalloutContainer type="info">
            <CalloutDescription>
              Supporting all architectures increases the app size. Best practice is to only add essential architectures based on your targets. For most use-cases, `armeabi-v7a` and `arm64-v8a` architectures are sufficient when releasing the Android app.
            </CalloutDescription>
          </CalloutContainer>

       2. Add a `sourceSets` node under the `android` node to include the jni libraries copied to the `libs` folder:

          ```text
          android {
                      // ...
                      sourceSets {
                          main {
                              jniLibs.srcDirs = ['libs']
                          }
                      }
                  }
          ```

       3. To include all `jar` files in the `libs` folder as dependencies, add the following under the `dependencies` node:

          ```text
          dependencies {
                      implementation fileTree(dir: 'libs', include: ['*.jar'])
                      // ...
                  }
          ```

    <CalloutContainer type="info">
      <CalloutDescription>
        To integrate Signaling SDK version `2.2.0` or later alongside Video SDK version `4.3.0` or later, refer to [handle integration issues](/en/api-reference/faq/integration/rtm2_rtc_integration_issue).
      </CalloutDescription>
    </CalloutContainer>

    ### Create a user interface [#create-a-user-interface]

    This section helps you create a simple user interface to explore the basic features of Signaling. Modify it according to your specific needs.

    The demo interface consists of the following UI elements:

    * Input boxes for user ID, channel name, and message
    * Buttons to log in and log out of Signaling
    * Buttons to subscribe and unsubscribe from a channel
    * A button to publish a message

    **Sample code to create the user interface**

    <br />

    Open the `/app/res/layout/activity_main.xml` file, and replace the contents with the following:

    ```xml
    <?xml version="1.0" encoding="utf-8"?>
    <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".MainActivity">

        <EditText
            android:id="@+id/uid"
            android:layout_width="130dp"
            android:layout_height="wrap_content"
            android:layout_marginStart="20dp"
            android:layout_marginTop="40dp"
            android:hint="@string/uid"
            android:inputType="text"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent" />

        <Button
            android:id="@+id/logout_button"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginStart="5dp"
            android:onClick="onClickLogout"
            android:text="@string/logout_button"
            app:layout_constraintStart_toEndOf="@+id/login_button"
            app:layout_constraintBottom_toBottomOf="@id/uid" />

        <Button
            android:id="@+id/login_button"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginStart="5dp"
            android:onClick="onClickLogin"
            android:text="@string/login_button"
            app:layout_constraintStart_toEndOf="@+id/uid"
            app:layout_constraintBottom_toBottomOf="@id/uid" />

        <EditText
            android:id="@+id/channel_name"
            android:layout_width="130dp"
            android:layout_height="wrap_content"
            android:layout_marginStart="20dp"
            android:layout_marginTop="20dp"
            android:hint="@string/channel_name"
            android:inputType="text"
            app:layout_constraintTop_toBottomOf="@+id/uid"
            app:layout_constraintStart_toStartOf="parent" />

        <Button
            android:id="@+id/subscribe_button"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginStart="5dp"
            android:onClick="onClickSubscribe"
            android:text="@string/subscribe_button"
            app:layout_constraintStart_toEndOf="@+id/channel_name"
            app:layout_constraintBottom_toBottomOf="@+id/channel_name" />

        <Button
            android:id="@+id/unsubscribe_button"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginStart="5dp"
            android:onClick="onClickUnsubscribe"
            android:text="@string/unsubscribe_button"
            app:layout_constraintStart_toEndOf="@+id/subscribe_button"
            app:layout_constraintBottom_toBottomOf="@+id/subscribe_button" />

        <EditText
            android:id="@+id/msg_box"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_marginTop="20dp"
            android:layout_marginStart="20dp"
            android:layout_marginEnd="20dp"
            android:hint="@string/msg"
            android:inputType="text"
            android:singleLine="false"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintTop_toBottomOf="@+id/channel_name" />

        <Button
            android:id="@+id/send_channel_msg_button"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="2dp"
            android:onClick="onClickSendChannelMsg"
            android:text="@string/send_channel_msg_button"
            app:layout_constraintTop_toBottomOf="@+id/msg_box"
            app:layout_constraintEnd_toEndOf="@id/msg_box" />

        <TextView
            android:id="@+id/message_history"
            android:layout_width="0dp"
            android:layout_height="0dp"
            android:layout_marginTop="20dp"
            android:background="#eee"
            android:paddingStart="10dp"
            android:paddingEnd="10dp"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@id/send_channel_msg_button" />

    </androidx.constraintlayout.widget.ConstraintLayout>
    ```

    Open the `app/res/values/strings.xml` file and add following string resources:

    ```xml
    <resources>
        <string name="app_name">Signaling Quickstart</string>
        <string name="login_button">Login</string>
        <string name="logout_button">Logout</string>
        <string name="subscribe_button">Subscribe</string>
        <string name="unsubscribe_button">Unsubscribe</string>
        <string name="send_channel_msg_button">Publish message</string>
        <string name="uid">User ID</string>
        <string name="msg">Message content</string>
        <string name="channel_name">Channel name</string>
        <string name="app_id">your_appid</string>
        <string name="token">your_token</string>
    </resources>
    ```

    ## Implement Signaling [#implement-signaling]

    A complete code sample that implements the basic features of Signaling is presented here for your reference. To use the sample code, copy the following lines into the `/app/src/main/java/com/example/<projectname>/MainActivity` file and replace `<projectname>` in `package com.example.<projectname>` with the name of your project.

    **Complete sample code for Signaling**

    <CodeBlockTabs defaultValue="java">
      <CodeBlockTabsList>
        <CodeBlockTabsTrigger value="java">
          Java
        </CodeBlockTabsTrigger>

        <CodeBlockTabsTrigger value="kotlin">
          Kotlin
        </CodeBlockTabsTrigger>
      </CodeBlockTabsList>

      <CodeBlockTab value="java">
        ```java
        package com.example.<projectname>;

        import android.os.Bundle;
        import android.view.View;
        import android.widget.EditText;
        import android.widget.TextView;
        import android.widget.Toast;
        import androidx.appcompat.app.AppCompatActivity;
        import io.agora.rtm.*;

        public class JavaActivity extends AppCompatActivity {
            private EditText etUserId;
            private EditText etChannelName;
            private EditText etMessageContent;
            private TextView mMessageHistory;

            private RtmClient mRtmClient;

            private final RtmEventListener eventListener = new RtmEventListener() {
                @Override
                public void onMessageEvent(MessageEvent event) {
                    String text = "Message received from " + event.getPublisherId()
                            + ", Message: " + event.getMessage().getData();
                    writeToMessageHistory(text);
                }

                @Override
                public void onPresenceEvent(PresenceEvent event) {
                    String text = "Received presence event, user: " + event.getPublisherId()
                            + ", Event: " + event.getEventType();
                    writeToMessageHistory(text);
                }

                @Override
                public void onLinkStateEvent(LinkStateEvent event) {
                    String text = "Connection state changed to " + event.getCurrentState()
                            + ", Reason: " + event.getReason();
                    writeToMessageHistory(text);
                }
            };

            @Override
            protected void onCreate(Bundle savedInstanceState) {
                super.onCreate(savedInstanceState);
                setContentView(R.layout.activity_main);
            }

            public void onClickLogin(View v) {
                etUserId = findViewById(R.id.uid);
                String userId = etUserId.getText().toString();
                String token = getString(R.string.token);
                if (createClient(userId)) {
                    login(token);
                }
            }

            public void onClickLogout(View v) {
                logout();
            }

            public void onClickSubscribe(View v) {
                etChannelName = findViewById(R.id.channel_name);
                String channelName = etChannelName.getText().toString();
                subscribe(channelName);
            }

            public void onClickUnsubscribe(View v) {
                etChannelName = findViewById(R.id.channel_name);
                String channelName = etChannelName.getText().toString();
                unsubscribe(channelName);
            }

            public void onClickSendChannelMsg(View v) {
                etChannelName = findViewById(R.id.channel_name);
                String channelName = etChannelName.getText().toString();

                etMessageContent = findViewById(R.id.msg_box);
                String message = etMessageContent.getText().toString();
                publishMessage(channelName, message);
            }

            private boolean createClient(String userId) {
                if (userId.isEmpty()) {
                    showToast("Invalid userId");
                    return false;
                }

                try {
                    RtmConfig config = new RtmConfig.Builder(getString(R.string.app_id), userId)
                            .eventListener(eventListener)
                            .build();
                    mRtmClient = RtmClient.create(config);
                    return true;
                } catch (Exception e) {
                    showToast("Error creating RTM client.");
                    return false;
                }
            }

            private void login(String token) {
                if (mRtmClient == null) {
                    showToast("RTM client is null");
                    return;
                }

                mRtmClient.login(token, new ResultCallback<>() {
                    @Override
                    public void onSuccess(Void responseInfo) {
                        writeToMessageHistory("Successfully logged in to Signaling!");
                    }

                    @Override
                    public void onFailure(ErrorInfo errorInfo) {
                        writeToMessageHistory("Failed to log in to Signaling: " + errorInfo);
                    }
                });
            }

            private void logout() {
                if (mRtmClient == null) {
                    showToast("RTM client is null");
                    return;
                }

                mRtmClient.logout(new ResultCallback<>() {
                    @Override
                    public void onSuccess(Void responseInfo) {
                        writeToMessageHistory("Successfully logged out.");
                    }

                    @Override
                    public void onFailure(ErrorInfo errorInfo) {
                        writeToMessageHistory("Failed to log out: " + errorInfo);
                    }
                });
            }

            private void subscribe(String channelName) {
                if (mRtmClient == null) {
                    showToast("RTM client is null");
                    return;
                }

                SubscribeOptions options = new SubscribeOptions();
                options.setWithMessage(true);

                mRtmClient.subscribe(channelName, options, new ResultCallback<>() {
                    @Override
                    public void onSuccess(Void responseInfo) {
                        writeToMessageHistory("Successfully subscribed to the channel!");
                    }

                    @Override
                    public void onFailure(ErrorInfo errorInfo) {
                        writeToMessageHistory("Failed to subscribe to the channel: " + errorInfo);
                    }
                });
            }

            private void unsubscribe(String channelName) {
                if (mRtmClient == null) {
                    showToast("RTM client is null");
                    return;
                }

                mRtmClient.unsubscribe(channelName, new ResultCallback<>() {
                    @Override
                    public void onSuccess(Void responseInfo) {
                        writeToMessageHistory("Successfully unsubscribed from the channel!");
                    }

                    @Override
                    public void onFailure(ErrorInfo errorInfo) {
                        writeToMessageHistory("Failed to unsubscribe from the channel: " + errorInfo);
                    }
                });
            }

            private void publishMessage(String channelName, String message) {
                if (mRtmClient == null) {
                    showToast("RTM client is null");
                    return;
                }

                PublishOptions options = new PublishOptions();
                options.setCustomType("");

                mRtmClient.publish(channelName, message, options, new ResultCallback<>() {
                    @Override
                    public void onSuccess(Void responseInfo) {
                        writeToMessageHistory("Message sent to channel " + channelName + ": " + message);
                    }

                    @Override
                    public void onFailure(ErrorInfo errorInfo) {
                        writeToMessageHistory("Failed to send message to channel " + channelName + ": " + errorInfo);
                    }
                });
            }

            private void writeToMessageHistory(String record) {
                if (mMessageHistory == null) {
                    mMessageHistory = findViewById(R.id.message_history);
                }
                mMessageHistory.append("- " + record + "
        ");
            }

            private void showToast(String text) {
                Toast.makeText(this, text, Toast.LENGTH_SHORT).show();
            }
        }
        ```
      </CodeBlockTab>

      <CodeBlockTab value="kotlin">
        ```kotlin
        package com.example.<projectname>

        import android.content.pm.ActivityInfo
        import android.os.Bundle
        import android.view.View
        import android.widget.EditText
        import android.widget.TextView
        import android.widget.Toast
        import androidx.appcompat.app.AppCompatActivity
        import io.agora.rtm.*

        class MainActivity : AppCompatActivity() {
            private lateinit var etUserId: EditText
            private lateinit var etChannelName: EditText
            private lateinit var etMessageContent: EditText

            private var mRtmClient: RtmClient? = null
            private lateinit var mMessageHistory: TextView

            private val eventListener = object : RtmEventListener {
                override fun onMessageEvent(event: MessageEvent) {
                    val text = "Message received from \${event.publisherId}, Message: \${event.message.data}"
                    writeToMessageHistory(text)
                }

                override fun onPresenceEvent(event: PresenceEvent) {
                    val text = "Received presence event, user: \${event.publisherId}, Event: \${event.eventType}"
                    writeToMessageHistory(text)
                }

                override fun onLinkStateEvent(event: LinkStateEvent) {
                    val text = "Connection state changed to \${event.currentState}, Reason: \${event.reason}"
                    writeToMessageHistory(text)
                }
            }

            override fun onCreate(savedInstanceState: Bundle?) {
                super.onCreate(savedInstanceState)
                setContentView(R.layout.activity_main)
                setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT)
            }

            fun onClickLogin(v: View) {
                etUserId = findViewById(R.id.uid)
                val userId = etUserId.text.toString()
                val token = getString(R.string.token)
                if (createClient(userId)) {
                    login(token)
                }
            }

            fun onClickLogout(v: View) {
                logout()
            }

            fun onClickSubscribe(v: View) {
                etChannelName = findViewById(R.id.channel_name)
                val channelName = etChannelName.text.toString()
                subscribe(channelName)
            }

            fun onClickUnsubscribe(v: View) {
                etChannelName = findViewById(R.id.channel_name)
                val channelName = etChannelName.text.toString()
                unsubscribe(channelName)
            }

            fun onClickSendChannelMsg(v: View) {
                etChannelName = findViewById(R.id.channel_name)
                val channelName = etChannelName.text.toString()

                etMessageContent = findViewById(R.id.msg_box)
                val message = etMessageContent.text.toString()
                publishMessage(channelName, message)
            }

            private fun createClient(userId: String): Boolean {
                if (userId.isEmpty()) {
                    showToast("Invalid userId")
                    return false
                }

                return try {
                    val config = RtmConfig.Builder(getString(R.string.app_id), userId)
                        .eventListener(eventListener)
                        .build()
                    mRtmClient = RtmClient.create(config)
                    true
                } catch (e: Exception) {
                    showToast("Error creating RTM client.")
                    false
                }
            }

            private fun login(token: String) {
                mRtmClient?.login(token, object : ResultCallback<Void?> {
                    override fun onSuccess(responseInfo: Void?) {
                        writeToMessageHistory("Successfully logged in to Signaling!")
                    }

                    override fun onFailure(errorInfo: ErrorInfo) {
                        writeToMessageHistory("Failed to log in to Signaling: $errorInfo")
                    }
                }) ?: showToast("RTM client is null")
            }

            private fun logout() {
                mRtmClient?.logout(object : ResultCallback<Void?> {
                    override fun onSuccess(responseInfo: Void?) {
                        writeToMessageHistory("Successfully logged out.")
                    }

                    override fun onFailure(errorInfo: ErrorInfo) {
                        writeToMessageHistory("Failed to log out: $errorInfo")
                    }
                }) ?: showToast("RTM client is null")
            }

            private fun subscribe(channelName: String) {
                mRtmClient ?: run {
                    showToast("RTM client is null")
                    return
                }

                val options = SubscribeOptions().apply {
                    withMessage = true
                }

                mRtmClient!!.subscribe(channelName, options, object : ResultCallback<Void?> {
                    override fun onSuccess(responseInfo: Void?) {
                        writeToMessageHistory("Successfully subscribed to the channel!")
                    }

                    override fun onFailure(errorInfo: ErrorInfo) {
                        writeToMessageHistory("Failed to subscribe to the channel: $errorInfo")
                    }
                })
            }

            private fun unsubscribe(channelName: String) {
                mRtmClient ?: run {
                    showToast("RTM client is null")
                    return
                }

                mRtmClient!!.unsubscribe(channelName, object : ResultCallback<Void?> {
                    override fun onSuccess(responseInfo: Void?) {
                        writeToMessageHistory("Successfully unsubscribed from the channel!")
                    }

                    override fun onFailure(errorInfo: ErrorInfo) {
                        writeToMessageHistory("Failed to unsubscribe from the channel: $errorInfo")
                    }
                })
            }

            private fun publishMessage(channelName: String, message: String) {
                mRtmClient ?: run {
                    showToast("RTM client is null")
                    return
                }

                val options = PublishOptions().apply {
                    customType = ""
                }

                mRtmClient!!.publish(channelName, message, options, object : ResultCallback<Void?> {
                    override fun onSuccess(responseInfo: Void?) {
                        writeToMessageHistory("Message sent to channel $channelName: $message")
                    }

                    override fun onFailure(errorInfo: ErrorInfo) {
                        writeToMessageHistory("Failed to send message to channel $channelName: $errorInfo")
                    }
                })
            }

            private fun writeToMessageHistory(record: String) {
                mMessageHistory = findViewById(R.id.message_history)
                mMessageHistory.append("- $record
        ")
            }

            private fun showToast(text: String) {
                Toast.makeText(this, text, Toast.LENGTH_SHORT).show()
            }
        }
        ```
      </CodeBlockTab>
    </CodeBlockTabs>

    Follow the implementation steps to understand the core API calls in the sample code or use the snippets in your own code.

    ### Import Agora classes [#import-agora-classes]

    To use Signaling APIs in your project, import the relevant Agora classes and interfaces:

    <CodeBlockTabs defaultValue="java">
      <CodeBlockTabsList>
        <CodeBlockTabsTrigger value="java">
          Java
        </CodeBlockTabsTrigger>

        <CodeBlockTabsTrigger value="kotlin">
          Kotlin
        </CodeBlockTabsTrigger>
      </CodeBlockTabsList>

      <CodeBlockTab value="java">
        ```java
        import io.agora.rtm.*;
        ```
      </CodeBlockTab>

      <CodeBlockTab value="kotlin">
        ```kotlin
        import io.agora.rtm.*
        ```
      </CodeBlockTab>
    </CodeBlockTabs>

    ### Initialize the Signaling engine [#initialize-the-signaling-engine]

    Before calling any other Signaling SDK API, initialize an `RtmClient` object instance.

    <CodeBlockTabs defaultValue="java">
      <CodeBlockTabsList>
        <CodeBlockTabsTrigger value="java">
          Java
        </CodeBlockTabsTrigger>

        <CodeBlockTabsTrigger value="kotlin">
          Kotlin
        </CodeBlockTabsTrigger>
      </CodeBlockTabsList>

      <CodeBlockTab value="java">
        ```java
        private boolean createClient(String userId) {
            if (userId.isEmpty()) {
                showToast("Invalid userId");
                return false;
            }
            try {
                RtmConfig config = new RtmConfig.Builder(getString(R.string.app_id), userId)
                        .eventListener(eventListener)
                        .build();
                mRtmClient = RtmClient.create(config);
                return true;
            } catch (Exception e) {
                showToast("Error creating RTM client.");
                return false;
            }
        }
        ```
      </CodeBlockTab>

      <CodeBlockTab value="kotlin">
        ```kotlin
        private fun createClient(userId: String): Boolean {
            if (userId.isEmpty()) {
                showToast("Invalid userId")
                return false
            }
            return try {
                // Create a configuration object
                val config = RtmConfig.Builder(getString(R.string.app_id), userId)
                    .eventListener(eventListener)
                    .build()
                // Use the configuration object to instantiate the engine
                mRtmClient = RtmClient.create(config)
                true
            } catch (e: Exception) {
                showToast("Error creating RTM client.")
                false
            }
        }
        ```
      </CodeBlockTab>
    </CodeBlockTabs>

    ### Add an event listener [#add-an-event-listener]

    The event listener enables you to implement the processing logic in response to Signaling events. Use the following code to handle event notifications or display received messages:

    <CodeBlockTabs defaultValue="java">
      <CodeBlockTabsList>
        <CodeBlockTabsTrigger value="java">
          Java
        </CodeBlockTabsTrigger>

        <CodeBlockTabsTrigger value="kotlin">
          Kotlin
        </CodeBlockTabsTrigger>
      </CodeBlockTabsList>

      <CodeBlockTab value="java">
        ```java
        private RtmEventListener eventListener = new RtmEventListener() {
            @Override
            public void onMessageEvent(MessageEvent event) {
                String text = "Message received from " + event.getPublisherId()
                        + " Message: " + event.getMessage().getData();
                writeToMessageHistory(text);
            }

            @Override
            public void onPresenceEvent(PresenceEvent event) {
                String text = "Received presence event, user: " + event.getPublisherId()
                        + " Event: " + event.getEventType();
                writeToMessageHistory(text);
           }

            @Override
            public void onLinkStateEvent(LinkStateEvent event) {
                String text = "Connection state changed to " + event.getCurrentState()
                        + ", Reason: " + event.getReason();
                writeToMessageHistory(text);
            }
        };
        ```
      </CodeBlockTab>

      <CodeBlockTab value="kotlin">
        ```kotlin
        private val eventListener = object : RtmEventListener {
            override fun onMessageEvent(event: MessageEvent) {
                val text = "Message received from \${event.publisherId} Message: \${event.message.data}"
                writeToMessageHistory(text)
            }
            override fun onPresenceEvent(event: PresenceEvent) {
                val text = "Received presence event, user: \${event.publisherId} event: \${event.eventType}"
                writeToMessageHistory(text)
            }
            override fun onLinkStateEvent(event: LinkStateEvent) {
                val text = "Connection state changed to \${event.currentState}, Reason: \${event.reason}"
                writeToMessageHistory(text)
            }
        }
        ```
      </CodeBlockTab>
    </CodeBlockTabs>

    ### Log in to Signaling [#log-in-to-signaling]

    To connect to Signaling and access Signaling network resources, such as sending messages, and subscribing to channels, call `login`.

    During a login operation, the client attempts to establish a connection with Signaling. Once the connection is established, the client transmits heartbeat information to the Signaling server at fixed intervals to keep the client active until the client actively logs out or is disconnected. The connection is interrupted when timeout occurs. During this period, users may freely access the Signaling network resources subject to their own permissions and usage restrictions.

    <CodeBlockTabs defaultValue="java">
      <CodeBlockTabsList>
        <CodeBlockTabsTrigger value="java">
          Java
        </CodeBlockTabsTrigger>

        <CodeBlockTabsTrigger value="kotlin">
          Kotlin
        </CodeBlockTabsTrigger>
      </CodeBlockTabsList>

      <CodeBlockTab value="java">
        ```java
        private void login(String token) {
            if (mRtmClient == null) {
                showToast("RTM client is null");
                return;
            }
            mRtmClient.login(token, new ResultCallback<>() {
                @Override
                public void onSuccess(Void responseInfo) {
                    writeToMessageHistory("Successfully logged in to Signaling!");
                }
                @Override
                public void onFailure(ErrorInfo errorInfo) {
                    writeToMessageHistory("Failed to log in to Signaling: " + errorInfo);
                }
            });
        }
        ```
      </CodeBlockTab>

      <CodeBlockTab value="kotlin">
        ```kotlin
        private fun login(token: String) {
            mRtmClient?.login(token, object : ResultCallback<Void?> {
                override fun onSuccess(responseInfo: Void?) {
                    writeToMessageHistory("Successfully logged in to Signaling!")
                }
                override fun onFailure(errorInfo: ErrorInfo) {
                    writeToMessageHistory("Failed to log in to Signaling: $errorInfo")
                }
            }) ?: showToast("RTM client is null")
        }
        ```
      </CodeBlockTab>
    </CodeBlockTabs>

    To confirm that login is successful, use the `login` return value, or listen to the `onLinkStateEvent` event notification which provides the error code and reason for the login failure. When performing a login operation, the client's network connection state is `CONNECTING`. After a successful login, the state is updated to `CONNECTED`.

    <CalloutContainer type="info">
      <CalloutTitle>
        Best practice
      </CalloutTitle>

      <CalloutDescription>
        To continuously monitor the network connection state of the client, best practice is to continue to listen for `onLinkStateEvent` notifications throughout the life cycle of the application. For further details, see [Event listeners](/en/realtime-media/rtm/build/send-and-receive-messages/add-event-listener).
      </CalloutDescription>
    </CalloutContainer>

    <CalloutContainer type="warning">
      <CalloutDescription>
        After a user successfully logs into Signaling, the application's PCU increases, which affects your billing data.
      </CalloutDescription>
    </CalloutContainer>

    ### Publish a message [#publish-a-message]

    To distribute a message to all subscribers of a message channel, call `publish`. The following code sends a string type message.

    <CodeBlockTabs defaultValue="java">
      <CodeBlockTabsList>
        <CodeBlockTabsTrigger value="java">
          Java
        </CodeBlockTabsTrigger>

        <CodeBlockTabsTrigger value="kotlin">
          Kotlin
        </CodeBlockTabsTrigger>
      </CodeBlockTabsList>

      <CodeBlockTab value="java">
        ```java
        private void publishMessage(String channelName, String message) {
            if (mRtmClient == null) {
                showToast("RTM client is null");
                return;
            }
            PublishOptions options = new PublishOptions();
            options.setCustomType("");
            mRtmClient.publish(channelName, message, options, new ResultCallback<>() {
                @Override
                public void onSuccess(Void responseInfo) {
                    writeToMessageHistory("Message sent to channel " + channelName + ": " + message);
                }
                @Override
                public void onFailure(ErrorInfo errorInfo) {
                    writeToMessageHistory("Failed to send message to channel " + channelName + ": " + errorInfo);
                }
            });
        }
        ```
      </CodeBlockTab>

      <CodeBlockTab value="kotlin">
        ```kotlin
        private fun publishMessage(channelName: String, message: String) {
            mRtmClient ?: run {
                showToast("RTM client is null")
                return
            }
            val options = PublishOptions().apply {
                customType = ""
            }
            mRtmClient!!.publish(channelName, message, options, object : ResultCallback<Void?> {
                override fun onSuccess(responseInfo: Void?) {
                    writeToMessageHistory("Message sent to channel $channelName: $message")
                }
                override fun onFailure(errorInfo: ErrorInfo) {
                    writeToMessageHistory("Failed to send message to channel $channelName: $errorInfo")
                }
            })
        }
        ```
      </CodeBlockTab>
    </CodeBlockTabs>

    <CalloutContainer type="info">
      <CalloutDescription>
        Before calling `publish` to send a message, serialize the message payload as a string.
      </CalloutDescription>
    </CalloutContainer>

    ### Subscribe and unsubscribe [#subscribe-and-unsubscribe]

    To subscribe to a channel, call `subscribe`. When you subscribe to a channel, you receive all messages published to the channel.

    <CodeBlockTabs defaultValue="java">
      <CodeBlockTabsList>
        <CodeBlockTabsTrigger value="java">
          Java
        </CodeBlockTabsTrigger>

        <CodeBlockTabsTrigger value="kotlin">
          Kotlin
        </CodeBlockTabsTrigger>
      </CodeBlockTabsList>

      <CodeBlockTab value="java">
        ```java
        private void subscribe(String channelName) {
            if (mRtmClient == null) {
                showToast("RTM client is null");
                return;
            }
            SubscribeOptions options = new SubscribeOptions();
            options.setWithMessage(true);
            mRtmClient.subscribe(channelName, options, new ResultCallback<>() {
                @Override
                public void onSuccess(Void responseInfo) {
                    writeToMessageHistory("Successfully subscribed to the channel!");
                }
                @Override
                public void onFailure(ErrorInfo errorInfo) {
                    writeToMessageHistory("Failed to subscribe to the channel: " + errorInfo);
                }
            });
        }
        ```
      </CodeBlockTab>

      <CodeBlockTab value="kotlin">
        ```kotlin
        private fun subscribe(channelName: String) {
            mRtmClient ?: run {
                showToast("RTM client is null")
                return
            }
            val options = SubscribeOptions().apply {
                withMessage = true
            }
            mRtmClient!!.subscribe(channelName, options, object : ResultCallback<Void?> {
                override fun onSuccess(responseInfo: Void?) {
                    writeToMessageHistory("Successfully subscribed to the channel!")
                }
                override fun onFailure(errorInfo: ErrorInfo) {
                    writeToMessageHistory("Failed to subscribe to the channel: $errorInfo")
                }
            })
        }
        ```
      </CodeBlockTab>
    </CodeBlockTabs>

    When you no longer need to receive messages from a channel, call `unsubscribe` to unsubscribe from the channel:

    <CodeBlockTabs defaultValue="java">
      <CodeBlockTabsList>
        <CodeBlockTabsTrigger value="java">
          Java
        </CodeBlockTabsTrigger>

        <CodeBlockTabsTrigger value="kotlin">
          Kotlin
        </CodeBlockTabsTrigger>
      </CodeBlockTabsList>

      <CodeBlockTab value="java">
        ```java
        private void unsubscribe(String channelName) {
            if (mRtmClient == null) {
                showToast("RTM client is null");
                return;
            }
            mRtmClient.unsubscribe(channelName, new ResultCallback<>() {
                @Override
                public void onSuccess(Void responseInfo) {
                    writeToMessageHistory("Successfully unsubscribed from the channel!");
                }
                @Override
                public void onFailure(ErrorInfo errorInfo) {
                    writeToMessageHistory("Failed to unsubscribe from the channel: " + errorInfo);
                }
            });
        }
        ```
      </CodeBlockTab>

      <CodeBlockTab value="kotlin">
        ```kotlin
        private fun unsubscribe(channelName: String) {
            mRtmClient ?: run {
                showToast("RTM client is null")
                return
            }
            mRtmClient!!.unsubscribe(channelName, object : ResultCallback<Void?> {
                override fun onSuccess(responseInfo: Void?) {
                    writeToMessageHistory("Successfully unsubscribed from the channel!")
                }
                override fun onFailure(errorInfo: ErrorInfo) {
                    writeToMessageHistory("Failed to unsubscribe from the channel: $errorInfo")
                }
            })
        }
        ```
      </CodeBlockTab>
    </CodeBlockTabs>

    For more information about subscribing and sending messages, see [Message channels](/en/realtime-media/rtm/build/work-with-channels/message-channel) and [Stream channels](/en/realtime-media/rtm/build/work-with-channels/stream-channel).

    ### Log out of Signaling [#log-out-of-signaling]

    When a user no longer needs to use Signaling, call `logout`. Logging out means closing the connection between the client and Signaling. The user is automatically logged out or unsubscribed from all message and stream channels. Other users in the channel receive an `onPresenceEvent` notification of the user leaving the channel.

    <CodeBlockTabs defaultValue="java">
      <CodeBlockTabsList>
        <CodeBlockTabsTrigger value="java">
          Java
        </CodeBlockTabsTrigger>

        <CodeBlockTabsTrigger value="kotlin">
          Kotlin
        </CodeBlockTabsTrigger>
      </CodeBlockTabsList>

      <CodeBlockTab value="java">
        ```java
        private void logout() {
            if (mRtmClient == null) {
                showToast("RTM client is null");
                return;
            }
            mRtmClient.logout(new ResultCallback<>() {
                @Override
                public void onSuccess(Void responseInfo) {
                    writeToMessageHistory("Successfully logged out.");
                }
                @Override
                public void onFailure(ErrorInfo errorInfo) {
                    writeToMessageHistory("Failed to log out: " + errorInfo);
                }
            });

            // Release resources
            mRtmClient.release();
        }
        ```
      </CodeBlockTab>

      <CodeBlockTab value="kotlin">
        ```kotlin
        private fun logout() {
            mRtmClient?.logout(object : ResultCallback<Void?> {
                override fun onSuccess(responseInfo: Void?) {
                    writeToMessageHistory("Successfully logged out.")
                }
                override fun onFailure(errorInfo: ErrorInfo) {
                    writeToMessageHistory("Failed to log out: $errorInfo")
                }
            }) ?: showToast("RTM client is null")

            // Release resources
            mRtmClient.release()
        }
        ```
      </CodeBlockTab>
    </CodeBlockTabs>

    ## Test Signaling [#test-signaling]

    Take the following steps to test the sample code:

    1. Use the [Token Builder](https://agora-token-generator-demo.vercel.app/) to generate a Signaling token:

       1. Select Signaling from the Agora products dropdown.
       2. Fill in your app ID and app certificate from [Agora Console](https://console.agora.io/v2). Leave the remaining fields blank.
       3. Click **Generate Token**.

    2. In `strings.xml`, replace the values for `app_id` and `token` with your app ID and generated token.

    3. Enable developer options on your Android test device. Turn on USB debugging, connect the Android device to your development machine through a USB cable, and check that your device appears in the Android device options.

    4. In Android Studio, click **Sync Project with Gradle Files** to resolve project dependencies and update the configuration.

    5. After synchronization is successful, click ▶️. Android Studio starts compilation. After a few moments, the app is installed and launched on your Android device.

    6. Use the device as the receiving end and perform the following operations:

       1. Enter your **User ID** and click **Login**.
       2. Enter the **Channel name** and click **Subscribe** .

    7. On a second Android device, repeat the previous steps to install and launch the app. Use this device as the sending end.

       1. Enter a different **User ID** and click **Login**.
       2. Enter the same **Channel name**.
       3. Type a message and click **Publish message**.

    8. Swap the sending and receiving device roles and repeat the previous steps.

       Congratulations! You have successfully integrated Signaling into your project.

    ## Reference [#reference]

    This section contains content that completes the information on this page, or points you to documentation that explains other aspects to this product.

    ### Token authentication [#token-authentication]

    In this guide you retrieve a temporary token from a [Token Builder](https://agora-token-generator-demo.vercel.app/). To understand how to create an authentication server for development purposes, see [Secure authentication with tokens](./build/connect-and-authenticate/authentication-workflow).

    ### Sample projects [#sample-projects]

    Agora provides the following open source sample projects on GitHub for your reference.

    **Java**

    * [Quickstart](https://github.com/AgoraIO/RTM2/tree/main/Agora-RTM2-QuickStart-Android-Java)
    * [Tutorial](https://github.com/AgoraIO/RTM2/tree/main/Agora-RTM2-Tutorial-Android-Java)

    **Kotlin**

    * [Quickstart](https://github.com/AgoraIO/RTM2/tree/main/Agora-RTM2-QuickStart-Android-Kotlin)
    * [Tutorial](https://github.com/AgoraIO/RTM2/tree/main/Agora-RTM2-Tutorial-Android-Kotlin)

    Download the projects or view the source code for more detailed examples.

    ### API reference [#api-reference]

    * [API reference](/en/api-reference/api-ref/signaling)
    * [Event listeners](./build/send-and-receive-messages/add-event-listener)

    <_PlatformProcessedMarker close="true" />
  </_PlatformPanel>

  <_PlatformPanel platform="ios">
    <_PlatformProcessedMarker groupMode="structured" canonicalPlatform="web" platform="ios" />

    ## Understand the tech [#understand-the-tech-1]

    To use Signaling features in your app, you initialize a Signaling client instance and add event listeners. To connect to Signaling, you login using an authentication token. To send a message to a message channel, you publish the message. Signaling creates a channel when a user subscribes to it. To receive messages other users publish to a channel, your app listens for events.

    To create a pub/sub session for Signaling, implement the following steps in your app:

    <Accordions>
      <Accordion title="Signaling workflow">
        ![Signaling workflow for iOS](https://assets-docs.agora.io/images/signaling/get-started-workflow-ios-macos.svg)
      </Accordion>
    </Accordions>

    ## Prerequisites [#prerequisites-1]

    To implement the code presented on this page you need to have:

    * An Agora [account](/en/realtime-media/rtm/manage-agora-account) and [project](/en/realtime-media/rtm/manage-agora-account).

    * [Enabled Signaling](/en/realtime-media/rtm/manage-agora-account) in Agora Console

    * Xcode 12.0 or higher.

    * A device running iOS 9.0 or higher.

    * Ensure that a firewall is not blocking your network communication.

    <CalloutContainer type="info">
      <CalloutDescription>
        Signaling 2.x is an enhanced version compared to 1.x with a wide range of new features. It follows a new pricing structure. See [Pricing](/en/realtime-media/rtm/reference/pricing) for details.
      </CalloutDescription>
    </CalloutContainer>

    ## Project setup [#project-setup-1]

    ### Create a project [#create-a-project-1]

    In Xcode, create a Single View app under the iOS platform. Configure the project settings as follows:

    * Product Name: `RtmQuickstart`
    * Organization Identifier: `agora`
    * User Interface: Storyboard
    * Language: Choose Swift or Objective-C

    ### Integrate the SDK [#integrate-the-sdk-1]

    Use either of the following methods to integrate Signaling SDK into your project.

    **Using CDN**

    1. [Download](/en/api-reference/sdks?product=signaling\&platform=ios) the latest version of Signaling SDK.

    2. Copy the files in the SDK package folder `/libs/AgoraRtmKit.xcframework` to the project path.

    3. Open Xcode and navigate to the **TARGETS > Project Name > General > Frameworks, Libraries, and Embedded Content** menu.

    4. Click **+ > Add Other… > Add Files** to add the dynamic library `EmbedAgoraRtmKit.xcframework`, and ensure that the **Embed** property of the added dynamic library is set to **Embed & Sign**.

    **Using Cocoapods**

    1. To follow this procedure, ensure that you have Cocoapods installed. To install Cocoapods, refer to [Getting Started with CocoaPods](https://guides.cocoapods.org/using/getting-started.html#getting-started).

    2. In the terminal, go to the project root directory and run the `pod init` command. A text file named `Podfile` is generated in the project folder .

    3. Open the `Podfile` and modify the content as follows:

       ```ruby
       platform :ios, '11.0'
            target 'Your App' do
            pod 'AgoraRtm', 'x.y.z'
          end
       ```

       Replace `Your App` with your target name and `x.y.z` with the specific SDK version number, such as 2.2.0. To get the latest version number, check the [Release notes](/en/realtime-media/rtm/reference/release-notes).

       <CalloutContainer type="warning">
         <CalloutDescription>
           If you are using an SDK version earlier than `2.2.0`, change the package name to `AgoraRtm_iOS`.
         </CalloutDescription>
       </CalloutContainer>

    4. Run the `pod install` command in the terminal to install the Signaling SDK. You see the message "Pod installation complete!".

    5. After successful installation, a file with the `.xcworkspace` suffix is generated in the project folder. Open the file in Xcode for subsequent operations.

    **Using Swift Package Manager**
    Use the following link to integrate the SDK using Swift Package Manager (SPM):

    ```text
    {`https://github.com/AgoraIO/AgoraRtm_Apple.git`}
    ```

    <CalloutContainer type="info">
      <CalloutDescription>
        To integrate Signaling SDK version 2.2.0 or above, and Video SDK version 4.3.0 or above at the same time, refer to [handle integration issues](/en/api-reference/faq/integration/rtm2_rtc_integration_issue).
      </CalloutDescription>
    </CalloutContainer>

    ### Create a user interface [#create-a-user-interface-1]

    This section helps you create a simple user interface to explore the basic features of Signaling. Modify it according to your specific needs.

    The demo interface consists of the following UI elements:

    * Input boxes for user ID, channel name, and message
    * Buttons to log in and log out of Signaling
    * Buttons to subscribe and unsubscribe from a channel
    * A button to publish a message

    **Sample code to create the user interface**

    <CodeBlockTabs defaultValue="swift">
      <CodeBlockTabsList>
        <CodeBlockTabsTrigger value="swift">
          Swift
        </CodeBlockTabsTrigger>

        <CodeBlockTabsTrigger value="objc">
          Objective-C
        </CodeBlockTabsTrigger>
      </CodeBlockTabsList>

      <CodeBlockTab value="swift">
        ```swift
        // User Interface
        struct ContentView: View {
            @StateObject private var viewModel = ChatViewModel()
            var body: some View {
                VStack {
                    TextField("Enter username", text: $viewModel.username)
                        .padding()
                        .textFieldStyle(RoundedBorderTextFieldStyle())
                        .font(.title)
                    HStack {
                        Button("Login") {
                            viewModel.login()
                        }
                        .buttonStyle(LoginButtonStyle())
                        Button("Logout") {
                            viewModel.logout()
                        }
                        .buttonStyle(LogoutButtonStyle())
                    }
                    TextField("Channel name", text: $viewModel.channel)
                        .padding()
                        .textFieldStyle(RoundedBorderTextFieldStyle())
                        .font(.title)
                    HStack {
                        Button("Subscribe") {
                            viewModel.subscribe()
                        }
                        .buttonStyle(SubscribeButtonStyle())
                        Button("Unsubscribe") {
                            viewModel.unsubscribe()
                        }
                        .buttonStyle(UnsubscribeButtonStyle())
                    }
                    TextField("Enter message", text: $viewModel.message)
                        .padding()
                        .textFieldStyle(RoundedBorderTextFieldStyle())
                        .font(.title)
                    Button("Send") {
                        viewModel.sendMessage()
                    }
                    .buttonStyle(SendButtonStyle())
                    // Display messages
                    ScrollViewReader { scrollProxy in
                        List(viewModel.messages) { message in
                            Text(message.content)
                                .id(message.id)
                        }
                        .onChange(of: viewModel.messages) { _ in
                            if let lastMessage = viewModel.messages.last {
                                withAnimation {
                                    scrollProxy.scrollTo(lastMessage.id, anchor: .bottom)
                                }
                            }
                        }
                    }
                    .padding()
                }
                .padding()
            }
        }
        // Preview
        struct ContentView_Previews: PreviewProvider {
            static var previews: some View {
                ContentView()
            }
        }
        ```
      </CodeBlockTab>

      <CodeBlockTab value="objc">
        Open `Main.storyboard` using **Code View** and replace the file contents with the following:

        ```xml
        <?xml version="1.0" encoding="UTF-8"?>
        <document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="21701" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="BYZ-38-t0r">
            <device id="retina6_1" orientation="portrait" appearance="light"/>
            <dependencies>
                <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="21679"/>
                <capability name="Safe area layout guides" minToolsVersion="9.0"/>
                <capability name="System colors in document resources" minToolsVersion="11.0"/>
                <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
            </dependencies>
            <scenes>
                <!--View Controller-->
                <scene sceneID="tne-QT-ifu">
                    <objects>
                        <viewController id="BYZ-38-t0r" customClass="ViewController" sceneMemberID="viewController">
                            <view key="view" contentMode="scaleToFill" id="8bC-Xf-vdC">
                                <rect key="frame" x="0.0" y="0.0" width="414" height="896"/>
                                <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
                                <subviews>
                                    <button opaque="NO" contentMode="scaleToFill" fixedFrame="YES" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="M2K-rz-dlO">
                                        <rect key="frame" x="254" y="103" width="38" height="30"/>
                                        <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
                                        <state key="normal" title="Login"/>
                                        <connections>
                                            <action selector="Login:" destination="BYZ-38-t0r" eventType="touchUpInside" id="UEU-up-ksL"/>
                                        </connections>
                                    </button>
                                    <button opaque="NO" contentMode="scaleToFill" fixedFrame="YES" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="CZ1-G5-AkG">
                                        <rect key="frame" x="326" y="103" width="48" height="30"/>
                                        <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
                                        <state key="normal" title="Logout"/>
                                        <connections>
                                            <action selector="Logout:" destination="BYZ-38-t0r" eventType="touchUpInside" id="a0d-8h-eyX"/>
                                        </connections>
                                    </button>
                                    <button opaque="NO" contentMode="scaleToFill" fixedFrame="YES" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="0px-1e-aMC">
                                        <rect key="frame" x="242" y="172" width="62" height="30"/>
                                        <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
                                        <state key="normal" title="Subcribe"/>
                                        <connections>
                                            <action selector="SubscribeChannel:" destination="BYZ-38-t0r" eventType="touchUpInside" id="12c-US-VQk"/>
                                        </connections>
                                    </button>
                                    <button opaque="NO" contentMode="scaleToFill" fixedFrame="YES" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="SDQ-Zt-rBD">
                                        <rect key="frame" x="307" y="172" width="87" height="30"/>
                                        <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
                                        <state key="normal" title="Unsubscribe"/>
                                        <connections>
                                            <action selector="UnsubscribeChannel:" destination="BYZ-38-t0r" eventType="touchUpInside" id="u4T-ze-wtj"/>
                                        </connections>
                                    </button>
                                    <textField opaque="NO" contentMode="scaleToFill" fixedFrame="YES" contentHorizontalAlignment="left" contentVerticalAlignment="center" text="User ID" borderStyle="roundedRect" textAlignment="natural" minimumFontSize="17" translatesAutoresizingMaskIntoConstraints="NO" id="69v-UU-ObH">
                                        <rect key="frame" x="40" y="99" width="187" height="34"/>
                                        <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
                                        <fontDescription key="fontDescription" type="system" pointSize="14"/>
                                        <textInputTraits key="textInputTraits"/>
                                    </textField>
                                    <textField opaque="NO" contentMode="scaleToFill" fixedFrame="YES" contentHorizontalAlignment="left" contentVerticalAlignment="center" text="Channel name" borderStyle="roundedRect" textAlignment="natural" minimumFontSize="17" translatesAutoresizingMaskIntoConstraints="NO" id="dGk-g2-qHK">
                                        <rect key="frame" x="40" y="168" width="187" height="34"/>
                                        <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
                                        <fontDescription key="fontDescription" type="system" pointSize="14"/>
                                        <textInputTraits key="textInputTraits"/>
                                    </textField>
                                    <button opaque="NO" contentMode="scaleToFill" fixedFrame="YES" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="Qqk-6X-Tcb">
                                        <rect key="frame" x="304" y="247" width="88" height="30"/>
                                        <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
                                        <state key="normal" title="Publish MSG"/>
                                        <connections>
                                            <action selector="SendMessageToMessageChannel:" destination="BYZ-38-t0r" eventType="touchUpInside" id="XcX-nz-fPl"/>
                                        </connections>
                                    </button>
                                    <textField opaque="NO" contentMode="scaleToFill" fixedFrame="YES" contentHorizontalAlignment="left" contentVerticalAlignment="center" text="Message content" borderStyle="roundedRect" textAlignment="natural" minimumFontSize="17" translatesAutoresizingMaskIntoConstraints="NO" id="lKg-ap-M6p">
                                        <rect key="frame" x="40" y="245" width="252" height="34"/>
                                        <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
                                        <fontDescription key="fontDescription" type="system" pointSize="14"/>
                                        <textInputTraits key="textInputTraits"/>
                                    </textField>
                                    <textView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" fixedFrame="YES" textAlignment="natural" translatesAutoresizingMaskIntoConstraints="NO" id="e1v-i8-spC">
                                        <rect key="frame" x="40" y="416" width="330" height="428"/>
                                        <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
                                        <color key="backgroundColor" systemColor="systemBackgroundColor"/>
                                        <color key="textColor" systemColor="labelColor"/>
                                        <fontDescription key="fontDescription" type="system" pointSize="14"/>
                                        <textInputTraits key="textInputTraits" autocapitalizationType="sentences"/>
                                    </textView>
                                </subviews>
                                <viewLayoutGuide key="safeArea" id="6Tk-OE-BBY"/>
                                <color key="backgroundColor" systemColor="systemBackgroundColor"/>
                            </view>
                            <connections>
                                <outlet property="ChannelIDTextField" destination="dGk-g2-qHK" id="Iuh-ow-1MH"/>
                                <outlet property="GroupMsgButton" destination="Qqk-6X-Tcb" id="TV4-sg-bY7"/>
                                <outlet property="GroupMsgTextField" destination="lKg-ap-M6p" id="2Me-Z0-QIH"/>
                                <outlet property="LoginButton" destination="M2K-rz-dlO" id="ZxJ-oV-UKy"/>
                                <outlet property="LogoutButton" destination="CZ1-G5-AkG" id="ZQE-B7-yya"/>
                                <outlet property="MsgTextView" destination="e1v-i8-spC" id="76J-8Q-kKp"/>
                                <outlet property="SubsctibeButton" destination="0px-1e-aMC" id="9TL-8M-l6w"/>
                                <outlet property="UnSubscribeButton" destination="SDQ-Zt-rBD" id="tgd-fK-EfO"/>
                                <outlet property="UserIDTextField" destination="69v-UU-ObH" id="3BH-ci-t7A"/>
                            </connections>
                        </viewController>
                        <placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
                    </objects>
                    <point key="canvasLocation" x="131.8840579710145" y="97.767857142857139"/>
                </scene>
            </scenes>
            <resources>
                <systemColor name="labelColor">
                    <color red="0.0" green="0.0" blue="0.0" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
                </systemColor>
                <systemColor name="systemBackgroundColor">
                    <color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
                </systemColor>
            </resources>
        </document>
        ```

        Open the file `ViewController.h` and replace the contents with the following:

        ```objc
        #import <UIKit/UIKit.h>

        #import <AgoraRtmKit/AgoraRtmKit.h>

        @interface ViewController : UIViewController

        // Buttons
        @property (weak, nonatomic) IBOutlet UIButton *LoginButton;
        @property (weak, nonatomic) IBOutlet UIButton *LogoutButton;
        @property (weak, nonatomic) IBOutlet UIButton *SubsctibeButton;
        @property (weak, nonatomic) IBOutlet UIButton *UnSubscribeButton;
        @property (weak, nonatomic) IBOutlet UIButton *GroupMsgButton;

        // TextFields
        @property (weak, nonatomic) IBOutlet UITextField *UserIDTextField;
        @property (weak, nonatomic) IBOutlet UITextField *ChannelIDTextField;
        @property (weak, nonatomic) IBOutlet UITextField *GroupMsgTextField;

        @property (weak, nonatomic) IBOutlet UITextView *MsgTextView;

        @end
        ```
      </CodeBlockTab>
    </CodeBlockTabs>

    ## Implement Signaling [#implement-signaling-1]

    A complete code sample that implements the basic features of Signaling is presented here for your reference. To use the sample code, open `ViewController.m` and replace the contents with the following:

    **Complete sample code for real-time Signaling**

    <CodeBlockTabs defaultValue="swift">
      <CodeBlockTabsList>
        <CodeBlockTabsTrigger value="swift">
          Swift
        </CodeBlockTabsTrigger>

        <CodeBlockTabsTrigger value="objc">
          Objective-C
        </CodeBlockTabsTrigger>
      </CodeBlockTabsList>

      <CodeBlockTab value="swift">
        ```swift
        import SwiftUI
        import Combine
        import AgoraRtmKit

        struct Message: Identifiable, Equatable {
            let id = UUID()
            let content: String
        }

        class ChatViewModel: NSObject, ObservableObject, AgoraRtmClientDelegate {
            var appid: String = <#YOUR APPID#>
            var token: String = <#YOUR TOKEN#>
            var rtmKit: AgoraRtmClientKit? = nil
            @Published var username: String = ""
            @Published var message: String = ""
            @Published var channel: String = ""
            @Published var messages: [Message] = []

            override init() {
                super.init()
                initializeEngine()
            }

            // Initialization the engine
            func initializeEngine() {
                if rtmKit == nil {
                    let config = AgoraRtmClientConfig(appId: appid)
                    rtmKit = try? AgoraRtmClientKit(config, delegate: self)
                }
            }

            // Log in to Signaling
            func login() {
                if rtmKit != nil {
                    addToMessageList(str: "RTM already logged in! Logout first!")
                    return
                }

                let config = AgoraRtmClientConfig(appId: appid, userId: username)
                rtmKit = try! AgoraRtmClientKit(config, delegate: self)

                rtmKit?.login(token, userId: username, completion: { response, error in
                    if let error = error {
                        self.addToMessageList(str: "Login failed. Error code: \(error.errorCode.rawValue), reason: \(error.reason)")
                    } else {
                        self.addToMessageList(str: "\(self.username) logged in successfully.")
                    }
                })
            }

            // Log out from the RTM server
            func logout() {
                guard let rtmKit = rtmKit else {
                    addToMessageList(str: "RTM is already logged out!")
                    return
                }

                rtmKit.logout()
                rtmKit.destroy()
                self.rtmKit = nil
                addToMessageList(str: "RTM logged out!")
            }

            // Subscribe to a channel
            func subscribe() {
                guard let rtmKit = rtmKit else { return }
                rtmKit.subscribe(channelName: channel, option: nil) { response, error in
                    if let error = error {
                        self.addToMessageList(str: "Subscribe to channel '\(self.channel)' failed. Error code: \(error.errorCode.rawValue), reason: \(error.reason)")
                    } else {
                        self.addToMessageList(str: "Subscribed to channel: \(self.channel) successfully.")
                    }
                }
            }

            // Unsubscribe from a channel
            func unsubscribe() {
                guard let rtmKit = rtmKit else { return }
                rtmKit.unsubscribe(channel) { response, error in
                    if let error = error {
                        self.addToMessageList(str: "Unsubscribe from channel '\(self.channel)' failed. Error code: \(error.errorCode.rawValue), reason: \(error.reason)")
                    } else {
                        self.addToMessageList(str: "Unsubscribed from channel: \(self.channel) successfully.")
                    }
                }
            }

            // Publish a message
            func sendMessage() {
                guard !message.isEmpty, let rtmKit = rtmKit else { return }
                rtmKit.publish(channelName: channel, message: message, option: nil) { response, error in
                    if let error = error {
                        self.addToMessageList(str: "Publish failed. Error code: \(error.errorCode.rawValue), reason: \(error.reason)")
                    } else {
                        self.addToMessageList(str: "Message published to channel: \(self.channel) successfully.")
                    }
                }
                message = ""
            }

            func addToMessageList(str: String) {
                messages.append(Message(content: str))
            }

            // AgoraRtmClientDelegate methods
            func rtmKit(_ rtmKit: AgoraRtmClientKit, didReceiveLinkStateEvent event: AgoraRtmLinkStateEvent) {
                addToMessageList(str: "RTM link state changed. Current state: \(event.currentState.rawValue), previous state: \(event.previousState.rawValue)")
            }

            func rtmKit(_ rtmKit: AgoraRtmClientKit, didReceiveMessageEvent event: AgoraRtmMessageEvent) {
                addToMessageList(str: "Message received. Channel: \(event.channelName), Publisher: \(event.publisher), Message: \(event.message.stringData!)")
            }
        }

        // User Interface
        struct ContentView: View {
            @StateObject private var viewModel = ChatViewModel()

            var body: some View {
                VStack {
                    TextField("Enter username", text: $viewModel.username)
                        .padding()
                        .textFieldStyle(RoundedBorderTextFieldStyle())
                        .font(.title)

                    HStack {
                        Button("Login") {
                            viewModel.login()
                        }

                        Button("Logout") {
                            viewModel.logout()
                        }
                    }

                    TextField("Channel name", text: $viewModel.channel)
                        .padding()
                        .textFieldStyle(RoundedBorderTextFieldStyle())
                        .font(.title)

                    HStack {
                        Button("Subscribe") {
                            viewModel.subscribe()
                        }

                        Button("Unsubscribe") {
                            viewModel.unsubscribe()
                        }
                    }

                    TextField("Enter message", text: $viewModel.message)
                        .padding()
                        .textFieldStyle(RoundedBorderTextFieldStyle())
                        .font(.title)

                    Button("Send") {
                        viewModel.sendMessage()
                    }
                    .buttonStyle(SendButtonStyle())

                    // Display messages
                    ScrollViewReader { scrollProxy in
                        List(viewModel.messages) { message in
                            Text(message.content)
                                .id(message.id)
                        }
                        .onChange(of: viewModel.messages) { _ in
                            if let lastMessage = viewModel.messages.last {
                                withAnimation {
                                    scrollProxy.scrollTo(lastMessage.id, anchor: .bottom)
                                }
                            }
                        }
                    }
                    .padding()
                }
                .padding()
            }
        }

        // Preview
        struct ContentView_Previews: PreviewProvider {
            static var previews: some View {
                ContentView()
            }
        }
        ```
      </CodeBlockTab>

      <CodeBlockTab value="objc">
        ```objc
        #import "ViewController.h"

        @interface ViewController ()<AgoraRtmClientDelegate>

        @property(nonatomic, strong) AgoraRtmClientKit* kit;

        @property NSString* appID;
        @property NSString* token;

        @property NSString* uid;
        @property NSString* peerID;
        @property NSString* channelID;
        @property NSString* peerMsg;
        @property NSString* channelMsg;

        @property NSString* text;
        @property NSMutableArray* textArray;

        - (void)AddMsgToRecord:(NSString*)text;

        @end

        @implementation ViewController

        - (void)viewDidLoad {
            [super viewDidLoad];
            // Enter your App ID
            self.appID = @"your_appid";
            self.MsgTextView.textColor = UIColor.blueColor;
            self.textArray = [[NSMutableArray alloc]init];
        }

        - (void)rtmKit:(AgoraRtmClientKit *)rtmKit didReceiveMessageEvent:(AgoraRtmMessageEvent *)event {
            NSLog(@"%@", self.text);
            self.text = [NSString stringWithFormat:@"receive message
        From channel: %@
        publisher:%@
        message:%@
        ", event.channelName, event.publisher, event.message.stringData];
            [self AddMsgToRecord:(self.text)];
        }

        // Add message to the UI TextView
        - (void)AddMsgToRecord:(NSString*)text {
            [self.textArray addObject:(self.text)];
            self.MsgTextView.text = [self.textArray componentsJoinedByString:(@"
        ")];
        }

        - (IBAction)Login:(id)sender {
            self.uid = self.UserIDTextField.text;
            // Enter your token
            self.token = @"your_token";

            AgoraRtmClientConfig* rtm_config = [[AgoraRtmClientConfig alloc] initWithAppId:_appID userId:_uid];

            NSError* initError = nil;
            _kit = [[AgoraRtmClientKit alloc] initWithConfig:rtm_config delegate:self error:&initError];
            if (initError != nil) {
                self.text = [NSString stringWithFormat:@"init error %@",initError];
                NSLog(@"%@", self.text);
                [self AddMsgToRecord:(self.text)];
            }
            // Log in to the RTM server
            [_kit loginByToken:self.token completion:^(AgoraRtmCommonResponse * _Nullable response, AgoraRtmErrorInfo * _Nullable errorInfo) {
                if (errorInfo.errorCode != AgoraRtmErrorOk) {
                    self.text = [NSString stringWithFormat:@"Login failed for user %@. Code: %ld", self.uid, (long)errorInfo.errorCode];
                    NSLog(@"%@", self.text);
                } else {
                    self.text = [NSString stringWithFormat:@"Login successful for user %@. Code: %ld", self.uid, (long)errorInfo.errorCode];
                    NSLog(@"%@", self.text);
                }
                [self AddMsgToRecord:(self.text)];
            }];
        }

        - (IBAction)Logout:(id)sender {
            if (_kit != nil) {
                // Log out from the RTM server
                [_kit logout:^(AgoraRtmCommonResponse * _Nullable response, AgoraRtmErrorInfo * _Nullable errorInfo) {
                    if (errorInfo == nil) {
                        self.text = [NSString stringWithFormat:@"Logout successful"];
                        NSLog(@"%@", self.text);
                        [_kit destroy];
                        _kit = nil;
                    } else {
                        self.text = [NSString stringWithFormat:@"Logout failed. Code: %ld", (long)errorInfo.errorCode];
                        NSLog(@"%@", self.text);
                    }

                    [self AddMsgToRecord:(self.text)];
                }];
            }
        }

        - (IBAction)SubscribeChannel:(id)sender {
            self.channelID = self.ChannelIDTextField.text;
            if (_kit != nil) {
                // Subscribe to a channel
                [_kit subscribeWithChannel:self.channelID option:nil completion:^(AgoraRtmCommonResponse * _Nullable response, AgoraRtmErrorInfo * _Nullable errorInfo) {
                    if (errorInfo == nil) {
                        self.text = [NSString stringWithFormat:@"Successfully subscribed to channel %@", self.channelID];
                        NSLog(@"%@", self.text);
                    } else {
                        self.text = [NSString stringWithFormat:@"Failed to subscribe to channel %@. Code: %ld", self.channelID, (long)errorInfo.errorCode];
                        NSLog(@"%@", self.text);
                    }

                    [self AddMsgToRecord:(self.text)];
                }];
            }
        }

        - (IBAction)UnsubscribeChannel:(id)sender {
            if (_kit == nil) return;
            // Unsubscribe from a channel
            [_kit unsubscribeWithChannel:self.channelID completion:^(AgoraRtmCommonResponse * _Nullable response, AgoraRtmErrorInfo * _Nullable errorInfo) {
                if (errorInfo == nil) {
                    self.text = [NSString stringWithFormat:@"Successfully unsubscribed from channel"];
                } else {
                    self.text = [NSString stringWithFormat:@"Failed to unsubscribe from channel. Code: %ld", (long)errorInfo.errorCode];
                }

                [self AddMsgToRecord:(self.text)];
            }];
        }

        - (IBAction)SendMessageToMessageChannel:(id)sender {
            self.channelID = self.ChannelIDTextField.text;
            self.channelMsg = self.GroupMsgTextField.text;
            // Publish a message
            [_kit publish:self.channelID message:self.channelMsg option:nil completion:^(AgoraRtmCommonResponse * _Nullable response, AgoraRtmErrorInfo * _Nullable errorInfo) {
                if (errorInfo == nil) {
                    self.text = [NSString stringWithFormat:@"Message sent to channel %@ : %@", self.channelID, self.channelMsg];
                } else {
                    self.text = [NSString stringWithFormat:@"Message failed to send to channel %@ : %@. ErrorCode: %ld", self.channelID, self.channelMsg, (long)errorInfo.errorCode];
                }

                [self AddMsgToRecord:(self.text)];
            }];
        }

        @end
        ```
      </CodeBlockTab>
    </CodeBlockTabs>

    Follow the implementation steps to understand the core API calls in the sample code or use the snippets in your own code.

    ### Declare the variables you need [#declare-the-variables-you-need]

    To connect to Signaling from your project and send messages to a channel, declare variables to hold the app ID, token, user ID, and channel name:

    <CodeBlockTabs defaultValue="swift">
      <CodeBlockTabsList>
        <CodeBlockTabsTrigger value="swift">
          Swift
        </CodeBlockTabsTrigger>

        <CodeBlockTabsTrigger value="objc">
          Objective-C
        </CodeBlockTabsTrigger>
      </CodeBlockTabsList>

      <CodeBlockTab value="swift">
        ```swift
        var appid: String = <#YOUR APPID#>
        var token: String = <#YOUR TOKEN#>
        var rtmKit: AgoraRtmClientKit? = nil

        @Published var username: String = ""
        @Published var message: String = ""
        @Published var channel: String = ""
        @Published var messages: [Message] = []
        ```
      </CodeBlockTab>

      <CodeBlockTab value="objc">
        ```objc
        #import "ViewController.h"

        @interface ViewController ()<AgoraRtmClientDelegate>

        @property(nonatomic, strong)AgoraRtmClientKit* kit;

        @property NSString* appID;
        @property NSString* token;

        @property NSString* uid;
        @property NSString* peerID;
        @property NSString* channelID;
        @property NSString* peerMsg;
        @property NSString* channelMsg;

        @property NSString* text;
        @property NSMutableArray* textArray;
        ```
      </CodeBlockTab>
    </CodeBlockTabs>

    ### Initialize the Signaling engine [#initialize-the-signaling-engine-1]

    Before calling any other Signaling SDK API, initialize an `AgoraRtmClientKit` object instance.

    <CodeBlockTabs defaultValue="swift">
      <CodeBlockTabsList>
        <CodeBlockTabsTrigger value="swift">
          Swift
        </CodeBlockTabsTrigger>

        <CodeBlockTabsTrigger value="objc">
          Objective-C
        </CodeBlockTabsTrigger>
      </CodeBlockTabsList>

      <CodeBlockTab value="swift">
        ```swift
        // Initialization the engine
        func initializeEngine() {
            if rtmKit == nil {
                let config = AgoraRtmClientConfig(appId: appid)
                rtmKit = try? AgoraRtmClientKit(config, delegate: self)
            }
        }
        ```
      </CodeBlockTab>

      <CodeBlockTab value="objc">
        ```objc
        self.uid = self.UserIDTextField.text;

        // Set engine configuration
        AgoraRtmClientConfig*  rtm_config = [[AgoraRtmClientConfig alloc] initWithAppId:_appID userId:_uid];

        // Initialize the engine
        NSError* initError = nil;
        _kit = [[AgoraRtmClientKit alloc] initWithConfig:rtm_config delegate:self error:&initError];
        if (initError != nil) {
            self.text = [NSString stringWithFormat:@"init error %@",initError];
            NSLog(@"%@", self.text);
            [self AddMsgToRecord:(self.text)];
        }
        ```
      </CodeBlockTab>
    </CodeBlockTabs>

    ### Add an event listener [#add-an-event-listener-1]

    The event listener enables you to implement the processing logic in response to Signaling events. Use the following code to handle event notifications or display received messages:

    <CodeBlockTabs defaultValue="swift">
      <CodeBlockTabsList>
        <CodeBlockTabsTrigger value="swift">
          Swift
        </CodeBlockTabsTrigger>

        <CodeBlockTabsTrigger value="objc">
          Objective-C
        </CodeBlockTabsTrigger>
      </CodeBlockTabsList>

      <CodeBlockTab value="swift">
        ```swift
        // Add the event listener
        func rtmKit(_ rtmKit: AgoraRtmClientKit, didReceiveLinkStateEvent event: AgoraRtmLinkStateEvent) {
            addToMessageList(str: "Signaling link state change current state is: \(event.currentState.rawValue) previous state is :\(event.previousState.rawValue)")
        }

        func rtmKit(_ rtmKit: AgoraRtmClientKit, didReceiveMessageEvent event: AgoraRtmMessageEvent) {
            addToMessageList(str: "Message received.
         channel: \(event.channelName), publisher: \(event.publisher),  message content: \(event.message.stringData!)")
        }
        ```
      </CodeBlockTab>

      <CodeBlockTab value="objc">
        ```objc
        // Add the event listener
        - (void)rtmKit:(AgoraRtmClientKit *)rtmKit didReceiveMessageEvent:(AgoraRtmMessageEvent *)event {
            NSLog(@"%@", self.text);
            self.text = [NSString stringWithFormat:@"receive message
        From channel: %@
        publisher:%@
        message:%@
        ",event.channelName,event.publisher, event.message.stringData];
            [self AddMsgToRecord:(self.text)];
        }
        ```
      </CodeBlockTab>
    </CodeBlockTabs>

    ### Log in to Signaling [#log-in-to-signaling-1]

    To connect to Signaling and access Signaling network resources, such as sending messages, and subscribing to channels, login to Signaling server.

    During a login operation, the client attempts to establish a connection with Signaling. Once the connection is established, the client transmits heartbeat information to the Signaling server at fixed intervals to keep the client active until the client actively logs out or is disconnected. The connection is interrupted when timeout occurs. During this period, users may freely access the Signaling network resources subject to their own permissions and usage restrictions.

    <CodeBlockTabs defaultValue="swift">
      <CodeBlockTabsList>
        <CodeBlockTabsTrigger value="swift">
          Swift
        </CodeBlockTabsTrigger>

        <CodeBlockTabsTrigger value="objc">
          Objective-C
        </CodeBlockTabsTrigger>
      </CodeBlockTabsList>

      <CodeBlockTab value="swift">
        ```swift
        // Log in to the Signaling server
        func login() {
            if rtmKit != nil {
                addToMessageList(str: "RTM already logged in! Logout first!")
                return
            }

            let config = AgoraRtmClientConfig(appId: appid, userId: username)
            rtmKit = try! AgoraRtmClientKit(config, delegate: self)

            rtmKit?.login(token, userId: username, completion: { response, error in
                if let error = error {
                    self.addToMessageList(str: "Login failed. Error code: \(error.errorCode.rawValue), reason: \(error.reason)")
                } else {
                    self.addToMessageList(str: "\(self.username) logged in successfully.")
                }
            })
        }
        ```
      </CodeBlockTab>

      <CodeBlockTab value="objc">
        ```objc
        // Log in to signaling
        [_kit loginByToken:self.token completion:^(AgoraRtmCommonResponse * _Nullable response, AgoraRtmErrorInfo * _Nullable errorInfo) {
            if (errorInfo.errorCode != AgoraRtmErrorOk){
                self.text = [NSString stringWithFormat:@"Login failed for user %@. Code: %ld",self.uid, (long)errorInfo.errorCode];
                NSLog(@"%@", self.text);
            } else {
                NSLog(@"%@", self.text);
                self.text = [NSString stringWithFormat:@"Login successful for user %@. Code: %ld",self.uid, (long)errorInfo.errorCode];
            }
            [self AddMsgToRecord:(self.text)];
        }];
        ```
      </CodeBlockTab>
    </CodeBlockTabs>

    Use the `login` return value, or listen to the `didReceiveLinkStateEvent` event notification to confirm that login is successful or obtain the error code and cause of login failure. When performing a login operation, the client's network connection state is `connecting`. After a successful login, the state is updated to `Connected`.

    <CalloutContainer type="info">
      <CalloutTitle>
        Best practice
      </CalloutTitle>

      <CalloutDescription>
        To continuously monitor the network connection state of the client, best practice is to continue to listen for `AgoraRtmLinkState` event notifications throughout the life cycle of the application. For further details, see [Event listeners](/en/realtime-media/rtm/build/send-and-receive-messages/add-event-listener).
      </CalloutDescription>
    </CalloutContainer>

    Use the `loginByToken` return value, or listen to the `connectionChangedToState` event notification to confirm that login is successful or obtain the error code and cause of login failure. When performing a login operation, the client's network connection state is `AgoraRtmClientConnectionStateConnecting`. After a successful login, the state is updated to `AgoraRtmClientConnectionStateConnected`.

    <CalloutContainer type="info">
      <CalloutTitle>
        Best practice
      </CalloutTitle>

      <CalloutDescription>
        To continuously monitor the network connection state of the client, best practice is to continue to listen for `connectionChangedToState` event notifications throughout the life cycle of the application. For further details, see [Event listeners](/en/realtime-media/rtm/build/send-and-receive-messages/add-event-listener).
      </CalloutDescription>
    </CalloutContainer>

    <CalloutContainer type="warning">
      <CalloutDescription>
        After a user successfully logs into Signaling, the application's Peak Connection Usage (PCU) increases, which affects your billing data.
      </CalloutDescription>
    </CalloutContainer>

    ### Send a message [#send-a-message]

    To distribute a message to all subscribers of a message channel, call `publish`. The following code sends a string type message.

    <CodeBlockTabs defaultValue="swift">
      <CodeBlockTabsList>
        <CodeBlockTabsTrigger value="swift">
          Swift
        </CodeBlockTabsTrigger>

        <CodeBlockTabsTrigger value="objc">
          Objective-C
        </CodeBlockTabsTrigger>
      </CodeBlockTabsList>

      <CodeBlockTab value="swift">
        ```swift
        // Publish a message
        func sendMessage() {
            guard !message.isEmpty, let rtmKit = rtmKit else { return }
            rtmKit.publish(channelName: channel, message: message, option: nil) { response, error in
                if let error = error {
                    self.addToMessageList(str: "Publish failed. Error code: (error.errorCode.rawValue), reason: (error.reason)")
                } else {
                    self.addToMessageList(str: "Message published to channel: (self.channel) successfully.")
                }
            }
            message = ""
        }
        ```
      </CodeBlockTab>

      <CodeBlockTab value="objc">
        ```objc
        // Send a message
        [_kit publish:self.channelID message:self.channelMsg option:nil completion:^(AgoraRtmCommonResponse * _Nullable response, AgoraRtmErrorInfo * _Nullable errorInfo) {
            if (errorInfo == nil) {
                self.text = [NSString stringWithFormat:@"Message sent to channel %@ : %@", self.channelID, self.channelMsg];
            } else {
                self.text = [NSString stringWithFormat:@"Message failed to send to channel %@ : %@ ErrorCode: %ld", self.channelID, self.channelMsg, (long)errorInfo.errorCode];
            }

            [self AddMsgToRecord:(self.text)];
        }];
        ```
      </CodeBlockTab>
    </CodeBlockTabs>

    ### Subscribe and unsubscribe [#subscribe-and-unsubscribe-1]

    To receive messages sent to a channel, subscribe to channel messages:

    <CodeBlockTabs defaultValue="swift">
      <CodeBlockTabsList>
        <CodeBlockTabsTrigger value="swift">
          Swift
        </CodeBlockTabsTrigger>

        <CodeBlockTabsTrigger value="objc">
          Objective-C
        </CodeBlockTabsTrigger>
      </CodeBlockTabsList>

      <CodeBlockTab value="swift">
        ```swift
        // Subscribe to a channel
        func subscribe() {
            guard let rtmKit = rtmKit else { return }
            rtmKit.subscribe(channelName: channel, option: nil) { response, error in
                if let error = error {
                    self.addToMessageList(str: "Subscribe to channel '(self.channel)' failed. Error code: (error.errorCode.rawValue), reason: (error.reason)")
                } else {
                    self.addToMessageList(str: "Subscribed to channel: (self.channel) successfully.")
                }
            }
        }
        ```
      </CodeBlockTab>

      <CodeBlockTab value="objc">
        ```objc
        // Subscribe to a channel
        [_kit subscribeWithChannel:self.channelID option:nil completion:^(AgoraRtmCommonResponse * _Nullable response, AgoraRtmErrorInfo * _Nullable errorInfo) {
            if(errorInfo == nil) {
                self.text = [NSString stringWithFormat:@"Successfully subscribe channel %@",self.channelID];
                NSLog(@"%@", self.text);
            } else {
                self.text = [NSString stringWithFormat:@"Failed to subscribe channel %@ Code: %ld",self.channelID, (long)errorInfo.errorCode];
                NSLog(@"%@", self.text);
            }

            [self AddMsgToRecord:(self.text)];
        }];
        ```
      </CodeBlockTab>
    </CodeBlockTabs>

    When you no longer need to receive messages from a channel, unsubscribe from the channel:

    <CodeBlockTabs defaultValue="swift">
      <CodeBlockTabsList>
        <CodeBlockTabsTrigger value="swift">
          Swift
        </CodeBlockTabsTrigger>

        <CodeBlockTabsTrigger value="objc">
          Objective-C
        </CodeBlockTabsTrigger>
      </CodeBlockTabsList>

      <CodeBlockTab value="swift">
        ```swift
        // Unsubscribe from a channel
        func unsubscribe() {
            guard let rtmKit = rtmKit else { return }
            rtmKit.unsubscribe(channel) { response, error in
                if let error = error {
                    self.addToMessageList(str: "Unsubscribe from channel '(self.channel)' failed. Error code: (error.errorCode.rawValue), reason: (error.reason)")
                } else {
                    self.addToMessageList(str: "Unsubscribed from channel: (self.channel) successfully.")
                }
            }
        }
        ```
      </CodeBlockTab>

      <CodeBlockTab value="objc">
        ```objc
        // Unsubscribe from a channel
        [_kit unsubscribeWithChannel:self.channelID completion:^(AgoraRtmCommonResponse * _Nullable response, AgoraRtmErrorInfo * _Nullable errorInfo) {
            if (errorInfo == nil){
                self.text = [NSString stringWithFormat:@"Leave channel successful"];
            } else {
                self.text = [NSString stringWithFormat:@"Failed to leave channel Code: %ld", (long)errorInfo.errorCode];
            }

            [self AddMsgToRecord:(self.text)];
        }];
        ```
      </CodeBlockTab>
    </CodeBlockTabs>

    For more information about sending and receiving messages see [Message channels](/en/realtime-media/rtm/build/work-with-channels/message-channel) and [Stream channels](/en/realtime-media/rtm/build/work-with-channels/stream-channel).

    ### Log out of Signaling [#log-out-of-signaling-1]

    When a user no longer needs to use Signaling, call `logout`. Logging out means closing the connection between the client and Signaling. The user is automatically logged out or unsubscribed from all message and stream channels. Other users in the channel receive a `didReceivePresenceEvent` notification of the user leaving the channel.

    <CodeBlockTabs defaultValue="swift">
      <CodeBlockTabsList>
        <CodeBlockTabsTrigger value="swift">
          Swift
        </CodeBlockTabsTrigger>

        <CodeBlockTabsTrigger value="objc">
          Objective-C
        </CodeBlockTabsTrigger>
      </CodeBlockTabsList>

      <CodeBlockTab value="swift">
        ```swift
        // Log out from the Signaling server
        func logout() {
            guard let rtmKit = rtmKit else {
                addToMessageList(str: "Signaling is already logged out!")
                return
            }
            rtmKit.logout()
            rtmKit.destroy()
            self.rtmKit = nil
            addToMessageList(str: "Signaling logged out!")
        }
        ```
      </CodeBlockTab>

      <CodeBlockTab value="objc">
        ```objc
        // Log out of Signaling
        [_kit logout:^(AgoraRtmCommonResponse * _Nullable response, AgoraRtmErrorInfo * _Nullable errorInfo) {
            if (errorInfo == nil){
                self.text = [NSString stringWithFormat:@"Logout successful"];
                NSLog(@"%@", self.text);
                [_kit destroy];
                _kit = nil;
            } else {
                self.text = [NSString stringWithFormat:@"Logout failed. Code: %ld",(long)errorInfo.errorCode];
                NSLog(@"%@", self.text);
            }

            [self AddMsgToRecord:(self.text)];
        }];

        // Clean up
        [_kit destroy];
        _kit = nil;
        ```
      </CodeBlockTab>
    </CodeBlockTabs>

    ## Test Signaling [#test-signaling-1]

    Take the following steps to test the sample code:

    1. Use the [Token Builder](https://agora-token-generator-demo.vercel.app/) to generate a Signaling token:

       1. Select Signaling from the Agora products dropdown.
       2. Fill in your app ID and app certificate from [Agora Console](https://console.agora.io/v2). Leave the remaining fields blank.
       3. Click **Generate Token**.

    2. In your code, replace the values for `appID` and `token` with your app ID and the generated token.

    3. Choose the device or simulator you want to run the app on from the device selection dropdown menu in the Xcode toolbar.

    4. Click **Run** in the Xcode toolbar, or press Command + R on your keyboard. Xcode builds your project and launches the app on the selected device or simulator.

    5. Use the device as the receiving end and perform the following operations:

       1. Enter your **User ID** and click **Login**.
       2. Enter the **Channel name** and click **Subscribe** .

    6. On a second device, repeat the previous steps to install and launch the app. Use this device as the sending end.

       1. Enter a different **User ID** and click **Login**.
       2. Enter the same **Channel name**.
       3. Type a message and click **Publish message**.

    7. Swap the sending and receiving device roles and repeat the previous steps.

       Congratulations! You have successfully integrated Signaling into your project.

    ## Reference [#reference-1]

    This section contains content that completes the information on this page, or points you to documentation that explains other aspects to this product.

    ### Token authentication [#token-authentication-1]

    In this guide you retrieve a temporary token from a [Token Builder](https://agora-token-generator-demo.vercel.app/). To understand how to create an authentication server for development purposes, see [Secure authentication with tokens](./build/connect-and-authenticate/authentication-workflow).

    ### Sample projects [#sample-projects-1]

    Agora provides the following open source sample projects on GitHub for your reference.

    * [Quickstart - Swift](https://github.com/AgoraIO/RTM2/tree/main/Agora-RTM2-QuickStart-iOS-Swift)
    * [Quickstart - Objective-C](https://github.com/AgoraIO/RTM2/tree/main/Agora-RTM2-QuickStart-iOS-Objective-C)

    ### API reference [#api-reference-1]

    **Swift**

    * [API reference](/en/api-reference/api-ref/signaling)
    * [Event listeners](./build/send-and-receive-messages/add-event-listener)

    **Objective-C**

    * [API reference](/en/api-reference/api-ref/signaling)
    * [Event listeners](./build/send-and-receive-messages/add-event-listener)

    <_PlatformProcessedMarker close="true" />
  </_PlatformPanel>

  <_PlatformPanel platform="web">
    <_PlatformProcessedMarker groupMode="structured" canonicalPlatform="web" platform="web" />

    ## Understand the tech [#understand-the-tech-2]

    To use Signaling features in your app, you initialize a Signaling client instance and add event listeners. To connect to Signaling, you login using an authentication token. To send a message to a message channel, you publish the message. Signaling creates a channel when a user subscribes to it. To receive messages other users publish to a channel, your app listens for events.

    To create a pub/sub session for Signaling, implement the following steps in your app:

    <Accordions>
      <Accordion title="Signaling workflow">
        ![Signaling workflow for Web](https://assets-docs.agora.io/images/signaling/get-started-workflow-javascript.svg)
      </Accordion>
    </Accordions>

    ## Prerequisites [#prerequisites-2]

    To implement the code presented on this page you need to have:

    * An Agora [account](/en/realtime-media/rtm/manage-agora-account) and [project](/en/realtime-media/rtm/manage-agora-account).

    * [Enabled Signaling](/en/realtime-media/rtm/manage-agora-account) in Agora Console

    * A [supported browser](/en/realtime-media/rtm/reference/supported-platforms).

    * A JavaScript package manager such as [npm](https://www.npmjs.com/package/npm).

    * Ensure that a firewall is not blocking your network communication.

    <CalloutContainer type="info">
      <CalloutDescription>
        Signaling 2.x is an enhanced version compared to 1.x with a wide range of new features. It follows a new pricing structure. See [Pricing](/en/realtime-media/rtm/reference/pricing) for details.
      </CalloutDescription>
    </CalloutContainer>

    ## Project setup [#project-setup-2]

    ### Create a project [#create-a-project-2]

    To create a folder for your project, and an `index.html` file for your app, execute the following commands in the terminal:

    ```bash
    mkdir HelloWorld
    cd HelloWorld
    touch index.html
    ```

    ### Integrate the SDK [#integrate-the-sdk-2]

    Use either of the following methods to integrate Signaling SDK into your project.

    **Using CDN**

    1. [Download](/en/api-reference/sdks?product=signaling\&platform=web) the latest version of Signaling SDK for Web.

    2. Add the following code to your project to reference the SDK:

       ```js
       <script src="path_to_sdk/agora-rtm.x.y.z.min.js"></script>
       ```

       Replace `x.y.z` with the specific SDK version number, such as 2.2.0. To get the latest version number, check the [Release notes](/en/realtime-media/rtm/reference/release-notes).

    **Using npm**

    1. Install the SDK using npm

       ```js
       npm install agora-rtm-sdk
       ```

    2. Import `AgoraRTM` from the SDK

       ```js
       import AgoraRTM from 'agora-rtm-sdk';
       ```

    ## Implement Signaling [#implement-signaling-2]

    A complete code sample that implements the basic features of Signaling is presented here for your reference. To use the sample code, copy the following lines into the `index.html` file:

    **Complete sample code for Signaling**

    ```js
    <!DOCTYPE html>
    <html lang="en">
    <head>
      <meta charset="UTF-8">
      <meta name="viewport" content="width=device-width, initial-scale=1.0">
      <title>Text Display App</title>
      <style>
        #container {
          width: 800px;
          margin: 0 auto;
          padding: 20px;
        }
        #textDisplay {
          width: 100%;
          height: 800px;
          border: 1px solid #b0b0b0;
          margin-bottom: 20px;
          overflow: auto;
          text-align: left;
          padding: 10px;
          box-sizing: border-box;
        }
        #inputContainer {
          display: flex;
          align-items: center;
        }
        #textInput {
          width: calc(100% - 100px);
          padding: 5px;
          margin-right: 10px;
        }
        #submitButton {
          width: 90px;
          padding: 5px;
        }
      </style>
      <script src="./AgoraRTM-production.js"></script>
    </head>
    <body>
      <script>
        const { RTM } = AgoraRTM;
        // Fill in the App ID of your project.
        const appId = "your_appId";
        // Fill in your user ID.
        const userId = "your_userId";
        // Fill in your channel name.
        const msChannelName = "Chat_room";
        const buttonClick = () => {
          var input = document.getElementById("textInput");
          publishMessage(input.value);
          input.value = '';
        }
        const setupRTM = async () => {
          // Initialize the RTM client.
          try {
            rtm = new RTM(appId, userId);
          } catch (status) {
            console.log("Error");
            console.log(status);
          }
          // Add the event listener.
          // Message event handler.
          rtm.addEventListener("message", event => {
            showMessage(event.publisher, event.message);
          });
          // Presence event handler.
          rtm.addEventListener("presence", event => {
            if (event.eventType === "SNAPSHOT") {
              showMessage("INFO", "I Join");
            }
            else {
              showMessage("INFO", event.publisher + " is " + event.eventType);
            }
          });
          // Connection state changed event handler.
          rtm.addEventListener("status", event => {
            // The current connection state.
            const currentState = event.state;
            // The reason why the connection state changes.
            const changeReason = event.reason;
            showMessage("INFO", JSON.stringify(event));
          });
          // Log in the RTM server.
          try {
            const result = await rtm.login({  token: 'your_token' });
            console.log(result);
          } catch (status) {
            console.log(status);
          }
          // Subscribe to a channel.
          try {
            const result = await rtm.subscribe(msChannelName);
            console.log(result);
          } catch (status) {
            console.log(status);
          }
        }
        const publishMessage = async (message) => {
          const payload = { type: "text", message: message };
          const publishMessage = JSON.stringify(payload);
          const publishOptions = { channelType: 'MESSAGE'}
          try {
            const result = await rtm.publish(msChannelName, publishMessage, publishOptions);
            showMessage(userId, publishMessage);
            console.log(result);
          } catch (status) {
            console.log(status);
          }
        }
        const showMessage = (user, msg) => {
          // Get text from the text box.
          const inputText = textInput.value;
          const newText = document.createTextNode(user + ": " + msg);
          const newLine = document.createElement("br");
          textDisplay.appendChild(newText);
          textDisplay.appendChild(newLine);
        }
        window.onload = setupRTM;
      </script>
      <div id="container">
        <h1>Hello RTM !</h1>
        <div>
          <div id="textDisplay"></div>
        </div>
        <div id="inputContainer">
          <input type="text" id="textInput" placeholder="Enter text">
          <button id="submitButton" onclick="buttonClick()"> Send </button>
        </div>
      </div>
    </body>
    </html>
    ```

    Follow the implementation steps to understand the core API calls in the sample code or use the snippets in your own code.

    ### Initialize the Signaling engine [#initialize-the-signaling-engine-2]

    Before calling any other Signaling SDK API, initialize an `RTM` object instance.

    ```js
    try {
      const rtm = new RTM(appId, userId);
    } catch (status) {
      console.log("Error");
      console.log(status);
    }
    ```

    ### Add an event listener [#add-an-event-listener-2]

    The event listener enables you to implement the processing logic in response to Signaling events. Use the following code to handle event notifications or display received messages:

    ```js
    // Message event handler.
    rtm.addEventListener("message", event => {
      showMessage(event.publisher, event.message);
    });

    // Presence event handler.
    rtm.addEventListener("presence", event => {
      if (event.eventType === "SNAPSHOT") {
        showMessage("INFO", "I Join");
      }
      else {
        showMessage("INFO", event.publisher + " is " + event.eventType);
      }
    });

    // Connection state changed event handler.
    rtm.addEventListener("status", event => {
      // The current connection state.
      const currentState = event.state;
      // The reason why the connection state changes.
      const changeReason = event.reason;
      showMessage("INFO", JSON.stringify(event));
    });
    ```

    ### Log in to Signaling [#log-in-to-signaling-2]

    To connect to Signaling and access Signaling network resources, such as sending messages, and subscribing to channels, call `login`.

    During a login operation, the client attempts to establish a connection with Signaling. Once the connection is established, the client transmits heartbeat information to the Signaling server at fixed intervals to keep the client active until the client actively logs out or is disconnected. The connection is interrupted when timeout occurs. During this period, users may freely access the Signaling network resources subject to their own permissions and usage restrictions.

    ```js
    // Log in to Signaling
    try {
      const result = await rtm.login({ token: 'your_token' });
      console.log(result);
    } catch (status) {
      console.log(status);
    }
    ```

    Use the `login` return value, or listen to the `status` event notification to confirm that login is successful or obtain the error code and cause of login failure. When performing a login operation, the client's network connection state is `CONNECTING`. After a successful login, the state is updated to `CONNECTED`.

    <CalloutContainer type="info">
      <CalloutTitle>
        Best practice
      </CalloutTitle>

      <CalloutDescription>
        To continuously monitor the network connection state of the client, best practice is to continue to listen for `status` event notifications throughout the life cycle of the application. For further details, see [Event listeners](/en/realtime-media/rtm/build/send-and-receive-messages/add-event-listener).
      </CalloutDescription>
    </CalloutContainer>

    <CalloutContainer type="warning">
      <CalloutDescription>
        After a user successfully logs into Signaling, the application's PCU increases, which affects your billing data.
      </CalloutDescription>
    </CalloutContainer>

    ### Send a message [#send-a-message-1]

    To distribute a message to all subscribers of a message channel, call `publish`. The following code sends a string type message.

    ```js
    // Send a message to a channel
    const publishMessage = async (message) => {
      const payload = { type: "text", message: message };
      const publishMessage = JSON.stringify(payload);
      const publishOptions = { channelType: 'MESSAGE'}
      try {
        const result = await rtm.publish(msChannelName, publishMessage, publishOptions);
        showMessage(userId, publishMessage);
        console.log(result);
      } catch (status) {
        console.log(status);
      }
    }
    ```

    <CalloutContainer type="info">
      <CalloutDescription>
        Before calling `publish` to send a message, serialize the message payload as a string.
      </CalloutDescription>
    </CalloutContainer>

    ### Subscribe and unsubscribe [#subscribe-and-unsubscribe-2]

    To receive messages sent to a channel, call `subscribe` to subscribe to channel messages:

    ```js
    // Subscribe to a channel
    try {
      const result = await rtm.subscribe(msChannelName);
      console.log(result);
    } catch (status) {
      console.log(status);
    }
    ```

    When you no longer need to receive messages from a channel, call `unsubscribe` to unsubscribe from the channel:

    ```js
    // Unsubscribe from a channel
    try {
        const result = await rtm.unsubscribe(msChannelName);
        console.log(result);
    } catch (status) {
        console.log(status);
    }

    ```

    For more information about sending and receiving messages see [Message channels](/en/realtime-media/rtm/build/work-with-channels/message-channel) and [Stream channels](/en/realtime-media/rtm/build/work-with-channels/stream-channel).

    ### Log out of Signaling [#log-out-of-signaling-2]

    When a user no longer needs to use Signaling, call `logout`. Logging out means closing the connection between the client and Signaling. The user is automatically logged out or unsubscribed from all message and stream channels. Other users in the channel receive a `presence` notification of the user leaving the channel.

    ```js
    // Logout of Signaling
    try {
        const result = await rtm.logout();
    } catch (status) {
        const { operation, reason, errorCode } = status;
        console.log(`${operation} failed, the error code is ${errorCode}, because of: ${reason}.`);
    }
    ```

    ## Test Signaling [#test-signaling-2]

    Take the following steps to test the sample code:

    1. Use the [Token Builder](https://agora-token-generator-demo.vercel.app/) to generate a Signaling token:

       1. Select Signaling from the Agora products dropdown.
       2. Fill in your app ID and app certificate from [Agora Console](https://console.agora.io/v2). Leave the remaining fields blank.
       3. Click **Generate Token**.

    2. In `index.html`, replace `your_appid` and `your_token` values in the code with your project's app ID and the generated token.

    3. Update the value for the `userId` with an integer value.

    4. Save the file and run it in your browser.

    5. Make a copy of the project. Use the same `app_id` but a different `userId`. Launch another instance of the page in a separate browser tab.

    6. Type a message in the text input box in either instance and click the **Send** button. The message is displayed in the other app instance.

    7. Swap the sending and receiving instances and send another message.

       Congratulations! You have successfully integrated Signaling into your project.

    ## Reference [#reference-2]

    This section contains content that completes the information on this page, or points you to documentation that explains other aspects to this product.

    ### Token authentication [#token-authentication-2]

    In this guide you retrieve a temporary token from a [Token Builder](https://agora-token-generator-demo.vercel.app/). To understand how to create an authentication server for development purposes, see [Secure authentication with tokens](./build/connect-and-authenticate/authentication-workflow).

    ### Sample project [#sample-project]

    Explore and experiment with the [Signaling Online demo](https://digitallysavvy.github.io/RTM2/). To download and customize the source code, refer to the [GitHub repository](https://github.com/digitallysavvy/RTM2).

    ### API reference [#api-reference-2]

    * [API reference](/en/api-reference/api-ref/signaling)
    * [Event listeners](./build/send-and-receive-messages/add-event-listener)

    <_PlatformProcessedMarker close="true" />
  </_PlatformPanel>

  <_PlatformPanel platform="flutter">
    <_PlatformProcessedMarker groupMode="structured" canonicalPlatform="web" platform="flutter" />

    ## Understand the tech [#understand-the-tech-3]

    To use Signaling features in your app, you initialize a Signaling client instance and add event listeners. To connect to Signaling, you login using an authentication token. To send a message to a message channel, you publish the message. Signaling creates a channel when a user subscribes to it. To receive messages other users publish to a channel, your app listens for events.

    To create a pub/sub session for Signaling, implement the following steps in your app:

    <Accordions>
      <Accordion title="Signaling workflow">
        ![Signaling workflow for Flutter](https://assets-docs.agora.io/images/signaling/get-started-workflow-flutter.svg)
      </Accordion>
    </Accordions>

    ## Prerequisites [#prerequisites-3]

    To implement the code presented on this page you need to have:

    * An Agora [account](/en/realtime-media/rtm/manage-agora-account) and [project](/en/realtime-media/rtm/manage-agora-account).

    * [Enabled Signaling](/en/realtime-media/rtm/manage-agora-account) in Agora Console

    * [Flutter](https://docs.flutter.dev/get-started/install) 2.0.0 or higher

    * Dart 2.15.1 or higher

    * [Android Studio](https://developer.android.com/studio), IntelliJ, VS Code, or any other IDE that supports Flutter, see [Set up an editor](https://docs.flutter.dev/get-started/editor).

    * If your target platform is iOS:
      * Xcode on macOS (latest version recommended)
      * A physical iOS device
      * iOS version 12.0 or later

    * If your target platform is Android:
      * Android Studio on macOS or Windows (latest version recommended)
      * An Android emulator or a physical Android device.

    * If you are developing a desktop application for Windows, macOS or Linux, make sure your development device meets the [Flutter desktop development requirements](https://docs.flutter.dev/development/platform-integration/desktop).

    * Ensure that a firewall is not blocking your network communication.

    <CalloutContainer type="info">
      <CalloutDescription>
        Signaling 2.x is an enhanced version compared to 1.x with a wide range of new features. It follows a new pricing structure. See [Pricing](/en/realtime-media/rtm/reference/pricing) for details.
      </CalloutDescription>
    </CalloutContainer>

    ## Project setup [#project-setup-3]

    To create a new Flutter project, follow these steps:

    1. Open a terminal and execute the following command:

       ```bash
       flutter create sdk_quickstart
       ```

    2. Open the project in your IDE. Add the Signaling SDK dependency to `pubspec.yaml` under `dependencies`:

       ```yaml
       dependencies:
       # Add the Signaling SDK dependency, use the latest version number
         agora_rtm: ^2.2.1
       ```

       <CalloutContainer type="info">
         <CalloutDescription>
           To integrate Signaling SDK version 2.2.1 or above, and Video SDK version 4.3.0 or above at the same time, refer to [handle integration issues](/en/api-reference/faq/integration/rtm2_rtc_integration_issue).
         </CalloutDescription>
       </CalloutContainer>

    3. To install the dependencies, open the terminal and execute the following command in the project path:

       ```bash
       flutter pub get
       ```

    4. To create a basic template for your project, open `main.dart`, delete the contents of the file, and paste the following code:

       ```dart
       import 'package:agora_rtm/agora_rtm.dart';
       import 'package:flutter/material.dart';
       import 'dart:convert';

       void main() async {
           // Wait for Widgets to be initialized.
           WidgetsFlutterBinding.ensureInitialized();

           // Create a Signaling client instance

           // Login to Signaling

           // Subscribe to a channel

           // Publish messages

           // Unsubscribe from a channel

           // Logout from Signaling

       }
       ```

    ## Implement Signaling [#implement-signaling-3]

    A complete code sample that implements the basic features of Signaling is presented here for your reference. To use the sample code, copy the following lines into the `lib/main.dart` file.

    **Complete sample code**

    ```dart
    import 'package:flutter/material.dart';
    import 'package:agora_rtm/agora_rtm.dart';
    import 'dart:convert';

    void main() async {
      WidgetsFlutterBinding.ensureInitialized();

      const userId = 'your_userId';
      const appId = 'your_appId';
      const token = 'your_token';
      const channelName = 'getting-started';
      late RtmClient rtmClient;

      // Create a Signaling client instance
      try {
        // Create rtm client
        final (status, client) = await RTM( appId, userId);
        if (status.error == true) {
            print('\${status.operation} failed due to \${status.reason}, error code: \${status.errorCode}');
        } else {
            rtmClient = client;
            print('Initialize success!');
        }

        // Add events listener
        rtmClient.addListener(
        // Add message event handler
        message: (event) {
          print('received a message from channel: \${event.channelName}, channel type : \${event.channelType}');
          print('message content: \${utf8.decode(event.message!)}, custom type: \${event.customType}');
        },
        // add link state event handler
        linkState: (event) {
          print('link state changed from \${event.previousState} to \${event.currentState}');
          print('reason: \${event.reason}, due to operation \${event.operation}');
        });
      } catch (e) {
        print('Initialize failed!:\${e}');
      }

      // Login to Signaling
      try {
        // login rtm service
        var (status,response) = await rtmClient.login(token);
        if (status.error == true) {
            print('\${status.operation} failed due to \${status.reason}, error code: \${status.errorCode}');
        } else {
            print('login RTM success!');
        }
      } catch (e) {
        print('Failed to login: $e');
      }

      // Subscribe to a channel
      try {
        var (status,response) = await rtmClient.subscribe(channelName);
        if (status.error == true) {
            print('\${status.operation} failed due to \${status.reason}, error code: \${status.errorCode}');
        } else {
            print('subscribe channel: \${channelName} success!');
        }
      } catch (e) {
        print('Failed to subscribe channel: $e');
      }

      // Publish messages
      // Send a message every second for 100 seconds
      for (var i = 0; i < 100; i++) {
        try {
          var (status, response) = await rtmClient.publish(
              channelName,
              'message number : $i',
              channelType: RtmChannelType.message,
              customType: 'PlainText'
              );
          if (status.error == true ){
            print('\${status.operation} failed, errorCode: \${status.errorCode}, due to \${status.reason}');
          } else {
            print('\${status.operation} success! message number:$i');
          }
        } catch (e) {
          print('Failed to publish message: $e');
        }
        await Future.delayed(Duration(seconds: 1));
      }

      // Unsubscribe from a channel
      try {
        var (status,response) = await rtmClient.unsubscribe(channelName);
        if (status.error == true) {
            print('\${status.operation} failed due to \${status.reason}, error code: \${status.errorCode}');
        } else {
            print('unsubscribe success!');
        }
      } catch (e) {
        print('something went wrong with logout: $e');
      }

      // Logout of Signaling
      try {
        // logout rtm service
        var (status,response) = await rtmClient.logout();
        if (status.error == true) {
            print('\${status.operation} failed due to \${status.reason}, error code: \${status.errorCode}');
        } else {
            print('logout RTM success!');
        }
      } catch (e) {
        print('something went wrong with logout: $e');
      }
    }
    ```

    Follow the implementation steps to understand the core API calls in the sample code or to build the demo step-by-step.

    ### Initialize the Signaling engine [#initialize-the-signaling-engine-3]

    Before calling any other Signaling SDK API, initialize an `RtmClient` object instance. Add the following code to the `lib/main.dart` file.

    ```dart
    import 'package:agora_rtm/agora_rtm.dart';
    import 'package:flutter/material.dart';
    import 'dart:convert';

    void main() async {
      // Wait for Widgets to be initialized.
      WidgetsFlutterBinding.ensureInitialized();

      const userId = 'your_userId';
      const appId = 'your_appId';
      const token = 'your_token';
      const channelName = 'getting-started';
      late RtmClient rtmClient;

      try {
        // Create rtm client
        final (status, client) = await RTM( appId, userId);
        if (status.error == true) {
            print('${status.operation} failed due to ${status.reason}, error code: ${status.errorCode}');
        } else {
            rtmClient = client;
            print('Initialize success!');
        }
      // Add events listener

      } catch (e) {
        print('Initialize failed!:${e}');
      }
    }
    ```

    ### Add an event listener [#add-an-event-listener-3]

    The event listener enables you to implement the processing logic in response to Signaling events. Use the following code to handle event notifications or display received messages:

    ```dart
    // Paste the following code snippet below the "add event listener" comment
    rtmClient.addListener(
      // Add message event handler
      message: (event) {
        print('received a message from channel: ${event.channelName}, channel type : ${event.channelType}');
        print('message content: ${utf8.decode(event.message!)}, custome type: ${event.customType}');
      },
      // Add linkState event handler
      linkState: (event) {
        print('link state changed from ${event.previousState} to ${event.currentState}');
        print('reason: ${event.reason}, due to operation ${event.operation}');
      });
    ```

    ### Log in to Signaling [#log-in-to-signaling-3]

    To connect to Signaling and access Signaling network resources, such as sending messages, and subscribing to channels, call `login`.

    During a login operation, the client attempts to establish a connection with Signaling. Once the connection is established, the client transmits heartbeat information to the Signaling server at fixed intervals to keep the client active until the client actively logs out or is disconnected. The connection is interrupted when timeout occurs. During this period, users may freely access the Signaling network resources subject to their own permissions and usage restrictions.

    ```dart
    // Paste the following code snippet below "Login to Signaling" comment
    try {
      // Login to Signaling
      var (status,response) = await rtmClient.login(token);
      if (status.error == true) {
          print('${status.operation} failed due to ${status.reason}, error code: ${status.errorCode}');
      } else {
          print('login RTM success!');
      }
    } catch (e) {
      print('Failed to login: $e');
    }
    ```

    ### Send a message [#send-a-message-2]

    To distribute a message to all subscribers of a message channel, call `publish`. The following code sends a string type message every second for 100 seconds.

    ```dart
    // Paste the following code snippet below the "Publish messages" comment
    // Send a message every second for 100 seconds
    for (var i = 0; i < 100; i++) {
      try {
        var (status, response) = await rtmClient.publish(
            channelName,
            'message number : $i',
            channelType: RtmChannelType.message,
            customType: 'PlainText'
            );
        if (status.error == true ){
          print('${status.operation} failed, errorCode: ${status.errorCode}, due to ${status.reason}');
        } else {
          print('${status.operation} success! message number:$i');
        }
      } catch (e) {
        print('Failed to publish message: $e');
      }
      await Future.delayed(Duration(seconds: 1));
    }
    ```

    ### Subscribe and unsubscribe [#subscribe-and-unsubscribe-3]

    To receive messages sent to a channel, call `subscribe` to subscribe to channel messages:

    ```dart
    // Paste the following code snippet below the "Subscribe to a channel" comment
    try {
      // Subscribe to a channel
      var (status,response) = await rtmClient.subscribe(channelName);
      if (status.error == true) {
          print('${status.operation} failed due to ${status.reason}, error code: ${status.errorCode}');
      } else {
          print('subscribe channel: ${channelName} success!');
      }
    } catch (e) {
      print('Failed to subscribe channel: $e');
    }
    ```

    When you no longer need to receive messages from a channel, `unsubscribe` from the channel:

    ```dart
    // Paste the following code snippet below the "Unsubscribe from a channel" comment
    try {
      // Unsubscribe from a channel
      var (status,response) = await rtmClient.unsubscribe(channelName);
      if (status.error == true) {
          print('${status.operation} failed due to ${status.reason}, error code: ${status.errorCode}');
      } else {
          print('unsubscribe success!');
      }
    } catch (e) {
      print('something went wrong with logout: $e');
    }
    ```

    For more information about subscribing and sending messages, see [Message channels](/en/realtime-media/rtm/build/work-with-channels/message-channel) and [Stream channels](/en/realtime-media/rtm/build/work-with-channels/stream-channel).

    ### Log out of Signaling [#log-out-of-signaling-3]

    When a user no longer needs to use Signaling, call `logout`. Logging out means closing the connection between the client and Signaling. The user is automatically logged out or unsubscribed from all message and stream channels. Other users in the channel receive an `onPresenceEvent` notification of the user leaving the channel.

    ```dart
    // Paste the following code snippet below the "Log out of Signaling" comment
    try {
      // Log out of Signaling
      var (status,response) = await rtmClient.logout();
      if (status.error == true) {
          print('${status.operation} failed due to ${status.reason}, error code: ${status.errorCode}');
      } else {
          print('logout RTM success!');
      }
    } catch (e) {
      print('something went wrong with logout: $e');
    }
    ```

    ## Test Signaling [#test-signaling-3]

    Take the following steps to test the sample code:

    1. Use the [Token Builder](https://agora-token-generator-demo.vercel.app/) to generate a Signaling token:

       1. Select Signaling from the Agora products dropdown.
       2. Fill in your app ID and app certificate from [Agora Console](https://console.agora.io/v2). Leave the remaining fields blank.
       3. Click **Generate Token**.

    2. In your code, replace `your_appId` with your app ID from Agora Console, `your_userId` with a numeric string and `your_token` with the generated token.

    3. Save the project.

    4. Connect the target device to your computer, or open the device emulator.

    5. In the terminal, execute the following command in the project path to run the sample project on the target device:

       ```bash
       flutter run
       ```

       You see the following output in the terminal:

       ```text
       flutter: publish success! message number:0
       flutter: publish success! message number:1
       flutter: publish success! message number:2
       flutter: publish success! message number:3
       ```

    6. Run a second instance of the project on another device or emulator using a different `userId`.

       You see the following notifications for messages sent from the first app instance:

       ```text
       flutter: received a message from channel: getting-started, channel type : RtmChannelType.message
       flutter: message content: message number : 0, custom type: PlainText
       flutter: received a message from channel: getting-started, channel type : RtmChannelType.message
       flutter: message content: message number : 1, custom type: PlainText
       ```

    ## Reference [#reference-3]

    This section contains content that completes the information on this page, or points you to documentation that explains other aspects to this product.

    ### Token authentication [#token-authentication-3]

    In this guide you retrieve a temporary token from a [Token Builder](https://agora-token-generator-demo.vercel.app/). To understand how to create an authentication server for development purposes, see [Secure authentication with tokens](./build/connect-and-authenticate/authentication-workflow).

    ### Sample project [#sample-project-1]

    Agora provides an open source [sample project](https://github.com/AgoraIO/RTM2/tree/main/Agora-RTM2-QuickStart-Flutter) on GitHub for your reference. Download it or view the source code for a more detailed example.

    ### API reference [#api-reference-3]

    * [API reference](/en/api-reference/api-ref/signaling)
    * [Event listeners](./build/send-and-receive-messages/add-event-listener)

    <_PlatformProcessedMarker close="true" />
  </_PlatformPanel>

  <_PlatformPanel platform="linux-cpp">
    <_PlatformProcessedMarker groupMode="structured" canonicalPlatform="web" platform="linux-cpp" />

    ## Understand the tech [#understand-the-tech-4]

    To use Signaling features in your app, you initialize a Signaling client instance and add event listeners. To connect to Signaling, you login using an authentication token. To send a message to a message channel, you publish the message. Signaling creates a channel when a user subscribes to it. To receive messages other users publish to a channel, your app listens for events.

    To create a pub/sub session for Signaling, implement the following steps in your app:

    <Accordions>
      <Accordion title="Signaling workflow">
        ![Signaling workflow for C++](https://assets-docs.agora.io/images/signaling/get-started-workflow-linux.svg)
      </Accordion>
    </Accordions>

    ## Prerequisites [#prerequisites-4]

    To implement the code presented on this page you need to have:

    * An Agora [account](/en/realtime-media/rtm/manage-agora-account) and [project](/en/realtime-media/rtm/manage-agora-account).
    * [Enabled Signaling](/en/realtime-media/rtm/manage-agora-account) in Agora Console

    - A device running Linux Ubuntu 14.04 or above; 18.04+ is recommended.
    - At least 2 GB of memory.
    - `cmake` 3.6.0 or above.

    * Ensure that a firewall is not blocking your network communication.

    <CalloutContainer type="info">
      <CalloutDescription>
        Signaling 2.x is an enhanced version compared to 1.x with a wide range of new features. It follows a new pricing structure. See [Pricing](/en/realtime-media/rtm/reference/pricing) for details.
      </CalloutDescription>
    </CalloutContainer>

    ## Project setup [#project-setup-4]

    ### Create a project [#create-a-project-3]

    Create the following folder structure for your project:

    ```bash
    RTMProject/
    ├── include/
    └── lib/
    ```

    ### Integrate the SDK [#integrate-the-sdk-3]

    To integrate the Linux C++ Signaling SDK into your project:

    1. [Download](/en/api-reference/sdks?product=signaling\&platform=linux) the latest version of the SDK and unzip it.

       <CalloutContainer type="info">
         <CalloutDescription>
           To integrate Signaling SDK 2.2.1 or later, and Video SDK 4.3.0 or later in the same project, refer to [handle integration issues](/en/api-reference/faq/integration/rtm2_rtc_integration_issue).
         </CalloutDescription>
       </CalloutContainer>

    2. Copy the files in the SDK package to the project folder as follows:

       * Copy the `/rtm/sdk/libagora_rtm_sdk.so` file to the project's `lib` folder.
       * Copy all `*.h` files in `/rtm/sdk/high_level_api/include` to the project's `include` folder.

    3. To use `CMake` to compile the project, create the following files in the `RTMProject` folder:

       * `CMakeLists.txt`

         ```bash
         # CMakeLists.txt
         cmake_minimum_required (VERSION 2.8)
         project(RTMProject)
         set(TARGET_NAME RTMQuickStart)
         set(SOURCES RTMQuickStart.cpp)
         set(HEADERS)
         set(TARGET_BUILD_TYPE "Debug")
         set(CMAKE_CXX_FLAGS "-fPIC -O2 -g -std=c++11 -msse2")
         include_directories(${CMAKE_SOURCE_DIR}/include)
         link_directories(${CMAKE_SOURCE_DIR}/lib)
         add_executable(${TARGET_NAME} ${SOURCES} ${HEADERS})
         target_link_libraries(${TARGET_NAME} agora_rtm_sdk pthread)
         ```

       * `clean_build.sh`

         ```bash
         # clean_build.sh
         rm -fr build
         mkdir -p build
         cd build
         cmake ..
         make
         cd ..
         ```

    ## Implement Signaling [#implement-signaling-4]

    A complete code sample that implements the basic features of Signaling is presented here for your reference. To use the sample code, create a new file named `RTMQuickStart.cpp` in the `RTMProject` folder and add the following code:

    **Complete sample code for Signaling**

    ```cpp
    #include <iostream>
    #include <memory>
    #include <string>
    #include <exception>
    //#include <unistd.h>

    #include "IAgoraRtmClient.h"

    // Pass in your App ID and token
    const std::string APP_ID = "<Your App ID>";
    const std::string TOKEN = "<Your token>";

    // Terminal color codes for UBUNTU/LINUX
    #define RESET   "\\033\[0m"
    #define BLACK   "\\033\[30m"      /* Black */
    #define RED     "\\033\[31m"      /* Red */
    #define GREEN   "\\033\[32m"      /* Green */
    #define YELLOW  "\\033\[33m"      /* Yellow */
    #define BLUE    "\\033\[34m"      /* Blue */
    #define MAGENTA "\\033\[35m"      /* Magenta */
    #define CYAN    "\\033\[36m"      /* Cyan */
    #define WHITE   "\\033\[37m"      /* White */
    #define BOLDBLACK   "\\033\[1m\\033\[30m"      /* Bold Black */
    #define BOLDRED     "\\033\[1m\\033\[31m"      /* Bold Red */
    #define BOLDGREEN   "\\033\[1m\\033\[32m"      /* Bold Green */
    #define BOLDYELLOW  "\\033\[1m\\033\[33m"      /* Bold Yellow */
    #define BOLDBLUE    "\\033\[1m\\033\[34m"      /* Bold Blue */
    #define BOLDMAGENTA "\\033\[1m\\033\[35m"      /* Bold Magenta */
    #define BOLDCYAN    "\\033\[1m\\033\[36m"      /* Bold Cyan */
    #define BOLDWHITE   "\\033\[1m\\033\[37m"      /* Bold White */

    using namespace agora::rtm;

    class RtmEventHandler : public IRtmEventHandler {
    public:
      // Add the event listener
      void onLoginResult(const uint64_t requestId, RTM_ERROR_CODE errorCode) override {
        cbPrint("onLoginResult, request id: %lld, errorCode: %d", requestId, errorCode);
      }

      void onLogoutResult(const uint64_t requestId, RTM_ERROR_CODE errorCode) {
        cbPrint("onLogoutResult, request id: %lld, errorCode: %d", requestId, errorCode);
      }

      void onConnectionStateChanged(const char *channelName, RTM_CONNECTION_STATE state, RTM_CONNECTION_CHANGE_REASON reason) override {
        cbPrint("onConnectionStateChanged, channelName: %s, state: %d, reason: %d", channelName, state, reason);
      }

      void onLinkStateEvent(const LinkStateEvent& event) override {
        cbPrint("onLinkStateEvent, state: %d -> %d, operation: %d, reason: %s", event.previousState, event.currentState, event.operation, event.reason);
      }

      void onPublishResult(const uint64_t requestId, RTM_ERROR_CODE errorCode) override {
        cbPrint("onPublishResult request id: %lld result: %d", requestId, errorCode);
      }

      void onMessageEvent(const MessageEvent &event) override {
        cbPrint("receive message from: %s, message: %s", event.publisher, event.message);
      }

      void onSubscribeResult(const uint64_t requestId, const char *channelName, RTM_ERROR_CODE errorCode) override {
        cbPrint("onSubscribeResult: channel:%s, request id: %lld result: %d", channelName, requestId, errorCode);
      }

      void onUnsubscribeResult(const uint64_t requestId, const char *channelName, RTM_ERROR_CODE errorCode) override {
        cbPrint("onUnsubscribeResult: channel:%s, request id: %lld result: %d", channelName, requestId, errorCode);
      }

    private:
      void cbPrint(const char* fmt, ...) {
        printf("\\x1b[32m<< RTM async callback: ");
        va_list args;
        va_start(args, fmt);
        vprintf(fmt, args);
        va_end(args);
        printf(" >>\\x1b[0m\\n");
      }
    };

    class RtmDemo {
    public:
      RtmDemo()
        : eventHandler_(new RtmEventHandler()),
          rtmClient_(nullptr) { Init(); }

      void Init() {
        std::cout << YELLOW << "Please enter userID (literal \\"null\\" or starting"
        << "with space is not allowed, no more than 64 characters!):" << std::endl;

        std::string userID;
        std::getline(std::cin, userID);

        RtmConfig config;
        config.appId = APP_ID.c_str();
        config.userId = userID.c_str();
        config.eventHandler = eventHandler_.get();
        // Create an IRtmClient instance
        int errorCode = 0;
        rtmClient_ = createAgoraRtmClient(config, errorCode);
        if (!rtmClient_ || errorCode != 0) {
          std::cout << RED <<"error creating rtm service!" << std::endl;
          exit(0);
        }
      }

      void start() {
        // Log in the RTM server
        uint64_t requestId = 0;
        rtmClient_->login(TOKEN.c_str(), requestId);

        // Sample codes for the user interface
        mainMeun();

        std::cout << YELLOW << "quit ? yes/no" << std::endl;
        std::string input;
        std::getline(std::cin, input);
        if (input.compare("yes") == 0) {
          exit(0);
        }
      }

      // Log out from the RTM server
      void logout() {
        uint64_t requestId = 0;
        rtmClient_->logout(requestId);
      }

      // Subscribe to a channel
      void subscribeChannel(const std::string& channelName) {
        uint64_t requestId = 0;
        rtmClient_->subscribe(channelName.c_str(), SubscribeOptions(), requestId);
      }

      // Unsubscribe from a channel
      void unsubscribeChannel(const std::string& channelName) {
        uint64_t requestId = 0;
        rtmClient_->unsubscribe(channelName.c_str(), requestId);
      }

      // Publish a message
      void publishMessage(const std::string& channelName, const std::string& message) {
        PublishOptions options;
        options.messageType = RTM_MESSAGE_TYPE_STRING;
        uint64_t requestId;
        rtmClient_->publish(channelName.c_str(), message.c_str(), message.size(), options, requestId);
      }

      // Sample codes for the user interface
      void mainMeun() {
        bool quit  = false;
        while (!quit) {
          std::cout << WHITE
                    << "1: subscribe channel\\n"
                    << "2: unsubscribe channel\\n"
                    << "3: publish message\\n"
                    << "4: logout" << std::endl;
          std::cout << YELLOW <<"please input your choice: " << std::endl;
          std::string input;
          std::getline(std::cin, input);
          int32_t choice = 0;
          try {
            choice = std::stoi(input);
          } catch(...) {
            std::cout <<RED << "invalid input" << std::endl;
            continue;
          }

          switch (choice)
          {
            case 1: {
              std::cout << YELLOW << "please input channel name:" << std::endl;
              std::string channelName;
              std::getline(std::cin, channelName);
              subscribeChannel(channelName);
              std::this_thread::sleep_for(std::chrono::seconds(1));
              break;
            }
            case 2: {
              std::cout << YELLOW << "please input channel name:" << std::endl;
              std::string channelName;
              std::getline(std::cin, channelName);
              unsubscribeChannel(channelName);
              std::this_thread::sleep_for(std::chrono::seconds(1));
              break;
            }
            case 3: {
              std::cout << YELLOW << "please input channel name:" << std::endl;
              std::string channelName;
              std::getline(std::cin, channelName);
              groupChat(channelName);
              break;
            }
            case 4: {
              logout();
              return;
            }
            default: {
              std::cout <<RED << "invalid input" << std::endl;
              break;
            }
          }
        }
      }

      void groupChat(const std::string& channelName) {
        std::string message;
        while (true) {
          std::cout << YELLOW << "please input message "
                    << "or input \\"quit\\" to leave group chat"
                    << std::endl;
          std::getline(std::cin, message);
          if (message.compare("quit") == 0) {
            return;
          } else {
            publishMessage(channelName, message);
            std::this_thread::sleep_for(std::chrono::seconds(1));
          }
        }
      }

    private:
      std::unique_ptr<IRtmEventHandler> eventHandler_;
      IRtmClient* rtmClient_;
    };

    int main(int argc, const char * argv[]) {
      RtmClient messaging;
      messaging.login();
      return 0;
    }
    ```

    Follow the implementation steps to understand the core API calls in the sample code or use the snippets in your own code.

    ### Include the header file [#include-the-header-file]

    To use Signaling APIs in your project, include the `IAgoraRtmClient` header file:

    ```cpp
    #include "IAgoraRtmClient.h"
    ```

    ### Initialize the Signaling engine [#initialize-the-signaling-engine-4]

    Before calling any other Signaling SDK API, initialize an `IRtmClient` object instance.

    ```cpp
    std::string userID;
    std::getline(std::cin, userID);

    RtmConfig config;
    config.appId = APP_ID.c_str();
    config.userId = userID.c_str();
    config.eventHandler = eventHandler_.get();

    // Create an IRtmClient instance
    int errorCode = 0;
    rtmClient_ = createAgoraRtmClient(config, errorCode);
    if (!rtmClient_ || errorCode != 0) {
      std::cout << RED <<"error creating rtm service!" << std::endl;
      exit(0);
    }
    ```

    ### Add an event listener [#add-an-event-listener-4]

    The event listener enables you to implement the processing logic in response to Signaling events. Use the following code to handle event notifications or display received messages:

    ```cpp
    void onLoginResult(const uint64_t requestId, RTM_ERROR_CODE errorCode) override {
      cbPrint("onLoginResult, request id: %lld, errorCode: %d", requestId, errorCode);
    }

    void onLogoutResult(const uint64_t requestId, RTM_ERROR_CODE errorCode) {
      cbPrint("onLogoutResult, request id: %lld, errorCode: %d", requestId, errorCode);
    }

    void onConnectionStateChanged(const char *channelName, RTM_CONNECTION_STATE state, RTM_CONNECTION_CHANGE_REASON reason) override {
      cbPrint("onConnectionStateChanged, channelName: %s, state: %d, reason: %d", channelName, state, reason);
    }

    void onLinkStateEvent(const LinkStateEvent& event) override {
      cbPrint("onLinkStateEvent, state: %d -> %d, operation: %d, reason: %s", event.previousState, event.currentState, event.operation, event.reason);
    }

    void onPublishResult(const uint64_t requestId, RTM_ERROR_CODE errorCode) override {
      cbPrint("onPublishResult request id: %lld result: %d", requestId, errorCode);
    }

    void onMessageEvent(const MessageEvent &event) override {
      cbPrint("receive message from: %s, message: %s", event.publisher, event.message);
    }

    void onSubscribeResult(const uint64_t requestId, const char *channelName, RTM_ERROR_CODE errorCode) override {
      cbPrint("onSubscribeResult: channel:%s, request id: %lld result: %d", channelName, requestId, errorCode);
    }

    void onUnsubscribeResult(const uint64_t requestId, const char *channelName, RTM_ERROR_CODE errorCode) override {
      cbPrint("onUnsubscribeResult: channel:%s, request id: %lld result: %d", channelName, requestId, errorCode);
    }
    ```

    ### Log in to Signaling [#log-in-to-signaling-4]

    To connect to Signaling and access Signaling network resources, such as sending messages, and subscribing to channels, call `login`.

    During a login operation, the client attempts to establish a connection with Signaling. Once the connection is established, the client transmits heartbeat information to the Signaling server at fixed intervals to keep the client active until the client actively logs out or is disconnected. The connection is interrupted when timeout occurs. During this period, users may freely access the Signaling network resources subject to their own permissions and usage restrictions.

    ```cpp
    // Log in to Signaling
    uint64_t requestId = 0;
    rtmClient_->login(TOKEN.c_str(), requestId);
    ```

    After you call this method, the SDK triggers the `onLoginResult` callback and returns the API call result.

    Use the `login` return value, or listen to the `onConnectionStateChanged` event notification to confirm that login is successful or obtain the error code and cause of login failure. When performing a login operation, the client's network connection state is `RTM_CONNECTION_STATE_CONNECTING`. After a successful login, the state is updated to `RTM_CONNECTION_STATE_CONNECTED`.

    <CalloutContainer type="info">
      <CalloutTitle>
        Best practice
      </CalloutTitle>

      <CalloutDescription>
        To continuously monitor the network connection state of the client, best practice is to continue to listen for `onConnectionStateChanged` event notifications throughout the life cycle of the application. For further details, see [Event listeners](/en/realtime-media/rtm/build/send-and-receive-messages/add-event-listener).
      </CalloutDescription>
    </CalloutContainer>

    <CalloutContainer type="warning">
      <CalloutDescription>
        After a user successfully logs into Signaling, the application's PCU increases, which affects your billing data.
      </CalloutDescription>
    </CalloutContainer>

    ### Send a message [#send-a-message-3]

    To distribute a message to all subscribers of a message channel, call `publish`. The following code sends a string type message.

    ```cpp
    // Publish a message to the channel
    void publishMessage(const std::string& channelName, const std::string& message) {
      PublishOptions options;
      options.messageType = RTM_MESSAGE_TYPE_STRING;
      uint64_t requestId;
      rtmClient_->publish(channelName.c_str(), message.c_str(), message.size(), options, requestId);
    }
    ```

    <CalloutContainer type="info">
      <CalloutDescription>
        Before calling `publish` to send a message, serialize the message payload as a string.
      </CalloutDescription>
    </CalloutContainer>

    ### Subscribe and unsubscribe [#subscribe-and-unsubscribe-4]

    To receive messages sent to a channel, call `subscribe` to subscribe to channel messages:

    ```cpp
    // Subscribe to a channel
    void subscribeChannel(const std::string& channelName) {
      uint64_t requestId = 0;
      rtmClient_->subscribe(channelName.c_str(), SubscribeOptions(), requestId);
    }
    ```

    When you no longer need to receive messages from a channel, call `unsubscribe` to unsubscribe from the channel:

    ```cpp
    // Unsubscribe from a channel
    void unsubscribeChannel(const std::string& channelName) {
      uint64_t requestId = 0;
      rtmClient_->unsubscribe(channelName.c_str(), requestId);
    }
    ```

    For more information about sending and receiving messages see [Message channels](/en/realtime-media/rtm/build/work-with-channels/message-channel) and [Stream channels](/en/realtime-media/rtm/build/work-with-channels/stream-channel).

    ### Log out of Signaling [#log-out-of-signaling-4]

    When a user no longer needs to use Signaling, call `logout`. Logging out means closing the connection between the client and Signaling. The user is automatically logged out or unsubscribed from all message and stream channels. Other users in the channel receive an `onPresenceEvent` notification of the user leaving the channel.

    ```cpp
    // Log out of Signaling
    void logout() {
      uint64_t requestId = 0;
      rtmClient_->logout(requestId);
    }

    // Clean up
    rtmClient_->release();
    ```

    ## Test Signaling [#test-signaling-4]

    Take the following steps to test the sample code:

    1. Use the [Token Builder](https://agora-token-generator-demo.vercel.app/) to generate a Signaling token:

       1. Select Signaling from the Agora products dropdown.
       2. Fill in your app ID and app certificate from [Agora Console](https://console.agora.io/v2). Leave the remaining fields blank.
       3. Click **Generate Token**.

    2. In your code, replace `<Your App ID>` with your app ID from Agora Console and `<Your token>` with the generated token. Save the project. Make sure Signaling is activated for your project in [Agora Console](https://console.agora.io/v2).

    3. In the terminal, run the following command to compile the project:

       ```bash
       sh +x clean_build.sh
       ```

    4. Use the following commands to navigate to the `build` folder and run the app:

       ```bash
       cd build
       ./RTMQuickStart
       ```

       You see menu options to **subscribe**, **unsubscribe**, **publish**, and **logout**.

    5. Follow the prompts to **subscribe** to a channel.

    6. Run another instance of the app using a different user ID. Follow the prompts to **publish** a message to the same channel that you subscribed to from the other instance.

    7. You see the message displayed in the instance that you used to subscribe to the channel.

       Congratulations! You have successfully integrated Signaling into your project.

    ## Reference [#reference-4]

    This section contains content that completes the information on this page, or points you to documentation that explains other aspects to this product.

    ### Token authentication [#token-authentication-4]

    In this guide you retrieve a temporary token from a [Token Builder](https://agora-token-generator-demo.vercel.app/). To understand how to create an authentication server for development purposes, see [Secure authentication with tokens](./build/connect-and-authenticate/authentication-workflow).

    ### Sample project [#sample-project-2]

    Agora provides an open source [sample project](https://github.com/AgoraIO/RTM2/tree/main/Agora-RTM2-QuickStart-Linux-C%2B%2B) on GitHub for your reference. Download it or view the source code for a more detailed example.

    ### API reference [#api-reference-4]

    * [API reference](/en/api-reference/api-ref/signaling)
    * [Event listeners](./build/send-and-receive-messages/add-event-listener)

    <_PlatformProcessedMarker close="true" />
  </_PlatformPanel>

  <_PlatformPanel platform="unity">
    <_PlatformProcessedMarker groupMode="structured" canonicalPlatform="web" platform="unity" />

    ## Understand the tech [#understand-the-tech-5]

    To use Signaling features in your game, you initialize a Signaling client instance and add event listeners. To connect to Signaling, you login using an authentication token. To send a message to a message channel, you publish the message. Signaling creates a channel when a user subscribes to it. To receive messages other users publish to a channel, your game listens for events.

    To create a pub/sub session for Signaling, implement the following steps in your game:

    <Accordions>
      <Accordion title="Signaling workflow">
        ![Signaling workflow for Unity](https://assets-docs.agora.io/images/signaling/get-started-workflow-unity.svg)
      </Accordion>
    </Accordions>

    ## Prerequisites [#prerequisites-5]

    To implement the code presented on this page you need to have:

    * An Agora [account](/en/realtime-media/rtm/manage-agora-account) and [project](/en/realtime-media/rtm/manage-agora-account).

    * [Enabled Signaling](/en/realtime-media/rtm/manage-agora-account) in Agora Console

    * [Unity Hub](https://unity.com/download)

    * [Unity Editor 2018.4.0 or higher](https://unity.com/releases/editor/archive)

    * Operating System and compiler requirements:

      | Development Platform | Operating system version | Compiler version                      |
      | -------------------- | ------------------------ | ------------------------------------- |
      | Android              | Android 4.1 or above     | Android Studio 3.0 or above           |
      | iOS                  | iOS 11.0 or above        | Xcode 9.0 or above                    |
      | macOS                | macOS 10.10 or above     | Xcode 9.0 or above                    |
      | Windows              | Windows 7 or above       | Microsoft Visual Studio 2017 or above |

    * Ensure that a firewall is not blocking your network communication.

    <CalloutContainer type="info">
      <CalloutDescription>
        Signaling 2.x is an enhanced version compared to 1.x with a wide range of new features. It follows a new pricing structure. See [Pricing](/en/realtime-media/rtm/reference/pricing) for details.
      </CalloutDescription>
    </CalloutContainer>

    ## Project setup [#project-setup-5]

    ### Create a project [#create-a-project-4]

    1. In Unity Hub, select **Projects**, then click **New Project**.

    2. In **All templates**, select **3D**. Set the **Project name** and **Location**, then click **Create Project**.

    3. In **Projects**, double-click the project you created. Your project opens in Unity.

    ### Integrate the SDK [#integrate-the-sdk-4]

    1. [Download](/en/api-reference/sdks?product=signaling\&platform=unity) the latest version of the Agora Signaling SDK, and unzip the contents to a local folder.

    2. In Unity, click **Assets > Import Package > Custom Package**.

    3. Navigate to the Signaling SDK package and click **Open**.

    4. In **Import Unity Package**, click **Import**.

    ### Create a user interface [#create-a-user-interface-2]

    To create a simple UI for verifying the basic features of Signaling, add the following elements to your interface:

    ```text
    SampleScene

    - Main Camera
    - Canvas
     - Image            // Window background
     - Txt1             // Text label, text content is Send string message to message channel
     - InputField1      // Message input box
     - Btn1             // Send message
     - Txt2             // Text label, text content is Send binary message to stream channel
     - Btn2             // Create Stream Channel
     - Btn3             // Join Stream Channel
     - Btn4             // Join Topic
     - Btn5             // Subscribe to message publishers in Topic
     - InputField2      // Message input box
     - Btn6             // Send message button
     - Btn7             // Release Stream Channel button
     - Btn8             // Leave Stream Channel button
     - Btn9             // Leave Topic button
     - Btn10            // Unsubscribe from message publishers in Topic
     - Pannel           // Information output panel
         - Txt3         // Information output text
    - EventSystem
    ```

    Your interface should look similar to the following:

    ![](https://assets-docs.agora.io/images/signaling/signaling-quickstart-ui-unity.png)

    ## Implement Signaling [#implement-signaling-5]

    A complete code sample that implements the basic features of Signaling is presented here for your reference. To use the sample code, create a new script for the project. In Unity editor, from the **Assets** menu, select **Create**, then **C# Script**. Name the new script file `HelloWorld.cs` and paste the following code in the file:

    **Complete sample code for Signaling**

    ```csharp
    using System.Collections.Generic;
    using UnityEngine;
    using UnityEngine.UI;
    using Agora.Rtm;

    public class HelloWorld : MonoBehaviour
    {
        [SerializeField]
        private string appId = "your_appId"; // Get an App ID from Agora console
        private string userId = "your_userId"; // Change the user ID to a numeric string
        private string token = "your_token"; // Login token
        private string rtcToken = "your_RTC_token"; // Token to join a stream channel
        private string msChannelName = "SeeYou";
        private string stChannelName = "Greeting";
        private string topicName = "Hello_world";
        private IRtmClient rtmClient;
        private IStreamChannel streamChannel;
        public void Awake()
        {
            Initialize();
        }
        // Initialize RTM
        public async void 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 Sucessfull");
                // 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)
        {
            GameObject.Find("Txt3").GetComponent<Text>().text += "
    " + 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 recieved a string type message: {0} from: {1} in channel:{2}", stMessage, publisher, channelName));
                ShowMessage(string.Format("The channel type is {0}", channelType));
            }
            else
            {
                var biMessage = message.GetData<byte[]>();
                ShowMessage(string.Format("You have recieved a binary type message: {0},from: {1} in channel:{2}", System.BitConverter.ToString(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<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 Messahe to Stream Channel
            var message = GameObject.Find("InputField2").GetComponent<InputField>().text;
            byte[] byteMsg = System.Text.Encoding.Default.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
            List<string> userList = new List<string>();
            userList.Add("Tony");
            TopicOptions options = new TopicOptions();
            options.users = userList.ToArray();
            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 async 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 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("Dispose Channel Success!");
                    ChangeComponentView(activeComponentsList, "ACTIVATE");
                    ChangeComponentView(inactiveComponentList, "INACTIVATE");
                }
                var status1 = streamChannel.Dispose();
            }
        }
        // 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 async void OnDestroy()
        {
            string[] activeComponentsList = { "Btn4", "Btn5", "Btn8" };
            string[] inactiveComponentList = { "Btn2", "Btn3", "Btn6", "Btn9", "Btn10", "Btn7" };
            // Dispose a Stream Channel
            if (rtmClient != null && 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("Dispose Channel Success!");
                    ChangeComponentView(activeComponentsList, "ACTIVATE");
                    ChangeComponentView(inactiveComponentList, "INACTIVATE");
                }
                var status1 = streamChannel.Dispose();
            }
            if (rtmClient != null)
            {
                var result = await rtmClient.UnsubscribeAsync(msChannelName);
                var status2 = result.Status;
                var response2 = result.Response;
                if (status2.Error)
                {
                    Debug.Log(string.Format("{0} is failed", status2.Operation));
                    Debug.Log(string.Format("The error code is {0}, because of: {1}", status2.ErrorCode, status2.Reason));
                }
                else
                {
                    Debug.Log("Unsubscribe Channel Success!");
                }
                var status3 = rtmClient.Dispose();
                Debug.Log("Dispose rtmClient Success!");
                rtmClient = null;
            }
        }
    }
    ```

    Follow the implementation steps to understand the core API calls in the sample code or use the snippets in your own code.

    ### Import Agora classes [#import-agora-classes-1]

    To use the relevant Unity and Signaling APIs in your project, import the corresponding namespaces:

    ```csharp
    using System.Collections;
    using System.Collections.Generic;
    using UnityEngine;
    using UnityEngine.UI;
    using Agora.Rtm;
    ```

    ### Initialize the Signaling engine [#initialize-the-signaling-engine-5]

    Before calling any other Signaling SDK API, initialize an `IRtmClient` instance.

    ```csharp
    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 Sucessfull");
        // 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 an event listener [#add-an-event-listener-5]

    The event listener enables you to implement the processing logic in response to Signaling events. Use the following code to handle event notifications or display received messages:

    ```csharp
    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 recieved a string type message: {0} from: {1} in channel:{2}", stMessage, publisher, channelName));
            ShowMessage(string.Format("The channel type is {0}", channelType));
        }
        else
        {
            var biMessage = message.GetData<byte[]>();
            ShowMessage(string.Format("You have recieved a binary type message: {0},from: {1} in channel:{2}", System.BitConverter.ToString(biMessage), publisher, channelName));
            ShowMessage(string.Format("The channel type is {0}", channelType));
        }
    }
    ```

    ### Log in to Signaling [#log-in-to-signaling-5]

    To connect to Signaling and access Signaling network resources, such as sending messages, and subscribing to channels, call `login`.

    During a login operation, the client attempts to establish a connection with Signaling. Once the connection is established, the client transmits heartbeat information to the Signaling server at fixed intervals to keep the client active until the client actively logs out or is disconnected. The connection is interrupted when timeout occurs. During this period, users may freely access the Signaling network resources subject to their own permissions and usage restrictions.

    ```csharp
    // Log in to Signaling
    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));
    }
    else
    {
        ShowMessage("Login Successfully");
    }
    ```

    Use the `LoginAsync` return value, or listen to the `OnConnectionStateChanged` event notification to confirm that login is successful or obtain the error code and cause of login failure. When performing a login operation, the client's network connection state is `CONNECTING`. After a successful login, the state is updated to `CONNECTED`.

    <CalloutContainer type="info">
      <CalloutTitle>
        Best practice
      </CalloutTitle>

      <CalloutDescription>
        To continuously monitor the network connection state of the client, best practice is to continue to listen for `OnConnectionStateChanged` event notifications throughout the life cycle of the application. For further details, see [Event listeners](/en/realtime-media/rtm/build/send-and-receive-messages/add-event-listener).
      </CalloutDescription>
    </CalloutContainer>

    <CalloutContainer type="warning">
      <CalloutDescription>
        After a user successfully logs into Signaling, the application's PCU increases, which affects your billing data.
      </CalloutDescription>
    </CalloutContainer>

    ### Send a message [#send-a-message-4]

    To distribute a message to all subscribers of a message channel, call `PublishAsync`. The following code sends a string type message.

    ```csharp
    // Send a message to a channel
    var message = GameObject.Find("InputField1").GetComponent<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!");
        }
    }
    ```

    <CalloutContainer type="info">
      <CalloutDescription>
        Before calling `PublishAsync` to send a message, serialize the message payload as a string.
      </CalloutDescription>
    </CalloutContainer>

    ### Subscribe and unsubscribe [#subscribe-and-unsubscribe-5]

    To receive messages sent to a channel, call `SubscribeAsync` to subscribe to channel messages:

    ```csharp
    //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));
    }
    ```

    When you no longer need to receive messages from a channel, call `UnsubscribeAsync` to unsubscribe from the channel:

    ```csharp
    if (rtmClient != null)
    {
        var result = await rtmClient.UnsubscribeAsync(msChannelName);
        var status2 = result.Status;
        var response2 = result.Response;
        if (status2.Error)
        {
            Debug.Log(string.Format("{0} is failed", status2.Operation));
            Debug.Log(string.Format("The error code is {0}, because of: {1}", status2.ErrorCode, status2.Reason));
        }
        else
        {
            Debug.Log("Unsubscribe Channel Success!");
        }
        var status3 = rtmClient.Dispose();
        Debug.Log("Dispose rtmClient Success!");
        rtmClient = null;
    }
    ```

    For more information about sending and receiving messages see [Message channels](/en/realtime-media/rtm/build/work-with-channels/message-channel) and [Stream channels](/en/realtime-media/rtm/build/work-with-channels/stream-channel).

    ### Log out of Signaling [#log-out-of-signaling-5]

    When a user no longer needs to use Signaling, call `LogoutAsync`. Logging out means closing the connection between the client and Signaling. The user is automatically logged out or unsubscribed from all message and stream channels. Other users in the channel receive an `OnPresenceEvent` notification of the user leaving the channel.

    ```csharp
    // Logout of Signaling
    var (status,response) = await rtmClient.LogoutAsync();
    ```

    ## Test Signaling [#test-signaling-5]

    Take the following steps to test the sample code:

    1. In your code, replace `your_appId` with your app ID from Agora Console and `your_userId` with a numeric string.

    2. Use the [Token Builder](https://agora-token-generator-demo.vercel.app/) to generate a Signaling token:

       1. Select Signaling from the Agora products dropdown.
       2. Fill in your app ID and app certificate from [Agora Console](https://console.agora.io/v2). Leave the remaining fields blank.
       3. Click **Generate Token**.
       4. In your code, replace `your_token` with the generated token.

    3. Repeat the previous step to generate a stream channel token but this time also fill in the channel name with the value for `stChannelName` and the User ID with the value for `userId` from your code. Use the generated token to replace `your_RTC_token` in your code.

    4. In the Unity Editor, mount the event handling functions defined in the code to the corresponding Button components.

    5. In Unity, click **Play**. You see the game running on your device.

    6. Run another instance of the game with the same `appId` but a different `userId` and tokens.

    7. In either game instance, type a message in the input box under **Send string message to message channel** and press **Send Message**.

       You see the message displayed in the other game instance.

       Congratulations! You have successfully integrated Signaling into your project.

    ## Reference [#reference-5]

    This section contains content that completes the information on this page, or points you to documentation that explains other aspects to this product.

    ### Token authentication [#token-authentication-5]

    In this guide you retrieve a temporary token from a [Token Builder](https://agora-token-generator-demo.vercel.app/). To understand how to create an authentication server for development purposes, see [Secure authentication with tokens](./build/connect-and-authenticate/authentication-workflow).

    ### API reference [#api-reference-5]

    * [API reference](/en/api-reference/api-ref/signaling)
    * [Event listeners](./build/send-and-receive-messages/add-event-listener)

    <_PlatformProcessedMarker close="true" />
  </_PlatformPanel>
</_PlatformTabsGroup>
