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

我们已经上线了 Agora Unreal SDK,提供了支持 Blueprint 和 C++ 的两个版本 SDK。我们分享了 如何基于 Blueprint 在游戏中创建实时音视频功能 。在本文中,我们来分享如何基于声网 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函数。这个函数的输入参数之一是我们的像素数据缓冲区。这样,每当我们修改像素数据缓冲区时,我们就需要调用这个函数来使变化在纹理中可见。现在用我们的RenderTargetTexture初始化Brush变量,然后在RenderTargetImage widget中设置这个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不等于memember的Width Height值,我们需要重新创建RenderTargetTexture以支持更新的值,并像Native Construct成员一样重复初始化。否则只需用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() 方法

通过调用来更新 Buffer 值。我们希望从 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;
 }
}

创建 VideoCallViewWidget C++类

VideoCallViewWidget 类的作用是显示本地和远程用户的视频。我们需要两个 VideoViewWidget 小部件,一个用来显示来自本地摄像头的视频,另一个用来显示从远程用户收到的视频(假设我们只支持一个远程用户)。

创建类和添加所需的 include

像之前那样创建Widget C++类,添加所需的include。

//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() 方法

``
//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/UpdateAdditionalVideoBuffe

//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();
}

创建 VideoCallWidget C++ 类

VideoCallWidget 类作为示例应用程序的音频/视频调用小部件。它包含以下控件,与蓝图资产中的UI元素绑定。

创建类和添加所需的include

像之前那样创建Widget C++类,添加必要的include和转发声明。

//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

为每个按钮找到asset图像,并将其分配到相应的纹理。然后用纹理初始化每个按钮。

//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的新目录,将所有的按钮图片拖放到那里,让它们在你的项目中可用。

添加Setters

//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);
}

增加用来更新 view 的方法

//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();
 }
}

增加 Blueprint 类

确保C++代码正确编译。没有成功编译的项目,你将无法进行下一步的操作。如果你已经成功地编译了C++代码,但在虚幻编辑器中仍然没有看到所需的类,请尝试重新打开项目。

创建 BP_EnterChannelWidget Blueprint Asset。

创建一个 UEnterChannelWidget 的 Blueprint,右键点击内容,选择用户界面菜单并选择 Widget Blueprint。

更改这个新的用户小工具的类的父类。打开 Blueprint,会出现 UMG 编辑器界面,默认情况下 Designer 选项卡是打开的。点击图形按钮(右上角按钮),选择 "类设置"。在面板 "Details "中,点击下拉列表 "父类",选择之前创建的C++ 类 UEnterChannelWidget。现在返回到设计页面。调色板窗口包含几种不同类型的小部件,你可以用它们来构造你的 UI 元素。找到 Text、Editable Text、Button 和 ComboBox(String)元素,然后将它们拖到工作区,如图中所示。然后进入 "EnterChannelWidget.h "文件中的 UEnterChannelWidget 的定义,查看成员变量的名称和对应的类型(UTextBlock、EditableTextBox、UButton和UComboBoxString)。返回到 BP_VideoCallWiewVidget 编辑器中,给你拖动的UI元素设置相同的名称。你可以通过点击元素并在 "详细信息 "面板中更改名称来完成。尝试编译蓝图。如果你忘了添加什么东西,或者在你的UserWidget类中出现了widget名称/类型不匹配的情况,你会出现一个错误。

保存到文件夹中,例如 /Content/Widgets/BP_EnterChannelWidget.uasset。

创建 BP_VideoViewWidget Asset。

设定图片的锚点

创建 BP_VideoCallViewWidget Asset

创建 BP VideoCallViewWidget Asset ,将父类设置为 UVideoCallViewWidget,并添加 BP VideoViewWidget 类型的 UI 元素MainVideoViewWidget 和ExtendedVideoViewWidget。同时添加 SizeBox 类型的 MainVideoSizeBox 和 AdditionalVideoSizeBox UI 元素。

创建 BP_VideoCallWidget Asset

创建BPVideoCallWidget Asset,将父类设置为UVideoCallWidget,在 Palette UI 元素BPVideoCallViewWidget 中找到并添加名称为VideoCallViewWidget,添加 EndCallButton、MuteLocalAudioButton 和 VideoModeButton 按钮。

创建 BP_VideoCallPlayerController blueprint asset

现在是创建 BPVideoCallPlayerPlayerController blueprint asset 的时候了,基于我们前面描述的 AVideoCallPlayerPlayerController 类,创建 BPVideoCallPlayerController 蓝图资产。

创建一个AVideoCallPlayerPlayerController的bluepringt。右键点击内容,按Add New按钮,选择Blueprint类,在窗口中选择父类,在Pick parent类进入All classes部分,找到VideoCallPlayerController类。

现在将我们之前创建的小部件分配给PlayerController,如下图所示。

将其保存到文件夹,例如 /Content/Widgets/BP_VideoCallPlayerController.uasset。

创建 BP_AgoraVideoCallGameModeBase Asset

创建一个 AVideoCallPlayerController 的 Blueprint,右键点击内容,按 Add New 按钮,选择 Blueprint 类,在 Pick parent class 窗口中选择 Game Mode Base Class。这是所有游戏模式的父类。

修改 GameMode

现在你需要设置你的自定义 GameMode 类和玩家控制器。到世界设置中,设置指定的变量:

指定项目的设置

进入 Edit->Project settings,打开 Maps & Modes。设定默认参数:

总结

