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

> 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` 方法修改用户角色，使其具备发流权限。

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

- [Unreal Engine](https://www.unrealengine.com/zh-CN/download) 4.27 及以上版本
- 两台设备，不可使用虚拟机。参考下方列出的 Unreal Engine 官方文档，根据你的目标平台和引擎版本准备开发环境：
  | 开发平台 | 参考文档 | 备注 |
  | :--- | :------ | :------------------------------ |
  | Android | [Android 开发环境要求](https://dev.epicgames.com/documentation/zh-cn/unreal-engine/android-development-requirements?application_version=4.27) | 无 |
  | iOS | [iOS 开发环境要求](https://dev.epicgames.com/documentation/zh-cn/unreal-engine/ios-and-tvos-development-requirements?application_version=4.27) | 有效的 Apple 开发者签名。 |
  | macOS | [macOS 开发环境要求](https://dev.epicgames.com/documentation/zh-cn/unreal-engine/hardware-and-software-specifications?application_version=4.27) | 有效的 Apple 开发者签名。 |
  | Windows | [Windows 开发环境要求](https://dev.epicgames.com/documentation/zh-cn/unreal-engine/hardware-and-software-specifications?application_version=4.27) | 32 位 Windows 仅支持 Unreal Engine 4 及以下版本，你需要在 `AgoraPluginLibrary.Build.cs` 文件中将 Windows 32 相关的代码取消注释。 |
- 可以访问互联网的计算机。如果你的网络环境部署了防火墙，参考[应对防火墙限制](/zh-CN/realtime-media/rtc/build/setup-and-access/firewall)以正常使用声网服务。
- 一个有效的声网账号以及声网项目。请参考[开通 RTC 服务](/zh-CN/realtime-media/rtc/build/setup-and-access/enable-service)从声网控制台获得以下信息：
    - App ID：声网随机生成的字符串，用于识别你的项目。
    - 临时 Token：Token 也称为动态密钥，在客户端加入频道时对用户鉴权。临时 Token 的有效期为 24 小时。
    - 频道名：用于标识频道的字符串。

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

参考以下步骤或 [Unreal 官方操作指南](https://dev.epicgames.com/documentation/en-us/unreal-engine/onboarding-guide-for-unreal-engine-games-licensees?application_version=5.1)创建一个 **Unreal** 项目。若已有 **Unreal** 项目，可以直接查看[集成 SDK](#integration)。

1. 打开 **Unreal Engine**，点击 **Games**。
2. 依次填入以下内容后，点击 **Create**。
    - **Template**：项目类型。选择 **Blank**。
    - **Project Defaults**：项目默认设置。
        - **Language**：开发语言。选择 **C++**。
        - **Target Platform**：目标平台。选择 **Desktop**。
    - **Project Location**：项目存储路径。
    - **Project Name**：项目名称。

![创建 Unreal 项目的界面示意图](https://web-cdn.agora.io/docs-files/1672739315112)

<a id="integration"></a>
## 集成 SDK [#集成-sdk-10]

1. 前往[下载](/zh-CN/realtime-media/rtc/reference/downloads)页面，在视频 SDK 中下载最新版本的 Unreal SDK，并解压缩。
2. 在你的项目根目录文件夹下，创建名为 `Plugins` 的文件夹。
3. 将 Unreal SDK 文件夹中的 `AgoraPlugin` 拷贝到 `Plugins` 中。

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

为直观地体验视频直播，需根据应用场景创建用户界面（UI）。

如果你想实现一个视频直播，推荐在 UI 上添加以下控件：

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

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

1. 创建 **Widget Blueprint**

    在 **Unreal Editor** 中，点击 **Content Drawer** > **Content**，右击选择 **User Interface** > **Widget Blueprint**。将创建的 **Widget Blueprint** 命名为 **AgoraWidget**，双击 **AgoraWidget** 进入蓝图。

![](https://web-cdn.agora.io/docs-files/1672126795296)

2. 在 **Widget Blueprint** 中创建本地及远端主播视图窗口

    1. 创建视图画布。选择 **Palette** > **PANEL** > **Canvas Panel**，将其拖至 **AgoraWidget** 中。

![](https://web-cdn.agora.io/docs-files/1672126877768)

    2. 创建本地视图窗口。选择 **COMMON** > **Image**，将其拖至 **Canvas** **Panel** 中，重命名为 **LocalVideo**，并调整在画布中的位置。设置示例如下：

        - **Position X**：350
        - **Position Y**：150
        - **size X**：450
        - **Size Y**：450

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

        - **Position X**：1200
        - **Position Y**：150
        - **size X**：450
        - **Size Y**：450

3. 在 **Widget Blueprint** 中创建加入和离开频道按钮

    1. 在 **AgoraWidget** 中，选择 **COMMON** > **Button**，将其拖至 **Canvas Panel** 中，重命名为 **JoinBtn**，并调整按钮在画布中的位置。设置示例如下：

        - **Position X**：300
        - **Position Y**：700
        - **size X**：240
        - **Size Y**：120

    2. 选择 **COMMON** > **Text**，将其拖至 **JoinBtn** 中。选中 **JoinBtn** 的 **Text** 控件，在 **Details** 面板中将 **Text** 的文本内容修改为 **Join**。

    3. 重复上述步骤来创建一个 **LeaveBtn**。你可以根据自己的需求调整按钮位置。

4. 保存上述步骤的更改。此时 **Widget Blueprint** 中的 UI 界面如下图所示：

![](https://web-cdn.agora.io/docs-files/1673105604043)

5. 创建 **Level Blueprint**，并关联已创建的 **Widget Blueprint**

    1. 在 **Unreal Editor** 中，点击 **Content Drawer**，右击选择 **Level** 并命名为 **agoraLevel**。

    2. 双击打开 **agoraLevel**，点击 **Open Level Blueprint**。

![](https://web-cdn.agora.io/docs-files/1672127126823)

    3. 右击在搜索框中输入 **Create Widget**，选择已创建的 **AgoraWidget** 按照同样的方式依次创建 **Event BeginPlay** 和 **Add to Viewport**，并按照下图进行连接：

![](https://web-cdn.agora.io/docs-files/1672127234668)

   4. 保存上述步骤的更改，并运行项目，此时你创建的 UI 界面如下图所示：

![](https://web-cdn.agora.io/docs-files/1672127270201)

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

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

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

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

下面列出了实现实时互动基本流程的完整代码以供参考。复制以下代码即可快速体验实时互动基础功能。

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

<Accordions>
<Accordion title="Project/Source/Project/AgoraWidget.h">

```cpp
#pragma once
#include "CoreMinimal.h"
#include "Blueprint/UserWidget.h"
#if PLATFORM_ANDROID
#include "AndroidPermission/Classes/AndroidPermissionFunctionLibrary.h"
#endif
#include "AgoraPluginInterface.h"
#include "Components/Image.h"
#include "Components/Button.h"
#include "AgoraWidget.generated.h"
UCLASS()
class UNREALLEARNING_API UAgoraWidget : public UUserWidget,public agora::rtc::IRtcEngineEventHandler
{
    GENERATED_BODY()
public:
    // 填入你的 App ID
    FString _appID = "";
    // 填入你的频道名
    FString _channelName = "";
    // 填入 Token
    FString _token = "";
    UPROPERTY(BlueprintReadWrite, meta = (BindWidget))
    UImage* RemoteVideo = nullptr;
    UPROPERTY(BlueprintReadWrite, meta = (BindWidget))
    UImage* LocalVideo = nullptr;
    UPROPERTY(VisibleAnywhere, BlueprintReadWrite, meta = (BindWidget))
    UButton* JoinBtn = nullptr;
    UPROPERTY(VisibleAnywhere, BlueprintReadOnly, meta = (BindWidget))
    UButton* LeaveBtn = nullptr;
    UFUNCTION(BlueprintCallable)
    void Join();
    UFUNCTION(BlueprintCallable)
    void Leave();
    agora::rtc::IRtcEngine* RtcEngineProxy;
    void onLeaveChannel(const agora::rtc::RtcStats& stats) override;
    void onUserJoined(agora::rtc::uid_t uid, int elapsed) override;
    void onUserOffline(agora::rtc::uid_t uid, agora::rtc::USER_OFFLINE_REASON_TYPE reason) override;
    void onJoinChannelSuccess(const char* channel, agora::rtc::uid_t uid, int elapsed) override;
private:
    void CheckAndroidPermission();
    void SetupVideoSDKEngine();
    void SetupUI();
protected:
    void NativeConstruct() override;
    void NativeDestruct() override;
};
```

</Accordion>

<Accordion title="Project/Source/Project/AgoraWidget.cpp">

```cpp
#include "AgoraWidget.h"
void UAgoraWidget::CheckAndroidPermission()
{
#if PLATFORM_ANDROID
    FString pathfromName = UGameplayStatics::GetPlatformName();
    if (pathfromName == "Android")
    {
        TArray AndroidPermission;
        AndroidPermission.Add(FString("android.permission.CAMERA"));
        AndroidPermission.Add(FString("android.permission.RECORD_AUDIO"));
        AndroidPermission.Add(FString("android.permission.READ_PHONE_STATE"));
        AndroidPermission.Add(FString("android.permission.WRITE_EXTERNAL_STORAGE"));
        UAndroidPermissionFunctionLibrary::AcquirePermissions(AndroidPermission);
    }
#endif
}
void UAgoraWidget::SetupVideoSDKEngine()
{
    agora::rtc::RtcEngineContext RtcEngineContext;
    RtcEngineContext.appId = TCHAR_TO_ANSI(*_appID);
    RtcEngineContext.eventHandler = this;
    RtcEngineProxy = agora::rtc::ue::createAgoraRtcEngine();
    RtcEngineProxy->initialize(RtcEngineContext);
}
void UAgoraWidget::SetupUI()
{
    JoinBtn->OnClicked.AddDynamic(this, &UAgoraWidget::Join);
    LeaveBtn->OnClicked.AddDynamic(this, &UAgoraWidget::Leave);
}
void UAgoraWidget::Join()
{
    RtcEngineProxy->enableVideo();
    agora::rtc::ChannelMediaOptions options;
    options.autoSubscribeAudio = true;
    options.autoSubscribeVideo = true;
    options.publishCameraTrack = true;
    options.publishMicrophoneTrack = true;
    options.channelProfile = agora::CHANNEL_PROFILE_TYPE::CHANNEL_PROFILE_LIVE_BROADCASTING;
    options.clientRoleType = agora::rtc::CLIENT_ROLE_TYPE::CLIENT_ROLE_AUDIENCE;
    RtcEngineProxy->joinChannel(TCHAR_TO_ANSI(_token), TCHAR_TO_ANSI(_channelName), 0 , options);
}
void UAgoraWidget::Leave()
{
    RtcEngineProxy->leaveChannel();
}
void UAgoraWidget::NativeConstruct()
{
    Super::NativeConstruct();
    #if PLATFORM_ANDROID
        CheckAndroidPermission()
    #endif
    SetupUI();
    SetupVideoSDKEngine();
}
void UAgoraWidget::NativeDestruct()
{
    Super::NativeDestruct();
    if (RtcEngineProxy != nullptr)
    {
        RtcEngineProxy->unregisterEventHandler(this);
        RtcEngineProxy->release();
        delete RtcEngineProxy;
        RtcEngineProxy = nullptr;
    }
}
void UAgoraWidget::onUserJoined(agora::rtc::uid_t uid, int elapsed)
{
    agora::rtc::VideoCanvas videoCanvas;
    videoCanvas.view = RemoteVideo;
    videoCanvas.uid = uid;
    videoCanvas.sourceType = agora::rtc::VIDEO_SOURCE_TYPE::VIDEO_SOURCE_REMOTE;
    agora::rtc::RtcConnection connection;
    connection.channelId = TCHAR_TO_ANSI(*_channelName);
    ((agora::rtc::IRtcEngineEx*)RtcEngineProxy)->setupRemoteVideoEx(videoCanvas, connection);
}
void UAgoraWidget::onUserOffline(agora::rtc::uid_t uid, agora::rtc::USER_OFFLINE_REASON_TYPE reason)
{
    agora::rtc::VideoCanvas videoCanvas;
    videoCanvas.view = nullptr;
    videoCanvas.uid = uid;
    videoCanvas.sourceType = agora::rtc::VIDEO_SOURCE_TYPE::VIDEO_SOURCE_REMOTE;
    agora::rtc::RtcConnection connection;
    connection.channelId = TCHAR_TO_ANSI(*_channelName);
    ((agora::rtc::IRtcEngineEx*)RtcEngineProxy)->setupRemoteVideoEx(videoCanvas, connection);
}
void UAgoraWidget::onJoinChannelSuccess(const char* channel, agora::rtc::uid_t uid, int elapsed)
{
    AsyncTask(ENamedThreads::GameThread, =
    {
        UE_LOG(LogTemp, Warning, TEXT("JoinChannelSuccess uid: %u"), uid);
        agora::rtc::VideoCanvas videoCanvas;
        videoCanvas.view = LocalVideo;
        videoCanvas.uid = 0;
        videoCanvas.sourceType = agora::rtc::VIDEO_SOURCE_TYPE::VIDEO_SOURCE_CAMERA;
        RtcEngineProxy->setupLocalVideo(videoCanvas);
    });
}
void UAgoraWidget::onLeaveChannel(const agora::rtc::RtcStats& stats)
{
    AsyncTask(ENamedThreads::GameThread, =
    {
        agora::rtc::VideoCanvas videoCanvas;
        videoCanvas.view = nullptr;
        videoCanvas.uid = 0;
        videoCanvas.sourceType = agora::rtc::VIDEO_SOURCE_TYPE::VIDEO_SOURCE_CAMERA;
        RtcEngineProxy->setupLocalVideo(videoCanvas);
    });
}
```

</Accordion>

<Accordion title="Project/Source/Project/Project.Build.cs">

```cpp
using UnrealBuildTool;
public class UnrealLearning : ModuleRules
{
    public UnrealLearning(ReadOnlyTargetRules Target) : base(Target)
    {
        PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;
    PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine", "InputCore","AgoraPlugin" });
    if (Target.Platform == UnrealTargetPlatform.Android)
    {
        PrivateDependencyModuleNames.AddRange(new string[] { "AndroidPermission" });
    }
    PrivateDependencyModuleNames.AddRange(new string[] {  });
    // Uncomment if you are using Slate UI
    // PrivateDependencyModuleNames.AddRange(new string[] { "Slate", "SlateCore" });
    // Uncomment if you are using online features
    // PrivateDependencyModuleNames.Add("OnlineSubsystem");
    // To include OnlineSubsystemSteam, add it to the plugins section in your uproject file with the Enabled attribute set to true
    }
}
```

</Accordion>
</Accordions>

### 处理系统逻辑 [#处理系统逻辑]

本步骤介绍如何导入实现视频直播所需的 C++ 库，并获取摄像头及麦克风设备权限。

1. 新建 C++ 类，生成头文件和库文件

    在 **Unreal Editor** 中，选择 **Tools** > **New C++ Class**，然后选择 **All Classes**，找到 **UserWidget** 并命名为 **AgoraWidget**，点击 **Create Class** 创建类。
    此时一个新的 C++ 类将添加到你的项目中，你可以看到一个 `AgoraWidget.h` 文件和一个 `AgoraWidget.cpp` 文件。

2. 添加声网依赖库

    在 `Project/Source/Project/Project.Build.cs` 文件中，向 `PublicDependencyModuleNames.AddRange()` 添加声网依赖库。

        ```cpp
        // 添加 AgoraPlugin 依赖库
        PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine", "InputCore","AgoraPlugin" });
        ```

3. 初始化自定义的 Widget

    在 `Project/Source/AgoraWidget.h` 文件中添加以下代码：

        ```cpp
        protected:
            // 初始化自定义的 Widget
            void NativeConstruct() override;
        ```

4. 添加系统所需权限

<Accordions type="multiple">
<Accordion title="Android">

如果目标平台为 Android，则添加 AndroidPermission 库并获取设备及网络权限：

    1. 在 `Project/Source/Project/AgoraWidget.h` 文件中添加以下 include 代码：

        ```cpp
        #if PLATFORM_ANDROID
        #include "AndroidPermission/Classes/AndroidPermissionFunctionLibrary.h"
        #endif
        ```

    2. 在 `Project/Source/Project/Project.Build.cs` 文件中添加 AndroidPermission 库：

        ```cpp
        if (Target.Platform == UnrealTargetPlatform.Android)
        {
            PrivateDependencyModuleNames.AddRange(new string[] { "AndroidPermission" });
        }
        ```

    3. 检查是否已获取安卓权限。在 `AgoraWidget.h` 和 `AgoraWidget.cpp` 文件中添加 `NativeConstruct` 和 `CheckAndroidPermission` 方法和实现，并在 `NativeConstruct` 中调用 `CheckAndroidPermission` 方法来检查是否已获取安卓系统所需权限。示例代码如下：

```cpp tab="AgoraWidget.h" tabGroup="ue-quickstart"
private:
    // 获取安卓权限
    void CheckAndroidPermission();
