教你用 C++ 在 Unreal 中为游戏增加实时音视频互动

Posted 声网

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了教你用 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();


  ...
;
以上是关于教你用 C++ 在 Unreal 中为游戏增加实时音视频互动的主要内容,如果未能解决你的问题,请参考以下文章

手把手教你用python写游戏

90行代码写一个游戏?教你用90行HasKell代码实现2048游戏

今天大佬教你用Python3-OpenCV实现实时摄像头人脸检测

一步步教你用Prometheus搭建实时监控系统系列——详细分析拉取和推送两种不同模式

虚幻引擎(Unreal Engine)4.13 版正式发布

教你用matlab制作一款黄金矿工小游戏