到此这篇关于C++ 在 Unreal 中为游戏增加实时音视频互动的文章就介绍到这了,更多相关C++ 游戏增加音视频互动内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

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

    我们已经上线了 Agora Unreal SDK,提供了支持 Blueprint 和 C++ 的两个版本 SDK.我们分享了 如何基于 Blueprint 在游戏中创建实时音视频功能 .在本文中,我们来分享如何基于声网 Agora Unreal SDK C++版本,在游戏中实现实时音视频功能. 本篇教程较长,建议在 Web 浏览器端浏览,体验更好. 准备工作 需要的开发环境和需要准备的与 Blueprint 一样: Unreal 4.34 以上版本 Visual Studio 或 Xcode(版

  • Mybatis中动态SQL,if,where,foreach的使用教程详解

    MyBatis的动态SQL是基于OGNL表达式的,它可以帮助我们方便的在SQL语句中实现某些逻辑. MyBatis中用于实现动态SQL的元素主要有: if choose(when,otherwise) trim where set foreach mybatis核心 对sql语句进行灵活操作,通过表达式进行判断,对sql进行灵活拼接.组装. 1.statement中直接定义使用动态SQL: 在statement中利用if 和 where 条件组合达到我们的需求,通过一个例子来说明: 原SQL语句

  • 如何基于 Blueprint 在游戏中创建实时音视频功能

    近日,Epic 公布了他们为次时代游戏准备的 Unreal 5 游戏引擎,以及一份效果惊艳的 Demo.据称,游戏中的每个资源有一百万个绘制三角面,每一帧有超过十亿个绘制三角面.也就是说,开发者构建的游戏场景,能获得电影特效场景般的视觉体验. 事实上,声网也有许多采用 Unreal 的开发者.为了能让开发者可以在游戏中快速实现实时音视频对话,声网 Agora Unreal SDK Beta 版也已上线. 由于 Unreal 为开发者提供了两种开发方式,一种是 Blueprint,即可视化编程,另

  • 在 CentOS 7 中安装 MySQL 8 的教程详解

    准备 本文环境信息: 软件 版本 CentOS CentOS 7.4 MySQL 8.0.x 安装前先更新系统所有包 sudo yum update 安装 1. 添加 Yum 包 wget https://dev.mysql.com/get/mysql80-community-release-el7-1.noarch.rpm # 或者 wget http://repo.mysql.com/mysql80-community-release-el7-1.noarch.rpm sudo yum up

  • 如何在Spring Boot应用中优雅的使用Date和LocalDateTime的教程详解

    Java8已经发布很多年了,但是很多人在开发时仍然坚持使用着Date和SimpleDateFormat进行时间操作.SimpleDateFormat不是线程安全的,而Date处理时间很麻烦,所以Java8提供了LocalDateTime.LocalDate和LocalTime等全新的时间操作API.无论是Date还是LocalDate,在开发Spring Boot应用时经常需要在每个实体类的日期字段上加上@DateTimeFormat注解来接收前端传值与日期字段绑定,加上@JsonFormat注

  • C++ 中的虚函数表及虚函数执行原理详解

    为了实现虚函数,C++ 使用了虚函数表来达到延迟绑定的目的.虚函数表在动态/延迟绑定行为中用于查询调用的函数. 尽管要描述清楚虚函数表的机制会多费点口舌,但其实其本身还是比较简单的. 首先,每个包含虚函数的类(或者继承自的类包含了虚函数)都有一个自己的虚函数表.这个表是一个在编译时确定的静态数组.虚函数表包含了指向每个虚函数的函数指针以供类对象调用. 其次,编译器还在基类中定义了一个隐藏指针,我们称为 *__vptr,*__vptr 是在类实例创建时自动设置的,以指向类的虚函数表.*__vptr

  • Vuex中actions的使用教程详解

    目录 简介 说明 官网 actions概述 说明 特点 用法 示例 测试 简介 说明 本文用示例介绍Vuex的五大核心之一:actions. 官网 Action | Vuex API 参考 | Vuex actions概述 说明 Vuex 中的 mutation 非常类似于事件:每个 mutation 都有一个字符串的 事件类型 (type) 和 一个 回调函数 (handler).这个回调函数就是我们实际进行状态更改的地方,并且它会接受 state 作为第一个参数. 特点 1.异步操作,通过m

  • Python快速从视频中提取视频帧的方法详解

    目录 1.抽取视频帧 2.多线程方法 3.整体代码 补充 Python快速提取视频帧(多线程) 今天介绍一种从视频中抽取视频帧的方法,由于单线程抽取视频帧速度较慢,因此这里我们增加了多线程的方法. 1.抽取视频帧 抽取视频帧主要使用了 Opencv 模块. 其中: camera = cv2.Videocapture( ) ,函数主要是通过调用笔记本内置摄像头读取视频帧: res, image = camera.read( ) 函数主要是按帧读取视频,返回值 “res” 是布尔型,成功读取返回 T

  • Angular中使用$watch监听object属性值的变化(详解)

    Angular中的$watch可以监听属性值的变化,然后并做出相应处理. 常见用法: $scope.$watch("person", function(n, o){ //todo something... }) 但是对于一个对象中的某个属性值变化时,$watch似乎不管用了. 示例代码: <body> <div ng-controller="mainCtrl"> <input id="myText" type=&qu

  • 在docker中部署tomcat并且部署java应用程序的步骤详解

    先给大家简单说下Docker的概念 Docker 是一个开源的应用容器引擎,让开发者可以打包他们的应用以及依赖包到一个可移植的容器中,然后发布到任何流行的 Linux 机器上,也可以实现虚拟化.容器是完全使用沙箱机制,相互之间不会有任何接口. 1.先说如何在docker中部署tomcat 第一步:root用户登录在系统根目录下创建文件夹tomcat7,命令如:mkdir tomcat7,并且切换到该目录下:cd tomcat7: 第二步:创建Dockerfile,命令如:touch Docker

随机推荐