C++在Unreal中为游戏增加实时音视频互动的教程详解

合集下载
  1. 1、下载文档前请自行甄别文档内容的完整性,平台不提供额外的编辑、内容补充、找答案等附加服务。
  2. 2、"仅部分预览"的文档,不可在线预览部分如存在完整性等问题,可反馈申请退款(可完整预览的文档不适用该条件!)。
  3. 3、如文档侵犯您的权益,请联系客服反馈,我们会尽快为您处理(人工客服工作时间:9:00-18:30)。

C++在Unreal中为游戏增加实时⾳视频互动的教程详解
我们已经上线了 Agora Unreal SDK,提供了⽀持 Blueprint 和 C++ 的两个版本 SDK。

我们分享了。

在本⽂中,我们来分享如何基于声⽹ Agora Unreal SDK C++版本,在游戏中实现实时⾳视频功能。

本篇教程较长,建议在 Web 浏览器端浏览,体验更好。

准备⼯作
需要的开发环境和需要准备的与 Blueprint ⼀样:
Unreal 4.34 以上版本
Visual Studio 或 Xcode(版本根据 Unreal 配置要求⽽定)
运⾏ Windows 7 以上系统的 PC 或⼀台 Mac
Agora 注册账号⼀枚(免费注册,见官⽹ Agora.io)
如果你的企业⽹络存在防⽕墙,请在声⽹⽂档中⼼搜索「应⽤企业防⽕墙限制」,进⾏配置。

新建项⽬
如果你已经有 Unreal 项⽬了,可以跳过这⼀步。

在 Unreal 中创建⼀个 C++类型的项⽬。

确保在 [your_project]/Source/[project_name]/[project_name].Build.cs⽂件的 PrivateDependencyModuleNames⼀⾏,去掉注释。

Unreal 默认是将它注释掉的,这会导致在编译的时候报错。

// Uncomment if you are using Slate UI
PrivateDependencyModuleNames.AddRange(new string[] { "UMG", "Slate", "SlateCore" });
接下来我们在项⽬中集成 Agora SDK
1.将 SDK 复制到这个路径下 [your_project]/Plugins
2.把插件依赖添加到[your_project]/Source/[project_name]/[project_name].Build.cs⽂件的私有依赖(Private Dependencies)部分 PrivateDependencyModuleNames.AddRange(new string[] { "AgoraPlugin", "AgoraBlueprintable" });
3.重启 Unreal
4.点击 Edit->Plugin,在分类中找到 Project->Other,确定插件已经⽣效
创建新的 Level
接下来我们将创建⼀个新的 Level,在那⾥建⽴我们的游戏环境。

有⼏种不同的⽅法可以创建⼀个新的 Level,我们将使⽤⽂件菜单的⽅法,其中列出了关卡选择选项。

在虚幻编辑器⾥⾯,点击⽂件菜单选项,然后选择新建 Level......
然后会打开⼀个新的对话框。

选择Empty Level ,然后指定⼀个存储的路径。

创建核⼼类
在这⾥我们要创建两个类:VideoFrameObserver 和VideoCall C++ Class。

他们会负责与 Agora SDK 进⾏通信。

⾸先是 VideoFrameObserver。

VideoFrameObserver 执⾏的是 agora::media::IVideoFrameObserver。

这个⽅法在VideoFrameObserver 类中负责管理视频帧的回调。

它是⽤ registerVideoFrameObserver 在 agora::media::IMediaEngine 中注册的。

在 Unreal 编辑器中,选择 File->Add New C++ Class。

⽗类谁定为 None,然后点击下⼀步。

为 VideoFrameObserver明明,然后选择 Create Class。

创建 VideoFrameObserver 类接⼝。

打开 VideoFrameObserver.h ⽂件然后添加如下代码:
//VideoFrameObserver.h
#include "CoreMinimal.h"
#include <functional>
#include "AgoraMediaEngine.h"
class AGORAVIDEOCALL_API VideoFrameObserver : public agora::media::IVideoFrameObserver
{
public:
virtual ~VideoFrameObserver() = default;
public:
bool onCaptureVideoFrame(VideoFrame& videoFrame) override;
bool onRenderVideoFrame(unsigned int uid, VideoFrame& videoFrame) override;
void setOnCaptureVideoFrameCallback(
std::function<void(std::uint8_t*, std::uint32_t, std::uint32_t, std::uint32_t)> callback);
void setOnRenderVideoFrameCallback(
std::function<void(std::uint8_t*, std::uint32_t, std::uint32_t, std::uint32_t)> callback);
virtual VIDEO_FRAME_TYPE getVideoFormatPreference() override { return FRAME_TYPE_RGBA; }
private:
std::function<void(std::uint8_t*, std::uint32_t, std::uint32_t, std::uint32_t)> OnCaptureVideoFrame;
std::function<void(std::uint8_t*, std::uint32_t, std::uint32_t, std::uint32_t)> OnRenderVideoFrame;
};
AGORAVIDEOCALL_API 是项⽬依赖的定义,⽽不是由Unreal ⽣成的你⾃⼰的定义。