```

```cpp tab="AgoraWidget.cpp" tabGroup="ue-quickstart"
// 在 #include "AgoraWidget.h" 后添加如下代码
void UAgoraWidget::CheckAndroidPermission()
{
#if PLATFORM_ANDROID
    FString pathfromName = UGameplayStatics::GetPlatformName();
    if (pathfromName == "Android")
    {
        TArray<FString> AndroidPermission;
        AndroidPermission.Add(FString("android.permission.CAMERA"));
        AndroidPermission.Add(FString("android.permission.RECORD_AUDIO"));
        AndroidPermission.Add(FString("android.permission.READ_PHONE_STATE"));
        AndroidPermission.Add(FString("android.permission.WRITE_EXTERNAL_STORAGE"));
        AndroidPermission.Add(FString("android.permission.ACCESS_WIFI_STATE"));
        AndroidPermission.Add(FString("android.permission.ACCESS_NETWORK_STATE"));
        UAndroidPermissionFunctionLibrary::AcquirePermissions(AndroidPermission);
    }
#endif
}

void UAgoraWidget::NativeConstruct()
{
    Super::NativeConstruct();
#if PLATFORM_ANDROID
    CheckAndroidPermission()
#endif
}
```

</Accordion>
</Accordions>


<Accordions type="multiple">
<Accordion title="iOS & macOS">

1. 如果目标平台为 iOS/macOS，请参考[如何为 Unreal Engine 项目添加实时互动所需的权限？](/zh-CN/reference/faq/integration/unreal_permissions)

2. 在 `Project/Source/Project.Target.cs` 中，添加如下代码：

    ```cpp
    public class unrealstartTarget : TargetRules
    {
        public unrealstartTarget( TargetInfo Target) : base(Target)
        {
            Type = TargetType.Game;
            DefaultBuildSettings = BuildSettingsVersion.V2;
            if (Target.Platform == UnrealTargetPlatform.IOS)
            {
                bOverrideBuildEnvironment = true;
                GlobalDefinitions.Add("FORCE_ANSI_ALLOCATOR=1")
            }
            ExtraModuleNames.AddRange( new string[] { "unrealstart" } );
        }
    }
    ```

</Accordion>
</Accordions>


3. 关联 C++ 类和 Widget

    在 **Unreal Editor** 中，点击 **Content Drawer**，选择 **Class Settings** > **Graph**，将 **Class Options** 下的 **Parent Class** 设置为 **AgoraWidget**。

![](https://web-cdn.agora.io/docs-files/1672129397404)

### 导入声网库 [#导入声网库]

在 `AgoraWidget.h` 中添加如下 include 代码：

   ```cpp
   #include "AgoraPluginInterface.h"
   ```

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

1. 导入变量相关库

   在 `AgoraWidget.h` 中添加如下 include 代码：

        ```cpp
        #include "Components/Image.h"
        #include "Components/Button.h"
        ```

2. 创建变量，用于创建和加入频道

   在 `GENERATED_BODY()` 后添加如下代码，变量名称必须与创建 UI 的控件名称一致：

        ```cpp
        // 填入你的 App ID
        FString _appID = "";
        // 填入你的频道名
        FString _channelName = "";
        // 填入 Token
        FString _token = "";

        // 定义变量属性
        UPROPERTY(BlueprintReadWrite, meta = (BindWidget))
        UImage* RemoteVideo = nullptr;

        UPROPERTY(BlueprintReadWrite, meta = (BindWidget))
        UImage* LocalVideo = nullptr;

        UPROPERTY(VisibleAnywhere, BlueprintReadWrite, meta = (BindWidget))
        UButton* JoinBtn = nullptr;

        UPROPERTY(VisibleAnywhere, BlueprintReadOnly, meta = (BindWidget))
        UButton* LeaveBtn = nullptr;

        // 定义 IRtcEngine 全局变量
        agora::rtc::IRtcEngine* RtcEngineProxy;
        ```

### 初始化引擎 [#初始化引擎-10]

在调用其他声网 API 前，需要创建并初始化 `IRtcEngine` 实例。在 `AgoraWidget.h` 和 `AgoraWidget.cpp` 文件中添加 `SetupVideoSDKEngine` 的方法和实现。示例代码如下：

:::warning[注意]
在初始化 SDK 前，需确保终端用户已经充分了解并同意相关的隐私政策。
:::

```cpp tab="AgoraWidget.h" tabGroup="ue-quickstart"
private:
    // 获取安卓权限
    void CheckAndroidPermission();
    // 创建并初始化 IRtcEngine
    void SetupVideoSDKEngine();
