NSDT工具推荐: Three.js AI纹理开发包 - YOLO合成数据生成器 - GLTF/GLB在线编辑 - 3D模型格式在线转换 - 可编程3D场景编辑器 - REVIT导出3D模型插件 - 3D模型语义搜索引擎 - AI模型在线查看 - Three.js虚拟轴心开发包 - 3D模型在线减面 - STL模型在线切割 - 3D道路快速建模
在这个教程里,我们将学习如何使用C++为Unreal引擎开发一个视频聊天插件,我们将使用Unreal 4。25和最新版的Agora SDK。
1、创建项目
现在,让我们从头开始构建一个Unreal项目!
- 打开虚幻编辑器,然后单击新建项目。
- 在"新建项目"面板上,选择"C++"作为项目类型,输入项目名称,选择项目位置,然后单击"创建项目"。
取消 [your_project]/Source/[project_name]/[project_name] .Build.cs文件中 PrivateDependencyModuleNames 行的注释。默认情况下,虚幻引擎会注释掉此行,如果该行被注释掉,则会导致编译错误。
// Uncomment if you are using Slate UI
PrivateDependencyModuleNames.AddRange(new string[] { "UMG", "Slate", "SlateCore" });
2、安装
请按照以下步骤将 Agora 插件集成到你的项目中。
1. 将插件复制到 [your_project]/Plugins。
2. 将插件依赖项添加到 [your_project]/Source/[project_name]/[project_name].Build.cs文件中:
Private Dependencies section PrivateDependencyModuleNames.AddRange(new string[] { "AgoraPlugin" });
3. 重新启动虚幻引擎。
4. 转到Edit>Plugins。找到"Project>Other"类别,并确保启用了该插件。
3、创建关卡
接下来,我们将创建一个关卡来构建游戏环境。有几种不同的方法可以创建新关卡。这里我们使用 File 菜单。
在虚幻编辑器中,选择File>New Level:
将打开"New Level"对话框:
4、创建核心类
现在是时候创建第一个C++类了,下面这些类将处理与 Agora SDK 的通信:
- VideoFrameObserver
- VideoCall
5、创建VideoFrameObserver
VideoFrameObserver 实现
agora::media::IVideoFrameObserver
VideoFrameObserver 类中的方法管理视频帧回调,应在使用registerVideoFrameObserver函数在以下位置注册:
agora::media::IMediaEngine
要创建VideoFrameObserver类,我们需要:
- 创建 VideoFrameObserver 类接口
- 重写 onCaptureVideoFrame 和 onRenderVideoFrame 方法
- 添加 setOnCaptureVideoFrameCallback 和 setOnRenderVideoFrameCallback 方法
在虚幻编辑器中,选择File>Add New C++ Class:
选择None作为父类,然后单击Next:
将类命名为 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
是项目依赖定义,请改用虚幻引擎生成的你自己的定义。
重写 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;
}
6、创建视频通话C++类
VideoCall 类管理与 Agora SDK 的通信。
创建类接口
返回虚幻编辑器,创建一个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"
接下来,将这些方法添加到 VideoCall.cpp,这将创建并初始化 Agora 引擎:
//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);
}
7、创建调用方法
本节中的方法管理加入或离开频道
添加 StartCall函数
创建 VideoFrameObserver 对象,并根据具体方案注册以下回调:
OnLocalFrameCallback
: 当SDK接收到本地摄像机捕获到视频帧时触发OnRemoteFrameCallback
: 当SDK接收到远程用户发送的视频帧时触发
在 InitAgora 函数中,使用registerVideoFrameObserver
方法在 MediaEngine 上注册 VideoFrameObserver 对象。如果 EncryptionType 和 EncryptionKey 不为空,则为 RtcEngine 设置 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);
}
}
注意:教程代码仅供参考和开发环境使用,不适用于生产环境。建议对在生产环境中运行的所有 RTE 应用进行令牌身份验证。有关 Agora 平台中基于令牌的身份验证的更多信息,请参阅本指南:https://bit.ly/3sNiFRs
添加 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);
}
}
}
添加EnableVideo() 方法
EnableVideo()
方法为示例应用程序启用视频。使用 0
值初始化nRet
。如果 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
。如果本地视频静音或取消静音成功,请设置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()
方法将本地音频静音或取消静音。在完成其余方法操作之前,请确保RtcEnginePtr
不是nullptr
。如果本地音频静音或取消静音成功,请设置bLocalAudioMuted
为 bMuted
。
//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()
方法指示本地音频是静音还是取消静音,返回 。bLocalAudioMuted
//VideoCall.cpp
bool VideoCall::IsLocalAudioMuted()
{
return bLocalAudioMuted;
}
8、创建GUI
现在,我们将使用以下类为创建图形用户界面 (GUI):
- VideoCallPlayerController
- EnterChannelWidget
- VideoViewWidget
- VideoCallViewWidget
- VideoCallWidget
- BP_EnterChannelWidget蓝图资源
- BP_VideoViewWidget资产
- BP_VideoCallViewWidget资产
- BP_VideoCallWidget资产
- BP_VideoCallPlayerController蓝图资源
- BP_AgoraVideoCallGameModeBase资产
创建 VideoCallPlayerController
要将我们的组件蓝图添加到虚幻编辑器视图,可以创建一个自定义播放器控制器类。
在内容浏览器中,单击"Add New"按钮,然后选择"新建C++类"。在"Add New C++ Class"窗口中,单击"Show All Classes"按钮,然后键入 PlayerController。单击"Next"按钮,并将类命名为 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蓝图资源的基类,该蓝图资源将在教程末尾创建。
添加必需的头文件
在 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资源的成员引用:
//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 widgets
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 reference 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));
}
为另一个部件方法添加切换开关
通过管理部件的可见性并传递VideoCall指针,我们定义一个活动小部件。
//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);
}
9、创建 EnterChannelWidget类
EnterChannelWidget类管理蓝图资产中的 UI 元素与应用程序的交互。
创建一个 UserWidget 类。在内容浏览器中,单击"Add New"按钮,然后选择"New C++ Class"。然后单击"Show All Classes"按钮,并键入"UserWidget"。单击"Next"按钮,然后为类设置一个名称: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 文件的顶部,包括所需的头文件和前置声明:
//EnterChannelWidget.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;
//EnterChannelWidget.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;
...
};
需要这些变量来控制蓝图资源中的相应 UI 元素。这里最重要的是 BindWidget 元属性。通过将指向小部件的指针标记为 BindWidget,你可以创建相同的小部件。
添加下一个成员
//EnterChannelWidget.h
...
UCLASS()
class AGORAVIDEOCALL_API UEnterChannelWidget : public UUserWidget
{
GENERATED_BODY()
...
public:
AVideoCallPlayerController* PlayerController = nullptr;
TUniquePtr<VideoCall> VideoCallPtr;
...
};
添加构造函数和构造/析构方法
//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);
}
添加更新方法:
//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));
}
10、创建 VideoViewWidget类
VideoViewWidget是一个存储、更新动态纹理的类,它使用从VideoCall 的OnLocalFrameCallback
/OnRemoteFrameCallback
函数接收的RGBA缓冲区。
创建类并添加必需的头文件
像以前一样C++类创建小部件,并添加所需的头文件:
//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 缓冲区、宽度、高度和缓冲区大小的变量,它们是视频帧的参数。
- RenderTargetImage — 用于在 UI 中显示Slate画笔、纹理或材质的图像小部
- RenderTargetTexture — 动态纹理,将使用 Buffer 变量对其进行更新。
- FUpdateTextureRegion2D — 指定纹理的更新区域。
- Brush — 包含有关如何绘制 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),然后分配缓冲器,缓冲区大小的计算公式为 Width * Height * 4(要存储 RGBA 格式,每个像素需要用 4 个字节表示)。
要更新纹理,可以使用 UpdateTextureRegions 调用。此函数的输入参数之一是像素数据缓冲区。每当修改像素数据缓冲区时,都需要调用此函数以使更改在纹理中可见。
现在,使用 RenderTargetTexture 初始化 Brush 变量,然后在 RenderTargetImage 小部件中设置此 Brush。
//VideoViewWidget.h
...
UCLASS()
class AGORAVIDEOCALL_API UVideoViewWidget : public UUserWidget
{
GENERATED_BODY()
public:
...
void NativeConstruct() override;
...
};
//VideoViewWidget.cpp
void UVideoViewWidget::NativeConstruct()
{
Super::NativeConstruct();
Width = 640;
Height = 360;
RenderTargetTexture = UTexture2D::CreateTransient(Width, Height, PF_R8G8B8A8);
RenderTargetTexture->UpdateResource();
BufferSize = Width * Height * 4;
Buffer = new uint8[BufferSize];
for (uint32 i = 0; i < Width * Height; ++i)
{
Buffer[i * 4 + 0] = 0x32;
Buffer[i * 4 + 1] = 0x32;
Buffer[i * 4 + 2] = 0x32;
Buffer[i * 4 + 3] = 0xFF;
}
UpdateTextureRegion = new FUpdateTextureRegion2D(0, 0, 0, 0, Width, Height);
RenderTargetTexture->UpdateTextureRegions(0, 1, UpdateTextureRegion, Width * 4, (uint32)4, Buffer);
Brush.SetResourceObject(RenderTargetTexture);
RenderTargetImage->SetBrush(Brush);
}
重写 NativeDestruct() 方法
//VideoViewWidget.h
...
UCLASS()
class AGORAVIDEOCALL_API UVideoViewWidget : public UUserWidget
{
GENERATED_BODY()
public:
...
void NativeDestruct() override;
...
};
//VideoViewWidget.cpp
void UVideoViewWidget::NativeDestruct()
{
Super::NativeDestruct();
delete[] Buffer;
delete UpdateTextureRegion;
}
重写 NativeTick()方法
如果 UpdateTextureRegion 的Width 或 Height 不等于成员Width和Height的值,则需要重新创建 RenderTargetTexture 以支持更新的值,并重新初始化,就像在构造函数中的处理一样。否则,只需使用Buffer的 UpdateTextureRegions。
//VideoViewWidget.h
...
UCLASS()
class AGORAVIDEOCALL_API UVideoViewWidget : public UUserWidget
{
GENERATED_BODY()
public:
...
void NativeTick(const FGeometry& MyGeometry, float DeltaTime) override;
...
};
//VideoViewWidget.cpp
void UVideoViewWidget::NativeTick(const FGeometry& MyGeometry, float DeltaTime)
{
Super::NativeTick(MyGeometry, DeltaTime);
FScopeLock lock(&Mutex);
if (UpdateTextureRegion->Width != Width ||
UpdateTextureRegion->Height != Height)
{
auto NewUpdateTextureRegion = new FUpdateTextureRegion2D(0, 0, 0, 0, Width, Height);
auto NewRenderTargetTexture = UTexture2D::CreateTransient(Width, Height, PF_R8G8B8A8);
NewRenderTargetTexture->UpdateResource();
NewRenderTargetTexture->UpdateTextureRegions(0, 1, NewUpdateTextureRegion, Width * 4, (uint32)4, Buffer);
Brush.SetResourceObject(NewRenderTargetTexture);
RenderTargetImage->SetBrush(Brush);
//UClass's such as UTexture2D are automatically garbage collected when there is no hard pointer references made to that object.
//So if you just leave it and don't reference it elsewhere then it will be destroyed automatically.
FUpdateTextureRegion2D* TmpUpdateTextureRegion = UpdateTextureRegion;
RenderTargetTexture = NewRenderTargetTexture;
UpdateTextureRegion = NewUpdateTextureRegion;
delete TmpUpdateTextureRegion;
return;
}
RenderTargetTexture->UpdateTextureRegions(0, 1, UpdateTextureRegion, Width * 4, (uint32)4, Buffer);
}
添加 UpdateBuffer()方法
从 Agora SDK 线程接收新值,由于 UE4 的限制,将该值保存到 Buffer 变量中,在 NativeTick 方法中更新纹理,并且不在此处调用 UpdateTextureRegions。
//VideoViewWidget.h
...
UCLASS()
class AGORAVIDEOCALL_API UVideoViewWidget : public UUserWidget
{
GENERATED_BODY()
public:
...
void UpdateBuffer( uint8* RGBBuffer, uint32_t Width, uint32_t Height, uint32_t Size );
void ResetBuffer();
...
};
//VideoViewWidget.cpp
void UVideoViewWidget::UpdateBuffer(
uint8* RGBBuffer,
uint32_t NewWidth,
uint32_t NewHeight,
uint32_t NewSize)
{
FScopeLock lock(&Mutex);
if (!RGBBuffer)
{
return;
}
if (BufferSize == NewSize)
{
std::copy(RGBBuffer, RGBBuffer + NewSize, Buffer);
}
else
{
delete[] Buffer;
BufferSize = NewSize;
Width = NewWidth;
Height = NewHeight;
Buffer = new uint8[BufferSize];
std::copy(RGBBuffer, RGBBuffer + NewSize, Buffer);
}
}
void UVideoViewWidget::ResetBuffer()
{
for (uint32 i = 0; i < Width * Height; ++i)
{
Buffer[i * 4 + 0] = 0x32;
Buffer[i * 4 + 1] = 0x32;
Buffer[i * 4 + 2] = 0x32;
Buffer[i * 4 + 3] = 0xFF;
}
}
11、创建 VideoCallViewWidget 类
VideoCallViewWidget 类用于显示本地和远程视频。需要两个 VideoViewWidget 小部件:一个用于显示来自本地摄像头的视频,另一个用于显示从远程用户接收的视频(假设仅支持一个远程用户)。
创建类并添加必需的头文件
像以前一样创建小部件C++类,并添加所需的头文件:
//VideoCallViewWidget.h
#include "CoreMinimal.h"
#include "Blueprint/UserWidget.h"
#include "Components/SizeBox.h"
#include "VideoViewWidget.h"
#include "VideoCallViewWidget.generated.h"
//VideoCallViewWidget.cpp
#include "Components/CanvasPanelSlot.h"
添加成员变量:
//VideoCallViewWidget.h
...
UCLASS()
class AGORAVIDEOCALL_API UVideoCallViewWidget : public UUserWidget
{
GENERATED_BODY()
public:
UPROPERTY(BlueprintReadOnly, meta = (BindWidget))
UVideoViewWidget* MainVideoViewWidget = nullptr;
UPROPERTY(BlueprintReadOnly, meta = (BindWidget))
USizeBox* MainVideoSizeBox = nullptr;
UPROPERTY(BlueprintReadOnly, meta = (BindWidget))
UVideoViewWidget* AdditionalVideoViewWidget = nullptr;
UPROPERTY(BlueprintReadOnly, meta = (BindWidget))
USizeBox* AdditionalVideoSizeBox = nullptr;
public:
int32 MainVideoWidth = 0;
int32 MainVideoHeight = 0;
...
};
重写 NativeTick()方法
在 NativeTick 中,可以更新部件的几何图形:
//VideoCallViewWidget.h
...
UCLASS()
class AGORAVIDEOCALL_API UVideoCallViewWidget : public UUserWidget
{
GENERATED_BODY()
public:
...
void NativeTick(const FGeometry& MyGeometry, float DeltaTime) override;
...
};
//VideoCallViewWidget.cpp
void UVideoCallViewWidget::NativeTick(const FGeometry& MyGeometry, float DeltaTime)
{
Super::NativeTick(MyGeometry, DeltaTime);
auto ScreenSize = MyGeometry.GetLocalSize();
if (MainVideoHeight != 0)
{
float AspectRatio = 0;
AspectRatio = MainVideoWidth / (float)MainVideoHeight;
auto MainVideoGeometry = MainVideoViewWidget->GetCachedGeometry();
auto MainVideoScreenSize = MainVideoGeometry.GetLocalSize();
if (MainVideoScreenSize.X == 0)
{
return;
}
auto NewMainVideoHeight = MainVideoScreenSize.Y;
auto NewMainVideoWidth = AspectRatio * NewMainVideoHeight;
MainVideoSizeBox->SetMinDesiredWidth(NewMainVideoWidth);
MainVideoSizeBox->SetMinDesiredHeight(NewMainVideoHeight);
UCanvasPanelSlot* CanvasSlot = Cast<UCanvasPanelSlot>(MainVideoSizeBox->Slot);
CanvasSlot->SetAutoSize(true);
FVector2D NewPosition;
NewPosition.X = -NewMainVideoWidth / 2;
NewPosition.Y = -NewMainVideoHeight / 2;
CanvasSlot->SetPosition(NewPosition);
}
}
添加UpdateMainVideoBuffer和UpdateAdditionalVideoBuffer
//VideoCallViewWidget.h
...
UCLASS()
class AGORAVIDEOCALL_API UVideoCallViewWidget : public UUserWidget
{
GENERATED_BODY()
public:
...
void UpdateMainVideoBuffer( uint8* RGBBuffer, uint32_t Width, uint32_t Height, uint32_t Size);
void UpdateAdditionalVideoBuffer( uint8* RGBBuffer, uint32_t Width, uint32_t Height, uint32_t Size);
void ResetBuffers();
...
};
//VideoCallViewWidget.cpp
void UVideoCallViewWidget::UpdateMainVideoBuffer(
uint8* RGBBuffer,
uint32_t Width,
uint32_t Height,
uint32_t Size)
{
if (!MainVideoViewWidget)
{
return;
}
MainVideoWidth = Width;
MainVideoHeight = Height;
MainVideoViewWidget->UpdateBuffer(RGBBuffer, Width, Height, Size);
}
void UVideoCallViewWidget::UpdateAdditionalVideoBuffer(
uint8* RGBBuffer,
uint32_t Width,
uint32_t Height,
uint32_t Size)
{
if (!AdditionalVideoViewWidget)
{
return;
}
AdditionalVideoViewWidget->UpdateBuffer(RGBBuffer, Width, Height, Size);
}
void UVideoCallViewWidget::ResetBuffers()
{
if (!MainVideoViewWidget || !AdditionalVideoViewWidget)
{
return;
}
MainVideoViewWidget->ResetBuffer();
AdditionalVideoViewWidget->ResetBuffer();
}
12、创建 VideoCallWidget类
VideoCallWidget类是示例应用程序的音频/视频小部件。它包含以下控件,这些控件与蓝图资源中的 UI 元素绑定:
- 本地和远程视频视图(由 VideoCallViewWidget 表示)
- 结束调用按钮(EndCallButton 变量)
- 静音本地音频按钮(MuteLocalAudioButton变量)
- 视频模式按钮(VideoModeButton变量)
创建类和必需的头文件
像以前一样创建C++小部件类,并添加必需的头文件包含和前置声明:
//VideoCallWidget.h
#include "CoreMinimal.h"
#include "Blueprint/UserWidget.h"
#include "Templates/UniquePtr.h"
#include "Components/Image.h"
#include "Components/Button.h"
#include "Engine/Texture2D.h"
#include "VideoCall.h"
#include "VideoCallViewWidget.h"
#include "VideoCallWidget.generated.h"
class AVideoCallPlayerController;
class UVideoViewWidget;
//VideoCallWidget.cpp
#include "Kismet/GameplayStatics.h"
#include "UObject/ConstructorHelpers.h"
#include "Components/CanvasPanelSlot.h"
#include "VideoViewWidget.h"
#include "VideoCallPlayerController.h"
添加成员变量
//VideoCallWidget.h
...
UCLASS()
class AGORAVIDEOCALL_API UVideoCallWidget : public UUserWidget
{
GENERATED_BODY()
public:
AVideoCallPlayerController* PlayerController = nullptr;
public:
UPROPERTY(BlueprintReadOnly, meta = (BindWidget))
UVideoCallViewWidget* VideoCallViewWidget = nullptr;
//Buttons
UPROPERTY(BlueprintReadOnly, meta = (BindWidget))
UButton* EndCallButton = nullptr;
UPROPERTY(BlueprintReadOnly, meta = (BindWidget))
UButton* MuteLocalAudioButton = nullptr;
UPROPERTY(BlueprintReadOnly, meta = (BindWidget))
UButton* VideoModeButton = nullptr;
//Button textures
int32 ButtonSizeX = 96;
int32 ButtonSizeY = 96;
UTexture2D* EndCallButtonTexture = nullptr;
UTexture2D* AudioButtonMuteTexture = nullptr;
UTexture2D* AudioButtonUnmuteTexture = nullptr;
UTexture2D* VideomodeButtonCameraoffTexture = nullptr;
UTexture2D* VideomodeButtonCameraonTexture = nullptr;
TUniquePtr<VideoCall> VideoCallPtr;
...
};
初始化 VideoCallWidget
找出每个按钮的资源图像,并将其分配给相应的纹理。然后使用纹理初始化每个按钮:
//VideoCallWidget.h
...
UCLASS()
class AGORAVIDEOCALL_API UVideoCallWidget : public UUserWidget
{
GENERATED_BODY()
public:
...
UVideoCallWidget(const FObjectInitializer& ObjectInitializer);
void NativeConstruct() override;
void NativeDestruct() override;
private:
void InitButtons();
...
};
//VideoCallWidget.cpp
void UVideoCallWidget::NativeConstruct()
{
Super::NativeConstruct();
InitButtons();
}
void UVideoCallWidget::NativeDestruct()
{
Super::NativeDestruct();
if (VideoCallPtr)
{
VideoCallPtr->StopCall();
}
}
UVideoCallWidget::UVideoCallWidget(const FObjectInitializer& ObjectInitializer)
: Super(ObjectInitializer)
{
static ConstructorHelpers::FObjectFinder<UTexture2D>
EndCallButtonTextureFinder(TEXT("Texture'/Game/ButtonTextures/hangup.hangup'"));
if (EndCallButtonTextureFinder.Succeeded())
{
EndCallButtonTexture = EndCallButtonTextureFinder.Object;
}
static ConstructorHelpers::FObjectFinder<UTexture2D>
AudioButtonMuteTextureFinder(TEXT("Texture'/Game/ButtonTextures/mute.mute'"));
if (AudioButtonMuteTextureFinder.Succeeded())
{
AudioButtonMuteTexture = AudioButtonMuteTextureFinder.Object;
}
static ConstructorHelpers::FObjectFinder<UTexture2D>
AudioButtonUnmuteTextureFinder(TEXT("Texture'/Game/ButtonTextures/unmute.unmute'"));
if (AudioButtonUnmuteTextureFinder.Succeeded())
{
AudioButtonUnmuteTexture = AudioButtonUnmuteTextureFinder.Object;
}
static ConstructorHelpers::FObjectFinder<UTexture2D>
VideomodeButtonCameraonTextureFinder(TEXT("Texture'/Game/ButtonTextures/cameraon.cameraon'"));
if (VideomodeButtonCameraonTextureFinder.Succeeded())
{
VideomodeButtonCameraonTexture = VideomodeButtonCameraonTextureFinder.Object;
}
static ConstructorHelpers::FObjectFinder<UTexture2D>
VideomodeButtonCameraoffTextureFinder(TEXT("Texture'/Game/ButtonTextures/cameraoff.cameraoff'"));
if (VideomodeButtonCameraoffTextureFinder.Succeeded())
{
VideomodeButtonCameraoffTexture = VideomodeButtonCameraoffTextureFinder.Object;
}
}
void UVideoCallWidget::InitButtons()
{
if (EndCallButtonTexture)
{
EndCallButton->WidgetStyle.Normal.SetResourceObject(EndCallButtonTexture);
EndCallButton->WidgetStyle.Normal.ImageSize = FVector2D(ButtonSizeX, ButtonSizeY);
EndCallButton->WidgetStyle.Normal.DrawAs = ESlateBrushDrawType::Type::Image;
EndCallButton->WidgetStyle.Hovered.SetResourceObject(EndCallButtonTexture);
EndCallButton->WidgetStyle.Hovered.ImageSize = FVector2D(ButtonSizeX, ButtonSizeY);
EndCallButton->WidgetStyle.Hovered.DrawAs = ESlateBrushDrawType::Type::Image;
EndCallButton->WidgetStyle.Pressed.SetResourceObject(EndCallButtonTexture);
EndCallButton->WidgetStyle.Pressed.ImageSize = FVector2D(ButtonSizeX, ButtonSizeY);
EndCallButton->WidgetStyle.Pressed.DrawAs = ESlateBrushDrawType::Type::Image;
}
EndCallButton->OnClicked.AddDynamic(this, &UVideoCallWidget::OnEndCall);
SetAudioButtonToMute();
MuteLocalAudioButton->OnClicked.AddDynamic(this, &UVideoCallWidget::OnMuteLocalAudio);
SetVideoModeButtonToCameraOff();
VideoModeButton->OnClicked.AddDynamic(this, &UVideoCallWidget::OnChangeVideoMode);
}
添加按钮纹理
在应用中找到 Content/ButtonTextures 目录。不必打开项目,只需在文件系统中找到此文件夹即可,所有按钮纹理都存储在那里。在项目内容中,创建一个名为 ButtonTextures的目录,然后将所有按钮图像拖放到该目录中,以使其在项目中可用。
添加设置器
//VideoCallWidget.h
...
UCLASS()
class AGORAVIDEOCALL_API UVideoCallWidget : public UUserWidget
{
GENERATED_BODY()
...
public:
void SetVideoCallPlayerController(AVideoCallPlayerController* VideoCallPlayerController);
void SetVideoCall(TUniquePtr<VideoCall> PassedVideoCallPtr);
...
};
//VideoCallWidget.cpp
void UVideoCallWidget::SetVideoCallPlayerController(AVideoCallPlayerController* VideoCallPlayerController)
{
PlayerController = VideoCallPlayerController;
}
void UVideoCallWidget::SetVideoCall(TUniquePtr<VideoCall> PassedVideoCallPtr)
{
VideoCallPtr = std::move(PassedVideoCallPtr);
}
添加方法以更新按钮视图
//VideoCallWidget.h
...
UCLASS()
class AGORAVIDEOCALL_API UVideoCallWidget : public UUserWidget
{
GENERATED_BODY()
...
private:
void SetVideoModeButtonToCameraOff();
void SetVideoModeButtonToCameraOn();
void SetAudioButtonToMute();
void SetAudioButtonToUnMute();
...
};
//VideoCallWidget.cpp
void UVideoCallWidget::SetVideoModeButtonToCameraOff()
{
if (VideomodeButtonCameraoffTexture)
{
VideoModeButton->WidgetStyle.Normal.SetResourceObject(VideomodeButtonCameraoffTexture);
VideoModeButton->WidgetStyle.Normal.ImageSize = FVector2D(ButtonSizeX, ButtonSizeY);
VideoModeButton->WidgetStyle.Normal.DrawAs = ESlateBrushDrawType::Type::Image;
VideoModeButton->WidgetStyle.Hovered.SetResourceObject(VideomodeButtonCameraoffTexture);
VideoModeButton->WidgetStyle.Hovered.ImageSize = FVector2D(ButtonSizeX, ButtonSizeY);
VideoModeButton->WidgetStyle.Hovered.DrawAs = ESlateBrushDrawType::Type::Image;
VideoModeButton->WidgetStyle.Pressed.SetResourceObject(VideomodeButtonCameraoffTexture);
VideoModeButton->WidgetStyle.Pressed.ImageSize = FVector2D(ButtonSizeX, ButtonSizeY);
VideoModeButton->WidgetStyle.Pressed.DrawAs = ESlateBrushDrawType::Type::Image;
}
}
void UVideoCallWidget::SetVideoModeButtonToCameraOn()
{
if (VideomodeButtonCameraonTexture)
{
VideoModeButton->WidgetStyle.Normal.SetResourceObject(VideomodeButtonCameraonTexture);
VideoModeButton->WidgetStyle.Normal.ImageSize = FVector2D(ButtonSizeX, ButtonSizeY);
VideoModeButton->WidgetStyle.Normal.DrawAs = ESlateBrushDrawType::Type::Image;
VideoModeButton->WidgetStyle.Hovered.SetResourceObject(VideomodeButtonCameraonTexture);
VideoModeButton->WidgetStyle.Hovered.ImageSize = FVector2D(ButtonSizeX, ButtonSizeY);
VideoModeButton->WidgetStyle.Hovered.DrawAs = ESlateBrushDrawType::Type::Image;
VideoModeButton->WidgetStyle.Pressed.SetResourceObject(VideomodeButtonCameraonTexture);
VideoModeButton->WidgetStyle.Pressed.ImageSize = FVector2D(ButtonSizeX, ButtonSizeY);
VideoModeButton->WidgetStyle.Pressed.DrawAs = ESlateBrushDrawType::Type::Image;
}
}
void UVideoCallWidget::SetAudioButtonToMute()
{
if (AudioButtonMuteTexture)
{
MuteLocalAudioButton->WidgetStyle.Normal.SetResourceObject(AudioButtonMuteTexture);
MuteLocalAudioButton->WidgetStyle.Normal.ImageSize = FVector2D(ButtonSizeX, ButtonSizeY);
MuteLocalAudioButton->WidgetStyle.Normal.DrawAs = ESlateBrushDrawType::Type::Image;
MuteLocalAudioButton->WidgetStyle.Hovered.SetResourceObject(AudioButtonMuteTexture);
MuteLocalAudioButton->WidgetStyle.Hovered.ImageSize = FVector2D(ButtonSizeX, ButtonSizeY);
MuteLocalAudioButton->WidgetStyle.Hovered.DrawAs = ESlateBrushDrawType::Type::Image;
MuteLocalAudioButton->WidgetStyle.Pressed.SetResourceObject(AudioButtonMuteTexture);
MuteLocalAudioButton->WidgetStyle.Pressed.ImageSize = FVector2D(ButtonSizeX, ButtonSizeY);
MuteLocalAudioButton->WidgetStyle.Pressed.DrawAs = ESlateBrushDrawType::Type::Image;
}
}
void UVideoCallWidget::SetAudioButtonToUnMute()
{
if (AudioButtonUnmuteTexture)
{
MuteLocalAudioButton->WidgetStyle.Normal.SetResourceObject(AudioButtonUnmuteTexture);
MuteLocalAudioButton->WidgetStyle.Normal.ImageSize = FVector2D(ButtonSizeX, ButtonSizeY);
MuteLocalAudioButton->WidgetStyle.Normal.DrawAs = ESlateBrushDrawType::Type::Image;
MuteLocalAudioButton->WidgetStyle.Hovered.SetResourceObject(AudioButtonUnmuteTexture);
MuteLocalAudioButton->WidgetStyle.Hovered.ImageSize = FVector2D(ButtonSizeX, ButtonSizeY);
MuteLocalAudioButton->WidgetStyle.Hovered.DrawAs = ESlateBrushDrawType::Type::Image;
MuteLocalAudioButton->WidgetStyle.Pressed.SetResourceObject(AudioButtonUnmuteTexture);
MuteLocalAudioButton->WidgetStyle.Pressed.ImageSize = FVector2D(ButtonSizeX, ButtonSizeY);
MuteLocalAudioButton->WidgetStyle.Pressed.DrawAs = ESlateBrushDrawType::Type::Image;
}
}
添加 OnStartCall 方法
//VideoCallWidget.h
...
UCLASS()
class AGORAVIDEOCALL_API UVideoCallWidget : public UUserWidget
{
GENERATED_BODY()
public:
...
void OnStartCall( const FString& ChannelName, const FString& EncryptionKey, const FString& EncryptionType );
...
};
//VideoCallWidget.cpp
void UVideoCallWidget::OnStartCall(
const FString& ChannelName,
const FString& EncryptionKey,
const FString& EncryptionType)
{
if (!VideoCallPtr)
{
return;
}
auto OnLocalFrameCallback = [this](
std::uint8_t* Buffer,
std::uint32_t Width,
std::uint32_t Height,
std::uint32_t Size)
{
VideoCallViewWidget->UpdateAdditionalVideoBuffer(Buffer, Width, Height, Size);
};
VideoCallPtr->RegisterOnLocalFrameCallback(OnLocalFrameCallback);
auto OnRemoteFrameCallback = [this](
std::uint8_t* Buffer,
std::uint32_t Width,
std::uint32_t Height,
std::uint32_t Size)
{
VideoCallViewWidget->UpdateMainVideoBuffer(Buffer, Width, Height, Size);
};
VideoCallPtr->RegisterOnRemoteFrameCallback(OnRemoteFrameCallback);
VideoCallPtr->StartCall(ChannelName, EncryptionKey, EncryptionType);
}
添加 OnEndCall 方法
//VideoCallWidget.h
...
UCLASS()
class AGORAVIDEOCALL_API UVideoCallWidget : public UUserWidget
{
GENERATED_BODY()
public:
...
UFUNCTION(BlueprintCallable)
void OnEndCall();
...
};
//VideoCallWidget.cpp
void UVideoCallWidget::OnEndCall()
{
if (VideoCallPtr)
{
VideoCallPtr->StopCall();
}
if (VideoCallViewWidget)
{
VideoCallViewWidget->ResetBuffers();
}
if (PlayerController)
{
SetVisibility(ESlateVisibility::Collapsed);
PlayerController->EndCall(std::move(VideoCallPtr));
}
}
添加 OnMuteLocalAudio 方法
//VideoCallWidget.h
...
UCLASS()
class AGORAVIDEOCALL_API UVideoCallWidget : public UUserWidget
{
GENERATED_BODY()
public:
...
UFUNCTION(BlueprintCallable)
void OnMuteLocalAudio();
...
};
//VideoCallWidget.cpp
void UVideoCallWidget::OnMuteLocalAudio()
{
if (!VideoCallPtr)
{
return;
}
if (VideoCallPtr->IsLocalAudioMuted())
{
VideoCallPtr->MuteLocalAudio(false);
SetAudioButtonToMute();
}
else
{
VideoCallPtr->MuteLocalAudio(true);
SetAudioButtonToUnMute();
}
}
添加 OnChangeVideoMode方法
//VideoCallWidget.h
...
UCLASS()
class AGORAVIDEOCALL_API UVideoCallWidget : public UUserWidget
{
GENERATED_BODY()
public:
...
UFUNCTION(BlueprintCallable)
void OnChangeVideoMode();
...
};
//VideoCallWidget.cpp
void UVideoCallWidget::OnChangeVideoMode()
{
if (!VideoCallPtr)
{
return;
}
if (!VideoCallPtr->IsLocalVideoMuted())
{
VideoCallPtr->MuteLocalVideo(true);
SetVideoModeButtonToCameraOn();
}
else
{
VideoCallPtr->EnableVideo(true);
VideoCallPtr->MuteLocalVideo(false);
SetVideoModeButtonToCameraOff();
}
}
13、创建蓝图类
确保C++代码正确编译。如果没有成功编译项目,则无法继续执行后续步骤。如果已成功编译C++代码,但在虚幻编辑器中仍未看到所需的类,请重新打开项目。
14、创建BP_EnterChannelWidget蓝图资源
创建 UEnterChannelWidget 蓝图。右键单击内容,然后从User Interface菜单中选择选择Widget Blueprint。
更改这个新的用户小组件的父类。
打开蓝图时,将显示 UMG 编辑器界面,默认情况下,该界面将打开到设计器选项卡。
单击右上角的图表按钮,然后选择类设置。在"Details"面板上,单击"Parent Class"下拉列表,然后选择以前创建的C++类:UEnterChannelWidget。
返回到"设计器"选项卡。"Pallette"窗口包含几种不同类型的构件,可用于构造 UI 元素。找到文本、可编辑文本、按钮和组合框(字符串)元素,并将它们拖到工作区,如屏幕截图所示。然后转到 EnterChannelWidget.h 文件中 UEnterChannelWidget 的定义,以查看具有相应类型(UTextBlock、UEditableTextBox、UButton 和 UComboBoxString)的成员变量的名称。
返回到BP_VideoCallViewWidget编辑器,并为已拖动到构件中的 UI 元素设置相同的名称。为此,可以在"Details"面板中单击元素并更改名称。尝试编译蓝图。如果忘记添加某些内容,或者 UserWidget 类中存在小部件名称/类型不匹配,将出现错误。
将其保存到首选文件夹。如:/Content/Widgets/BP_EnterChannelWidget.uasset
15、创建BP_VideoViewWidget资产
创建BP_VideoViewWidget资源,将父类设置为 UVideoViewWidget,并将 Image 元素命名为 RenderTargetImage。
在此处设置图像锚点非常重要:
16、创建BP_VideoCallViewWidget资产
创建BP_VideoCallViewWidget资产,将父类设置为 UVideoCallViewWidget,并添加BP_VideoViewWidget类型的 UI 元素 MainVideoViewWidget 和 OtherVideoViewWidget。还要添加 SizeBox 类型的 MainVideoSizeBox 和 OtherVideoSizeBox UI 元素。
17、创建BP_VideoCallWidget资产
创建BP_VideoCallWidget资产,将父类设置为 UVideoCallWidget,在组件面板中找到 UI 元素BP_VideoCallViewWidget UI 元素,并添加名为 VideoCallViewWidget 的 UI 元素,然后添加 EndCallButton、MuteLocalAudioButton 和 VideoModeButton 按钮。
18、创建BP_VideoCallPlayerController蓝图资源
现在是时候基于前面描述的 AVideoCallPlayerController 类创建BP_VideoCallPlayerController蓝图资产了。
19、创建 AVideoCallPlayerController 蓝图
右键单击内容,单击添加新按钮,然后选择蓝图类。在"选取父类"窗口中,转到"所有类"部分并找到 VideoCallPlayerController 类。
现在,将以前创建的部件分配给 Player 控制器,如下所示:
将其保存到文件夹,如 /Content/Widgets/BP_VideoCallPlayerController.uasset。
20、创建BP_AgoraVideoCallGameModeBase
接下来,创建 AVideoCallPlayerController 的蓝图。单击新增按钮,选择蓝图类,然后选择游戏模式基类。
修改游戏模式
现在,需要设置自定义游戏模式类和玩家控制器。转到世界设置并设置指定的变量:
指定项目设置
转至编辑>项目设置,然后打开地图和模式选项卡。指定默认参数:
21、运行游戏
Windows
选择"文件>包项目"> Windows > Windows(64 位),选择要放置包的文件夹,然后等待结果。
Mac
选择"文件>包项目> Mac",然后指定要生成到的"生成"文件夹。在运行游戏之前,必须添加权限。
Mac 构建设置
在 info.plist 文件中添加用于设备访问的权限
注: 要访问 .plist 文件,请右键单击<YourBuildName>.app文件,然后选择"显示包内容"。info.plist 文件位于"内容"中。
将以下权限添加到文件中:
- 隐私 — 相机使用说明
- 隐私 — 麦克风用法说明
将 AgoraRtcKit.framework 文件夹添加到构建中
- 返回到项目目录并打开插件文件夹。
- 从 Plugins/AgoraPlugin/Source/ThirdParty/Agora/Mac/Release 中,复制 AgoraRtcKit.framework 文件。
- 将AgoraRtcKit.framework粘贴到新建的项目文件夹中:<Packaged_project_dir>/MacNoEditor/[project_name]/Contents/MacOS/
iOS 打包
若要打包适用于 iOS 的项目,需要生成签名证书和预配置文件。按照UE4文档中的说明进行操作:iOS配置
提示: 建议转到 ProjectSettings > Build >,然后选择"自动签名"复选框。
然后,需要将证书和预配配置文件添加到项目中:
选择"编辑>"项目设置>平台: iOS",然后选择之前创建的证书和预配配置文件。
如果在表格中没有看到其中之一,请点按"导入证书"或"导入预配",在 Finder 中选取正确的文件,然后点按"打开"。
然后输入捆绑包标识符:它必须是在证书创建期间使用的捆绑包 ID。
iOS 权限
对于在iOS中进行测试,建议通过单击虚幻编辑器顶部栏中的"启动"按钮进行测试,并在启动设置中选择你的iOS设备。
在 info.plist 文件中添加以下权限以进行设备访问:
- 隐私 — 相机使用说明
- 隐私 — 麦克风用法说明
若要在 info.plist 文件中添加权限,请选择"编辑>平台:iOS >项目设置",然后将以下行添加到"其他 Plist 数据"中:<key>NSCameraUsageDescription</key><string>AgoraVideoCall</string> <key>NSMicrophoneUsageDescription</key><string>AgoraVideoCall</string>
现在,已准备好为iOS打包项目或在iOS设备上启动它。
原文链接:Using C++ to Implement a Video Chat Feature in Unreal Engine
BimAnt翻译整理,转载请标明出处