重写onCaptureVideoFrame/onRenderVideoFrame⽅法
onCaptureVideoFrame 会获取到摄像头捕获的画⾯,转换为 ARGB 格式并触发 OnCaptureVideoFrame 回调。

onRenderVideoFrame 讲收到的特定⽤户画⾯转换为 ARGB 格式,然后触发 onRenderVideoFrame 回调。

//VideoFrameObserver.cpp
bool VideoFrameObserver::onCaptureVideoFrame(VideoFrame& Frame)
{
const auto BufferSize = Frame.yStride*Frame.height;
if (OnCaptureVideoFrame)
{
OnCaptureVideoFrame( static_cast< uint8_t* >( Frame.yBuffer ), Frame.width, Frame.height, BufferSize );
}
return true;
}
bool VideoFrameObserver::onRenderVideoFrame(unsigned int uid, VideoFrame& Frame)
{
const auto BufferSize = Frame.yStride*Frame.height;
if (OnRenderVideoFrame)
{
OnRenderVideoFrame( static_cast<uint8_t*>(Frame.yBuffer), Frame.width, Frame.height, BufferSize );
}
return true;
}
增加setOnCaptureVideoFrameCallback/setOnRenderVideoFrameCallback⽅法。

设定回调,⽤来获取摄像头获取到的本地画⾯和远端的画⾯。

//VideoFrameObserver.cpp
void VideoFrameObserver::setOnCaptureVideoFrameCallback(
std::function<void(std::uint8_t*, std::uint32_t, std::uint32_t, std::uint32_t)> Callback)
{
OnCaptureVideoFrame = Callback;
}
void VideoFrameObserver::setOnRenderVideoFrameCallback(
std::function<void(std::uint8_t*, std::uint32_t, std::uint32_t, std::uint32_t)> Callback)
{
OnRenderVideoFrame = Callback;
}
创建视频通话C++类
VideoCall 类管理与 Agora SDK 的通信。

需要创建多个⽅法和接⼝。

创建类接⼝
回到 Unreal 编辑器,再创建⼀个新的 C++类,命名为 VideoCall.h。

然后进⼊VideoCall.h⽂件,添加⼀下接⼝://VideoCall.h
#pragma once
#include "CoreMinimal.h"
#include <functional>
#include <vector>
#include "AgoraRtcEngine.h"
#include "AgoraMediaEngine.h"
class VideoFrameObserver;
class AGORAVIDEOCALL_API VideoCall
{
public:
VideoCall();
~VideoCall();
FString GetVersion() const;
void RegisterOnLocalFrameCallback(
std::function<void(std::uint8_t*, std::uint32_t, std::uint32_t, std::uint32_t)> OnLocalFrameCallback);
void RegisterOnRemoteFrameCallback(
std::function<void(std::uint8_t*, std::uint32_t, std::uint32_t, std::uint32_t)> OnRemoteFrameCallback);
void StartCall(
const FString& ChannelName,
const FString& EncryptionKey,
const FString& EncryptionType);
void StopCall();
bool MuteLocalAudio(bool bMuted = true);
bool IsLocalAudioMuted();
bool MuteLocalVideo(bool bMuted = true);
bool IsLocalVideoMuted();
bool EnableVideo(bool bEnable = true);
private:
void InitAgora();
private:
TSharedPtr<agora::rtc::ue4::AgoraRtcEngine> RtcEnginePtr;
TSharedPtr<agora::media::ue4::AgoraMediaEngine> MediaEnginePtr;
TUniquePtr<VideoFrameObserver> VideoFrameObserverPtr;
//callback
//data, w, h, size
std::function<void(std::uint8_t*, std::uint32_t, std::uint32_t, std::uint32_t)> OnLocalFrameCallback;
std::function<void(std::uint8_t*, std::uint32_t, std::uint32_t, std::uint32_t)> OnRemoteFrameCallback;
bool bLocalAudioMuted = false;
bool bLocalVideoMuted = false;
};
创建初始化⽅法
进⼊ VideoCall.cpp ⽂件,添加以下代码:
//VideoCall.cpp
#include "AgoraVideoDeviceManager.h"
#include "AgoraAudioDeviceManager.h"
#include "MediaShaders.h"
#include "VideoFrameObserver.h"
⽤agora::rtc::ue4::AgoraRtcEngine::createAgoraRtcEngine()创建引擎,初始化 RtcEnginePtr 变量。

