# 实现音视频互动 (/zh-CN/realtime-media/rtc/get-started/quick-start/unity)

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

本文介绍如何集成声网实时互动 SDK，通过少量代码从 0 开始实现一个简单的实时互动 App，适用于互动直播和视频通话场景。
首先，你需要了解以下有关音视频实时互动的基础概念：

- 声网实时互动 SDK：由声网开发的、帮助开发者在 App 中实现实时音视频互动的 SDK。
- 频道：用于传输数据的通道，在同一个频道内的用户可以进行实时互动。
- 主播：可以在频道内**发布**音视频，同时也可以**订阅**其他主播发布的音视频。
- 观众：可以在频道内**订阅**音视频，不具备**发布**音视频权限。

更多概念详见[关键概念](/zh-CN/realtime-media/rtc/reference/key-concept)。

下图展示在 App 中实现音视频互动的基本工作流程：

<DocsImage alt="实现音视频互动" src="https://web-cdn.agora.io/docs-portal/img/rtc/quick-start-tech.png" displayWidth="60%" />

1. 所有用户调用 `joinChannel` 方法加入频道，并根据需要设置用户角色：

   - 互动直播：如果用户需要在频道中发流，则设为主播；如果用户只需要收流，则设为观众。
   - 视频通话：将所有用户的角色都设为主播。
2. 加入频道后，不同角色的用户具备不同的行为：

   - 所有用户默认都可以接收频道中的音视频流。
   - 主播可以在频道内发布音视频流。
   - 观众如果需要发流，可在频道内调用 `setClientRole` 方法修改用户角色，使其具备发流权限。

## 前提条件 [#前提条件-8]

在实现功能以前，请按照以下要求准备开发环境：