```

```cpp tab="AgoraWidget.cpp" tabGroup="ue-quickstart"
// 在 #include "AgoraWidget.h" 后添加代码
void UAgoraWidget::SetupVideoSDKEngine()
{
    agora::rtc::RtcEngineContext RtcEngineContext;

    RtcEngineContext.appId = TCHAR_TO_ANSI(*_appID);
    RtcEngineContext.eventHandler = this;

    // 创建 IRtcEngine 实例
    RtcEngineProxy = agora::rtc::ue::createAgoraRtcEngine();
    // 初始化 IRtcEngine
    RtcEngineProxy->initialize(RtcEngineContext);
}
```

### 引用 UI 元素 [#引用-ui-元素]

在 `AgoraWidget.h` 和 `AgoraWidget.cpp` 文件中的 `SetupVideoSDKEngine()` 后添加设置 UI 元素的方法和实现。示例代码如下：

```cpp tab="AgoraWidget.h" tabGroup="ue-quickstart"
private:
    // 获取安卓权限
    void CheckAndroidPermission();
    // 创建并初始化 IRtcEngine
    void SetupVideoSDKEngine();
    // 设置 UI 元素
    void SetupUI();
```

```cpp tab="AgoraWidget.cpp" tabGroup="ue-quickstart"
void UAgoraWidget::SetupUI()
{
    JoinBtn->OnClicked.AddDynamic(this, &UAgoraWidget::Join);
    LeaveBtn->OnClicked.AddDynamic(this, &UAgoraWidget::Leave);
}
```

### 加入和离开频道 [#加入和离开频道]

在 `AgoraWidget.h` 和 `AgoraWidget.cpp` 文件中添加本地用户加入和离开频道的方法和实现。示例代码如下：

```cpp tab="AgoraWidget.h" tabGroup="ue-quickstart"
// 在 UPROPERTY 后添加如下代码：
UFUNCTION(BlueprintCallable)
void Join();
UFUNCTION(BlueprintCallable)
void Leave();
```

```cpp tab="AgoraWidget.cpp" tabGroup="ue-quickstart"
// 在 SetupUI() 后添加如下代码：
void UAgoraWidget::Join()
{
    // 启用视频模块
    RtcEngineProxy->enableVideo();
    // 设置频道媒体选项
    agora::rtc::ChannelMediaOptions options;
    // 自动订阅所有音频流
    options.autoSubscribeAudio = true;
    // 自动订阅所有视频流
    options.autoSubscribeVideo = true;
    // 发布摄像头采集的视频
    options.publishCameraTrack = true;
    // 发布麦克风采集的音频
    options.publishMicrophoneTrack = true;
    // 将频道场景设为直播
    options.channelProfile = agora::CHANNEL_PROFILE_TYPE::CHANNEL_PROFILE_LIVE_BROADCASTING;
    // 将用户角色设为主播或观众
    options.clientRoleType = agora::rtc::CLIENT_ROLE_TYPE::CLIENT_ROLE_AUDIENCE;
    // 加入频道
    RtcEngineProxy->joinChannel(TCHAR_TO_ANSI(*_token), TCHAR_TO_ANSI(*_channelName), 0, options);
}