创建⼀个RtcEngineContext对象,然后在ctx.eventHandler 和ctx.appId中设定 event handler 和 App ID 。

初始化引擎,并创建AgoraMediaEngine对象,初始化 MediaEnginePtr。

//VideoCall.cpp
VideoCall::VideoCall()
{
InitAgora();
}
VideoCall::~VideoCall()
{
StopCall();
}
void VideoCall::InitAgora()
{
RtcEnginePtr = TSharedPtr<agora::rtc::ue4::AgoraRtcEngine>(agora::rtc::ue4::AgoraRtcEngine::createAgoraRtcEngine());
static agora::rtc::RtcEngineContext ctx;
ctx.appId = "aab8b8f5a8cd4469a63042fcfafe7063";
ctx.eventHandler = new agora::rtc::IRtcEngineEventHandler();
int ret = RtcEnginePtr->initialize(ctx);
if (ret < 0)
{
UE_LOG(LogTemp, Warning, TEXT("RtcEngine initialize ret: %d"), ret);
}
MediaEnginePtr = TSharedPtr<agora::media::ue4::AgoraMediaEngine>(agora::media::ue4::AgoraMediaEngine::Create(RtcEnginePtr.Get())); }
FString VideoCall::GetVersion() const
{
if (!RtcEnginePtr)
{
return "";
}
int build = 0;
const char* version = RtcEnginePtr->getVersion(&build);
return FString(ANSI_TO_TCHAR(version));
}
创建回调⽅法
接下来创建回调⽅法,返回本地和远端的视频帧
//VideoCall.cpp
void VideoCall::RegisterOnLocalFrameCallback(
std::function<void(std::uint8_t*, std::uint32_t, std::uint32_t, std::uint32_t)> OnFrameCallback)
{
OnLocalFrameCallback = std::move(OnFrameCallback);
}
void VideoCall::RegisterOnRemoteFrameCallback(
std::function<void(std::uint8_t*, std::uint32_t, std::uint32_t, std::uint32_t)> OnFrameCallback)
{
OnRemoteFrameCallback = std::move(OnFrameCallback);
}
创建呼叫⽅法
我们需要利⽤这个⽅法来实现“加⼊频道”和“离开频道”。

增加 StartCall
⾸先创建 VideoFrameObserver 对象,然后根据你的场景来设置以下回调。

OnLocalFrameCallback:⽤于 SDK 获取本地摄像头采集到的视频帧。

OnRemoteFrameCallback:⽤于 SDK 获取远端摄像头采集到的视频帧。

在 InitAgora 的 MediaEngine 对象中通过 registerVideoFrameObserver ⽅法注册 VideoFrameObserver。

为了保证EncryptionType 和 EncryptionKey 不为空,需要先设置 EncryptionMode 和 EncryptionSecret。

然后按照你的需要来设置频道参数,并调⽤ joinChannel。