- 已下载 [Unity Hub](https://unity.com/download)。

- Unity 2018.4.0 或以上版本。

- 可以访问互联网的计算机。如果你的网络环境部署了防火墙，参考[应对防火墙限制](/zh-CN/realtime-media/rtc/build/setup-and-access/firewall)以正常使用声网服务。

- 不同开发平台的操作系统与编译器需满足下列版本要求：

| 开发平台 | 操作系统版本 | 编译器版本 |
| --- | --- | --- |
| Android | Android 4.1 或以上 | Android Studio 3.0 或以上 |
| iOS | iOS 9.0 或以上 | Xcode 9.0 或以上 |
| macOS | macOS 10.10 或以上 | Xcode 9.0 或以上 |
| Windows | Windows 7 或以上 | Microsoft Visual Studio 2017 或以上 |

- 一个有效的声网账号以及声网项目。请参考 [开通 RTC 服务](/zh-CN/realtime-media/rtc/build/setup-and-access/enable-service)，从声网控制台获得 App ID 和临时 Token。

  :::warning[注意]
  临时 Token 的有效期是 24 小时。Token 过期会导致加入频道失败。
  :::

## 创建项目 [#创建项目-7]

参考以下步骤或 [Unity 官方文档](https://docs.unity.com/en-us/hub/project-create)创建一个 **Unity** 项目。若已有 **Unity** 项目，可略过此步骤至下一步[集成 SDK](#integrate)。

1. 开始前确保你已安装 Unity。若未安装，点击[此处](https://unity.com/cn/products#plans-individual)下载。
2. 打开 Unity Editor，点击 **New** 新建一个项目。
3. 依次填入以下内容后，点击 **Create project** 创建一个 Unity 项目。
   - **Project name**：项目名称。
    - **Location**：项目存储路径。
    - **Template**：项目类型。选择 **3D**。

<a id="integrate"></a>
## 集成 SDK [#集成-sdk-7]

参考下列步骤来集成声网实时互动 SDK：

1. 前往[下载](/zh-CN/realtime-media/rtc/reference/downloads)页面，下载最新版的 Unity SDK。
2. 下载完成后在你的 Unity 编辑器中打开下载好的 SDK 包并取消勾选不需要的插件（默认会勾选 SDK 包含的所有插件），然后点击 **Import** 来导入 SDK。

## 创建用户界面 [#创建用户界面-6]

根据应用场景为你的项目创建一个用户界面（UI）。

例如，你可以在 UI 上添加以下控件：

- 本地视图窗口
- 远端视图窗口
- 加入/离开频道按钮

创建好的用户界面如下图所示：

![Unity 用户界面示意图](https://web-cdn.agora.io/docs-files/1657121618456)

你可以参考以下步骤创建 UI。若你的项目中已有用户界面可略过此步骤。

<Accordions>
<Accordion title="创建用户界面">

1. 创建加入/离开频道按钮。

   1. 在你的 Unity 项目中，右击 **Sample Scene**，选择 **Game Object** > **UI** > **Button**。此时你可以在场景画布中看到你刚刚创建的按钮。

   2. 在右侧的 **Inspector** 面板中，将 **Button** 重命名为 **Leave**。选中 **Button** 按钮的 **Text** 控件，将 **Text** 的文本内容修改为 **Leave**，并调整按钮的位置，例如：

      - **Pos X**：`- 350`
      - **Pos Y**：`- 172`

   3. 重复上述步骤来创建一个 **Join** 按钮。

2. 创建本地及远端视图窗口。

   1. 右击 **Canvas**，选择 **UI** > **Raw Image**。

   2. 创建本地视图窗口。在右侧的 **Inspector** 面板中，将 **Raw Image** 重命名为 **LocalView**，并调整在画布中位置，例如：

      - **Pos X**：`-250`
      - **Pos Y**：`0`
      - **Width**：`250`
      - **Height**：`250`

   3. 重复上述步骤创建远端视图窗口，将其命名为 **RemoteView**，并调整在画布中的位置，例如：

      - **Pos X**：`250`
      - **Pos Y**：`0`
      - **Width**：`250`
      - **Height**：`250`

3. 保存上述步骤的更改。

</Accordion>
</Accordions>

## 实现步骤 [#实现步骤-8]

本小节介绍如何实现一个实时音视频互动 App。你可以先复制完整的示例代码到你的项目中，快速体验实时音视频互动的基础功能，再按照实现步骤了解核心 API 调用。

下图展示了使用声网 RTC SDK 实现音视频互动的基本流程：

<DocsImage alt="实现流程" src="https://web-cdn.agora.io/docs-portal/img/rtc/quick-start-sequence.svg" displayWidth="60%" />

:::info[信息]
在 `_appId`、`_token` 和 `_channelName` 字段中传入你在控制台获取到的 App ID、临时 Token，以及生成临时 Token 时填入的频道名。
:::

<Accordions>
<Accordion title="实现音视频互动示例代码">

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

 #if (UNITY_2018_3_OR_NEWER && UNITY_ANDROID)
 using UnityEngine.Android;
 #endif

public class JoinChannelVideo : MonoBehaviour
{
    // 填入你的 app ID
    private string _appID= "";
    // 填入你的频道名
    private string _channelName = "";
    // 填入 Token
    private string _token = "";
    internal VideoSurface LocalView;
    internal VideoSurface RemoteView;
    internal IRtcEngine RtcEngine;

    #if (UNITY_2018_3_OR_NEWER && UNITY_ANDROID)
    private ArrayList permissionList = new ArrayList() { Permission.Camera, Permission.Microphone };
    #endif

    void Start()
    {
        SetupVideoSDKEngine();
        InitEventHandler();
        SetupUI();
        PreviewSelf();
    }

    void Update()
    {
        CheckPermissions();
    }

    void OnApplicationQuit()
    {
        if (RtcEngine != null)
        {
            Leave();
            // 销毁 IRtcEngine
            RtcEngine.Dispose();
            RtcEngine = null;
        }
    }

    private void CheckPermissions() {
    #if (UNITY_2018_3_OR_NEWER && UNITY_ANDROID)
            foreach (string permission in permissionList)
            {
                if (!Permission.HasUserAuthorizedPermission(permission))
                {
                    Permission.RequestUserPermission(permission);
                }
            }
    #endif
    }

    private void PreviewSelf()
    {
        // 启用视频模块
        RtcEngine.EnableVideo();
        // 开启本地视频预览
        RtcEngine.StartPreview();
        // 设置本地视频显示
        LocalView.SetForUser(0, "");
        // 渲染视频
        LocalView.SetEnable(true);
    }

    private void SetupUI()
    {
        GameObject go = GameObject.Find("LocalView");
        LocalView = go.AddComponent<VideoSurface>();
        go.transform.Rotate(0.0f, 0.0f, -180.0f);
        go = GameObject.Find("RemoteView");
        RemoteView = go.AddComponent<VideoSurface>();
        go.transform.Rotate(0.0f, 0.0f, -180.0f);
        go = GameObject.Find("Leave");
        go.GetComponent<Button>().onClick.AddListener(Leave);
        go = GameObject.Find("Join");
        go.GetComponent<Button>().onClick.AddListener(Join);
    }

    private void SetupVideoSDKEngine()
    {
        // 创建 IRtcEngine 实例
        RtcEngine = Agora.Rtc.RtcEngine.CreateAgoraRtcEngine();
        RtcEngineContext context = new RtcEngineContext();
            context.appId = _appID;
            context.channelProfile = CHANNEL_PROFILE_TYPE.CHANNEL_PROFILE_LIVE_BROADCASTING;
            context.audioScenario = AUDIO_SCENARIO_TYPE.AUDIO_SCENARIO_DEFAULT;
        // 初始化 IRtcEngine
        RtcEngine.Initialize(context);
    }

    // 创建用户回调类实例，并设置回调
    private void InitEventHandler()
    {
        UserEventHandler handler = new UserEventHandler(this);
        RtcEngine.InitEventHandler(handler);
    }

    public void Join()
    {
        // 设置频道媒体选项
        ChannelMediaOptions options = new ChannelMediaOptions();
        // 开始视频渲染
        LocalView.SetEnable(true);
        // 发布麦克风采集的音频流
        options.publishMicrophoneTrack.SetValue(true);
        // 发布摄像头采集的视频流
        options.publishCameraTrack.SetValue(true);
        // 自动订阅所有音频流
        options.autoSubscribeAudio.SetValue(true);
        // 自动订阅所有视频流
        options.autoSubscribeVideo.SetValue(true);
        // 将频道场景设为直播
        options.channelProfile.SetValue(CHANNEL_PROFILE_TYPE.CHANNEL_PROFILE_LIVE_BROADCASTING);
        // 将用户角色设为主播
        options.clientRoleType.SetValue(CLIENT_ROLE_TYPE.CLIENT_ROLE_BROADCASTER);
        // 加入频道
        RtcEngine.JoinChannel(_token, _channelName, 0, options);
    }

    public void Leave()
    {
        Debug.Log("Leaving _channelName");
        // 关闭视频模块
        RtcEngine.StopPreview();
        // 离开频道
        RtcEngine.LeaveChannel();
        // 停止远端视频渲染
        RemoteView.SetEnable(false);
    }

    // 实现你自己的回调类，可以继承 IRtcEngineEventHandler 接口类实现
    internal class UserEventHandler : IRtcEngineEventHandler
    {
        private readonly JoinChannelVideo _videoSample;

        internal UserEventHandler(JoinChannelVideo videoSample)
        {
            _videoSample = videoSample;
        }

        // 发生错误回调
        public override void OnError(int err, string msg)
        {
        }

        // 本地用户成功加入频道时，会触发该回调
        public override void OnJoinChannelSuccess(RtcConnection connection, int elapsed)
        {
        }

        // SDK 接收到第一帧远端视频并成功解码时，会触发 OnUserJoined 回调
        public override void OnUserJoined(RtcConnection connection, uint uid, int elapsed)
        {
            // 设置远端视频显示
            _videoSample.RemoteView.SetForUser(uid, connection.channelId, VIDEO_SOURCE_TYPE.VIDEO_SOURCE_REMOTE);
            // 开始视频渲染
            _videoSample.RemoteView.SetEnable(true);
            Debug.Log("Remote user joined");
        }

        // 远端用户离开当前频道时会触发该回调
        public override void OnUserOffline(RtcConnection connection, uint uid, USER_OFFLINE_REASON_TYPE reason)
        {
            _videoSample.RemoteView.SetEnable(false);
            Debug.Log("Remote user offline");
        }
    }
}
```

</Accordion>
</Accordions>

### 导入类 [#导入类]

新建脚本并导入 Unity 相关的类和声网 RTC Unity SDK 相关的类和接口。

1. 在 **Project** 中，打开 **Assets** > **Agora-Unity-RTC-SDK** > **Code** >**Rtc**，右击然后选择 **Create** > **C# Script**，此时你的 **Assets** 中会有一个 `NewBehaviourScript.cs` 文件，将这个文件重新命名为 `JoinChannelVideo.cs`。后续步骤中的示例代码均指该文件中的代码。

2. 将新建的脚本和 Canvas 绑定。选择 `JoinChannelVideo.cs` 文件，将其拖至左侧的 Canvas。在 **Inspector** 面板中你可以看到该文件已经和 Canvas 绑定。

3. 导入 Unity 相关的类。

   ```csharp
   using UnityEngine;
   using UnityEngine.UI;
   ```

4. 导入 `Agora.Rtc` 命名空间，其中包含了实现实时音视频功能所需的各种类和接口。

   ```csharp
   using Agora.Rtc;
   ```

### 处理权限请求 [#处理权限请求-2]

本小节介绍如何获取设备的摄像头、麦克风等权限。在 Unity 2018.3 或以上版本中，Unity 不会主动向用户获取麦克风和相机权限，因此你需要请求获取权限。

:::warning[注意]
Windows 系统会在应用程序首次尝试使用摄像头或麦克风等功能时，自动向用户弹出权限请求提示，因此你无需额外处理权限请求。
:::

<Tabs defaultValue="android" groupId="permission" persist>
<TabsList>
  <TabsTrigger value="android">Android</TabsTrigger>
  <TabsTrigger value="apple">iOS 和 macOS</TabsTrigger>
</TabsList>

<TabsContent value="android">

如果你开发的目标平台是 Android，Unity 提供了权限请求的 API `CheckPermission`。你可以调用该方法来获取相关权限。参考下列步骤来获取安卓权限：

1. 导入 Unity 的 Android 命名空间。

   ```csharp
   #if (UNITY_2018_3_OR_NEWER && UNITY_ANDROID)
   using UnityEngine.Android;
   #endif
   ```

2. 创建需要获取的权限列表，包括摄像头和麦克风。

   ```csharp
   #if (UNITY_2018_3_OR_NEWER && UNITY_ANDROID)
   private ArrayList permissionList = new ArrayList() { Permission.Camera, Permission.Microphone };
   #endif
   ```

3. 检查是否已获取权限。

   ```csharp
   private void CheckPermissions() {
       #if (UNITY_2018_3_OR_NEWER && UNITY_ANDROID)
       foreach (string permission in permissionList) {
           if (!Permission.HasUserAuthorizedPermission(permission)) {
               Permission.RequestUserPermission(permission);
           }
       }
       #endif
   }
   ```

</TabsContent>

<TabsContent value="apple">

如果你开发的目标平台是 iOS 或 macOS，声网 RTC Unity SDK 在 `Editor` 文件夹下提供一个构建后处理脚本 `BL_BuildPostProcess.cs`。在你将项目从 Unity Editor 中构建并导出为 iOS 项目后，该脚本会自动向 `Info.plist` 文件添加摄像头和麦克风权限，无需你手动处理。

</TabsContent>
</Tabs>

### 定义 App ID 和 Token [#定义-app-id-和-token-1]

传入从声网控制台获取的 App ID、临时 Token，以及生成临时 Token 时填入的频道名，用于后续初始化引擎和加入频道。

```csharp
// 填入你的 App ID
private string _appID= "";
// 填入你的频道名
private string _channelName = "";
// 填入 Token
private string _token = "";
```

<a id="init"></a>
### 初始化引擎 [#初始化引擎-7]

在调用其他声网 API 前，需要创建并初始化 `IRtcEngine` 实例。调用 `CreateAgoraRtcEngine` 来创建 `IRtcEngine` 实例，调用 `Initialize` 进行初始化。

:::warning[注意]
你需要在用户同意 App 的隐私政策之后再调用下列代码来初始化 SDK。
:::

```csharp
private void SetupVideoSDKEngine()
{
    // 创建 IRtcEngine 实例
    RtcEngine = Agora.Rtc.RtcEngine.CreateAgoraRtcEngine();
    RtcEngineContext context = new RtcEngineContext();
        context.appId = _appID;
        context.channelProfile = CHANNEL_PROFILE_TYPE.CHANNEL_PROFILE_LIVE_BROADCASTING;
        context.audioScenario = AUDIO_SCENARIO_TYPE.AUDIO_SCENARIO_DEFAULT;
    // 初始化 IRtcEngine
    RtcEngine.Initialize(context);
}
```

### 设置 UI 元素 [#设置-ui-元素]

将 UI 组件与代码进行绑定，包括加入频道、离开频道按钮的点击事件，以及本地视频显示和远端视频显示。

```csharp
private void SetupUI()
{
    GameObject go = GameObject.Find("LocalView");
    LocalView = go.AddComponent<VideoSurface>();
    go.transform.Rotate(0.0f, 0.0f, -180.0f);
    go = GameObject.Find("RemoteView");
    RemoteView = go.AddComponent<VideoSurface>();
    go.transform.Rotate(0.0f, 0.0f, -180.0f);
    go = GameObject.Find("Leave");
    go.GetComponent<Button>().onClick.AddListener(Leave);
    go = GameObject.Find("Join");
    go.GetComponent<Button>().onClick.AddListener(Join);
}
```

### 启用视频模块 [#启用视频模块-7]

按照以下步骤启用视频模块并开启本地视频预览：

1. 调用 `EnableVideo` 启用视频模块。
2. 调用 `StartPreview` 开启本地视频预览。
3. 调用 `SetForUser` 设置本地视频显示。
4. 调用 `SetEnable(true)` 渲染本地视频。

```csharp
private void PreviewSelf()
{
    // 启用视频模块
    RtcEngine.EnableVideo();
    // 开启本地视频预览
    RtcEngine.StartPreview();
    // 设置本地视频显示
    LocalView.SetForUser(0, "");
    // 渲染视频
    LocalView.SetEnable(true);
}
```

### 加入频道并发布音视频流 [#加入频道并发布音视频流-7]

调用 `JoinChannel` [2/2] 加入频道。在 `ChannelMediaOptions` 中进行如下配置：

- 设置频道场景为 `CHANNEL_PROFILE_LIVE_BROADCASTING` (直播场景) 并设置用户角色设置为 `BROADCASTER` (主播)。
- 将 `publishMicrophoneTrack` 和 `publishCameraTrack` 设置为 `true`，发布麦克风采集的音频和摄像头采集的视频。
- 将 `autoSubscribeAudio` 和 `autoSubscribeVideo` 设置为 `true`，自动订阅所有音视频流。

```csharp
public void Join()
{
    // 设置频道媒体选项
    ChannelMediaOptions options = new ChannelMediaOptions();
    // 发布麦克风采集的音频流
    options.publishMicrophoneTrack.SetValue(true);
    // 发布摄像头采集的视频流
    options.publishCameraTrack.SetValue(true);
    // 自动订阅所有音频流
    options.autoSubscribeAudio.SetValue(true);
    // 自动订阅所有视频流
    options.autoSubscribeVideo.SetValue(true);
    // 将频道场景设为直播
    options.channelProfile.SetValue(CHANNEL_PROFILE_TYPE.CHANNEL_PROFILE_LIVE_BROADCASTING);
    // 将用户角色设为主播
    options.clientRoleType.SetValue(CLIENT_ROLE_TYPE.CLIENT_ROLE_BROADCASTER);
    // 加入频道
    RtcEngine.JoinChannel(_token, _channelName, 0, options);
}
```

### 实现常用回调 [#实现常用回调-7]

根据使用场景，定义必要的回调。下列示例代码展示本地用户加入频道、远端用户加入、离开频道的回调。

```csharp
// 实现你自己的回调类，可以继承 IRtcEngineEventHandler 接口类实现
internal class UserEventHandler : IRtcEngineEventHandler
{
    private readonly JoinChannelVideo _videoSample;
    internal UserEventHandler(JoinChannelVideo videoSample)
    {
        _videoSample = videoSample;
    }

    // 本地用户成功加入频道时，会触发该回调
    public override void OnJoinChannelSuccess(RtcConnection connection, int elapsed)
    {
    }

    // SDK 接收到第一帧远端视频并成功解码时，会触发 OnUserJoined 回调
    public override void OnUserJoined(RtcConnection connection, uint uid, int elapsed)
    {
        // 设置远端视频显示
        _videoSample.RemoteView.SetForUser(uid, connection.channelId, VIDEO_SOURCE_TYPE.VIDEO_SOURCE_REMOTE);
        // 开始视频渲染
        _videoSample.RemoteView.SetEnable(true);
        Debug.Log("Remote user joined");
    }

    // 远端用户离开当前频道时会触发该回调
    public override void OnUserOffline(RtcConnection connection, uint uid, USER_OFFLINE_REASON_TYPE reason)
    {
        _videoSample.RemoteView.SetEnable(false);
    }
}
```

### 设置远端视图 [#设置远端视图-3]

当远端用户加入频道时，触发 `OnUserJoined` 回调。调用 `SetForUser` 设置远端视频显示，调用 `SetEnable(true)` 渲染视频。

```csharp
// SDK 接收到第一帧远端视频并成功解码时，会触发 OnUserJoined 回调
public override void OnUserJoined(RtcConnection connection, uint uid, int elapsed) {
    // 设置远端视频显示
    _videoSample.RemoteView.SetForUser(uid, connection.channelId, VIDEO_SOURCE_TYPE.VIDEO_SOURCE_REMOTE);
    // 开始视频渲染
    _videoSample.RemoteView.SetEnable(true);
    Debug.Log("Remote user joined");
}
```

设置远端用户离开频道或掉线后停止远端视频显示。远端用户离开频道后，触发 `OnUserOffline` 回调。调用 `SetEnable(false)` 停止显示视频。

```csharp
// 远端用户离开当前频道时会触发该回调
public override void OnUserOffline(RtcConnection connection, uint uid, USER_OFFLINE_REASON_TYPE reason)
{
    _videoSample.RemoteView.SetEnable(false);
    Debug.Log("Remote user offline");
}
```

### 开始音视频互动 [#开始音视频互动-5]

在开始音频互动前，先检查是否已获取设备权限，然后在 `Start` 中调用一系列方法来初始化引擎、注册事件回调、加入频道，开始音频互动。

1. 检查是否已获取设备权限。调用 `CheckPermissions` 方法检查是否已获取实现音视频互动所需的权限。

   ```csharp
   void Update() {
       CheckPermissions();
   }
   ```

2. 开始音视频互动。在 `Start` 方法中初始化引擎并开始实时音视频互动。

   ```csharp
   void Start()
   {
       SetupVideoSDKEngine();
       InitEventHandler();
       SetupUI();
       PreviewSelf();
   }
   ```

### 结束音视频互动 [#结束音视频互动-5]

按照以下步骤结束音视频互动：

1. 调用 `StopPreview` 停止本地视频预览。
2. 调用 `LeaveChannel` 来离开当前频道，释放所有会话相关的资源。
3. 调用 `SetEnable` 停止远端视频渲染。

```csharp
public void Leave()
{
    Debug.Log("Leaving _channelName");
    // 离开频道
    RtcEngine.LeaveChannel();
    // 关闭视频模块
    RtcEngine.StopPreview();
    // 停止远端视频渲染
    RemoteView.SetEnable(false);
}
```

如果你想在退出时释放 `IRtcEngine` 内存，调用 `Dispose` 方法销毁 `IRtcEngine`。

:::warning[注意]
- 调用 `Dispose` 后，你将无法再使用 SDK 的所有方法和回调。如需再次使用实时音视频互动功能，你必须重新创建一个新的引擎。详见[初始化引擎](#init)。
- 该方法为同步调用。需要等待引擎资源释放后才能执行其他操作，因此建议在子线程中调用该方法，避免主线程阻塞。
:::

```csharp
void OnApplicationQuit()
{
    if (RtcEngine != null)
    {
        Leave();
        // 销毁 IRtcEngine
        RtcEngine.Dispose();
        RtcEngine = null;
    }
}
```

## 测试你的项目 [#测试你的项目-1]

按照以下步骤来测试你的音视频互动项目：

1. 在声网控制台获取一个临时 Token。请参考[开通 RTC 服务](/zh-CN/realtime-media/rtc/build/setup-and-access/enable-service)获取。
3. 在 `JoinChannelVideo.cs` 中，将你的项目的 App ID、频道名以及临时 Token 分别填入到 `_appID`、`_channelName`、`_token` 之后。
4. 在 Unity Editor 中，点击 **Play** 按钮来运行你的项目，然后点击 **Join** 按钮加入频道。
6. 邀请一位朋友通过另一台设备来使用相同的 App ID、Token、频道名加入频道。你的朋友加入频道后，你们可以听见、看见对方。

## 后续步骤 [#后续步骤-6]

在完成音视频互动后，你可以阅读以下文档进一步了解：

- 本文的示例使用了临时 Token 加入频道。在测试或生产环境中，为确保通信安全，声网推荐使用 Token 服务器来生成 Token，详见[使用 Token 鉴权](/zh-CN/realtime-media/rtc/build/setup-and-access/token-authentication)。
- 如果你想要实现极速直播场景，可以在互动直播的基础上，通过修改观众端的延时级别为低延时 (`AUDIENCE_LATENCY_LEVEL_LOW_LATENCY`)实现。详见[实现极速直播](/zh-CN/realtime-media/rtc/build/advanced-channel/ls-quick-start)。

## 参考信息 [#参考信息-8]

### 示例项目 [#示例项目-8]

声网提供了开源的音视频互动示例项目供你参考，你可以前往下载或查看其中的源代码。

<Cards>
  <Card title={<RepositoryCardTitle provider="gitee">JoinChannelVideo</RepositoryCardTitle>} href="https://gitee.com/agoraio-community/Agora-Unity-Quickstart/tree/main/API-Example-Unity/Assets/API-Example/Examples/Basic/JoinChannelVideo" />
  <Card title={<RepositoryCardTitle provider="github">JoinChannelVideo</RepositoryCardTitle>} href="https://github.com/AgoraIO-Extensions/Agora-Unity-Quickstart/tree/main/API-Example-Unity/Assets/API-Example/Examples/Basic/JoinChannelVideo" />
</Cards>

### 常见问题 [#常见问题-7]

- [直播场景下，如何监听远端观众角色用户加入/离开频道的事件？](/zh-CN/reference/faq/integration/audience_event)
- [为什么使用 Unity 4.x SDK 开发的 iOS App 上传到 App Store 会报错？](/zh-CN/reference/faq/integration/ios_app_unity_reports_error)
- [如何处理视频黑屏问题？](/zh-CN/reference/faq/quality/video_blank)
- [为什么我无法打开摄像头？](/zh-CN/reference/faq/quality/video_camera)
- [如何处理频道相关常见问题？](/zh-CN/reference/faq/integration/channel)
- [为什么 SDK 中使用的是动态库而不是静态库？](/zh-CN/reference/faq/integration/dynamic_or_static_library)

### 相关文档 [#相关文档-7]

- [SDK 错误码](/zh-CN/realtime-media/rtc/reference/error-code)
- [频道连接状态管理](/zh-CN/realtime-media/rtc/build/initialize-and-channel/channel-connection)










