实现纯语音互动
本文介绍如何集成声网实时互动 SDK,通过少量代码从 0 开始实现一个简单的纯语音互动 App,适用于语音通话场景。
首先,你需要了解以下有关音视频实时互动的基础概念:
- 声网实时互动 SDK:由声网开发的、帮助开发者在 App 中实现实时音视频互动的 SDK。
- 频道:用于传输数据的通道,在同一个频道内的用户可以进行实时互动。
- 主播:可以在频道内发布音视频,同时也可以订阅其他主播发布的音视频。
- 观众:可以在频道内订阅音视频,不具备发布音视频权限。
更多概念详见关键概念。
下图展示在 App 中实现纯语音互动的基本工作流程:
- 所有用户调用
joinChannel
方法加入频道,并将所有用户角色都设置为主播。 - 加入频道后,所有用户都可以在频道内发布音频流,并订阅对方的音频流。
前提条件
- Unreal Engine 4.27 及以上版本
- 两台设备,不可使用虚拟机。参考下方列出的 Unreal Engine 官方文档,根据你的目标平台和引擎版本准备开发环境:
开发平台 参考文档 备注 Android Android 开发环境要求 无 iOS iOS 开发环境要求 有效的 Apple 开发者签名。 macOS macOS 开发环境要求 有效的 Apple 开发者签名。 Windows Windows 开发环境要求 32 位 Windows 仅支持 Unreal Engine 4 及以下版本,你需要在 AgoraPluginLibrary.Build.cs
文件中将 Windows 32 相关的代码取消注释。 - 可以访问互联网的计算机。如果你的网络环境部署了防火墙,参考应对防火墙限制以正常使用声网服务。
- 一个有效的声网账号以及声网项目。请参考开通服务从声网控制台获得以下信息:
- App ID:声网随机生成的字符串,用于识别你的项目。
- 临时 Token:Token 也称为动态密钥,在客户端加入频道时对用户鉴权。临时 Token 的有效期为 24 小时。
- 频道名:用于标识频道的字符串。
创建项目
参考以下步骤或 Unreal 官方操作指南创建一个 Unreal 项目。若已有 Unreal 项目,可以直接查看集成 SDK。
-
打开 Unreal Engine,点击 Games。
-
依次填入以下内容后,点击 Create。
- Template:项目类型。选择 Blank。
- Project Defaults:项目默认设置。
- Language:开发语言。选择 C++。
- Target Platform:目标平台。选择 Desktop。
- Project Location:项目存储路径。
- Project Name:项目名称。
集成 SDK
- 前往下载,下载最新版本的 Unreal SDK,并解压缩。
- 在你的项目根目录文件夹下,创建名为
Plugins
的文件夹。 - 将 Unreal SDK 文件夹中的
AgoraPlugin
拷贝到Plugins
中。
创建用户界面
为直观地体验语音通话,需根据应用场景创建用户界面 (UI)。
如果你想实现一个语音通话,推荐在 UI 上添加以下控件:
- 加入语音通话按钮
- 结束语音通话按钮
参考以下步骤创建 UI。若你的项目中已有用户界面可略过此步骤。
-
创建 Widget Blueprint
在 Unreal Editor 中,点击 Content Drawer > Content,右击选择 User Interface > Widget Blueprint。将创建的 Widget Blueprint 命名为 AgoraWidget,双击 AgoraWidget 进入蓝图。
-
在 Widget Blueprint 中创建加入和离开频道按钮
-
在 AgoraWidget 中,选择 COMMON > Button,将其拖至 Canvas Panel 中,重命名为 JoinBtn,并调整按钮在画布中的位置。设置示例如下:
- Position X:300
- Position Y:700
- size X:240
- Size Y:120
-
选择 COMMON > Text,将其拖至 JoinBtn 中。选中 JoinBtn 的 Text 控件,在 Details 面板中将 Text 的文本内容修改为 Join。
-
重复上述步骤来创建一个 LeaveBtn。你可以根据自己的需求调整按钮位置。
-
-
保存上述步骤的更改。
-
创建 Level Blueprint,并关联已创建的 Widget Blueprint
-
在 Unreal Editor 中,点击 Content Drawer,右击选择 Level 并命名为 agoraLevel。
-
双击打开 agoraLevel,点击 Open Level Blueprint。
-
右击在搜索框中输入 Create Widget,选择已创建的 AgoraWidget 按照同样的方式依次创建 Event BeginPlay 和 Add to Viewport,并按照下图进行连接:
-
保存上述步骤的更改,并运行项目。
-
实现步骤
本小节介绍如何实现一个实时音频互动 App。你可以先复制完整的示例代码到你的项目中,快速体验实时音频互动的基础功能,再按照实现步骤了解核心 API 调用。
下图展示了使用声网 RTC SDK 实现纯语音互动的基本流程:
下面列出了实现实时互动基本流程的完整代码以供参考。复制以下代码即可快速体验实时互动基础功能。
在 _appID
、_token
和 _channelName
字段中传入你在控制台获取到的 App ID、临时 Token,以及生成临时 Token 时填入的频道名。
#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(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();
// 创建并初始化 IRtcEngine
void SetupSDKEngine();
// 设置 UI 元素
void SetupUI();
protected:
// 初始化自定义的 Widget
void NativeConstruct() override;
// 清理所有会话相关的资源
void NativeDestruct() override;
};
#include "AgoraWidget.h"
void UAgoraWidget::CheckAndroidPermission()
{
#if PLATFORM_ANDROID
FString pathfromName = UGameplayStatics::GetPlatformName();
if (pathfromName == "Android")
{
TArray AndroidPermission;
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::SetupSDKEngine()
{
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()
{
// 设置频道媒体选项
agora::rtc::ChannelMediaOptions options;
// 自动订阅所有音频流
options.autoSubscribeAudio = true;
// 发布麦克风采集的音频
options.publishMicrophoneTrack = true;
// 将频道场景设为直播
options.channelProfile = agora::CHANNEL_PROFILE_TYPE::CHANNEL_PROFILE_LIVE_BROADCASTING;
// 将用户角色设为主播
options.clientRoleType = agora::rtc::CLIENT_ROLE_TYPE::CLIENT_ROLE_BROADCASTER;
// 加入频道
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();
SetupSDKEngine();
}
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::RtcConnection connection;
connection.channelId = TCHAR_TO_ANSI(*_channelName);
}
void UAgoraWidget::onUserOffline(agora::rtc::uid_t uid, agora::rtc::USER_OFFLINE_REASON_TYPE reason)
{
agora::rtc::RtcConnection connection;
connection.channelId = TCHAR_TO_ANSI(*_channelName);
}
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);
});
}
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
}
}
处理系统逻辑
本节介绍如何导入实现语音通话所需的 C++ 库,并获取摄像头及麦克风设备权限。
-
新建 C++ 类,生成头文件和库文件
在 Unreal Editor 中,选择 Tools > New C++ Class,然后选择 All Classes,找到 UserWidget 并命名为 AgoraWidget,点击 Create Class 创建类。此时一个新的 C++ 类将添加到你的项目中,你可以看到一个
AgoraWidget.h
文件和一个AgoraWidget.cpp
文件。 -
添加声网依赖库
在
Project/Source/Project/Project.Build.cs
文件中,向PublicDependencyModuleNames.AddRange()
添加声网依赖库。C++// 添加 AgoraPlugin 依赖库
PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine", "InputCore","AgoraPlugin" }); -
初始化自定义的 Widget
在
Project/Source/Project/AgoraWidget.h
文件中添加以下代码:C++protected:
// 初始化自定义的 Widget
void NativeConstruct() override; -
添加系统所需权限
Android如果目标平台为 Android,则添加 AndroidPermission 库并获取设备及网络权限:
-
在
Project/Source/Project/AgoraWidget.h
文件中添加以下 include 代码:C++#if PLATFORM_ANDROID
#include "AndroidPermission/Classes/AndroidPermissionFunctionLibrary.h"
#endif -
在
Project/Source/Project/Project.Build.cs
文件中添加 AndroidPermission 库:C++if (Target.Platform == UnrealTargetPlatform.Android)
{
PrivateDependencyModuleNames.AddRange(new string[] { "AndroidPermission" });
} -
检查是否已获取安卓权限。在
AgoraWidget.h
和AgoraWidget.cpp
文件中添加NativeConstruct
和CheckAndroidPermission
方法和实现,并在NativeConstruct
中调用CheckAndroidPermission
方法来检查是否已获取安卓系统所需权限。示例代码如下:- AgoraWidget.h
- AgoraWidget.cpp
C++private:
// 获取安卓权限
void CheckAndroidPermission();C++// 在 #include "AgoraWidget.h" 后添加如下代码
void UAgoraWidget::CheckAndroidPermission()
{
#if PLATFORM_ANDROID
FString pathfromName = UGameplayStatics::GetPlatformName();
if (pathfromName == "Android")
{
TArray<FString> AndroidPermission;
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
}
iOS & macOS-
如果目标平台为 iOS/macOS,请参考如何为 Unreal Engine 项目添加实时互动所需的权限?
-
在
Project/Source/Project.Target.cs
中,添加如下代码:C++public class ProjectTarget : TargetRules
{
public ProjectTarget( 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" } );
}
}
-
-
关联 C++ 类和 Widget
在 Unreal Editor 中,点击 Content Drawer,选择 Class Settings > Graph,将 Class Options 下的 Parent Class 设置为 AgoraWidget。
导入声网库
在 AgoraWidget.h
中添加如下 include 代码:
#include "AgoraPluginInterface.h"
定义 App ID 和 Token
-
导入变量相关库
在
AgoraWidget.h
中添加如下 include 代码:C++#include "Components/Image.h"
#include "Components/Button.h" -
创建变量,用于创建和加入频道。
在
GENERATED_BODY()
后添加如下代码,变量名称必须与创建 UI 的控件名称一致:C++// 填入你的 App ID
FString _appID = "";
// 填入你的频道名
FString _channelName = "";
// 填入 Token
FString _token = "";
// 定义变量属性
UPROPERTY(VisibleAnywhere, BlueprintReadWrite, meta = (BindWidget))
UButton* JoinBtn = nullptr;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, meta = (BindWidget))
UButton* LeaveBtn = nullptr;
// 定义 IRtcEngine 全局变量
agora::rtc::IRtcEngine* RtcEngineProxy;
初始化引擎
在调用其他声网 API 前,需要创建并初始化 IRtcEngine
实例。在 AgoraWidget.h
和 AgoraWidget.cpp
文件中添加 SetupSDKEngine
的方法和实现。
在初始化 SDK 前,需确保终端用户已经充分了解并同意相关的隐私政策。
- AgoraWidget.h
- AgoraWidget.cpp
// 在 private: 后添加代码
private:
// 获取安卓权限(仅安卓平台需要)
void CheckAndroidPermission();
// 创建并初始化 IRtcEngine
void SetupSDKEngine();
// 在 include 后添加代码
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 元素
在 AgoraWidget.h
和 AgoraWidget.cpp
文件中的 SetupSDKEngine()
后添加设置 UI 元素的方法和实现。示例代码如下:
- AgoraWidget.h
- AgoraWidget.cpp
private:
// 获取安卓权限
void CheckAndroidPermission();
// 创建并初始化 IRtcEngine
void SetupSDKEngine();
// 设置 UI 元素
void SetupUI();
void UAgoraWidget::SetupUI()
{
JoinBtn->OnClicked.AddDynamic(this, &UAgoraWidget::Join);
LeaveBtn->OnClicked.AddDynamic(this, &UAgoraWidget::Leave);
}
加入和离开频道
在 AgoraWidget.h
和 AgoraWidget.cpp
文件中添加本地用户加入和离开频道的方法和实现。示例代码如下:
- AgoraWidget.h
- AgoraWidget.cpp
// 在 UPROPERTY 后添加如下代码:
UFUNCTION(BlueprintCallable)
void Join();
UFUNCTION(BlueprintCallable)
void Leave();
// 在 SetupUI() 后添加如下代码:
void UAgoraWidget::Join()
{
// 设置频道媒体选项
agora::rtc::ChannelMediaOptions options;
// 自动订阅所有音频流
options.autoSubscribeAudio = 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
文件中添加回调的方法和实现。示例代码如下:
- AgoraWidget.h
- AgoraWidget.cpp
// 在 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;
// 在 SetupSDKEngine() 后添加如下代码:
// 设置远端主播加入频道后触发的回调
void UAgoraWidget::onUserJoined(agora::rtc::uid_t uid, int elapsed)
{
agora::rtc::RtcConnection connection;
connection.channelId = TCHAR_TO_ANSI(*_channelName);
}
// 设置远端主播离开频道时触发的回调
void UAgoraWidget::onUserOffline(agora::rtc::uid_t uid, agora::rtc::USER_OFFLINE_REASON_TYPE reason)
{
agora::rtc::RtcConnection connection;
connection.channelId = TCHAR_TO_ANSI(*_channelName);
}
// 设置本地用户加入频道时触发的回调
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);
});
}
清理所有会话相关的资源
离开频道后,如果你想退出应用或者释放 IRtcEngine
内存,需调用 release
方法销毁 IRtcEngine
。在 AgoraWidget.h
和 AgoraWidget.cpp
文件中的 NativeConstruct()
后添加 NativeDestruct()
的方法和实现。示例代码如下:
- AgoraWidget.h
- AgoraWidget.cpp
void NativeDestruct() override;
void UAgoraWidget::NativeDestruct()
{
Super::NativeDestruct();
if (RtcEngineProxy != nullptr)
{
RtcEngineProxy->unregisterEventHandler(this);
RtcEngineProxy->release();
delete RtcEngineProxy;
RtcEngineProxy = nullptr;
}
}
测试你的项目
按照以下步骤来测试你的音频互动项目:
- 在
AgoraWidget.h
中,将你的项目的 App ID、频道名以及临时 Token 分别填入到_appID
、_channelName
、_token
之后。 - 在 Unreal Editor 中,点击播放按钮来运行你的项目,然后点击 Join 来加入语音通话。
- 邀请一位朋友通过另一台设备来使用相同的 App ID、频道名、Token 加入频道。你的朋友以主播身份加入,你们可以听见对方。
后续步骤
- 如果你的目标平台是 macOS 或 iOS,则需要在打包时添加实时互动所需的摄像头和麦克风等权限,详见如何为 Unreal Engine 项目添加实时互动所需的权限?
- 在本文示例中,使用了临时 Token 加入频道。在测试或生产环境中,为确保通信安全,声网推荐使用 Token 服务器来生成 Token,详见使用 Token 鉴权。
参考信息
示例项目
声网提供了开源的示例项目供你参考,你可以前往下载或查看其中的源代码。
常见问题
- 直播场景下,如何监听远端观众角色用户加入/离开频道的事件?
- 如何处理频道相关常见问题?
- 如何设置日志文件?
- 为什么部分 Android 版本应用锁屏或切后台后采集音视频无效?
- 编译 Xcode 项目时遇到无法打开 framework 的弹窗警告怎么办?