//VideoCall.cpp
void VideoCall::StartCall(
const FString& ChannelName,
const FString& EncryptionKey,
const FString& EncryptionType)
{
if (!RtcEnginePtr)
{
return;
}
if (MediaEnginePtr)
{
if (!VideoFrameObserverPtr)
{
VideoFrameObserverPtr = MakeUnique<VideoFrameObserver>();
std::function<void(std::uint8_t*, std::uint32_t, std::uint32_t, std::uint32_t)> OnCaptureVideoFrameCallback
= [this](std::uint8_t* buffer, std::uint32_t width, std::uint32_t height, std::uint32_t size)
{
if (OnLocalFrameCallback)
{
OnLocalFrameCallback(buffer, width, height, size);
}
else { UE_LOG(LogTemp, Warning, TEXT("VideoCall OnLocalFrameCallback isn't set")); }
};
VideoFrameObserverPtr->setOnCaptureVideoFrameCallback(std::move(OnCaptureVideoFrameCallback));
std::function<void(std::uint8_t*, std::uint32_t, std::uint32_t, std::uint32_t)> OnRenderVideoFrameCallback
= [this](std::uint8_t* buffer, std::uint32_t width, std::uint32_t height, std::uint32_t size)
{
if (OnRemoteFrameCallback)
{
OnRemoteFrameCallback(buffer, width, height, size);
}
else { UE_LOG(LogTemp, Warning, TEXT("VideoCall OnRemoteFrameCallback isn't set")); }
};
VideoFrameObserverPtr->setOnRenderVideoFrameCallback(std::move(OnRenderVideoFrameCallback));
}
MediaEnginePtr->registerVideoFrameObserver(VideoFrameObserverPtr.Get());
}
int nRet = RtcEnginePtr->enableVideo();
if (nRet < 0)
{
UE_LOG(LogTemp, Warning, TEXT("enableVideo : %d"), nRet)
}
if (!EncryptionType.IsEmpty() && !EncryptionKey.IsEmpty())
{
if (EncryptionType == "aes-256")
{
RtcEnginePtr->setEncryptionMode("aes-256-xts");
}
else
{
RtcEnginePtr->setEncryptionMode("aes-128-xts");
}
nRet = RtcEnginePtr->setEncryptionSecret(TCHAR_TO_ANSI(*EncryptionKey));
if (nRet < 0)
{
UE_LOG(LogTemp, Warning, TEXT("setEncryptionSecret : %d"), nRet)
}
}
nRet = RtcEnginePtr->setChannelProfile(agora::rtc::CHANNEL_PROFILE_COMMUNICATION);
if (nRet < 0)
{
UE_LOG(LogTemp, Warning, TEXT("setChannelProfile : %d"), nRet)
}
//"demoChannel1";
std::uint32_t nUID = 0;
nRet = RtcEnginePtr->joinChannel(NULL, TCHAR_TO_ANSI(*ChannelName), NULL, nUID);
if (nRet < 0)
{
UE_LOG(LogTemp, Warning, TEXT("joinChannel ret: %d"), nRet);
}
}
增加 StopCall 功能
根据你的场景需要,通过调⽤ leaveChannel ⽅法来结束通话,⽐如当要结束通话的时候,当你需要关闭应⽤的时候,或是当你的应⽤运⾏于后台的时候。

调⽤ nullptr 作为实参的 registerVideoFrameObserver,⽤来取消 VideoFrameObserver的注册。

//VideoCall.cpp
void VideoCall::StopCall()
{
if (!RtcEnginePtr)
{
return;
}
auto ConnectionState = RtcEnginePtr->getConnectionState();
if (agora::rtc::CONNECTION_STATE_DISCONNECTED != ConnectionState)
{
int nRet = RtcEnginePtr->leaveChannel();
if (nRet < 0)
{
UE_LOG(LogTemp, Warning, TEXT("leaveChannel ret: %d"), nRet);
}
if (MediaEnginePtr)
{
MediaEnginePtr->registerVideoFrameObserver(nullptr);
}
}
}
创建 Video ⽅法
这些⽅法是⽤来管理视频的。

加 EnableVideo() ⽅法
EnableVideo() 会启⽤本⽰例中的视频。

初始化 nRet,值为 0。

如果 bEnable 为 true,则通过 RtcEnginePtr->enableVideo()启⽤视频。

否则,通过 RtcEnginePtr->disableVideo() 关闭视频。

//VideoCall.cpp
bool VideoCall::EnableVideo(bool bEnable)
{
if (!RtcEnginePtr)
{
return false;
}
int nRet = 0;
if (bEnable)
nRet = RtcEnginePtr->enableVideo();
else
nRet = RtcEnginePtr->disableVideo();
return nRet == 0 ? true : false;
}
增加 MuteLocalVideo() ⽅法
MuteLocalVideo() ⽅法负责开启或关闭本地视频。

在其余⽅法完成运⾏之前,需要保证 RtcEnginePtr 不为 nullptr。

如果可以成功mute 或 unmute 本地视频,那么把 bLocalVideoMuted 设置为 bMuted。

//VideoCall.cpp
bool VideoCall::MuteLocalVideo(bool bMuted)
{
if (!RtcEnginePtr)
{
return false;
}
int ret = RtcEnginePtr->muteLocalVideoStream(bMuted);
if (ret == 0)
bLocalVideoMuted = bMuted;
return ret == 0 ? true : false;
}
增加 IsLocalVideoMuted() ⽅法
IsLocalVideoMuted() ⽅法的作⽤是,当本地视频开启或关闭的时候,返回 bLocalVideoMuted。

//VideoCall.cpp
bool VideoCall::IsLocalVideoMuted()
{
return bLocalVideoMuted;
}
创建⾳频相关的⽅法
这些⽅法是⽤来管理⾳频的。

添加 MuteLocalAudio() ⽅法
MuteLocalAudio()⽤于 mute 或 unmute 本地⾳频:
//VideoCall.cpp
bool VideoCall::MuteLocalAudio(bool bMuted)
{
if (!RtcEnginePtr)
{
return false;
}
int ret = RtcEnginePtr->muteLocalAudioStream(bMuted);
if (ret == 0)
bLocalAudioMuted = bMuted;
return ret == 0 ? true : false;
}
增加 IsLocalAudioMuted() ⽅法
IsLocalAudioMuted()⽅法的作⽤是,当 mute 或 unmute 本地⾳频的时候,返回 bLocalAudioMuted。

