在这个教程里,我们将学习如何使用C++为Unreal引擎开发一个视频聊天插件,我们将使用Unreal 4。25和最新版的Agora SDK。

1、创建项目

现在,让我们从头开始构建一个Unreal项目!

  1. 打开虚幻编辑器,然后单击新建项目。
  2. 在"新建项目"面板上,选择"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类,我们需要:

  1. 创建 VideoFrameObserver 类接口
  2. 重写 onCaptureVideoFrame 和 onRenderVideoFrame 方法
  3. 添加 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。如果 bEnabletrue,则使用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。如果本地视频静音或取消静音成功,请设置bLocalVideoMutedbMuted

//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。如果本地音频静音或取消静音成功,请设置bLocalAudioMutedbMuted

//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 文件夹添加到构建中

  1. 返回到项目目录并打开插件文件夹。
  2. 从 Plugins/AgoraPlugin/Source/ThirdParty/Agora/Mac/Release 中,复制 AgoraRtcKit.framework 文件。
  3. 将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翻译整理,转载请标明出处