void UAgoraWidget::Leave()
{
    // 离开频道
    RtcEngineProxy->leaveChannel();
}
```

### 实现回调 [#实现回调]

设置本地用户和远端主播加入和离开频道后的视频显示。在 `AgoraWidget.h` 和 `AgoraWidget.cpp` 文件中添加回调的方法和实现。示例代码如下：

```cpp tab="AgoraWidget.h" tabGroup="ue-quickstart"
// 在 agora::rtc::IRtcEngine* RtcEngineProxy; 后添加如下代码：
// 本地用户离开频道时，会触发该回调
void onLeaveChannel(const agora::rtc::RtcStats& stats) override;

// 远端主播加入频道时，会触发该回调
void onUserJoined(agora::rtc::uid_t uid, int elapsed) override;

// 远端主播离开频道时，会触发该回调
void onUserOffline(agora::rtc::uid_t uid, agora::rtc::USER_OFFLINE_REASON_TYPE reason) override;

// 本地用户加入频道时，会触发该回调
void onJoinChannelSuccess(const char* channel, agora::rtc::uid_t uid, int elapsed) override;
```

```cpp tab="AgoraWidget.cpp" tabGroup="ue-quickstart"
// 在 SetupVideoSDKEngine() 后添加如下代码：
// 设置远端主播加入频道后触发的回调
void UAgoraWidget::onUserJoined(agora::rtc::uid_t uid, int elapsed)
{
    // 设置远端视频显示
    agora::rtc::VideoCanvas videoCanvas;
    videoCanvas.view = RemoteVideo;
    videoCanvas.uid = uid;
    videoCanvas.sourceType = agora::rtc::VIDEO_SOURCE_TYPE::VIDEO_SOURCE_REMOTE;

    agora::rtc::RtcConnection connection;
    connection.channelId = TCHAR_TO_ANSI(*_channelName);

    ((agora::rtc::IRtcEngineEx*)RtcEngineProxy)->setupRemoteVideoEx(videoCanvas, connection);
}