//VideoCall.cpp
bool VideoCall::IsLocalAudioMuted()
{
return bLocalAudioMuted;
}
创建 GUI
接下来就是要为⼀对⼀对话创建⽤户交互界⾯了,包括:
创建 VideoCallPlayerController
创建 EnterChannelWidget C++ Class
创建 VideoViewWidget C++ Class
创建 VideoCallViewWidget C++ Class
创建 VideoCallWidget C++ Class
创建 BP_EnterChannelWidget blueprint asset
创建 BP_VideoViewWidget Asset
创建 BP_VideoCallViewWidget Asset
创建 BP_VideoCallWidget Asset
创建 BP_VideoCallPlayerController blueprint asset
创建 BP_AgoraVideoCallGameModeBase Asset
修改 Game Mode
创建 VideoCallPlayerController
为了能够将我们的Widget Blueprints添加到Viewport中,我们创建我们的⾃定义播放器控制器类。

在 "内容浏览器 "中,按 "Add New "按钮,选择 "新建C++类"。

在 "添加C++类 "窗⼝中,勾选 "显⽰所有类 "按钮,并输⼊PlayerController。

按 "下⼀步 "按钮,给类命名为 VideoCallPlayerController。

按Create Class按钮。

//VideoCallPlayerController.h
#include "CoreMinimal.h"
#include "GameFramework/PlayerController.h"
#include "VideoCallPlayerController.generated.h"
UCLASS()
class AGORAVIDEOCALL_API AVideoCallPlayerController : public APlayerController
{
GENERATED_BODY()
public:
};
这个类是 BP_VideoCallPlayerController 的 Blueprint Asset 的基类,我们将在最后创建。

增加需要的 Include
在 VideoCallPlayerController.h ⽂件的头部包括了所需的头⽂件。

//VideoCallPlayerController.h
#include "CoreMinimal.h"
#include "GameFramework/PlayerController.h"
#include "Templates/UniquePtr.h"
#include "VideoCall.h"
#include "VideoCallPlayerController.generated.h"
//VideoCallPlayerController.cpp
#include "Blueprint/UserWidget.h"
#include "EnterChannelWidget.h"
#include "VideoCallWidget.h"
类声明
为下⼀个类添加转发声明:
//VideoCallPlayerController.h
class UEnterChannelWidget;
class UVideoCallWidget;
稍后我们将跟进其中的两个创建,即 UEnterChannelWidget 和 UVideoCallWidget。

添加成员变量
现在,在编辑器中添加成员引⽤到 UMG Asset 中。

//VideoCallPlayerController.h
...
UCLASS()
class AGORAVIDEOCALL_API AVideoCallPlayerController : public APlayerController
{
GENERATED_BODY()
public:
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Widgets")
TSubclassOf<class UUserWidget> wEnterChannelWidget;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Widgets")
TSubclassOf<class UUserWidget> wVideoCallWidget;
...
};
变量来保持创建后的⼩部件,以及⼀个指向VideoCall的指针。

//VideoCallPlayerController.h
...
UCLASS()
class AGORAVIDEOCALL_API AVideoCallPlayerController : public APlayerController
{
GENERATED_BODY()
public:
...
UEnterChannelWidget* EnterChannelWidget = nullptr;
UVideoCallWidget* VideoCallWidget = nullptr;
TUniquePtr<VideoCall> VideoCallPtr;
...
};
覆盖 BeginPlay/EndPlay
//VideoCallPlayerController.h
...
UCLASS()
class AGORAVIDEOCALL_API AVideoCallPlayerController : public APlayerController
{
GENERATED_BODY()
public:
...
void BeginPlay() override;
void EndPlay(const EEndPlayReason::Type EndPlayReason) override;
...
};
//VideoCallPlayerController.cpp
void AVideoCallPlayerController::BeginPlay()
{
Super::BeginPlay();
//initialize wigets
if (wEnterChannelWidget) // Check if the Asset is assigned in the blueprint.
{
// Create the widget and store it.
if (!EnterChannelWidget)
{
EnterChannelWidget = CreateWidget<UEnterChannelWidget>(this, wEnterChannelWidget);
EnterChannelWidget->SetVideoCallPlayerController(this);
}
// now you can use the widget directly since you have a referance for it.
// Extra check to make sure the pointer holds the widget.
if (EnterChannelWidget)
{
//let add it to the view port
EnterChannelWidget->AddToViewport();
}
//Show the Cursor.
bShowMouseCursor = true;
}
if (wVideoCallWidget)
{
if (!VideoCallWidget)
{
VideoCallWidget = CreateWidget<UVideoCallWidget>(this, wVideoCallWidget);
VideoCallWidget->SetVideoCallPlayerController(this);
}
if (VideoCallWidget)
{
VideoCallWidget->AddToViewport();
}
VideoCallWidget->SetVisibility(ESlateVisibility::Collapsed);
}
//create video call and switch on the EnterChannelWidget
VideoCallPtr = MakeUnique<VideoCall>();
FString Version = VideoCallPtr->GetVersion();
Version = "Agora version: " + Version;
EnterChannelWidget->UpdateVersionText(Version);
SwitchOnEnterChannelWidget(std::move(VideoCallPtr));
}
void AVideoCallPlayerController::EndPlay(const EEndPlayReason::Type EndPlayReason)
{
Super::EndPlay(EndPlayReason);
}
这时你可能注意到EnterChannelWidget和VideoCallWidget⽅法被标记为错误,那是因为它们还没有实现。

