您好,登錄后才能下訂單哦!
小編這次要給大家分享的是C++在Unreal中如何為游戲增加實時音視頻互動,文章內容豐富,感興趣的小伙伴可以來了解一下,希望大家閱讀完這篇文章之后能夠有所收獲。
我們已經上線了 Agora Unreal SDK,提供了支持 Blueprint 和 C++ 的兩個版本 SDK。我們分享了如何基于 Blueprint 在游戲中創建實時音視頻功能 。在本文中,我們來分享如何基于聲網 Agora Unreal SDK C++版本,在游戲中實現實時音視頻功能。
本篇教程較長,建議在 Web 瀏覽器端瀏覽,體驗更好。
準備工作
需要的開發環境和需要準備的與 Blueprint 一樣:
新建項目
如果你已經有 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 對象,然后根據你的場景來設置以下回調。
在 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
為了能夠將我們的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>
添加成員變量
FUpdateTextureRegion2D:指定一個紋理的更新區域 刷子 - 一個包含如何繪制Slate元素的筆刷。我們將用它來繪制RenderTargetImage上的RenderTargetTexture。
//VideoViewWidget.h ... UCLASS() class AGORAVIDEOCALL_API UVideoViewWidget : public UUserWidget { GENERATED_BODY() public: UPROPERTY(BlueprintReadOnly, meta = (BindWidget)) UImage* RenderTargetImage = nullptr; UPROPERTY(EditDefaultsOnly) UTexture2D* RenderTargetTexture = nullptr; UTexture2D* CameraoffTexture = nullptr; uint8* Buffer = nullptr; uint32_t Width = 0; uint32_t Height = 0; uint32 BufferSize = 0; FUpdateTextureRegion2D* UpdateTextureRegion = nullptr; FSlateBrush Brush; FCriticalSection Mutex; ... };
覆蓋 NativeConstruct() 方法
在NativeConstruct中,我們將用默認顏色初始化我們的圖像。為了初始化我們的RenderTargetTexture,我們需要使用CreateTransient調用創建動態紋理(Texture2D)。然后分配BufferSize為Width * Height * 4的BufferSize(用于存儲RGBA格式,每個像素可以用4個字節表示)。為了更新我們的紋理,我們可以使用UpdateTextureRegions函數。這個函數的輸入參數之一是我們的像素數據緩沖區。這樣,每當我們修改像素數據緩沖區時,我們就需要調用這個函數來使變化在紋理中可見。現在用我們的RenderTargetTexture初始化Brush變量,然后在RenderTargetImage widget中設置這個Brush。
//VideoViewWidget.h ... UCLASS() class AGORAVIDEOCALL_API UVideoViewWidget : public UUserWidget { GENERATED_BODY() public: ... void NativeConstruct() override; ... }; //VideoViewWidget.cpp void UVideoViewWidget::NativeConstruct() { Super::NativeConstruct(); Width = 640; Height = 360; RenderTargetTexture = UTexture2D::CreateTransient(Width, Height, PF_R8G8B8A8); RenderTargetTexture->UpdateResource(); BufferSize = Width * Height * 4; Buffer = new uint8[BufferSize]; for (uint32 i = 0; i < Width * Height; ++i) { Buffer[i * 4 + 0] = 0x32; Buffer[i * 4 + 1] = 0x32; Buffer[i * 4 + 2] = 0x32; Buffer[i * 4 + 3] = 0xFF; } UpdateTextureRegion = new FUpdateTextureRegion2D(0, 0, 0, 0, Width, Height); RenderTargetTexture->UpdateTextureRegions(0, 1, UpdateTextureRegion, Width * 4, (uint32)4, Buffer); Brush.SetResourceObject(RenderTargetTexture); RenderTargetImage->SetBrush(Brush); }
覆蓋 NativeDestruct() 方法
//VideoViewWidget.h ... UCLASS() class AGORAVIDEOCALL_API UVideoViewWidget : public UUserWidget { GENERATED_BODY() public: ... void NativeDestruct() override; ... }; //VideoViewWidget.cpp void UVideoViewWidget::NativeDestruct() { Super::NativeDestruct(); delete[] Buffer; delete UpdateTextureRegion; }
覆蓋 NativeTick() 方法
如果UpdateTextureRegion Width或Height不等于memember的Width Height值,我們需要重新創建RenderTargetTexture以支持更新的值,并像Native Construct成員一樣重復初始化。否則只需用Buffer調用UpdateTextureRegions。
//VideoViewWidget.h ... UCLASS() class AGORAVIDEOCALL_API UVideoViewWidget : public UUserWidget { GENERATED_BODY() public: ... void NativeTick(const FGeometry& MyGeometry, float DeltaTime) override; ... }; //VideoViewWidget.cpp void UVideoViewWidget::NativeTick(const FGeometry& MyGeometry, float DeltaTime) { Super::NativeTick(MyGeometry, DeltaTime); FScopeLock lock(&Mutex); if (UpdateTextureRegion->Width != Width || UpdateTextureRegion->Height != Height) { auto NewUpdateTextureRegion = new FUpdateTextureRegion2D(0, 0, 0, 0, Width, Height); auto NewRenderTargetTexture = UTexture2D::CreateTransient(Width, Height, PF_R8G8B8A8); NewRenderTargetTexture->UpdateResource(); NewRenderTargetTexture->UpdateTextureRegions(0, 1, NewUpdateTextureRegion, Width * 4, (uint32)4, Buffer); Brush.SetResourceObject(NewRenderTargetTexture); RenderTargetImage->SetBrush(Brush); //UClass's such as UTexture2D are automatically garbage collected when there is no hard pointer references made to that object. //So if you just leave it and don't reference it elsewhere then it will be destroyed automatically. FUpdateTextureRegion2D* TmpUpdateTextureRegion = UpdateTextureRegion; RenderTargetTexture = NewRenderTargetTexture; UpdateTextureRegion = NewUpdateTextureRegion; delete TmpUpdateTextureRegion; return; } RenderTargetTexture->UpdateTextureRegions(0, 1, UpdateTextureRegion, Width * 4, (uint32)4, Buffer); }
增加 UpdateBuffer() 方法
通過調用來更新 Buffer 值。我們希望從 Agora SDK 線程接收到新的值。由于 UE4 的限制,我們將值保存到變量 Buffer 中,并在 NativeTick 方法中更新紋理,所以這里不調用UpdateTextureRegions。
//VideoViewWidget.h ... UCLASS() class AGORAVIDEOCALL_API UVideoViewWidget : public UUserWidget { GENERATED_BODY() public: ... void UpdateBuffer( uint8* RGBBuffer, uint32_t Width, uint32_t Height, uint32_t Size ); void ResetBuffer(); ... }; //VideoViewWidget.cpp void UVideoViewWidget::UpdateBuffer( uint8* RGBBuffer, uint32_t NewWidth, uint32_t NewHeight, uint32_t NewSize) { FScopeLock lock(&Mutex); if (!RGBBuffer) { return; } if (BufferSize == NewSize) { std::copy(RGBBuffer, RGBBuffer + NewSize, Buffer); } else { delete[] Buffer; BufferSize = NewSize; Width = NewWidth; Height = NewHeight; Buffer = new uint8[BufferSize]; std::copy(RGBBuffer, RGBBuffer + NewSize, Buffer); } } void UVideoViewWidget::ResetBuffer() { for (uint32 i = 0; i < Width * Height; ++i) { Buffer[i * 4 + 0] = 0x32; Buffer[i * 4 + 1] = 0x32; Buffer[i * 4 + 2] = 0x32; Buffer[i * 4 + 3] = 0xFF; } }
創建 VideoCallViewWidget C++類
VideoCallViewWidget 類的作用是顯示本地和遠程用戶的視頻。我們需要兩個 VideoViewWidget 小部件,一個用來顯示來自本地攝像頭的視頻,另一個用來顯示從遠程用戶收到的視頻(假設我們只支持一個遠程用戶)。
創建類和添加所需的 include
像之前那樣創建Widget C++類,添加所需的include。
//VideoCallViewWidget.h #include "CoreMinimal.h" #include "Blueprint/UserWidget.h" #include "Components/SizeBox.h" #include "VideoViewWidget.h" #include "VideoCallViewWidget.generated.h" //VideoCallViewWidget.cpp #include "Components/CanvasPanelSlot.h"
添加成員變量
//VideoCallViewWidget.h ... UCLASS() class AGORAVIDEOCALL_API UVideoCallViewWidget : public UUserWidget { GENERATED_BODY() public: UPROPERTY(BlueprintReadOnly, meta = (BindWidget)) UVideoViewWidget* MainVideoViewWidget = nullptr; UPROPERTY(BlueprintReadOnly, meta = (BindWidget)) USizeBox* MainVideoSizeBox = nullptr; UPROPERTY(BlueprintReadOnly, meta = (BindWidget)) UVideoViewWidget* AdditionalVideoViewWidget = nullptr; UPROPERTY(BlueprintReadOnly, meta = (BindWidget)) USizeBox* AdditionalVideoSizeBox = nullptr; public: int32 MainVideoWidth = 0; int32 MainVideoHeight = 0; ... };
覆蓋 NativeTick() 方法
`` //VideoCallViewWidget.h ... UCLASS() class AGORAVIDEOCALL_API UVideoCallViewWidget : public UUserWidget { GENERATED_BODY() public: ... void NativeTick(const FGeometry& MyGeometry, float DeltaTime) override; ... }; //VideoCallViewWidget.cpp void UVideoCallViewWidget::NativeTick(const FGeometry& MyGeometry, float DeltaTime) { Super::NativeTick(MyGeometry, DeltaTime); auto ScreenSize = MyGeometry.GetLocalSize(); if (MainVideoHeight != 0) { float AspectRatio = 0; AspectRatio = MainVideoWidth / (float)MainVideoHeight; auto MainVideoGeometry = MainVideoViewWidget->GetCachedGeometry(); auto MainVideoScreenSize = MainVideoGeometry.GetLocalSize(); if (MainVideoScreenSize.X == 0) { return; } auto NewMainVideoHeight = MainVideoScreenSize.Y; auto NewMainVideoWidth = AspectRatio * NewMainVideoHeight; MainVideoSizeBox->SetMinDesiredWidth(NewMainVideoWidth); MainVideoSizeBox->SetMinDesiredHeight(NewMainVideoHeight); UCanvasPanelSlot* CanvasSlot = Cast<UCanvasPanelSlot>(MainVideoSizeBox->Slot); CanvasSlot->SetAutoSize(true); FVector2D NewPosition; NewPosition.X = -NewMainVideoWidth / 2; NewPosition.Y = -NewMainVideoHeight / 2; CanvasSlot->SetPosition(NewPosition); } }
更新 UpdateMainVideoBuffer/UpdateAdditionalVideoBuffe
//VideoCallViewWidget.h ... UCLASS() class AGORAVIDEOCALL_API UVideoCallViewWidget : public UUserWidget { GENERATED_BODY() public: ... void UpdateMainVideoBuffer( uint8* RGBBuffer, uint32_t Width, uint32_t Height, uint32_t Size); void UpdateAdditionalVideoBuffer( uint8* RGBBuffer, uint32_t Width, uint32_t Height, uint32_t Size); void ResetBuffers(); ... }; //VideoCallViewWidget.cpp void UVideoCallViewWidget::UpdateMainVideoBuffer( uint8* RGBBuffer, uint32_t Width, uint32_t Height, uint32_t Size) { if (!MainVideoViewWidget) { return; } MainVideoWidth = Width; MainVideoHeight = Height; MainVideoViewWidget->UpdateBuffer(RGBBuffer, Width, Height, Size); } void UVideoCallViewWidget::UpdateAdditionalVideoBuffer( uint8* RGBBuffer, uint32_t Width, uint32_t Height, uint32_t Size) { if (!AdditionalVideoViewWidget) { return; } AdditionalVideoViewWidget->UpdateBuffer(RGBBuffer, Width, Height, Size); } void UVideoCallViewWidget::ResetBuffers() { if (!MainVideoViewWidget || !AdditionalVideoViewWidget) { return; } MainVideoViewWidget->ResetBuffer(); AdditionalVideoViewWidget->ResetBuffer(); }
創建 VideoCallWidget C++ 類
VideoCallWidget 類作為示例應用程序的音頻/視頻調用小部件。它包含以下控件,與藍圖資產中的UI元素綁定。
創建類和添加所需的include
像之前那樣創建Widget C++類,添加必要的include和轉發聲明。
//VideoCallWidget.h #include "CoreMinimal.h" #include "Blueprint/UserWidget.h" #include "Templates/UniquePtr.h" #include "Components/Image.h" #include "Components/Button.h" #include "Engine/Texture2D.h" #include "VideoCall.h" #include "VideoCallViewWidget.h" #include "VideoCallWidget.generated.h" class AVideoCallPlayerController; class UVideoViewWidget; //VideoCallWidget.cpp #include "Kismet/GameplayStatics.h" #include "UObject/ConstructorHelpers.h" #include "Components/CanvasPanelSlot.h" #include "VideoViewWidget.h" #include "VideoCallPlayerController.h"
增加成員變量
//VideoCallWidget.h ... UCLASS() class AGORAVIDEOCALL_API UVideoCallWidget : public UUserWidget { GENERATED_BODY() public: AVideoCallPlayerController* PlayerController = nullptr; public: UPROPERTY(BlueprintReadOnly, meta = (BindWidget)) UVideoCallViewWidget* VideoCallViewWidget = nullptr; //Buttons UPROPERTY(BlueprintReadOnly, meta = (BindWidget)) UButton* EndCallButton = nullptr; UPROPERTY(BlueprintReadOnly, meta = (BindWidget)) UButton* MuteLocalAudioButton = nullptr; UPROPERTY(BlueprintReadOnly, meta = (BindWidget)) UButton* VideoModeButton = nullptr; //Button textures int32 ButtonSizeX = 96; int32 ButtonSizeY = 96; UTexture2D* EndCallButtonTexture = nullptr; UTexture2D* AudioButtonMuteTexture = nullptr; UTexture2D* AudioButtonUnmuteTexture = nullptr; UTexture2D* VideomodeButtonCameraoffTexture = nullptr; UTexture2D* VideomodeButtonCameraonTexture = nullptr; TUniquePtr<VideoCall> VideoCallPtr; ... };
初始化VideoCallWidget
為每個按鈕找到asset圖像,并將其分配到相應的紋理。然后用紋理初始化每個按鈕。
//VideoCallWidget.h ... UCLASS() class AGORAVIDEOCALL_API UVideoCallWidget : public UUserWidget { GENERATED_BODY() public: .. UVideoCallWidget(const FObjectInitializer& ObjectInitializer); void NativeConstruct() override; void NativeDestruct() override; private: void InitButtons(); ... }; //VideoCallWidget.cpp void UVideoCallWidget::NativeConstruct() { Super::NativeConstruct(); InitButtons(); } void UVideoCallWidget::NativeDestruct() { Super::NativeDestruct(); if (VideoCallPtr) { VideoCallPtr->StopCall(); } } UVideoCallWidget::UVideoCallWidget(const FObjectInitializer& ObjectInitializer) : Super(ObjectInitializer) { static ConstructorHelpers::FObjectFinder<UTexture2D> EndCallButtonTextureFinder(TEXT("Texture'/Game/ButtonTextures/hangup.hangup'")); if (EndCallButtonTextureFinder.Succeeded()) { EndCallButtonTexture = EndCallButtonTextureFinder.Object; } static ConstructorHelpers::FObjectFinder<UTexture2D> AudioButtonMuteTextureFinder(TEXT("Texture'/Game/ButtonTextures/mute.mute'")); if (AudioButtonMuteTextureFinder.Succeeded()) { AudioButtonMuteTexture = AudioButtonMuteTextureFinder.Object; } static ConstructorHelpers::FObjectFinder<UTexture2D> AudioButtonUnmuteTextureFinder(TEXT("Texture'/Game/ButtonTextures/unmute.unmute'")); if (AudioButtonUnmuteTextureFinder.Succeeded()) { AudioButtonUnmuteTexture = AudioButtonUnmuteTextureFinder.Object; } static ConstructorHelpers::FObjectFinder<UTexture2D> VideomodeButtonCameraonTextureFinder(TEXT("Texture'/Game/ButtonTextures/cameraon.cameraon'")); if (VideomodeButtonCameraonTextureFinder.Succeeded()) { VideomodeButtonCameraonTexture = VideomodeButtonCameraonTextureFinder.Object; } static ConstructorHelpers::FObjectFinder<UTexture2D> VideomodeButtonCameraoffTextureFinder(TEXT("Texture'/Game/ButtonTextures/cameraoff.cameraoff'")); if (VideomodeButtonCameraoffTextureFinder.Succeeded()) { VideomodeButtonCameraoffTexture = VideomodeButtonCameraoffTextureFinder.Object; } } void UVideoCallWidget::InitButtons() { if (EndCallButtonTexture) { EndCallButton->WidgetStyle.Normal.SetResourceObject(EndCallButtonTexture); EndCallButton->WidgetStyle.Normal.ImageSize = FVector2D(ButtonSizeX, ButtonSizeY); EndCallButton->WidgetStyle.Normal.DrawAs = ESlateBrushDrawType::Type::Image; EndCallButton->WidgetStyle.Hovered.SetResourceObject(EndCallButtonTexture); EndCallButton->WidgetStyle.Hovered.ImageSize = FVector2D(ButtonSizeX, ButtonSizeY); EndCallButton->WidgetStyle.Hovered.DrawAs = ESlateBrushDrawType::Type::Image; EndCallButton->WidgetStyle.Pressed.SetResourceObject(EndCallButtonTexture); EndCallButton->WidgetStyle.Pressed.ImageSize = FVector2D(ButtonSizeX, ButtonSizeY); EndCallButton->WidgetStyle.Pressed.DrawAs = ESlateBrushDrawType::Type::Image; } EndCallButton->OnClicked.AddDynamic(this, &UVideoCallWidget::OnEndCall); SetAudioButtonToMute(); MuteLocalAudioButton->OnClicked.AddDynamic(this, &UVideoCallWidget::OnMuteLocalAudio); SetVideoModeButtonToCameraOff(); VideoModeButton->OnClicked.AddDynamic(this, &UVideoCallWidget::OnChangeVideoMode); }
添加按鈕紋理
在演示程序中找到目錄Content/ButtonTextures(你不必打開項目,只需在文件系統中找到這個文件夾即可)。所有的按鈕紋理都存儲在那里。在你的項目內容中創建一個名為ButtonTextures的新目錄,將所有的按鈕圖片拖放到那里,讓它們在你的項目中可用。
添加Setters
//VideoCallWidget.h ... UCLASS() class AGORAVIDEOCALL_API UVideoCallWidget : public UUserWidget { GENERATED_BODY() ... public: void SetVideoCallPlayerController(AVideoCallPlayerController* VideoCallPlayerController); void SetVideoCall(TUniquePtr<VideoCall> PassedVideoCallPtr); ... }; //VideoCallWidget.cpp void UVideoCallWidget::SetVideoCallPlayerController(AVideoCallPlayerController* VideoCallPlayerController) { PlayerController = VideoCallPlayerController; } void UVideoCallWidget::SetVideoCall(TUniquePtr<VideoCall> PassedVideoCallPtr) { VideoCallPtr = std::move(PassedVideoCallPtr); }
增加用來更新 view 的方法
//VideoCallWidget.h ... UCLASS() class AGORAVIDEOCALL_API UVideoCallWidget : public UUserWidget { GENERATED_BODY() ... private: void SetVideoModeButtonToCameraOff(); void SetVideoModeButtonToCameraOn(); void SetAudioButtonToMute(); void SetAudioButtonToUnMute(); ... }; //VideoCallWidget.cpp void UVideoCallWidget::SetVideoModeButtonToCameraOff() { if (VideomodeButtonCameraoffTexture) { VideoModeButton->WidgetStyle.Normal.SetResourceObject(VideomodeButtonCameraoffTexture); VideoModeButton->WidgetStyle.Normal.ImageSize = FVector2D(ButtonSizeX, ButtonSizeY); VideoModeButton->WidgetStyle.Normal.DrawAs = ESlateBrushDrawType::Type::Image; VideoModeButton->WidgetStyle.Hovered.SetResourceObject(VideomodeButtonCameraoffTexture); VideoModeButton->WidgetStyle.Hovered.ImageSize = FVector2D(ButtonSizeX, ButtonSizeY); VideoModeButton->WidgetStyle.Hovered.DrawAs = ESlateBrushDrawType::Type::Image; VideoModeButton->WidgetStyle.Pressed.SetResourceObject(VideomodeButtonCameraoffTexture); VideoModeButton->WidgetStyle.Pressed.ImageSize = FVector2D(ButtonSizeX, ButtonSizeY); VideoModeButton->WidgetStyle.Pressed.DrawAs = ESlateBrushDrawType::Type::Image; } } void UVideoCallWidget::SetVideoModeButtonToCameraOn() { if (VideomodeButtonCameraonTexture) { VideoModeButton->WidgetStyle.Normal.SetResourceObject(VideomodeButtonCameraonTexture); VideoModeButton->WidgetStyle.Normal.ImageSize = FVector2D(ButtonSizeX, ButtonSizeY); VideoModeButton->WidgetStyle.Normal.DrawAs = ESlateBrushDrawType::Type::Image; VideoModeButton->WidgetStyle.Hovered.SetResourceObject(VideomodeButtonCameraonTexture); VideoModeButton->WidgetStyle.Hovered.ImageSize = FVector2D(ButtonSizeX, ButtonSizeY); VideoModeButton->WidgetStyle.Hovered.DrawAs = ESlateBrushDrawType::Type::Image; VideoModeButton->WidgetStyle.Pressed.SetResourceObject(VideomodeButtonCameraonTexture); VideoModeButton->WidgetStyle.Pressed.ImageSize = FVector2D(ButtonSizeX, ButtonSizeY); VideoModeButton->WidgetStyle.Pressed.DrawAs = ESlateBrushDrawType::Type::Image; } } void UVideoCallWidget::SetAudioButtonToMute() { if (AudioButtonMuteTexture) { MuteLocalAudioButton->WidgetStyle.Normal.SetResourceObject(AudioButtonMuteTexture); MuteLocalAudioButton->WidgetStyle.Normal.ImageSize = FVector2D(ButtonSizeX, ButtonSizeY); MuteLocalAudioButton->WidgetStyle.Normal.DrawAs = ESlateBrushDrawType::Type::Image; MuteLocalAudioButton->WidgetStyle.Hovered.SetResourceObject(AudioButtonMuteTexture); MuteLocalAudioButton->WidgetStyle.Hovered.ImageSize = FVector2D(ButtonSizeX, ButtonSizeY); MuteLocalAudioButton->WidgetStyle.Hovered.DrawAs = ESlateBrushDrawType::Type::Image; MuteLocalAudioButton->WidgetStyle.Pressed.SetResourceObject(AudioButtonMuteTexture); MuteLocalAudioButton->WidgetStyle.Pressed.ImageSize = FVector2D(ButtonSizeX, ButtonSizeY); MuteLocalAudioButton->WidgetStyle.Pressed.DrawAs = ESlateBrushDrawType::Type::Image; } } void UVideoCallWidget::SetAudioButtonToUnMute() { if (AudioButtonUnmuteTexture) { MuteLocalAudioButton->WidgetStyle.Normal.SetResourceObject(AudioButtonUnmuteTexture); MuteLocalAudioButton->WidgetStyle.Normal.ImageSize = FVector2D(ButtonSizeX, ButtonSizeY); MuteLocalAudioButton->WidgetStyle.Normal.DrawAs = ESlateBrushDrawType::Type::Image; MuteLocalAudioButton->WidgetStyle.Hovered.SetResourceObject(AudioButtonUnmuteTexture); MuteLocalAudioButton->WidgetStyle.Hovered.ImageSize = FVector2D(ButtonSizeX, ButtonSizeY); MuteLocalAudioButton->WidgetStyle.Hovered.DrawAs = ESlateBrushDrawType::Type::Image; MuteLocalAudioButton->WidgetStyle.Pressed.SetResourceObject(AudioButtonUnmuteTexture); MuteLocalAudioButton->WidgetStyle.Pressed.ImageSize = FVector2D(ButtonSizeX, ButtonSizeY); MuteLocalAudioButton->WidgetStyle.Pressed.DrawAs = ESlateBrushDrawType::Type::Image; } }
增加 OnStartCall 方法
//VideoCallWidget.h ... UCLASS() class AGORAVIDEOCALL_API UVideoCallWidget : public UUserWidget { GENERATED_BODY() public: ... void OnStartCall( const FString& ChannelName, const FString& EncryptionKey, const FString& EncryptionType ); ... }; //VideoCallWidget.cpp void UVideoCallWidget::OnStartCall( const FString& ChannelName, const FString& EncryptionKey, const FString& EncryptionType) { if (!VideoCallPtr) { return; } auto OnLocalFrameCallback = [this]( std::uint8_t* Buffer, std::uint32_t Width, std::uint32_t Height, std::uint32_t Size) { VideoCallViewWidget->UpdateAdditionalVideoBuffer(Buffer, Width, Height, Size); }; VideoCallPtr->RegisterOnLocalFrameCallback(OnLocalFrameCallback); auto OnRemoteFrameCallback = [this]( std::uint8_t* Buffer, std::uint32_t Width, std::uint32_t Height, std::uint32_t Size) { VideoCallViewWidget->UpdateMainVideoBuffer(Buffer, Width, Height, Size); }; VideoCallPtr->RegisterOnRemoteFrameCallback(OnRemoteFrameCallback); VideoCallPtr->StartCall(ChannelName, EncryptionKey, EncryptionType); }
增加 OnEndCall方法
//VideoCallWidget.h ... UCLASS() class AGORAVIDEOCALL_API UVideoCallWidget : public UUserWidget { GENERATED_BODY() public: ... UFUNCTION(BlueprintCallable) void OnEndCall(); ... }; //VideoCallWidget.cpp void UVideoCallWidget::OnEndCall() { if (VideoCallPtr) { VideoCallPtr->StopCall(); } if (VideoCallViewWidget) { VideoCallViewWidget->ResetBuffers(); } if (PlayerController) { SetVisibility(ESlateVisibility::Collapsed); PlayerController->EndCall(std::move(VideoCallPtr)); } }
增加 OnMuteLocalAudio 方法
//VideoCallWidget.h ... UCLASS() class AGORAVIDEOCALL_API UVideoCallWidget : public UUserWidget { GENERATED_BODY() public: ... UFUNCTION(BlueprintCallable) void OnMuteLocalAudio(); ... }; //VideoCallWidget.cpp void UVideoCallWidget::OnMuteLocalAudio() { if (!VideoCallPtr) { return; } if (VideoCallPtr->IsLocalAudioMuted()) { VideoCallPtr->MuteLocalAudio(false); SetAudioButtonToMute(); } else { VideoCallPtr->MuteLocalAudio(true); SetAudioButtonToUnMute(); } }
增加 OnChangeVideoMode方法
//VideoCallWidget.h ... UCLASS() class AGORAVIDEOCALL_API UVideoCallWidget : public UUserWidget { GENERATED_BODY() public: ... UFUNCTION(BlueprintCallable) void OnChangeVideoMode(); ... }; //VideoCallWidget.cpp void UVideoCallWidget::OnChangeVideoMode() { if (!VideoCallPtr) { return; } if (!VideoCallPtr->IsLocalVideoMuted()) { VideoCallPtr->MuteLocalVideo(true); SetVideoModeButtonToCameraOn(); } else { VideoCallPtr->EnableVideo(true); VideoCallPtr->MuteLocalVideo(false); SetVideoModeButtonToCameraOff(); } }
增加 Blueprint 類
確保C++代碼正確編譯。沒有成功編譯的項目,你將無法進行下一步的操作。如果你已經成功地編譯了C++代碼,但在虛幻編輯器中仍然沒有看到所需的類,請嘗試重新打開項目。
創建 BP_EnterChannelWidget Blueprint Asset。
創建一個 UEnterChannelWidget 的 Blueprint,右鍵點擊內容,選擇用戶界面菜單并選擇 Widget Blueprint。
更改這個新的用戶小工具的類的父類。打開 Blueprint,會出現 UMG 編輯器界面,默認情況下 Designer 選項卡是打開的。點擊圖形按鈕(右上角按鈕),選擇 "類設置"。在面板 "Details "中,點擊下拉列表 "父類",選擇之前創建的C++ 類 UEnterChannelWidget。現在返回到設計頁面。調色板窗口包含幾種不同類型的小部件,你可以用它們來構造你的 UI 元素。找到 Text、Editable Text、Button 和 ComboBox(String)元素,然后將它們拖到工作區,如圖中所示。然后進入 "EnterChannelWidget.h "文件中的 UEnterChannelWidget 的定義,查看成員變量的名稱和對應的類型(UTextBlock、EditableTextBox、UButton和UComboBoxString)。返回到 BP_VideoCallWiewVidget 編輯器中,給你拖動的UI元素設置相同的名稱。你可以通過點擊元素并在 "詳細信息 "面板中更改名稱來完成。嘗試編譯藍圖。如果你忘了添加什么東西,或者在你的UserWidget類中出現了widget名稱/類型不匹配的情況,你會出現一個錯誤。
保存到文件夾中,例如 /Content/Widgets/BP_EnterChannelWidget.uasset。
創建 BP_VideoViewWidget Asset。
設定圖片的錨點
創建 BP_VideoCallViewWidget Asset
創建 BP VideoCallViewWidget Asset ,將父類設置為 UVideoCallViewWidget,并添加 BP VideoViewWidget 類型的 UI 元素MainVideoViewWidget 和ExtendedVideoViewWidget。同時添加 SizeBox 類型的 MainVideoSizeBox 和 AdditionalVideoSizeBox UI 元素。
創建 BP_VideoCallWidget Asset
創建BPVideoCallWidget Asset,將父類設置為UVideoCallWidget,在 Palette UI 元素BPVideoCallViewWidget 中找到并添加名稱為VideoCallViewWidget,添加 EndCallButton、MuteLocalAudioButton 和 VideoModeButton 按鈕。
創建 BP_VideoCallPlayerController blueprint asset
現在是創建 BPVideoCallPlayerPlayerController blueprint asset 的時候了,基于我們前面描述的 AVideoCallPlayerPlayerController 類,創建 BPVideoCallPlayerController 藍圖資產。
創建一個AVideoCallPlayerPlayerController的bluepringt。右鍵點擊內容,按Add New按鈕,選擇Blueprint類,在窗口中選擇父類,在Pick parent類進入All classes部分,找到VideoCallPlayerController類。
現在將我們之前創建的小部件分配給PlayerController,如下圖所示。
將其保存到文件夾,例如 /Content/Widgets/BP_VideoCallPlayerController.uasset。
創建 BP_AgoraVideoCallGameModeBase Asset
創建一個 AVideoCallPlayerController 的 Blueprint,右鍵點擊內容,按 Add New 按鈕,選擇 Blueprint 類,在 Pick parent class 窗口中選擇 Game Mode Base Class。這是所有游戲模式的父類。
修改 GameMode
現在你需要設置你的自定義 GameMode 類和玩家控制器。到世界設置中,設置指定的變量:
指定項目的設置
進入 Edit->Project settings,打開 Maps & Modes。設定默認參數:
看完這篇關于C++在Unreal中如何為游戲增加實時音視頻互動的文章,如果覺得文章內容寫得不錯的話,可以把它分享出去給更多人看到。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。