// 设置远端主播离开频道时触发的回调
void UAgoraWidget::onUserOffline(agora::rtc::uid_t uid, agora::rtc::USER_OFFLINE_REASON_TYPE reason)
{
    // 设置远端视频显示
    agora::rtc::VideoCanvas videoCanvas;
    videoCanvas.view = nullptr;
    videoCanvas.uid = uid;
    videoCanvas.sourceType = agora::rtc::VIDEO_SOURCE_TYPE::VIDEO_SOURCE_REMOTE;

    agora::rtc::RtcConnection connection;
    connection.channelId = TCHAR_TO_ANSI(*_channelName);

    ((agora::rtc::IRtcEngineEx*)RtcEngineProxy)->setupRemoteVideoEx(videoCanvas, connection);
}

// 设置本地用户加入频道时触发的回调
void UAgoraWidget::onJoinChannelSuccess(const char* channel, agora::rtc::uid_t uid, int elapsed)
{
    AsyncTask(ENamedThreads::GameThread, [=]()
    {
        UE_LOG(LogTemp, Warning, TEXT("JoinChannelSuccess uid: %u"), uid);
        agora::rtc::VideoCanvas videoCanvas;
        videoCanvas.view = LocalVideo;
        videoCanvas.uid = 0;
        videoCanvas.sourceType = agora::rtc::VIDEO_SOURCE_TYPE::VIDEO_SOURCE_CAMERA;
        RtcEngineProxy->setupLocalVideo(videoCanvas);
    });
}