我们将在接下来的步骤中实现它们。

增加 StartCall/EndCall
//VideoCallPlayerController.h
...
UCLASS()
class AGORAVIDEOCALL_API AVideoCallPlayerController : public APlayerController
{
GENERATED_BODY()
public:
...
void StartCall(
TUniquePtr<VideoCall> PassedVideoCallPtr,
const FString& ChannelName,
const FString& EncryptionKey,
const FString& EncryptionType
);
void EndCall(TUniquePtr<VideoCall> PassedVideoCallPtr);
...
};
//VideoCallPlayerController.cpp
void AVideoCallPlayerController::StartCall(
TUniquePtr<VideoCall> PassedVideoCallPtr,
const FString& ChannelName,
const FString& EncryptionKey,
const FString& EncryptionType)
{
SwitchOnVideoCallWidget(std::move(PassedVideoCallPtr));
VideoCallWidget->OnStartCall(
ChannelName,
EncryptionKey,
EncryptionType);
}
void AVideoCallPlayerController::EndCall(TUniquePtr<VideoCall> PassedVideoCallPtr)
{
SwitchOnEnterChannelWidget(std::move(PassedVideoCallPtr));
}
增加打开另⼀个⼩⼯具的⽅法
//VideoCallPlayerController.h
...
UCLASS()
class AGORAVIDEOCALL_API AVideoCallPlayerController : public APlayerController
{
GENERATED_BODY()
public:
...
void SwitchOnEnterChannelWidget(TUniquePtr<VideoCall> PassedVideoCallPtr);
void SwitchOnVideoCallWidget(TUniquePtr<VideoCall> PassedVideoCallPtr);
...
};
//VideoCallPlayerController.cpp
void AVideoCallPlayerController::SwitchOnEnterChannelWidget(TUniquePtr<VideoCall> PassedVideoCallPtr)
{
if (!EnterChannelWidget)
{
return;
}
EnterChannelWidget->SetVideoCall(std::move(PassedVideoCallPtr));
EnterChannelWidget->SetVisibility(ESlateVisibility::Visible);
}
void AVideoCallPlayerController::SwitchOnVideoCallWidget(TUniquePtr<VideoCall> PassedVideoCallPtr)
{
if (!VideoCallWidget)
{
return;
}
VideoCallWidget->SetVideoCall(std::move(PassedVideoCallPtr));
VideoCallWidget->SetVisibility(ESlateVisibility::Visible);
}
创建 EnterChannelWidget C++类
EnterChannelWidget是负责管理 UI 元素交互的。

我们要创建⼀个新的 UserWidget 类型的类。

在内容浏览器中,按Add New 按钮,选择New C++类,然后勾选Show All Classes按钮,输⼊UserWidget。

按下 "下⼀步 "按钮,为类设置⼀个名
称,EnterChannelWidget。

