# Use tokens (/en/realtime-media/video/build/authenticate-users/authentication-workflow)

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

To protect your business, it is best practice to authenticate every client that joins a channel. This guide explains how to fetch an authentication token from your token server, use it to join a channel, and renew the token when it expires.

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

When a user attempts to connect to an Agora channel, your app retrieves a token from the token server in your security infrastructure. Your app then sends this token to Agora SDRTN® for authentication. Agora SDRTN® reads the information stored in the token to validate the request.

The following figure shows the call flow you implement to create step-up-authentication with Agora Video Calling:

<Accordions>
  <Accordion title="Token authentication flow">
    ![token authentication flow](https://assets-docs.agora.io/images/video-sdk/token-authentication.svg)
  </Accordion>
</Accordions>

## Prerequisites [#prerequisites]

Before starting, ensure that you have:

* Implemented the [Quickstart](/en/realtime-media/video/get-started-sdk) in your project.

* Deployed a token server using either of the following guides:

  * [Deploy a token server](/en/realtime-media/video/build/authenticate-users/deploy-token-server)
  * [Deploy a middleware server](/en/realtime-media/video/build/authenticate-users/middleware-token-server)

## Implement basic authentication [#implement-basic-authentication]

This section shows you how to implement basic authentication by acquiring a token and using it to join a channel.

<_PlatformTabsGroup groupMode="structured" canonicalPlatform="web" platforms="[&#x22;android&#x22;,&#x22;electron&#x22;,&#x22;flutter&#x22;,&#x22;ios&#x22;,&#x22;macos&#x22;,&#x22;javascript&#x22;,&#x22;react-native&#x22;,&#x22;unity&#x22;,&#x22;unreal&#x22;,&#x22;blueprint&#x22;,&#x22;web&#x22;,&#x22;windows&#x22;]" showTabs="true">
  <_PlatformPanel platform="android">
    <_PlatformProcessedMarker groupMode="structured" canonicalPlatform="web" platform="android" />

    ### Use a token to join a channel [#use-a-token-to-join-a-channel]

    The client requests a token from your authentication server corresponding to the user ID and the channel name. You use the received token to join a channel.

    <Tabs defaultValue="java">
      <TabsList>
        <TabsTrigger value="java">
          Java
        </TabsTrigger>

        <TabsTrigger value="kotlin">
          Kotlin
        </TabsTrigger>
      </TabsList>

      <TabsContent value="java">
        ```java
        // Channel name
        String channelId = "xxx";
        // User ID
        int uid = 0;
        // Request a token from the server corresponding to channelId and uid
        String token = getToken(channelId, uid);
        // Set channel media options
        ChannelMediaOptions option = new ChannelMediaOptions();
        option.clientRoleType = Constants.CLIENT_ROLE_BROADCASTER;
        // Join a channel
        engine.joinChannel(token, channelId, 0, option);
        ```
      </TabsContent>

      <TabsContent value="kotlin">
        ```kotlin
        // Channel name
        val channelId = "xxx"
        // User ID
        val uid = 0
        // Request a token from the server corresponding to channelId and uid
        val token = getToken(channelId, uid)
        // Set channel media options
        val option = ChannelMediaOptions().apply {
            clientRoleType = Constants.CLIENT_ROLE_BROADCASTER
        }
        // Join a channel
        engine.joinChannel(token, channelId, 0, option)
        ```
      </TabsContent>
    </Tabs>

    ### Token expiration [#token-expiration]

    After you join a channel using a token, the SDK triggers an `onTokenPrivilegeWillExpire` callback, 30 seconds before the token is set to expire.

    When the token expires, the SDK triggers an `onRequestToken` callback. After receiving the callback, you regenerate a new token on the server side, and then update the token in one of the following ways:

    #### Single channel use-case [#single-channel-use-case]

    * Call `renewToken` to pass in the newly generated Token (Recommended).

    * Call `updateChannelMediaOptions` to update the token.

    * Call `leaveChannel` \[2/2] to leave the current channel, and then pass in a new token when calling `joinChannel` \[2/2] to rejoin the channel.

    #### Multi-channel use-case [#multi-channel-use-case]

    If you call `joinChannelEx` to join multiple channels, call the `updateChannelMediaOptionsEx` method to update the token.

    The following sample code demonstrates how to call `renewToken` to update the token upon receiving an `onTokenPrivilegeWillExpire` callback notification.

    <Tabs defaultValue="java">
      <TabsList>
        <TabsTrigger value="java">
          Java
        </TabsTrigger>

        <TabsTrigger value="kotlin">
          Kotlin
        </TabsTrigger>
      </TabsList>

      <TabsContent value="java">
        ```java
        class RtcEngineEventHandlerImpl extends IRtcEngineEventHandler {
            // Callback is triggered when the token is about to expire
            @Override
            public void onTokenPrivilegeWillExpire(String token) {
                super.onTokenPrivilegeWillExpire(token);

                // Request to generate a fresh token
                String token = getToken();
                // Update token
                engine.renewToken(token);
            }
        }
        ```
      </TabsContent>

      <TabsContent value="kotlin">
        ```kotlin
        class RtcEngineEventHandlerImpl : IRtcEngineEventHandler() {
            // Callback triggered when the token is about to expire
            override fun onTokenPrivilegeWillExpire(token: String) {
                super.onTokenPrivilegeWillExpire(token)

                // Request to generate a fresh token
                val newToken = getToken()
                // Update the token
                engine.renewToken(newToken)
            }
        }
        ```
      </TabsContent>
    </Tabs>

    ### Complete sample code [#complete-sample-code]

    This section presents sample code to implement token authentication in your client.

    1. In `/Gradle Scripts/build.gradle(Module: <projectname>.app)`, add the following dependencies for making HTTP requests to the token server:

       ```yaml
       dependencies {
         implementation 'com.squareup.okhttp3:okhttp:3.10.0'
         implementation 'com.google.code.gson:gson:2.8.4'
       }
       ```

    2. In `MainActivity`, replace the content with the following code:

    **Complete sample code for token authentication**

    <Tabs defaultValue="java">
      <TabsList>
        <TabsTrigger value="java">
          Java
        </TabsTrigger>

        <TabsTrigger value="kotlin">
          Kotlin
        </TabsTrigger>
      </TabsList>

      <TabsContent value="java">
        ```java
        package com.example.rtcquickstart;
           import androidx.appcompat.app.AppCompatActivity;
           import android.os.Bundle;
           import androidx.core.app.ActivityCompat;
           import androidx.core.content.ContextCompat;

           import android.Manifest;
           import android.content.pm.PackageManager;
           import android.view.SurfaceView;
           import android.widget.FrameLayout;
           import android.widget.Toast;
           import io.agora.rtc2.Constants;
           import io.agora.rtc2.IRtcEngineEventHandler;
           import io.agora.rtc2.RtcEngine;
           import io.agora.rtc2.video.VideoCanvas;
           import io.agora.rtc2.ChannelMediaOptions;

           import okhttp3.MediaType;
           import okhttp3.OkHttpClient;
           import okhttp3.Request;
           import okhttp3.RequestBody;
           import okhttp3.Response;
           import okhttp3.Call;
           import okhttp3.Callback;

           import com.google.gson.Gson;
           import java.util.Map;
           import org.json.JSONException;
           import org.json.JSONObject;
           import java.io.IOException;

           public class MainActivity extends AppCompatActivity {

              // Fill in the App ID from Agora console
              private String appId = "Your App ID";
              // Fill in the channel name
              private String channelName = "demo";
              private String token = "";
              private RtcEngine mRtcEngine;
              private int joined = 1;

              private final IRtcEngineEventHandler mRtcEventHandler = new IRtcEngineEventHandler() {
                 @Override
                 public void onUserJoined(int uid, int elapsed) {
                       runOnUiThread(() -> setupRemoteVideo(uid));
                 }

                 @Override
                 public void onTokenPrivilegeWillExpire(String token) {
                       fetchToken(1234, channelName, 1);
                       runOnUiThread(() -> {
                          Toast toast = Toast.makeText(MainActivity.this, "Token renewed", Toast.LENGTH_SHORT);
                          toast.show();
                       });
                       super.onTokenPrivilegeWillExpire(token);
                 }

                 @Override
                 public void onRequestToken() {
                       joined = 1;
                       fetchToken(1234, channelName, 1);
                       super.onRequestToken();
                 }
              };

              private static final int PERMISSION_REQ_ID = 22;

              private static final String[] REQUESTED_PERMISSIONS = {
                       Manifest.permission.RECORD_AUDIO,
                       Manifest.permission.CAMERA
              };

              private boolean checkSelfPermission(String permission, int requestCode) {
                 if (ContextCompat.checkSelfPermission(this, permission) != PackageManager.PERMISSION_GRANTED) {
                       ActivityCompat.requestPermissions(this, REQUESTED_PERMISSIONS, requestCode);
                       return false;
                 }
                 return true;
              }

              // Get RTC Token
              private void fetchToken(int uid, String channelName, int tokenRole) {
                 OkHttpClient client = new OkHttpClient();
                 MediaType JSON = MediaType.parse("application/json; charset=utf-8");
                 JSONObject json = new JSONObject();
                 try {
                       json.put("uid", uid);
                       json.put("ChannelName", channelName);
                       json.put("role", tokenRole);
                 } catch (JSONException e) {
                       e.printStackTrace();
                 }

                 RequestBody requestBody = RequestBody.create(JSON, String.valueOf(json));
                 Request request = new Request.Builder()
                          .url("http://<Your Host URL and port>/fetch_rtc_token")
                          .header("Content-Type", "application/json; charset=UTF-8")
                          .post(requestBody)
                          .build();
                 Call call = client.newCall(request);
                 call.enqueue(new Callback() {
                       @Override
                       public void onFailure(Call call, IOException e) {
                       }

                       @Override
                       public void onResponse(Call call, Response response) throws IOException {
                          if (response.isSuccessful()) {
                             Gson gson = new Gson();
                             String result = response.body().string();
                             Map map = gson.fromJson(result, Map.class);
                             new Thread(() -> {
                                   token = map.get("token").toString();
                                   ChannelMediaOptions options = new ChannelMediaOptions();
                                   if (joined != 0) {
                                      joined = mRtcEngine.joinChannel(token, channelName, 1234, options);
                                   } else {
                                      mRtcEngine.renewToken(token);
                                   }
                             }).start();
                          }
                       }
                 });
              }

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

                 if (checkSelfPermission(REQUESTED_PERMISSIONS[0], PERMISSION_REQ_ID) &&
                          checkSelfPermission(REQUESTED_PERMISSIONS[1], PERMISSION_REQ_ID)) {
                       initializeAndJoinChannel();
                 }
              }

              @Override
              protected void onDestroy() {
                 super.onDestroy();
                 mRtcEngine.leaveChannel();
                 mRtcEngine.destroy();
              }

              private void initializeAndJoinChannel() {
                 try {
                       mRtcEngine = RtcEngine.create(getBaseContext(), appId, mRtcEventHandler);
                 } catch (Exception e) {
                       throw new RuntimeException("Check the error.");
                 }

                 mRtcEngine.setChannelProfile(Constants.CHANNEL_PROFILE_LIVE_BROADCASTING);
                 mRtcEngine.setClientRole(Constants.CLIENT_ROLE_BROADCASTER);
                 mRtcEngine.enableVideo();

                 FrameLayout container = findViewById(R.id.local_video_view_container);
                 SurfaceView surfaceView = new SurfaceView(getBaseContext());
                 container.addView(surfaceView);
                 mRtcEngine.setupLocalVideo(new VideoCanvas(surfaceView, VideoCanvas.RENDER_MODE_FIT, 0));
                 mRtcEngine.startPreview();
                 fetchToken(1234, channelName, 1);
              }

              private void setupRemoteVideo(int uid) {
                 FrameLayout container = findViewById(R.id.remote_video_view_container);
                 SurfaceView surfaceView = new SurfaceView(getBaseContext());
                 surfaceView.setZOrderMediaOverlay(true);
                 container.addView(surfaceView);
                 mRtcEngine.setupRemoteVideo(new VideoCanvas(surfaceView, VideoCanvas.RENDER_MODE_FIT, uid));
              }
           }
        ```
      </TabsContent>

      <TabsContent value="kotlin">
        ```kotlin
        package com.example.rtcquickstart

           import android.Manifest;
           import android.content.pm.PackageManager;
           import android.os.Bundle;
           import android.view.SurfaceView;
           import android.widget.FrameLayout;
           import android.widget.Toast;

           import androidx.appcompat.app.AppCompatActivity;
           import androidx.core.app.ActivityCompat;
           import androidx.core.content.ContextCompat;

           import io.agora.rtc2.*;
           import io.agora.rtc2.video.VideoCanvas;

           import com.google.gson.Gson;
           import okhttp3.*;
           import org.json.JSONException;
           import org.json.JSONObject;
           import java.io.IOException;
           import java.util.*;

           class MainActivity : AppCompatActivity() {

              // Fill in the App ID from Agora console
              private val appId = "Your App ID"
              // Fill in the channel name
              private val channelName = "demo"
              private var token = ""
              private var mRtcEngine: RtcEngine? = null
              private var joined = 1

              private val mRtcEventHandler = object : IRtcEngineEventHandler() {
                 override fun onUserJoined(uid: Int, elapsed: Int) {
                       runOnUiThread {
                          // After obtaining the user ID in the onUserJoined callback, call setupRemoteVideo to set the remote user view
                          setupRemoteVideo(uid)
                       }
                 }

                 override fun onTokenPrivilegeWillExpire(token: String) {
                       fetchToken(1234, channelName, 1)
                       runOnUiThread {
                          val toast = Toast.makeText(this@MainActivity, "Token renewed", Toast.LENGTH_SHORT)
                          toast.show()
                       }
                       super.onTokenPrivilegeWillExpire(token)
                 }

                 override fun onRequestToken() {
                       joined = 1
                       fetchToken(1234, channelName, 1)
                       super.onRequestToken()
                 }
              }

              companion object {
                 private const val PERMISSION_REQ_ID = 22
                 private val REQUESTED_PERMISSIONS = arrayOf(
                       Manifest.permission.RECORD_AUDIO,
                       Manifest.permission.CAMERA
                 )
              }

              private fun checkSelfPermission(permission: String, requestCode: Int): Boolean {
                 return if (ContextCompat.checkSelfPermission(this, permission) != PackageManager.PERMISSION_GRANTED) {
                       ActivityCompat.requestPermissions(this, REQUESTED_PERMISSIONS, requestCode)
                       false
                 } else {
                       true
                 }
              }

              // Get RTC Token
              private fun fetchToken(uid: Int, channelName: String, tokenRole: Int) {
                 val client = OkHttpClient()
                 val JSON = MediaType.parse("application/json; charset=utf-8")
                 val json = JSONObject()
                 try {
                       json.put("uid", uid)
                       json.put("ChannelName", channelName)
                       json.put("role", tokenRole)
                 } catch (e: JSONException) {
                       e.printStackTrace()
                 }

                 val requestBody = RequestBody.create(JSON, json.toString())
                 val request = Request.Builder()
                       .url("http://<Your Host URL and port>/fetch_rtc_token")
                       .header("Content-Type", "application/json; charset=UTF-8")
                       .post(requestBody)
                       .build()

                 client.newCall(request).enqueue(object : Callback {
                       override fun onFailure(call: Call, e: IOException) {
                          // Handle failure
                       }

                       override fun onResponse(call: Call, response: Response) {
                          if (response.isSuccessful) {
                             val gson = Gson()
                             val result = response.body?.string()
                             val map: Map<String, Any> = gson.fromJson(result, Map::class.java)
                             Thread {
                                   token = map["token"].toString()
                                   // If the user has not joined the channel, use token to join the channel
                                   val options = ChannelMediaOptions()
                                   if (joined != 0) {
                                      joined = mRtcEngine?.joinChannel(token, channelName, 1234, options) ?: -1
                                   } else {
                                      mRtcEngine?.renewToken(token)
                                   }
                             }.start()
                          }
                       }
                 })
              }

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

                 // If all permissions are granted, initialize the RtcEngine object and join the channel
                 if (checkSelfPermission(REQUESTED_PERMISSIONS[0], PERMISSION_REQ_ID) &&
                       checkSelfPermission(REQUESTED_PERMISSIONS[1], PERMISSION_REQ_ID)) {
                       initializeAndJoinChannel()
                 }
              }

              override fun onDestroy() {
                 super.onDestroy()
                 mRtcEngine?.leaveChannel()
                 mRtcEngine?.destroy()
              }

              private fun initializeAndJoinChannel() {
                 try {
                       mRtcEngine = RtcEngine.create(baseContext, appId, mRtcEventHandler)
                 } catch (e: Exception) {
                       throw RuntimeException("Check the error.")
                 }

                 // In interactive live broadcast, set the channel scene to BROADCASTING
                 mRtcEngine?.setChannelProfile(Constants.CHANNEL_PROFILE_LIVE_BROADCASTING)
                 // Depending on the actual situation, set the user role to BROADCASTER or AUDIENCE
                 mRtcEngine?.setClientRole(Constants.CLIENT_ROLE_BROADCASTER)

                 // SDK turns off video by default. Call enableVideo to enable video
                 mRtcEngine?.enableVideo()

                 val container: FrameLayout = findViewById(R.id.local_video_view_container)
                 // Call CreateRendererView to create a SurfaceView object and add it as a child to the FrameLayout
                 val surfaceView = SurfaceView(baseContext)
                 container.addView(surfaceView)
                 // Pass the SurfaceView object to Agora to render local video
                 mRtcEngine?.setupLocalVideo(VideoCanvas(surfaceView, VideoCanvas.RENDER_MODE_FIT, 0))
                 // Start local video preview
                 mRtcEngine?.startPreview()
                 // Get a token from the token server
                 fetchToken(1234, channelName, 1)
              }

              private fun setupRemoteVideo(uid: Int) {
                 val container: FrameLayout = findViewById(R.id.remote_video_view_container)
                 val surfaceView = SurfaceView(baseContext)
                 surfaceView.setZOrderMediaOverlay(true)
                 container.addView(surfaceView)
                 mRtcEngine?.setupRemoteVideo(VideoCanvas(surfaceView, VideoCanvas.RENDER_MODE_FIT, uid))
              }
           }
        ```
      </TabsContent>
    </Tabs>

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

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

    ### Use a token to join a channel [#use-a-token-to-join-a-channel-1]

    This section shows you how to integrate token authentication in your app.

    1. Open the [SDK quickstart](/en/realtime-media/video/get-started-sdk) project you created earlier.

    2. In `renderer.js` replace the contents with the following:

    **Sample code for basic authentication**

    ```dart
    const {
            createAgoraRtcEngine,
            VideoMirrorModeType,
            VideoSourceType,
            RenderModeType,
            ChannelProfileType,
            ClientRoleType,
        } = require("agora-electron-sdk");

        let rtcEngine;
        let localVideoContainer;
        let remoteVideoContainer;
        let isJoined = false;

        const EventHandles = {
            // Listen to local user joining channel events
            onJoinChannelSuccess: ({ channelId, localUid }, elapsed) => {
                console.log('Successfully joined the channel：' + channelId);
                isJoined = true;
                // After local users join the channel, set the local video window
                rtcEngine.setupLocalVideo({
                    sourceType: VideoSourceType.VideoSourceCameraPrimary,
                    view: localVideoContainer,
                    mirrorMode: VideoMirrorModeType.VideoMirrorModeDisabled,
                    renderMode: RenderModeType.RenderModeFit,
                });
            },

            onLeaveChannel: ({ channelId, localUid }, stats) => {
                console.log('Successfully left the channel:' + channelId);
                isJoined = false;
            },

            // Listen to remote user join event
            onUserJoined: ({ channelId, localUid }, remoteUid, elapsed) => {
                console.log('Remote user ' + remoteUid + ' 已加入');
                // Listen to remote user join event
                rtcEngine.setupRemoteVideoEx(
                    {
                        sourceType: VideoSourceType.VideoSourceRemote,
                        uid: remoteUid,
                        view: remoteVideoContainer,
                        mirrorMode: VideoMirrorModeType.VideoMirrorModeDisabled,
                        renderMode: RenderModeType.RenderModeFit,
                    },
                    { channelId },
                );
            },

            // Listen for the token expiration event
            onTokenPrivilegeWillExpire: ({ channelId, localUid }, token) => {
                console.log('token ' + token + '即将过期');
                // Obtain a new token from the server
                fetchToken(channelId, localUid, ClientRoleType.ClientRoleBroadcaster)
            },

            // Listen for the token expired event
            onRequestToken: ({ channelId, localUid }) => {
                console.log('token expired');
                token = ''
            }
        };

        // Send a token request to the token server
        function fetchToken(channelName, uid, role) {
            //Token server URL example: http://12.123.1.123:8082/fetch_rtc_token
            const url = '<Your host URL and port>/fetch_rtc_token';
            const body = JSON.stringify({
                uid,
                ChannelName: channelName,
                role
            });
            fetch(url, {
                method: 'POST',
                body,
            })
                .then((res) => res.json())
                .then((res) => {
                    console.log('token ' + res.token);
                    if (+res.code === 200) {
                        if (isJoined) {
                            rtcEngine.renewToken(res.token)
                        } else {
                            rtcEngine.joinChannel(res.token, channelName, uid, { clientRoleType: role })
                        }
                    }
                    return res;
                });
        }

        window.onload = () => {
            const os = require("os");
            const path = require("path");

            // Fill in your app ID
            const APPID = "<Your app ID>";
            // Fill in the channel name
            const channel = "Test";

            localVideoContainer = document.getElementById("join-channel-local-video");
            remoteVideoContainer = document.getElementById("join-channel-remote-video");
            const sdkLogPath = path.resolve(os.homedir(), "./test.log");

            // Create an RtcEngine instance
            rtcEngine = createAgoraRtcEngine();

            // Initialize the RtcEngine instance
            rtcEngine.initialize({
                appId: APPID,
                logConfig: { filePath: sdkLogPath }
            });

            // Register event handler
            rtcEngine.registerEventHandler(EventHandles);

            //Set the channel profile to live broadcast
            rtcEngine.setChannelProfile(ChannelProfileType.ChannelProfileLiveBroadcasting);

            // Set the user role, for host use ClientRoleBroadcaster, for audience use ClientRoleAudience
            rtcEngine.setClientRole(ClientRoleType.ClientRoleBroadcaster);

            // Enable the video module
            rtcEngine.enableVideo();

            // Enable camera preview
            rtcEngine.startPreview();

            // Use the token to join a channel
            // You need to specify the user ID yourself and ensure its uniqueness within the channel
            fetchToken(channel, 123456, ClientRoleType.ClientRoleBroadcaster);
        };
    ```

    Replace `<Your app ID>` with your app ID, which must be consistent with the app ID you specified in the server configuration. Update `<Your host URL and port>` with the host URL and port of the local Golang server you have deployed. For example `99.9.9.99:8082`.

    The sample code implements the following logic:

    * Calls `joinChannel` to join a channel using the user ID, the channel name, and a token you obtain from the server. The user ID and channel name you specify must be consistent with the values you used to generate the token.

    * The SDK triggers an `onTokenPrivilegeWillExpire` callback 30 seconds before the token expires. After receiving the callback, you obtain a new token from the server and call `renewToken` to pass the newly generated token to the SDK.

    * If the token expires, the SDK triggers an `onRequestToken` callback. After receiving the callback, obtain a new token from the server and call `joinChannel` with the new token to rejoin the channel.

    Build and run the project on the local device, the app performs the following operations:

    * Obtains a token from your token server.
    * Joins the channel.
    * Automatically renews the token when it is about to expire.

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

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

    ### Use a token to join a channel [#use-a-token-to-join-a-channel-2]

    This section shows you how to integrate token authentication in your app.

    1. Add the following to the `pubspec.yaml` file, under `dependencies`:

       ```yaml
       dependencies:
           # Agora Flutter SDK, use the latest version of agora_rtc_engine
           agora_rtc_engine: ^6.3.0
           # For making http requests
           http: ^0.13.5
       ```

    2. Replace the contents in `/lib/main.dart` with the following code.

    **Sample code for basic authentication**

    ```dart
    import 'dart:convert';

        import 'package:agora_rtc_engine/agora_rtc_engine.dart';
        import 'package:flutter/material.dart';
        import 'package:http/http.dart' as http;

        void main() => runApp(const MyApp());

        /// This widget is the root of your application.
        class MyApp extends StatefulWidget {
        /// Construct the [MyApp]
        const MyApp({Key? key}) : super(key: key);

        @override
        State<MyApp> createState() => _MyAppState();
        }

        class _MyAppState extends State<MyApp> {

        @override
        Widget build(BuildContext context) {
            return MaterialApp(
            theme: ThemeData(
                primarySwatch: Colors.blue,
            ),
            home: Scaffold(
                appBar: AppBar(
                    title: const Text('APIExample'),
                ),
                body: const JoinChannelVideoToken()),
            );
        }
        }

        class JoinChannelVideoToken extends StatefulWidget {
        const JoinChannelVideoToken({Key? key}) : super(key: key);

        @override
        State<StatefulWidget> createState() => _State();
        }

        class _State extends State<JoinChannelVideoToken> {
        late final RtcEngine _engine;
        bool _isReadyPreview = false;

        bool isJoined = false, switchCamera = true, switchRender = true;
        Set<int> remoteUid = {};
        static const String appId = '<Your app ID>'; // Fill in the information from Agora console
        static const String channelId = '<Your channel name>'; // Fill in the channel name
        static const String hostUrl = '<Your host URL and port>'; // Fill in the server URL and port

        @override
        void initState() {
            super.initState();
            _initEngine();
        }

        @override
        void dispose() {
            super.dispose();
            _dispose();
        }

        Future<void> _dispose() async {
            await _engine.leaveChannel();
            await _engine.release();
        }

        Future<void> _initEngine() async {
            _engine = createAgoraRtcEngine();
            await _engine.initialize(const RtcEngineContext(
            appId: appId,
            ));

            _engine.registerEventHandler(RtcEngineEventHandler(
            onJoinChannelSuccess: (RtcConnection connection, int elapsed) {
                setState(() {
                isJoined = true;
                });
            },
            onUserJoined: (RtcConnection connection, int rUid, int elapsed) {
                setState(() {
                remoteUid.add(rUid);
                });
            },
            onUserOffline:
                (RtcConnection connection, int rUid, UserOfflineReasonType reason) {
                setState(() {
                remoteUid.removeWhere((element) => element == rUid);
                });
            },
            onLeaveChannel: (RtcConnection connection, RtcStats stats) {
                setState(() {
                isJoined = false;
                remoteUid.clear();
                });
            },
            onTokenPrivilegeWillExpire: (RtcConnection connection, String token) {
                _fetchToken(1234, channelId, 1, false);
            },
            onRequestToken: (RtcConnection connection) {
                _fetchToken(1234, channelId, 1, true);
            },
            ));

            await _engine.enableVideo();

            await _engine.startPreview();
            await _fetchToken(1234, channelId, 1, true);

            setState(() {
            _isReadyPreview = true;
            });
        }

        Future<void> _fetchToken(
            int uid,
            String channelName,
            int toeknRole,
            bool needJoinChannel,
        ) async {
            var client = http.Client();
            try {
            Map<String, String> headers = {
                'Content-type': 'application/json',
                'Accept': 'application/json',
            };

            var response = await client.post(Uri.parse(hostUrl),
                headers: headers,
                body: jsonEncode(
                    {'uid': uid, 'ChannelName': channelName, 'role': toeknRole}));
            var decodedResponse = jsonDecode(utf8.decode(response.bodyBytes)) as Map;

            final token = decodedResponse['token'];
            if (needJoinChannel) {
                await _engine.joinChannel(
                token: token,
                channelId: channelName,
                uid: uid,
                options: const ChannelMediaOptions(
                    channelProfile: ChannelProfileType.channelProfileLiveBroadcasting,
                    clientRoleType: ClientRoleType.clientRoleBroadcaster,
                ),
                );
            } else {
                await _engine.renewToken(token);
            }
            } finally {
            client.close();
            }
        }

        @override
        Widget build(BuildContext context) {
            if (!_isReadyPreview) return Container();
            return Stack(
            children: [
                AgoraVideoView(
                controller: VideoViewController(
                    rtcEngine: _engine,
                    canvas: const VideoCanvas(uid: 0),
                ),
                ),
                Align(
                alignment: Alignment.topLeft,
                child: SingleChildScrollView(
                    scrollDirection: Axis.horizontal,
                    child: Row(
                    children: List.of(remoteUid.map(
                        (e) => SizedBox(
                        width: 120,
                        height: 120,
                        child: AgoraVideoView(
                            controller: VideoViewController.remote(
                            rtcEngine: _engine,
                            canvas: VideoCanvas(uid: e),
                            connection: const RtcConnection(channelId: channelId),
                            ),
                        ),
                        ),
                    )),
                    ),
                ),
                )
            ],
            );
        }
        }
    ```

    Replace `<Your app ID>` with your app ID, which must be consistent with the app ID you specified in the server configuration. Update `<Your host URL and port>` with the host URL and port of the local Golang server you have deployed. For example `99.9.9.99:8082`.

    The sample code implements the following logic:

    * Calls `joinChannel` to join a channel using the user ID, the channel name, and a token you obtain from the server. The user ID and channel name you specify must be consistent with the values you used to generate the token.

    * The SDK triggers an `onTokenPrivilegeWillExpire` callback 30 seconds before the token expires. After receiving the callback, you obtain a new token from the server and call `renewToken` to pass the newly generated token to the SDK.

    * If the token expires, the SDK triggers an `onRequestToken` callback. After receiving the callback, obtain a new token from the server and call `joinChannel` with the new token to rejoin the channel.

    Build and run the project on the local device, the app performs the following operations:

    * Obtains a token from your token server.
    * Joins the channel.
    * Automatically renews the token when it is about to expire.

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

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

    ### Use a token to join a channel [#use-a-token-to-join-a-channel-3]

    The client requests a token from your authentication server corresponding to the user ID and the channel name. You use the received token to join a channel.

    ```swift
    // Channel name
    let channelId: String = "xxxx"
    // User ID
    let uid: UInt = 0
    // Request the server to generate a token corresponding to channelId and uid
    let token = getToken()
    // Set channel media options
    let mediaOption = AgoraRtcChannelMediaOptions()
    // Set the user role as host
    mediaOption.clientRoleType = .broadcaster
    // Use the token to join a channel
    agoraKit.joinChannel(byToken: token, channelId: channelId, uid: uid, mediaOptions: mediaOption)
    ```

    ### Token expiration [#token-expiration-1]

    After you join a channel using a token, the SDK triggers an `onTokenPrivilegeWillExpire` callback, 30 seconds before the token is set to expire.

    When the token expires, the SDK triggers an `onRequestToken` callback. After receiving the callback, you regenerate a new token on the server side, and then update the token in one of the following ways:

    #### Single channel use-case [#single-channel-use-case-1]

    * Call `renewToken` to pass in the newly generated Token (Recommended).

    * Call `updateChannelWithMediaOptions` to update the token.

    * Call `leaveChannel` \[2/2] to leave the current channel, and then pass in a new token when calling `joinChannelByToken` \[2/4] to rejoin the channel.

    #### Multi-channel use-case [#multi-channel-use-case-1]

    If you call `joinChannelEx` to join multiple channels, call the `updateChannelExWithMediaOptions` method to update the token.

    The following sample code demonstrates how to call `renewToken` to update the token upon receiving an `tokenPrivilegeWillExpire` callback notification.

    ```swift
    extension JoinChannelVideoToken: AgoraRtcEngineDelegate {
        // Callback is triggered when the token is about to expire
        func rtcEngine(_ engine: AgoraRtcEngineKit, tokenPrivilegeWillExpire token: String) {
            // Request to generate a fresh token
            let token = getToken()
            // Update token
            engine.renewToken(token)
        }
    }
    ```

    ### Complete sample code [#complete-sample-code-1]

    For a complete implementation of token authentication, replace the content in `ViewController.swift` with the following code. Replace `Your App ID` with your App ID and `<Your Host URL and port>` with the host URL and port of the local Golang server you have deployed. For example, 123.1.23.123:8082.

    **Complete sample code for token authentication**

    ```swift
        import UIKit
        import AgoraRtcKit
        import Foundation

        public enum TokenError: Error{
            case noData
            case invalidData
        }

        class ViewController: UIViewController {
            var localView: UIView!
            var remoteView: UIView!

            var agoraKit: AgoraRtcEngineKit!

            override func viewDidLoad() {
                super.viewDidLoad()
                //After loading the view, you can make other settings
                initView()
                initializeAgoraEngine()
                setClientRole()
                setupLocalVideo()
                fetchToken(channelName: "test", userId: 1234, role: 1){ result in
                    switch result {
                    case .success(let token):
                        print("token is: \(token)")
                        self.joinChannel(token: token)
                    case .failure(let err):
                        print("Could not fetch token: \(err)")
                    }
                }
            }

            override func viewDidLayoutSubviews(){
                super.viewDidLayoutSubviews()
                remoteView.frame = self.view.bounds
                localView.frame = CGRect(x: self.view.bounds.width - 90, y: 0, width: 90, height: 160)
            }

            override func viewDidDisappear(_ animated: Bool) {
                super.viewDidDisappear(true)
                leaveChannel()
                destroy()
            }

            func initView(){
                remoteView = UIView()
                self.view.addSubview(remoteView)
                localView = UIView()
                self.view.addSubview(localView)
            }

            func initializeAgoraEngine(){
                let config = AgoraRtcEngineConfig()
                config.appId = "Your App ID"
                config.channelProfile = .liveBroadcasting
                agoraKit = AgoraRtcEngineKit.sharedEngine(with: config, delegate: self)
                if agoraKit != nil{
                    print("Initialization successful")
                }
                else{
                    print("Initialization failed")
                }
            }

            func setClientRole(){
                agoraKit.setClientRole(.broadcaster)
            }

            func setupLocalVideo(){
                agoraKit.enableVideo()
                agoraKit.startPreview()
                let videoCanvas = AgoraRtcVideoCanvas()
                videoCanvas.uid = 0
                videoCanvas.renderMode = .hidden
                videoCanvas.view = localView
                agoraKit.setupLocalVideo(videoCanvas)
            }

            func joinChannel(token:String){
                let option = AgoraRtcChannelMediaOptions()
                agoraKit.joinChannel(byToken: token, channelId: "test", uid: 123456, mediaOptions: option)
            }

            func leaveChannel(){
                agoraKit.stopPreview()
                agoraKit.leaveChannel(nil)
            }

            func destroy(){
                AgoraRtcEngineKit.destroy()
            }

            func fetchToken(channelName: String, userId: UInt, role: UInt,
                callback: @escaping (Result<String, Error>) -> Void
            ){
                let url = URL(string: "http://<Your Host URL and port>/fetch_rtc_token")
                let parameters = ["uid":userId,"channelName": channelName, "role": role] as [String : Any]

                print(parameters.self)

                var request = URLRequest(
                    url: url!,
                    timeoutInterval: 10
                )

                request.httpMethod = "POST"

                do {
                    request.httpBody = try JSONSerialization.data(withJSONObject: parameters, options: .prettyPrinted)
                }
                catch let error {
                    print(error.localizedDescription)
                }

                URLSession.shared.dataTask(with: request){data, _, err in
                    guard let data = data else {
                        if let err = err {
                            callback(.failure(err))
                        }
                        else {
                            callback(.failure(TokenError.noData))
                        }
                    return
                }

                let responseJSON = try? JSONSerialization.jsonObject(with: data, options: [])

                if let responseDict = responseJSON as? [String: Any], let token = responseDict["token"] as? String {
                    callback(.success(token))
                } else {
                    callback(.failure(TokenError.invalidData))
                }

            }.resume()
        }
        }

        extension ViewController: AgoraRtcEngineDelegate{
            func rtcEngine(_ engine: AgoraRtcEngineKit, didJoinedOfUid uid: UInt, elapsed: Int){
                let videoCanvas = AgoraRtcVideoCanvas()
                videoCanvas.uid = uid
                videoCanvas.renderMode = .hidden
                videoCanvas.view = remoteView
                agoraKit.setupRemoteVideo(videoCanvas)
            }

            func rtcEngine(_ engine: AgoraRtcEngineKit, tokenPrivilegeWillExpire token: String) {
                self.fetchToken(channelName: "test", userId: 1234, role: 1){ result in
                    switch result {
                    case .success(let token):
                        print("token is: \(token)")
                        self.agoraKit.renewToken(token)
                        print("Renewed the token")
                    case .failure(let err):
                        print("Could not fetch token: \(err)")
                    }
                }
            }

            func rtcEngine(_ engine: AgoraRtcEngineKit, connectionStateChanged state: AgoraConnectionState, reason: AgoraConnectionChangedReason) {
                print("Connection state changed to")
                print(state.rawValue)
            }

            func rtcEngineRequestToken(_ engine: AgoraRtcEngineKit) {
                fetchToken(channelName: "test", userId: 1234, role: 1){ result in
                    switch result {
                        case .success(let token):
                            print("token is: \(token)")
                            self.joinChannel(token: token)
                        case .failure(let err):
                            print("Could not fetch token: \(err)")
                        }
                }
            }
        }
    ```

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

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

    ### Use a token to join a channel [#use-a-token-to-join-a-channel-4]

    The client requests a token from your authentication server corresponding to the user ID and the channel name. You use the received token to join a channel.

    ```swift
    // Channel name
    let channelId: String = "xxxx"
    // User ID
    let uid: UInt = 0
    // Request the server to generate a token corresponding to channelId and uid
    let token = getToken()
    // Set channel media options
    let mediaOption = AgoraRtcChannelMediaOptions()
    // Set the user role as host
    mediaOption.clientRoleType = .broadcaster
    // Use the token to join a channel
    agoraKit.joinChannel(byToken: token, channelId: channelId, uid: uid, mediaOptions: mediaOption)
    ```

    ### Token expiration [#token-expiration-2]

    After you join a channel using a token, the SDK triggers an `onTokenPrivilegeWillExpire` callback, 30 seconds before the token is set to expire.

    When the token expires, the SDK triggers an `onRequestToken` callback. After receiving the callback, you regenerate a new token on the server side, and then update the token in one of the following ways:

    #### Single channel use-case [#single-channel-use-case-2]

    * Call `renewToken` to pass in the newly generated Token (Recommended).

    * Call `updateChannelWithMediaOptions` to update the token.

    * Call `leaveChannel` \[2/2] to leave the current channel, and then pass in a new token when calling `joinChannelByToken` \[2/4] to rejoin the channel.

    #### Multi-channel use-case [#multi-channel-use-case-2]

    If you call `joinChannelEx` to join multiple channels, call the `updateChannelExWithMediaOptions` method to update the token.

    The following sample code demonstrates how to call `renewToken` to update the token upon receiving an `tokenPrivilegeWillExpire` callback notification.

    ```swift
    extension JoinChannelVideoToken: AgoraRtcEngineDelegate {
        // Callback is triggered when the token is about to expire
        func rtcEngine(_ engine: AgoraRtcEngineKit, tokenPrivilegeWillExpire token: String) {
            // Request to generate a fresh token
            let token = getToken()
            // Update token
            engine.renewToken(token)
        }
    }
    ```

    ### Complete sample code [#complete-sample-code-2]

    For a complete implementation of token authentication, replace the content in `ViewController.swift` with the following code. Replace `Your App ID` with your App ID and `<Your Host URL and port>` with the host URL and port of the local Golang server you have deployed. For example, `123.1.23.123:8082`.

    **Complete sample code for token authentication**

    ```swift
    import UIKit
    import AgoraRtcKit
    import Foundation

    public enum TokenError: Error {
        case noData
        case invalidData
    }

    class ViewController: UIViewController {
        var localView: UIView!
        var remoteView: UIView!

        var agoraKit: AgoraRtcEngineKit!

        override func viewDidLoad() {
            super.viewDidLoad()
            // After loading the view, you can make other settings.
            initView()
            initializeAgoraEngine()
            setClientRole()
            setupLocalVideo()
            fetchToken(channelName: "test", userId: 1234, role: 1) { result in
                switch result {
                    case .success(let token):
                        print("token is: \\(token)")
                        self.joinChannel(token: token)
                    case .failure(let err):
                        print("Could not fetch token: \\(err)")
                }
            }
        }

        override func viewDidLayoutSubviews() {
            super.viewDidLayoutSubviews()
            remoteView.frame = self.view.bounds
            localView.frame = CGRect(x: self.view.bounds.width - 90, y: 0, width: 90, height: 160)
        }

        override func viewDidDisappear(_ animated: Bool) {
            super.viewDidDisappear(true)
            leaveChannel()
            destroy()
        }

        func initView() {
            remoteView = UIView()
            self.view.addSubview(remoteView)
            localView = UIView()
            self.view.addSubview(localView)
        }

        func initializeAgoraEngine() {
            let config = AgoraRtcEngineConfig()
            config.appId = "Your App ID"
            config.channelProfile = .liveBroadcasting
            agoraKit = AgoraRtcEngineKit.sharedEngine(with: config, delegate: self)
            if agoraKit != nil {
                print("Initialization successful")
            } else {
                print("Initialization failed")
            }
        }

        func setClientRole() {
            agoraKit.setClientRole(.broadcaster)
        }

        func setupLocalVideo() {
            agoraKit.enableVideo()
            agoraKit.startPreview()
            let videoCanvas = AgoraRtcVideoCanvas()
            videoCanvas.uid = 0
            videoCanvas.renderMode = .hidden
            videoCanvas.view = localView
            agoraKit.setupLocalVideo(videoCanvas)
        }

        func joinChannel(token: String) {
            let option = AgoraRtcChannelMediaOptions()
            agoraKit.joinChannel(byToken: token, channelId: "test", uid: 123456, mediaOptions: option)
        }

        func leaveChannel() {
            agoraKit.stopPreview()
            agoraKit.leaveChannel(nil)
        }

        func destroy() {
            AgoraRtcEngineKit.destroy()
        }

        func fetchToken(
            channelName: String,
            userId: UInt,
            role: UInt,
            callback: @escaping (Result<String, Error>) -> Void
        ) {
            let url = URL(string: "http://<Your Host URL and port>/fetch_rtc_token")
            let parameters = ["uid": userId, "channelName": channelName, "role": role] as [String : Any]

            print(parameters.self)

            var request = URLRequest(
                url: url!,
                timeoutInterval: 10
            )

            request.httpMethod = "POST"

            do {
                request.httpBody = try JSONSerialization.data(withJSONObject: parameters, options: .prettyPrinted)
            } catch let error {
                print(error.localizedDescription)
            }

            URLSession.shared.dataTask(with: request) { data, _, err in
                guard let data = data else {
                    if let err = err {
                        callback(.failure(err))
                    } else {
                        callback(.failure(TokenError.noData))
                    }
                    return
                }

                let responseJSON = try? JSONSerialization.jsonObject(with: data, options: [])

                if let responseDict = responseJSON as? [String: Any], let token = responseDict["token"] as? String {
                    callback(.success(token))
                } else {
                    callback(.failure(TokenError.invalidData))
                }
            }.resume()
        }
    }

    extension ViewController: AgoraRtcEngineDelegate {
        func rtcEngine(_ engine: AgoraRtcEngineKit, didJoinedOfUid uid: UInt, elapsed: Int) {
            let videoCanvas = AgoraRtcVideoCanvas()
            videoCanvas.uid = uid
            videoCanvas.renderMode = .hidden
            videoCanvas.view = remoteView
            agoraKit.setupRemoteVideo(videoCanvas)
        }

        func rtcEngine(_ engine: AgoraRtcEngineKit, tokenPrivilegeWillExpire token: String) {
            self.fetchToken(channelName: "test", userId: 1234, role: 1) { result in
                switch result {
                    case .success(let token):
                        print("token is: \\(token)")
                        self.agoraKit.renewToken(token)
                        print("Renewed the token")
                    case .failure(let err):
                        print("Could not fetch token: \\(err)")
                }
            }
        }

        func rtcEngine(
            _ engine: AgoraRtcEngineKit,
            connectionStateChanged state: AgoraConnectionState,
            reason: AgoraConnectionChangedReason
        ) {
            print("Connection state changed to")
            print(state.rawValue)
        }

        func rtcEngineRequestToken(_ engine: AgoraRtcEngineKit) {
            fetchToken(channelName: "test", userId: 1234, role: 1) { result in
                switch result {
                    case .success(let token):
                        print("token is: \\(token)")
                        self.joinChannel(token: token)
                    case .failure(let err):
                        print("Could not fetch token: \\(err)")
                }
            }
        }
    }
    ```

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

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

    ### Use a token [#use-a-token]

    1. **Import the components and hooks you need to handle the authentication workflow**:

       ```typescript
       import { useClientEvent, useRTCClient } from "agora-rtc-react";
       ```

    2. **Retrieve a token from the authentication server**:

       ```typescript
       async function fetchRTCToken(channelName: string) {
         if (config.serverUrl !== "") {
           try {
             const response = await fetch(
               `${config.proxyUrl}${config.serverUrl}/rtc/${channelName}/publisher/uid/${config.uid}/?expiry=${config.tokenExpiryTime}`
             );
             const data = await response.json();
             console.log("RTC token fetched from server: ", data.rtcToken);
             return data.rtcToken;
           } catch (error) {
             console.error(error);
             throw error;
           }
         } else {
           return config.rtcToken;
         }
       }
       ```

    3. **Handle the event triggered by Agora SDRTN® when the token is about to expire**:

       A token expires after the `tokenExpiryTime` specified in the call to the token server or after 24 hours, if the time is not specified. The `useTokenWillExpire` method receives a callback when the current token is about to expire so that a fresh token may be retrieved and used.

       ```typescript
       const useTokenWillExpire = () => {
         const agoraEngine = useRTCClient();
         useClientEvent(agoraEngine, "token-privilege-will-expire", () => {
           if (config.serverUrl !== "") {
             fetchRTCToken(config.channelName)
               .then((token: string) => {
                 console.log("RTC token fetched from server: ", token);
                 return agoraEngine.renewToken(token);
               })
               .catch((error) => {
                 console.error(error);
               });
           } else {
             console.log("Please make sure you specified the token server URL in the configuration file");
           }
         });
       };
       ```

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

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

    ### Use a token to join a channel [#use-a-token-to-join-a-channel-5]

    This section shows you how to integrate token authentication in your app.

    1. Open the [SDK quickstart](/en/realtime-media/video/get-started-sdk) project you created earlier.

    2. In `ProjectName.tsx` replace the contents with the following:

    **Sample code for basic authentication**

    ```js
    import React, { useCallback, useEffect, useRef, useState } from 'react';
        import {
            PermissionsAndroid,
            Platform,
            SafeAreaView,
            ScrollView,
            StyleSheet,
            Switch,
            Text,
            View,
        } from 'react-native';

        import {
            ChannelProfileType,
            ClientRoleType,
            createAgoraRtcEngine,
            IRtcEngine,
            IRtcEngineEventHandler,
            RtcConnection,
            RtcSurfaceView,
        } from 'react-native-agora';

        // Define basic information
        const appId = '<Your app ID>';
        const channelName = 'test';
        const uid = 123; // Local user Uid

        const App = () => {
        const agoraEngineRef = useRef<IRtcEngine>(); // IRtcEngine instance
        const eventHandlerRef = useRef<IRtcEngineEventHandler>(); // IRtcEngine event handler
        const [token, setToken] = useState<string>(''); // RTC Token
        const [isJoined, setIsJoined] = useState(false); // Whether the local user has joined the channel
        const [isHost, setIsHost] = useState(true); // User role
        const [remoteUid, setRemoteUid] = useState(0); // Uid of remote user
        const [message, setMessage] = useState(''); // Message to the user

        // Send a request to obtain a token to the token server
        const fetchToken = useCallback(() => {
            // Token server URL example: http://12.123.1.123:8082/fetch_rtc_token
            const url = '<Your host URL and port>/fetch_rtc_token';
            const body = JSON.stringify({
            uid,
            ChannelName: channelName,
            role: isHost
                ? ClientRoleType.ClientRoleBroadcaster
                : ClientRoleType.ClientRoleAudience,
            });
            console.log('fetchToken', url, body);
            return fetch(url, {
                method: 'POST',
                body,
            })
            .then((res) => res.json())
            .then((res) => {
                showMessage('token ' + res.token + 'Already refreshed');
                if (+res.code === 200) {
                setToken(res.token);
                }
                return res;
            });
            }, [isHost]);

        // Use the obtained token to join the channel
        const join = useCallback(async () => {
            if (!token) {
                await fetchToken();
            } else {
            try {
                // After joining the channel, set the channel profile to live broadcast
                agoraEngineRef.current?.setChannelProfile(
                ChannelProfileType.ChannelProfileLiveBroadcasting
                );
                if (isHost) {
                agoraEngineRef.current?.startPreview();
                // Join the channel as a host
                agoraEngineRef.current?.joinChannel(token, channelName, uid, {
                    publishCameraTrack: true,
                    clientRoleType: ClientRoleType.ClientRoleBroadcaster,
                });
                } else {
                // Join the channel as audience
                agoraEngineRef.current?.joinChannel(token, channelName, uid, {
                    clientRoleType: ClientRoleType.ClientRoleAudience,
                });
                }
            } catch (e) {
                console.log(e);
            }
        }
        }, [fetchToken, isHost, token]);

        const leave = () => {
            try {
                agoraEngineRef.current?.leaveChannel();
                setRemoteUid(0);
                setIsJoined(false);
                showMessage('Left the channel');
            } catch (e) {
                console.error(e);
            }
        };

        useEffect(() => {
            const setupVideoSDKEngine = async () => {
            try {
                if (Platform.OS === 'android') {
                await getPermission();
                }
                agoraEngineRef.current = createAgoraRtcEngine();
                eventHandlerRef.current = {
                onJoinChannelSuccess: () => {
                    showMessage('Successfully joined the channel: ' + channelName);
                    setIsJoined(true);
                },
                onUserJoined: (_connection, Uid) => {
                    showMessage('Remote user ' + Uid + ' joined');
                    setRemoteUid(Uid);
                },
                onUserOffline: (_connection, Uid) => {
                    showMessage('Remote user ' + Uid + ' left the channel');
                    setRemoteUid(0);
                },
                onTokenPrivilegeWillExpire(connection: RtcConnection, token: string) {
                    showMessage('token ' + token + 'Expires soon');
                    fetchToken();
                },
                onRequestToken(connection: RtcConnection) {
                    showMessage('token expired');
                    setToken('');
                },
                };
                const agoraEngine = agoraEngineRef.current;
                //Initialize engine
                agoraEngine.initialize({
                    appId: appId,
                    channelProfile: ChannelProfileType.ChannelProfileLiveBroadcasting,
                });
                // Register event handler
                agoraEngine.registerEventHandler(eventHandlerRef.current);
                // Start local video
                agoraEngine.enableVideo();
            } catch (e) {
                console.error(e);
            }
        };

            setupVideoSDKEngine();

            return () => {
                agoraEngineRef.current?.release();
            };
        }, []); // eslint-disable-line react-hooks/exhaustive-deps

        useEffect(() => {
            const checkJoined = () => {
            if (isJoined) {
                console.log('renewToken');
                // agoraEngineRef.current?.renewToken(token);
            } else {
                console.log('joinChannel');
                join();
            }
            };
            checkJoined();
        }, [token]); // eslint-disable-line react-hooks/exhaustive-deps

        // Render the user interface
        return (
            <SafeAreaView style={styles.main}>
            <Text style={styles.head}>Agora SDK Quickstart</Text>
            <View style={styles.btnContainer}>
                <Text onPress={join} style={styles.button}>
                Join channel
                </Text>
                <Text onPress={leave} style={styles.button}>
                Leave Channel
                </Text>
            </View>
            <View style={styles.btnContainer}>
                <Text>Audience</Text>
                <Switch
                onValueChange={(switchValue) => {
                    setIsHost(switchValue);
                    if (isJoined) {
                    leave();
                    }
                }}
                value={isHost}
                />
                <Text>Host</Text>
            </View>
            <ScrollView
                style={styles.scroll}
                contentContainerStyle={styles.scrollContainer}
            >
                {isJoined ? (
                <React.Fragment key={0}>
                    <RtcSurfaceView canvas={{ uid: 0 }} style={styles.videoView} />
                    <Text>Local user uid: {uid}</Text>
                </React.Fragment>
                ) : (
                <Text>Join a channel</Text>
                )}
                {isJoined && !isHost && remoteUid !== 0 ? (
                <React.Fragment key={remoteUid}>
                    <RtcSurfaceView
                    canvas={{ uid: remoteUid }}
                    style={styles.videoView}
                    />
                    <Text>Remote user uid: {remoteUid}</Text>
                </React.Fragment>
                ) : (
                <Text>{isJoined && !isHost ? 'Wait for a remote user to join' : ''}</Text>
                )}
                <Text style={styles.info}>{message}</Text>
            </ScrollView>
            </SafeAreaView>
        );

        // Show message
        function showMessage(msg: string) {
            console.info(msg);
            setMessage(msg);
        }
        };

        // Define user interface style
        const styles = StyleSheet.create({
            button: {
                paddingHorizontal: 25,
                paddingVertical: 4,
                fontWeight: 'bold',
                color: '#ffffff',
                backgroundColor: '#0055cc',
                margin: 5,
            },
            main: { flex: 1, alignItems: 'center' },
            scroll: { flex: 1, backgroundColor: '#ddeeff', width: '100%' },
            scrollContainer: { alignItems: 'center' },
            videoView: { width: '90%', height: 200 },
            btnContainer: { flexDirection: 'row', justifyContent: 'center' },
            head: { fontSize: 20 },
            info: { backgroundColor: '#ffffe0', paddingHorizontal: 8, color: '#0000ff' },
        });

        const getPermission = async () => {
            if (Platform.OS === 'android') {
                await PermissionsAndroid.requestMultiple([
                'android.permission.RECORD_AUDIO',
                'android.permission.CAMERA',
                ]);
            }
        };

        export default App;
    ```

    Replace `<Your app ID>` with your app ID, which must be consistent with the app ID you specified in the server configuration. Update `<Your host URL and port>` with the host URL and port of the local Golang server you have deployed. For example `99.9.9.99:8082`.

    The sample code implements the following logic:

    * Calls `joinChannel` to join a channel using the user ID, the channel name, and a token you obtain from the server. The user ID and channel name you specify must be consistent with the values you used to generate the token.

    * The SDK triggers an `onTokenPrivilegeWillExpire` callback 30 seconds before the token expires. After receiving the callback, you obtain a new token from the server and call `renewToken` to pass the newly generated token to the SDK.

    * If the token expires, the SDK triggers an `onRequestToken` callback. After receiving the callback, obtain a new token from the server and call `joinChannel` with the new token to rejoin the channel.

    Build and run the project on the local device, the app performs the following operations:

    * Obtains a token from your token server.
    * Joins the channel.
    * Automatically renews the token when it is about to expire.

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

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

    ### Use a token to join a channel [#use-a-token-to-join-a-channel-6]

    This section shows you how to integrate token authentication in your app.

    1. Open the [SDK quickstart](/en/realtime-media/video/get-started-sdk) project you created earlier.

    2. In `joinChannelVideo.cs` replace the contents with the following:

    **Sample code for basic authentication**

    ```cpp
    using UnityEngine;
        using UnityEngine.UI;
        using UnityEngine.Serialization;
        using Agora.Rtc;
        using Agora.Util;
        using Logger = Agora.Util.Logger;

        namespace Agora_RTC_Plugin.API_Example.Examples.Advanced.JoinChannelVideoToken
        {
            public class JoinChannelVideoToken : MonoBehaviour
            {
                // Fill in your App ID and channel name
                public string appID = "<Your App ID>";
                public string channelName = "test";
                public uint localUid = 0;
                internal static string channelToken = "";
                internal IRtcEngine RtcEngine = null;

                internal CONNECTION_STATE_TYPE _state = CONNECTION_STATE_TYPE.CONNECTION_STATE_DISCONNECTED;

                private void Start()
                {

                    if (CheckAppId())
                    {
                        InitEngine();
                        JoinChannel();
                    }
                }

                internal void RenewOrJoinToken(TokenObject tokenObject)
                {
                    if (tokenObject ==  null || tokenObject.code != 200) {
                        Debug.Log("get token failed");
                        return;
                    }

                    JoinChannelVideoToken.channelToken = tokenObject.token;
                    if (_state == CONNECTION_STATE_TYPE.CONNECTION_STATE_DISCONNECTED
                        || _state == CONNECTION_STATE_TYPE.CONNECTION_STATE_DISCONNECTED
                        || _state == CONNECTION_STATE_TYPE.CONNECTION_STATE_FAILED
                    )
                    {
                        // Use the token to join the channel
                        JoinChannel();
                    }
                    else
                    {
                        // Update token in the channel
                        UpdateToken();
                    }
                }

                private void Update()
                {
                    PermissionHelper.RequestMicrophonePermission();
                    PermissionHelper.RequestCameraPermission();
                }

                private void UpdateToken()
                {
                    RtcEngine.RenewToken(JoinChannelVideoToken.channelToken);
                    Debug.Log("renewToken: " + JoinChannelVideoToken.channelToken);
                }

                private bool CheckAppId()
                {
                    if (appID.Length < 10) {
                        Debug.Log("Please fill in your appId in Inspector");
                        return false;
                    }
                    return true;
                }

                // Initialization
                private void InitEngine()
                {
                    RtcEngine = Agora.Rtc.RtcEngine.CreateAgoraRtcEngine();
                    UserEventHandler handler = new UserEventHandler(this);
                    RtcEngineContext context = new RtcEngineContext(appID, 0,
                        CHANNEL_PROFILE_TYPE.CHANNEL_PROFILE_LIVE_BROADCASTING, null,
                        AUDIO_SCENARIO_TYPE.AUDIO_SCENARIO_DEFAULT);
                    RtcEngine.Initialize(context);
                    RtcEngine.InitEventHandler(handler);
                }
                // Join channel
                private void JoinChannel()
                {
                    RtcEngine.SetClientRole(CLIENT_ROLE_TYPE.CLIENT_ROLE_BROADCASTER);
                    RtcEngine.EnableAudio();
                    RtcEngine.EnableVideo();

                    if (channelToken.Length == 0)
                    {
                        StartCoroutine(HelperClass.FetchToken(this.localUid, channelName,CLIENT_ROLE_TYPE.CLIENT_ROLE_BROADCASTER, this.RenewOrJoinToken));
                        return;
                    }

                    RtcEngine.JoinChannel(channelToken, channelName, "");
                    Debug.Log("joinChannel with token:" + channelToken);
                }

                internal void  StartUpdateTokenEveryTenSecond() {
                    InvokeRepeating("RequestToken", 10, 10);
                }

                private void RequestToken() {
                    StartCoroutine(HelperClass.FetchToken(this.localUid, channelName, CLIENT_ROLE_TYPE.CLIENT_ROLE_BROADCASTER, this.RenewOrJoinToken));
                }

                private void OnDestroy()
                {
                    Debug.Log("OnDestroy");
                    if (RtcEngine == null) return;
                    RtcEngine.InitEventHandler(null);
                    RtcEngine.LeaveChannel();
                    RtcEngine.Dispose();
                }

                internal string GetChannelName()
                {
                    return channelName;
                }

                #region -- Video Render UI Logic ---

                internal static void MakeVideoView(uint uid, string channelId = "")
                {
                    GameObject go = GameObject.Find(uid.ToString());
                    if (!ReferenceEquals(go, null))
                    {
                        return;
                    }

                    VideoSurface videoSurface = MakeImageSurface(uid.ToString());
                    if (!ReferenceEquals(videoSurface, null))
                    {

                        if (uid == 0)
                        {
                            videoSurface.SetForUser(uid, channelId);
                        }
                        else
                        {
                            videoSurface.SetForUser(uid, channelId, VIDEO_SOURCE_TYPE.VIDEO_SOURCE_REMOTE);
                        }

                        videoSurface.OnTextureSizeModify += (int width, int height) =>
                        {
                            float scale = (float)height / (float)width;
                            videoSurface.transform.localScale = new Vector3(-5, 5 * scale, 1);
                            Debug.Log("OnTextureSizeModify: " + width + "  " + height);
                        };

                        videoSurface.SetEnable(true);
                    }
                }

                private static VideoSurface MakePlaneSurface(string goName)
                {
                    GameObject go = GameObject.CreatePrimitive(PrimitiveType.Plane);

                    if (go == null)
                    {
                        return null;
                    }

                    go.name = goName;

                    go.transform.Rotate(-90.0f, 0.0f, 0.0f);
                    go.transform.position = Vector3.zero;
                    go.transform.localScale = new Vector3(0.25f, 0.5f, .5f);

                    var videoSurface = go.AddComponent<VideoSurface>();
                    return videoSurface;
                }

                private static VideoSurface MakeImageSurface(string goName)
                {
                    GameObject go = new GameObject();

                    if (go == null)
                    {
                        return null;
                    }

                    go.name = goName;
                    go.AddComponent<RawImage>();
                    go.AddComponent<UIElementDrag>();
                    GameObject canvas = GameObject.Find("VideoCanvas");
                    if (canvas != null)
                    {
                        go.transform.parent = canvas.transform;
                        Debug.Log("add video view");
                    }
                    else
                    {
                        Debug.Log("Canvas is null video view");
                    }

                    go.transform.Rotate(0f, 0.0f, 180.0f);
                    go.transform.localPosition = Vector3.zero;
                    go.transform.localScale = new Vector3(3f, 4f, 1f);

                    var videoSurface = go.AddComponent<VideoSurface>();
                    return videoSurface;
                }

                internal static void DestroyVideoView(uint uid)
                {
                    GameObject go = GameObject.Find(uid.ToString());
                    if (!ReferenceEquals(go, null))
                    {
                        Object.Destroy(go);
                    }
                }

                #endregion
            }

            internal class UserEventHandler : IRtcEngineEventHandler
            {
                private readonly JoinChannelVideoToken _helloVideoTokenAgora;

                internal UserEventHandler(JoinChannelVideoToken helloVideoTokenAgora)
                {
                    _helloVideoTokenAgora= helloVideoTokenAgora;
                }

                public override void OnError(int err, string msg)
                {
                    Debug.Log(string.Format("OnError err: {0}, msg: {1}", err, msg));
                }
                // Joined the channel successfully
                public override void OnJoinChannelSuccess(RtcConnection connection, int elapsed)
                {
                    int build = 0;
                    Debug.Log(string.Format("sdk version: ${0}",
                        _helloVideoTokenAgora.RtcEngine.GetVersion(ref build)));
                    Debug.Log(
                        string.Format("OnJoinChannelSuccess channelName: {0}, uid: {1}, elapsed: {2}",
                            connection.channelId, connection.localUid, elapsed));
                    Debug.Log(string.Format("New Token: {0}",
                        JoinChannelVideoToken.channelToken));
                    JoinChannelVideoToken.MakeVideoView(0);

                    _helloVideoTokenAgora.localUid = connection.localUid;
                    _helloVideoTokenAgora.StartUpdateTokenEveryTenSecond();
                }

                public override void OnUserJoined(RtcConnection connection, uint uid, int elapsed)
                {
                    Debug.Log(string.Format("OnUserJoined uid: ${0} elapsed: ${1}", uid,
                        elapsed));
                    JoinChannelVideoToken.MakeVideoView(uid, _helloVideoTokenAgora.GetChannelName());
                }

                public override void OnUserOffline(RtcConnection connection, uint uid, USER_OFFLINE_REASON_TYPE reason)
                {
                    Debug.Log(string.Format("OnUserOffLine uid: ${0}, reason: ${1}", uid,
                        (int)reason));
                    JoinChannelVideoToken.DestroyVideoView(uid);
                }
                //The token is about to expire
                public override void OnTokenPrivilegeWillExpire(RtcConnection connection, string token)
                {
                    _helloVideoTokenAgora.StartCoroutine(HelperClass.FetchToken(_helloVideoTokenAgora.localUid,
                        _helloVideoTokenAgora.GetChannelName(), CLIENT_ROLE_TYPE.CLIENT_ROLE_BROADCASTER, _helloVideoTokenAgora.RenewOrJoinToken));
                }

                public override void OnConnectionStateChanged(RtcConnection connection, CONNECTION_STATE_TYPE state,
                    CONNECTION_CHANGED_REASON_TYPE reason)
                {
                    _helloVideoTokenAgora._state = state;
                }

                public override void OnConnectionLost(RtcConnection connection)
                {
                    Debug.Log(string.Format("OnConnectionLost "));
                }
            }

            #endregion
        }

        namespace Agora.Util
        {
            public static class HelperClass
            {
                public static IEnumerator FetchToken(uint uid, string channelName, CLIENT_ROLE_TYPE role, Action<TokenObject> callback = null)
                {

                    Dictionary<string, object> postParams = new Dictionary<string, object>();
                    postParams.Add("uid", uid);
                    postParams.Add("ChannelName", channelName);
                    postParams.Add("role", role);
                    string json = AgoraJson.ToJson<Dictionary<string, object>>(postParams);
                    var url = "http://127.0.0.1/fetch_rtc_token";

                    byte[] postBytes = System.Text.Encoding.Default.GetBytes(json);
                    UnityWebRequest request = new UnityWebRequest(url, "POST");

                    request.uploadHandler = (UploadHandler)new UploadHandlerRaw(postBytes);
                    request.downloadHandler = (DownloadHandler)new DownloadHandlerBuffer();
                    request.SetRequestHeader("Content-Type", "application/json");
                    yield return request.SendWebRequest();

                    if (request.isNetworkError || request.isHttpError)
                    {
                        Debug.Log(request.error);
                        callback(null);
                        yield break;
                    }
                    Debug.Log("text: " + request.downloadHandler.text);
                    TokenObject tokenInfo = JsonUtility.FromJson<TokenObject>(request.downloadHandler.text);
                    callback(tokenInfo);
                }
            }
        }
    ```

    Replace `<Your app ID>` with your app ID, which must be consistent with the app ID you specified in the server configuration. Update `url` with the host URL and port of the local Golang server you have deployed.

    The sample code implements the following logic:

    * Calls `JoinChannel` to join a channel using the user ID, the channel name, and a token you obtain from the server. The user ID and channel name you specify must be consistent with the values you used to generate the token.

    * The SDK triggers an `OnTokenPrivilegeWillExpire` callback 30 seconds before the token expires. After receiving the callback, you obtain a new token from the server and call `RenewToken` to pass the newly generated token to the SDK.

    Build and run the project on the local device, the app performs the following operations:

    * Obtains a token from your token server.
    * Joins the channel.
    * Automatically renews the token when it is about to expire.

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

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

    ### Use a token to join a channel [#use-a-token-to-join-a-channel-7]

    The client requests a token from your authentication server corresponding to the user ID and the channel name. You use the received token to join a channel.

    ```cpp
    // Channel name
    const char* channelId;
    // User ID
    int uid = 0
    // Request a token from the server corresponding to channelId and uid
    const char* token = getToken();
    // Set channel media options
    ChannelMediaOptions options;
    // Set the user role as host
    options.clientRoleType = CLIENT_ROLE_TYPE::CLIENT_ROLE_BROADCASTER;
    // Join the channel
    m_rtcEngine->joinChannel(token, channelId, uid, options);
    ```

    ### Token expiration [#token-expiration-3]

    After you join a channel using a token, the SDK triggers an `onTokenPrivilegeWillExpire` callback, 30 seconds before the token is set to expire.

    When the token expires, the SDK triggers an `onRequestToken` callback. After receiving the callback, you regenerate a new token on the server side, and then update the token in one of the following ways:

    #### Single channel use-case [#single-channel-use-case-3]

    * Call `renewToken` to pass in the newly generated Token (Recommended).

    * Call `updateChannelMediaOptions` to update the token.

    * Call `leaveChannel` \[2/2] to leave the current channel, and then pass in a new token when calling `joinChannel` \[2/2] to rejoin the channel.

    #### Multi-channel use-case [#multi-channel-use-case-3]

    If you call `joinChannelEx` to join multiple channels, call the `updateChannelMediaOptionsEx` method to update the token.

    The following sample code demonstrates how to call `renewToken` to update the token upon receiving an `onTokenPrivilegeWillExpire` callback notification.

    ```cpp
    class CJoinChannelVideoByTokenRtcEngineEventHandler
        : public IRtcEngineEventHandler
    {
    public:
        // Triggered when a token is about to expire
        virtual void onTokenPrivilegeWillExpire(const char* token) {
        // Request to generate a fresh token
        const char* token = getToken();
        // Renew token
        m_rtcEngine->renewToken(token);
        }
    }
    ```

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

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

    ### Use a token to join a channel [#use-a-token-to-join-a-channel-8]

    The client requests a token from your authentication server corresponding to the user ID and the channel name. You use the received token to join a channel.

    ```cpp
    // Channel name
    const char* channelId;
    // User ID
    int uid = 0
    // Request a token from the server corresponding to channelId and uid
    const char* token = getToken();
    // Set channel media options
    ChannelMediaOptions options;
    // Set the user role as host
    options.clientRoleType = CLIENT_ROLE_TYPE::CLIENT_ROLE_BROADCASTER;
    // Join the channel
    m_rtcEngine->joinChannel(token, channelId, uid, options);
    ```

    ### Token expiration [#token-expiration-4]

    After you join a channel using a token, the SDK triggers an `onTokenPrivilegeWillExpire` callback, 30 seconds before the token is set to expire.

    When the token expires, the SDK triggers an `onRequestToken` callback. After receiving the callback, you regenerate a new token on the server side, and then update the token in one of the following ways:

    #### Single channel use-case [#single-channel-use-case-4]

    * Call `renewToken` to pass in the newly generated Token (Recommended).

    * Call `updateChannelMediaOptions` to update the token.

    * Call `leaveChannel` \[2/2] to leave the current channel, and then pass in a new token when calling `joinChannel` \[2/2] to rejoin the channel.

    #### Multi-channel use-case [#multi-channel-use-case-4]

    If you call `joinChannelEx` to join multiple channels, call the `updateChannelMediaOptionsEx` method to update the token.

    The following sample code demonstrates how to call `renewToken` to update the token upon receiving an `onTokenPrivilegeWillExpire` callback notification.

    ```cpp
    class CJoinChannelVideoByTokenRtcEngineEventHandler
        : public IRtcEngineEventHandler
    {
    public:
        // Triggered when a token is about to expire
        virtual void onTokenPrivilegeWillExpire(const char* token) {
        // Request to generate a fresh token
        const char* token = getToken();
        // Renew token
        m_rtcEngine->renewToken(token);
        }
    }
    ```

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

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

    ### Use a token to join a channel [#use-a-token-to-join-a-channel-9]

    This section shows you how to integrate token authentication in your app.

    1. Open the [Quickstart](/en/realtime-media/video/get-started-sdk) project you created earlier.

    2. In `index.html`, include an HTTP client library such as `axios` for sending token requests to the authentication server.

       ```html
       <html>
           <head>
               <title>Token demo</title>
           </head>
           <script src="https://unpkg.com/axios/dist/axios.min.js"></script>
           <body>
               <h1>Token demo</h1>
               <script src="https://download.agora.io/sdk/release/AgoraRTC_N-4.24.5.js"></script>
               <script src="../agoraLogic.js"></script>
           </body>
       </html>
       ```

    3. Add a `fetchToken` method to retrieve a token from your token server to join a channel.

       ```js
       // Retrieve a token from your token server
       function fetchToken(uid, channelName, tokenRole) {

           return new Promise(function (resolve) {
               axios.post('http://<Your host URL and port>/fetch_rtc_token', {
                   uid: uid,
                   channelName: channelName,
                   role: tokenRole
               }, {
                   headers: {
                       'Content-Type': 'application/json; charset=UTF-8'
                   }
               })
                   .then(function (response) {
                       const token = response.data.token;
                       resolve(token);
                   })
                   .catch(function (error) {
                       console.log(error);
                   });
           })
       }
       ```

    4. Use the token to join a channel

       ```js
       // Assign the obtained token to the token parameter in the join method
       let token = await fetchToken(uid, options.channel, 1);
       await client.join(options.appId, options.channel, token, uid);
       ```

    ### Token expiration [#token-expiration-5]

    After you join a channel using a token, the SDK triggers the `token-privilege-will-expire` callback, 30 seconds before the token is set to expire. Upon receiving this callback, retrieve a fresh token from the server and call the `renewToken` method to pass the newly generated token to the SDK.

    ```javascript
    client.on("token-privilege-will-expire", async function () {
        // When you receive the token-privilege-will-expire callback, request a fresh token from the server
        let token = await fetchToken(uid, options.channel, 1);
        // Call renewToken to pass the new token to the SDK
        await client.renewToken(token);
    });
    ```

    When the token expires, the SDK triggers the `token-privilege-did-expire` callback. In this case, retrieve a fresh token from the server and call the `join` method to rejoin the channel with the new token:

    ```javascript
    // The token expired.
    client.on("token-privilege-did-expire", async function () {
    console.log("Fetching a new token")
    // Request a new token from the server
    let token = await fetchToken(uid, options.channel, 1);
    console.log("Rejoining the channel with new token")
    // Call join to rejoin the channel
    await client.join(options.appId, options.channel, token, uid);
    });
    ```

    ### Complete sample code [#complete-sample-code-3]

    For a complete implementation of token authentication, refer to the following code:

    **Sample code for basic authentication**

    ```js
    var rtc = {
            // Set local audio track and video track
            localAudioTrack: null,
            localVideoTrack: null,
        };

        var options = {
            // Fill in app ID
            appId: "<Your app ID>",
            // Fill in a channel name
            channel: "test",
            // Set the user as host or audience
            role: "host"
        };

        // Get a token from your token server

        function fetchToken(uid, channelName, tokenRole) {

            return new Promise(function (resolve) {
                axios.post('http://<Your host URL and port>/fetch_rtc_token', {
                    uid: uid,
                    channelName: channelName,
                    role: tokenRole
                }, {
                    headers: {
                        'Content-Type': 'application/json; charset=UTF-8'
                    }
                })
                    .then(function (response) {
                        const token = response.data.token;
                        resolve(token);
                    })
                    .catch(function (error) {
                        console.log(error);
                    });
            })
        }

        async function startBasicCall() {

            const client = AgoraRTC.createClient({ mode: "live", codec: "vp8" });
            client.setClientRole(options.role);
            const uid = 123456;

            // Assign the obtained token to the token parameter of the join method, and join the channel
            let token = await fetchToken(uid, options.channel, 1);

            await client.join(options.appId, options.channel, token, uid);
            rtc.localAudioTrack = await AgoraRTC.createMicrophoneAudioTrack();
            rtc.localVideoTrack = await AgoraRTC.createCameraVideoTrack();
            await client.publish([rtc.localAudioTrack, rtc.localVideoTrack]);
            const localPlayerContainer = document.createElement("div");
            localPlayerContainer.id = uid;
            localPlayerContainer.style.width = "640px";
            localPlayerContainer.style.height = "480px";
            document.body.append(localPlayerContainer);

            rtc.localVideoTrack.play(localPlayerContainer);

            console.log("publish success!");

            client.on("user-published", async (user, mediaType) => {
                await client.subscribe(user, mediaType);
                console.log("subscribe success");

                if (mediaType === "video") {
                    const remoteVideoTrack = user.videoTrack;
                    const remotePlayerContainer = document.createElement("div");
                    remotePlayerContainer.textContent = "Remote user " + user.uid.toString();
                    remotePlayerContainer.style.width = "640px";
                    remotePlayerContainer.style.height = "480px";
                    document.body.append(remotePlayerContainer);
                    remoteVideoTrack.play(remotePlayerContainer);

                }

                if (mediaType === "audio") {
                    const remoteAudioTrack = user.audioTrack;
                    remoteAudioTrack.play();
                }

                client.on("user-unpublished", user => {
                    const remotePlayerContainer = document.getElementById(user.uid);
                    remotePlayerContainer.remove();
                });

            });

            // When you receive the token-privilege-will-expire callback, request a new token from the server and call renewToken to pass the new token to the SDK
            client.on("token-privilege-will-expire", async function () {
                let token = await fetchToken(uid, options.channel, 1);
                await client.renewToken(token);
            });

            // When you receive the token-privilege-did-expire callback, request a new token from the server and call join to rejoin the channel.
            client.on("token-privilege-did-expire", async function () {
                console.log("Fetching a new token")
                let token = await fetchToken(uid, options.channel, 1);
                console.log("Rejoining the channel with a new token")
                await client.join(options.appId, options.channel, token, uid);
            });

        }

        startBasicCall()
    ```

    Replace `<Your app ID>` with your app ID, which must be consistent with the app ID you specified in the server configuration. Update `<Your Host URL and port>` with the host URL and port of the local Golang server you have deployed. For example `99.9.9.99:8082`.

    Build and run the project on the local device, the app performs the following operations:

    * Obtains a token from your token server.
    * Joins the channel.
    * Automatically renews the token when it is about to expire.

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

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

    ### Use a token to join a channel [#use-a-token-to-join-a-channel-10]

    The client requests a token from your authentication server corresponding to the user ID and the channel name. You use the received token to join a channel.

    ```cpp
    // Channel name
    const char* channelId;
    // User ID
    int uid = 0
    // Request the server to generate a token corresponding to channelId and uid
    const char* token = getToken();
    // Set channel media options
    ChannelMediaOptions options;
    // Set the user role as host
    options.clientRoleType = CLIENT_ROLE_TYPE::CLIENT_ROLE_BROADCASTER;
    // Use the token to join a channel
    m_rtcEngine->joinChannel(token, channelId, uid, options);
    ```

    ### Token expiration [#token-expiration-6]

    After you join a channel using a token, the SDK triggers an `onTokenPrivilegeWillExpire` callback, 30 seconds before the token is set to expire.

    When the token expires, the SDK triggers an `onRequestToken` callback. After receiving the callback, you regenerate a new token on the server side, and then update the token in one of the following ways:

    #### Single channel use-case [#single-channel-use-case-5]

    * Call `renewToken` to pass in the newly generated Token (Recommended).

    * Call `updateChannelMediaOptions` to update the token.

    * Call `leaveChannel` \[2/2] to leave the current channel, and then pass in a new token when calling `joinChannel` \[2/2] to rejoin the channel.

    #### Multi-channel use-case [#multi-channel-use-case-5]

    If you call `joinChannelEx` to join multiple channels, call the `updateChannelMediaOptionsEx` method to update the token.

    The following sample code demonstrates how to call `renewToken` to update the token upon receiving an `onTokenPrivilegeWillExpire` callback notification.

    ```cpp
    class CJoinChannelVideoByTokenRtcEngineEventHandler
        : public IRtcEngineEventHandler
    {
    public:
        // Callback is triggered when the token is about to expire
        virtual void onTokenPrivilegeWillExpire(const char* token) {
            // Request to generate a fresh token
            const char* newToken = getToken();
            // Update token
            m_rtcEngine->renewToken(newToken);
        }
    };
    ```

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

<CalloutContainer type="warning">
  <CalloutTitle>
    Note
  </CalloutTitle>

  <CalloutDescription>
    The user ID and channel name used to join a channel must be consistent with the values used to generate the token.
  </CalloutDescription>
</CalloutContainer>

## 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.

<_PlatformTabsGroup groupMode="structured" canonicalPlatform="web" platforms="[&#x22;javascript&#x22;,&#x22;android&#x22;,&#x22;ios&#x22;,&#x22;macos&#x22;,&#x22;web&#x22;,&#x22;react-native&#x22;,&#x22;unity&#x22;,&#x22;electron&#x22;,&#x22;flutter&#x22;,&#x22;unreal&#x22;,&#x22;windows&#x22;]" showTabs="true">
  <_PlatformPanel platform="javascript">
    <_PlatformProcessedMarker groupMode="structured" canonicalPlatform="web" platform="javascript" />

    * [Reference app](https://github.com/AgoraIO/video-sdk-samples-reactjs/tree/main#samples)

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

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

    ### API reference [#api-reference]

    * [`joinChannel` \[2/2\]](https://api-ref.agora.io/en/video-sdk/android/4.x/API/class_irtcengine.html#api_irtcengine_joinchannel2)

    * [`renewToken`](https://api-ref.agora.io/en/video-sdk/android/4.x/API/class_irtcengine.html#api_irtcengine_renewtoken)

    * [`onTokenPrivilegeWillExpire`](https://api-ref.agora.io/en/video-sdk/android/4.x/API/class_irtcengineeventhandler.html#callback_irtcengineeventhandler_ontokenprivilegewillexpire)

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

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

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

    * [`joinChannel`](https://api-ref.agora.io/en/video-sdk/ios/4.x/documentation/agorartckit/agorartcenginekit/joinchannel\(bytoken\:channelid\:info\:uid\:joinsuccess:\))

    * [`renewToken`](https://api-ref.agora.io/en/video-sdk/ios/4.x/documentation/agorartckit/agorartcenginekit/renewtoken\(_:\))

    * [`rtcEngine(_:tokenPrivilegeWillExpire:)`](https://api-ref.agora.io/en/video-sdk/ios/4.x/documentation/agorartckit/agorartcenginedelegate/rtcengine\(_\:tokenprivilegewillexpire:\))

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

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

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

    * [`joinChannel`](https://api-ref.agora.io/en/video-sdk/macos/4.x/documentation/agorartckit/agorartcenginekit/joinchannel\(bytoken\:channelid\:info\:uid\:joinsuccess:\))

    * [`renewToken`](https://api-ref.agora.io/en/video-sdk/macos/4.x/documentation/agorartckit/agorartcenginekit/renewtoken\(_:\))

    * [`rtcEngine(_:tokenPrivilegeWillExpire:)`](https://api-ref.agora.io/en/video-sdk/macos/4.x/documentation/agorartckit/agorartcenginedelegate/rtcengine\(_\:tokenprivilegewillexpire:\))

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

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

    * [Reference app](https://github.com/AgoraIO/video-sdk-samples-web/tree/main/Demo/basicVideoCall)

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

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

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

    * [renewToken](https://api-ref.agora.io/en/video-sdk/react-native/4.x/API/class_irtcengine.html#api_irtcengine_renewtoken)
    * [onTokenPrivilegeWillExpire](https://api-ref.agora.io/en/video-sdk/react-native/4.x/API/class_irtcengineeventhandler.html#callback_irtcengineeventhandler_ontokenprivilegewillexpire)
    * [onRequestToken](https://api-ref.agora.io/en/video-sdk/react-native/4.x/API/class_irtcengineeventhandler.html#callback_irtcengineeventhandler_onrequesttoken)

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

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

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

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

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

    * [renewToken](https://api-ref.agora.io/en/voice-sdk/electron/4.x/API/class_irtcengine.html#api_irtcengine_renewtoken)
    * [onTokenPrivilegeWillExpire](https://api-ref.agora.io/en/voice-sdk/electron/4.x/API/class_irtcengineeventhandler.html#callback_irtcengineeventhandler_ontokenprivilegewillexpire)
    * [onRequestToken](https://api-ref.agora.io/en/voice-sdk/electron/4.x/API/class_irtcengineeventhandler.html#callback_irtcengineeventhandler_onrequesttoken)

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

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

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

    * [renewToken](https://api-ref.agora.io/en/video-sdk/flutter/6.x/API/class_irtcengine.html#api_irtcengine_renewtoken)

    * [onTokenPrivilegeWillExpire](https://api-ref.agora.io/en/video-sdk/flutter/6.x/API/class_irtcengineeventhandler.html#callback_irtcengineeventhandler_ontokenprivilegewillexpire)

    * [onConnectionStateChanged](https://api-ref.agora.io/en/video-sdk/flutter/6.x/API/class_irtcengineeventhandler.html#callback_irtcengineeventhandler_onconnectionstatechanged)

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

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

    ### API reference [#api-reference-6]

    * [renewToken](https://api-ref.agora.io/en/video-sdk/unreal-engine/4.x/API/class_irtcengine.html#api_irtcengine_renewtoken)

    * [onTokenPrivilegeWillExpire](https://api-ref.agora.io/en/video-sdk/unreal-engine/4.x/API/class_irtcengineeventhandler.html#callback_irtcengineeventhandler_ontokenprivilegewillexpire)

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

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

    ### API Reference [#api-reference-7]

    * [joinChannel](https://api-ref.agora.io/en/video-sdk/cpp/4.x/API/class_irtcengine.html#api_irtcengine_joinchannel)

    * [renewToken](https://api-ref.agora.io/en/video-sdk/cpp/4.x/API/class_irtcengine.html#api_irtcengine_renewtoken)

    * [onTokenPrivilegeWillExpire](https://api-ref.agora.io/en/video-sdk/cpp/4.x/API/class_irtcengineeventhandler.html#callback_irtcengineeventhandler_ontokenprivilegewillexpire)

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