// 设置本地用户加入频道时触发的回调
void UAgoraWidget::onLeaveChannel(const agora::rtc::RtcStats& stats)
{
    AsyncTask(ENamedThreads::GameThread, [=]()
    {
        agora::rtc::VideoCanvas videoCanvas;
        videoCanvas.view = nullptr;
        videoCanvas.uid = 0;
        videoCanvas.sourceType = agora::rtc::VIDEO_SOURCE_TYPE::VIDEO_SOURCE_CAMERA;
        RtcEngineProxy->setupLocalVideo(videoCanvas);
    });
}
```

### 清理所有会话相关的资源 [#清理所有会话相关的资源]

离开频道后，如果你想退出应用或者释放 `IRtcEngine` 内存，需调用 `release` 方法销毁 `IRtcEngine`。在 `AgoraWidget.h` 和 `AgoraWidget.cpp` 文件中的 `NativeConstruct()` 后添加 `NativeDestruct()` 的方法和实现。

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

```cpp tab="AgoraWidget.h" tabGroup="ue-quickstart"
void NativeDestruct() override;
```

```cpp tab="AgoraWidget.cpp" tabGroup="ue-quickstart"
// AgoraWidget.cpp
void UAgoraWidget::NativeDestruct()
{
    Super::NativeDestruct();
    if (RtcEngineProxy != nullptr)
    {
        RtcEngineProxy->unregisterEventHandler(this);
        RtcEngineProxy->release();
        delete RtcEngineProxy;

        RtcEngineProxy = nullptr;
    }
}
```

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

按照以下步骤来测试你的视频直播项目：

1. 在 `AgoraWidget.h` 中，将你的项目的 App ID、频道名以及临时 Token 分别填入到 `_appID`、`_channelName`、`_token` 之后。
2. 在 **Unreal Editor** 中，点击播放按钮来运行你的项目，然后点击 **Join** 来加入直播。
3. 邀请一位朋友通过另一台设备来使用相同的 App ID、频道名、Token 加入频道。如果你的朋友以主播身份加入，你们可以听见、看见对方；如果作为观众加入，你只能看到自己，你的朋友可以看到你并听到你的声音。

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

- 如果你的目标平台是 macOS 或 iOS，则需要在打包时添加实时互动所需的摄像头和麦克风等权限，详见[如何为 Unreal Engine 项目添加实时互动所需的权限？](/zh-CN/reference/faq/integration/unreal_permissions)
- 在本文示例中，使用了临时 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)。

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

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

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

<Cards>
  <Card title="Unreal C++ 示例项目" href="https://github.com/AgoraIO-Extensions/Agora-Unreal-RTC-SDK/tree/main/Agora-Unreal-SDK-CPP-Example" />
  <Card title="Unreal Blueprint 示例项目" href="https://github.com/AgoraIO-Extensions/Agora-Unreal-RTC-SDK/tree/main/Agora-Unreal-SDK-Blueprint-Example" />
</Cards>

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

- [直播场景下，如何监听远端观众角色用户加入/离开频道的事件？](/zh-CN/reference/faq/integration/audience_event)
- [如何处理视频黑屏问题？](/zh-CN/reference/faq/quality/video_blank)
- [为什么我无法打开摄像头？](/zh-CN/reference/faq/quality/video_camera)
- [如何处理频道相关常见问题？](/zh-CN/reference/faq/integration/channel)
- [如何设置日志文件？](/zh-CN/reference/faq/integration/set_log_file)
- [为什么部分 Android 版本应用锁屏或切后台后采集音视频无效？](/zh-CN/reference/faq/quality/android_background)
- [编译 Xcode 项目时遇到无法打开 framework 的弹窗警告怎么办？](/zh-CN/reference/faq/integration/framework_cannot_be_opened)
- [为什么 SDK 中使用的是动态库而不是静态库？](/zh-CN/reference/faq/integration/dynamic_or_static_library)

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

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