我们会得到如下代码:
//EnterChannelWidget.h
#include "CoreMinimal.h"
#include "Blueprint/UserWidget.h"
#include "EnterChannelWidget.generated.h"
UCLASS()
class AGORAVIDEOCALL_API UEnterChannelWidget : public UUserWidget {
GENERATED_BODY()
};
在EnterChannelWidget.h⽂件中增加⼀些必要的 include:
//EnterCahnnelWidget.h
#include "CoreMinimal.h"
#include "Blueprint/UserWidget.h"
#include "Components/TextBlock.h"
#include "Components/RichTextBlock.h"
#include "Components/EditableTextBox.h"
#include "Components/ComboBoxString.h"
#include "Components/Button.h"
#include "Components/Image.h"
#include "VideoCall.h"
#include "EnterChannelWidget.generated.h"
class AVideoCallPlayerController;
//EnterCahnnelWidget.cpp
#include "Blueprint/WidgetTree.h"
#include "VideoCallPlayerController.h"
然后我们需要增加如下变量:
//EnterChannelWidget.h
...
UCLASS()
class AGORAVIDEOCALL_API UEnterChannelWidget : public UUserWidget {
GENERATED_BODY()
public:
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, meta = (BindWidget)) UTextBlock* HeaderTextBlock = nullptr;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, meta = (BindWidget)) UTextBlock* DescriptionTextBlock = nullptr;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, meta = (BindWidget)) UEditableTextBox* ChannelNameTextBox = nullptr;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, meta = (BindWidget)) UEditableTextBox* EncriptionKeyTextBox = nullptr;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, meta = (BindWidget)) UTextBlock* EncriptionTypeTextBlock = nullptr;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, meta = (BindWidget)) UComboBoxString* EncriptionTypeComboBox = nullptr;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, meta = (BindWidget)) UButton* JoinButton = nullptr;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, meta = (BindWidget)) UButton* TestButton = nullptr;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, meta = (BindWidget))
UButton* VideoSettingsButton = nullptr;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, meta = (BindWidget))
UTextBlock* ContactsTextBlock = nullptr;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, meta = (BindWidget))
UTextBlock* BuildInfoTextBlock = nullptr;
...
};
这些变量⽤来公职 blueprint asset 中相关的 UI 元素。

这⾥最重要的是 BindWidget 元属性。

通过将指向⼩部件的指针标记为BindWidget,你可以在你的 C++类的 Blueprint ⼦类中创建⼀个同名的⼩部件,并在运⾏时从 C++中访问它。

同时,还要添加如下成员:
//EnterChannelWidget.h
...
UCLASS()
class AGORAVIDEOCALL_API UEnterChannelWidget : public UUserWidget
{
GENERATED_BODY()
...
public:
AVideoCallPlayerController* PlayerController = nullptr;
TUniquePtr<VideoCall> VideoCallPtr;
...
};
添加 Constructor 和 Construct/Destruct ⽅法
//EnterChannelWidget.h
...
UCLASS()
class AGORAVIDEOCALL_API UEnterChannelWidget : public UUserWidget
{
GENERATED_BODY()
public:
...
UEnterChannelWidget(const FObjectInitializer& objectInitializer);
void NativeConstruct() override;
...
};
//EnterChannelWidget.cpp
UEnterChannelWidget::UEnterChannelWidget(const FObjectInitializer& objectInitializer)
: Super(objectInitializer)
{
}
void UEnterChannelWidget::NativeConstruct()
{
Super::NativeConstruct();
if (HeaderTextBlock)
HeaderTextBlock->SetText(FText::FromString("Enter a conference room name"));
if (DescriptionTextBlock)
DescriptionTextBlock->SetText(FText::FromString("If you are the first person to specify this name, \
the room will be created and you will\nbe placed in it. \
If it has already been created you will join the conference in progress"));
if (ChannelNameTextBox)
ChannelNameTextBox->SetHintText(FText::FromString("Channel Name"));
if (EncriptionKeyTextBox)
EncriptionKeyTextBox->SetHintText(FText::FromString("Encription Key"));
if (EncriptionTypeTextBlock)
EncriptionTypeTextBlock->SetText(FText::FromString("Enc Type:"));
if (EncriptionTypeComboBox)
{
EncriptionTypeComboBox->AddOption("aes-128");
EncriptionTypeComboBox->AddOption("aes-256");
EncriptionTypeComboBox->SetSelectedIndex(0);
}
if (JoinButton)
{
UTextBlock* JoinTextBlock = WidgetTree->ConstructWidget<UTextBlock>(UTextBlock::StaticClass());
JoinTextBlock->SetText(FText::FromString("Join"));
JoinButton->AddChild(JoinTextBlock);
JoinButton->OnClicked.AddDynamic(this, &UEnterChannelWidget::OnJoin);
}
if (ContactsTextBlock)
ContactsTextBlock->SetText(FText::FromString("agora.io Contact support: 400 632 6626"));
if (BuildInfoTextBlock)
BuildInfoTextBlock->SetText(FText::FromString(" "));
}
增加 Setter ⽅法
初始化 PlayerController 和 VideoCallPtr 变量
//EnterChannelWidget.h
...
UCLASS()
class AGORAVIDEOCALL_API UEnterChannelWidget : public UUserWidget
{
GENERATED_BODY()
public:
...
void SetVideoCallPlayerController(AVideoCallPlayerController* VideoCallPlayerController);
void SetVideoCall(TUniquePtr<VideoCall> PassedVideoCallPtr);
...
};
//EnterChannelWidget.cpp
void UEnterChannelWidget::SetVideoCallPlayerController(AVideoCallPlayerController* VideoCallPlayerController) {
PlayerController = VideoCallPlayerController;
void UEnterChannelWidget::SetVideoCall(TUniquePtr<VideoCall> PassedVideoCallPtr) {
VideoCallPtr = std::move(PassedVideoCallPtr);
}
增加 BlueprintCallable⽅法
要对相应的按钮 "onButtonClick "事件做出反应。

//EnterChannelWidget.h
..
UCLASS()
class AGORAVIDEOCALL_API UEnterChannelWidget : public UUserWidget
{
GENERATED_BODY()
public:
...
UFUNCTION(BlueprintCallable)
void OnJoin();
...
};
//EnterChannelWidget.cpp
void UEnterChannelWidget::OnJoin()
{
if (!PlayerController || !VideoCallPtr)
{
return;
}
FString ChannelName = ChannelNameTextBox->GetText().ToString();
FString EncryptionKey = EncriptionKeyTextBox->GetText().ToString();
FString EncryptionType = EncriptionTypeComboBox->GetSelectedOption();
SetVisibility(ESlateVisibility::Collapsed);
PlayerController->StartCall(
std::move(VideoCallPtr),
ChannelName,
EncryptionKey,
EncryptionType);
}
增加 update ⽅法
//EnterChannelWidget.h
...
UCLASS()
class AGORAVIDEOCALL_API UEnterChannelWidget : public UUserWidget
{
GENERATED_BODY()
public:
...
void UpdateVersionText(FString newValue);
...
};
//EnterChannelWidget.cpp
void UEnterChannelWidget::UpdateVersionText(FString newValue)
if (BuildInfoTextBlock)
BuildInfoTextBlock->SetText(FText::FromString(newValue));
}
创建 VideoViewWidget C++ 类
VideoViewWidget是⼀个存储动态纹理并使⽤RGBA buffer 更新动态纹理的类,该类是从VideoCall OnLocalFrameCallback/OnRemoteFrameCallback函数中接收到的。

创建类和添加所需的 include
//VideoViewWidget.h
#include "CoreMinimal.h"
#include "Blueprint/UserWidget.h"
#include "Components/Image.h"
#include "VideoViewWidget.generated.h"
//VideoViewWidget.cpp
#include "EngineUtils.h"
#include "Engine/Texture2D.h"
#include <algorithm>
添加成员变量
Buffer:⽤于存储RGBA缓冲区、Width、Height和BufferSize的变量 - 视频帧的参数。

RenderTargetImage:允许你在UI中显⽰Slate Brush或纹理或材质的图像⼩部件。

RenderTargetTexture:动态纹理,我们将使⽤Buffer变量更新。

FUpdateTextureRegion2D:指定⼀个纹理的更新区域刷⼦ - ⼀个包含如何绘制Slate元素的笔刷。

我们将⽤它来绘制RenderTargetImage上的RenderTargetTexture。

//VideoViewWidget.h
...
UCLASS()
class AGORAVIDEOCALL_API UVideoViewWidget : public UUserWidget
{
GENERATED_BODY()
public:
UPROPERTY(BlueprintReadOnly, meta = (BindWidget))
UImage* RenderTargetImage = nullptr;
UPROPERTY(EditDefaultsOnly)
UTexture2D* RenderTargetTexture = nullptr;
UTexture2D* CameraoffTexture = nullptr;
uint8* Buffer = nullptr;
uint32_t Width = 0;
uint32_t Height = 0;
uint32 BufferSize = 0;
FUpdateTextureRegion2D* UpdateTextureRegion = nullptr;
FSlateBrush Brush;
FCriticalSection Mutex;
...
};
覆盖 NativeConstruct() ⽅法
在NativeConstruct中,我们将⽤默认颜⾊初始化我们的图像。

为了初始化我们的RenderTargetTexture,我们需要使⽤CreateTransient调⽤创建动态纹理(Texture2D)。

然后分配BufferSize为Width * Height * 4的BufferSize(⽤于存储RGBA格式,每个像素可以⽤4个字节表⽰)。

为了更新我们的纹理,我们可以使⽤UpdateTextureRegions函数。

这个函数的输⼊参数之⼀是我们的像素数据缓冲区。

这样,每当我们修改像素数据缓冲区时,我们就需要调⽤这个函数来使变化在纹理中可见。

现。

相关文档
最新文档