diff --git a/UE4-27-0/Config/DefaultEngine.ini b/UE4-27-0/Config/DefaultEngine.ini new file mode 100644 index 0000000..4f3b445 --- /dev/null +++ b/UE4-27-0/Config/DefaultEngine.ini @@ -0,0 +1,6 @@ + + +[/Script/EngineSettings.GameMapsSettings] +GameDefaultMap=/Game/Main.Main +EditorStartupMap=/Game/Main.Main + diff --git a/UE4-27-0/Content/Main.umap b/UE4-27-0/Content/Main.umap new file mode 100644 index 0000000..a691757 Binary files /dev/null and b/UE4-27-0/Content/Main.umap differ diff --git a/UE4-27-0/Content/SimpleWebSocket.uasset b/UE4-27-0/Content/SimpleWebSocket.uasset new file mode 100644 index 0000000..ad9e487 Binary files /dev/null and b/UE4-27-0/Content/SimpleWebSocket.uasset differ diff --git a/UE4-27-0/Content/Tests/JsonTest/FTEST_JsonTest.uasset b/UE4-27-0/Content/Tests/JsonTest/FTEST_JsonTest.uasset new file mode 100644 index 0000000..bca9392 Binary files /dev/null and b/UE4-27-0/Content/Tests/JsonTest/FTEST_JsonTest.uasset differ diff --git a/UE4-27-0/Content/Tests/JsonTest/FTEST_JsonTestBooleanStruct.uasset b/UE4-27-0/Content/Tests/JsonTest/FTEST_JsonTestBooleanStruct.uasset new file mode 100644 index 0000000..06a7af0 Binary files /dev/null and b/UE4-27-0/Content/Tests/JsonTest/FTEST_JsonTestBooleanStruct.uasset differ diff --git a/UE4-27-0/Content/Tests/JsonTest/FTEST_JsonTestComplexStruct.uasset b/UE4-27-0/Content/Tests/JsonTest/FTEST_JsonTestComplexStruct.uasset new file mode 100644 index 0000000..ea565f6 Binary files /dev/null and b/UE4-27-0/Content/Tests/JsonTest/FTEST_JsonTestComplexStruct.uasset differ diff --git a/UE4-27-0/Content/Tests/NewLevelSequence.uasset b/UE4-27-0/Content/Tests/NewLevelSequence.uasset new file mode 100644 index 0000000..9206fbb Binary files /dev/null and b/UE4-27-0/Content/Tests/NewLevelSequence.uasset differ diff --git a/UE4-27-0/Content/Tests/UnrealHelperTest.umap b/UE4-27-0/Content/Tests/UnrealHelperTest.umap new file mode 100644 index 0000000..699b21b Binary files /dev/null and b/UE4-27-0/Content/Tests/UnrealHelperTest.umap differ diff --git a/UE4-27-0/Content/Tests/WebSocketTest/FTEST_WebSocketTest.uasset b/UE4-27-0/Content/Tests/WebSocketTest/FTEST_WebSocketTest.uasset new file mode 100644 index 0000000..63791eb Binary files /dev/null and b/UE4-27-0/Content/Tests/WebSocketTest/FTEST_WebSocketTest.uasset differ diff --git a/UE4-27-0/Plugins/UnrealHelper/Source/UnrealHelper/Private/UnrealHelper.cpp b/UE4-27-0/Plugins/UnrealHelper/Source/UnrealHelper/Private/UnrealHelper.cpp new file mode 100644 index 0000000..4a018d3 --- /dev/null +++ b/UE4-27-0/Plugins/UnrealHelper/Source/UnrealHelper/Private/UnrealHelper.cpp @@ -0,0 +1,20 @@ +#include "UnrealHelper.h" + +#define LOCTEXT_NAMESPACE "FUnrealHelperModule" + +void FUnrealHelperModule::StartupModule() +{ + // This code will execute after your module is loaded into memory; the exact timing is specified in the .uplugin file per-module + +} + +void FUnrealHelperModule::ShutdownModule() +{ + // This function may be called during shutdown to clean up your module. For modules that support dynamic reloading, + // we call this function before unloading the module. + +} + +#undef LOCTEXT_NAMESPACE + +IMPLEMENT_MODULE(FUnrealHelperModule, UnrealHelper) \ No newline at end of file diff --git a/UE4-27-0/Plugins/UnrealHelper/Source/UnrealHelper/Private/UnrealHelperBPLibrary.cpp b/UE4-27-0/Plugins/UnrealHelper/Source/UnrealHelper/Private/UnrealHelperBPLibrary.cpp new file mode 100644 index 0000000..839186b --- /dev/null +++ b/UE4-27-0/Plugins/UnrealHelper/Source/UnrealHelper/Private/UnrealHelperBPLibrary.cpp @@ -0,0 +1,158 @@ +#include "UnrealHelperBPLibrary.h" +#include "UnrealHelper.h" +#include "IImageWrapper.h" +#include "IImageWrapperModule.h" +#include "ImageUtils.h" +#include "RenderUtils.h" +#include "Engine/World.h" +#include "HAL/IPlatformFileModule.h" +#include "IPlatformFilePak.h" +#include "AssetRegistryModule.h" +#include "Engine/StreamableManager.h" +#include "Kismet/GameplayStatics.h" +#include "Runtime/Core/Public/Serialization/ArrayReader.h" + +UUnrealHelperBPLibrary::UUnrealHelperBPLibrary(const FObjectInitializer& ObjectInitializer) +: Super(ObjectInitializer) +{ + +} + +void UUnrealHelperBPLibrary::GetStringFromGameConfig(const FString Section, const FString Key, bool& Succeeded, FString& String) +{ + if (!GConfig) + { + return; + } + + Succeeded = GConfig->GetString( + *Section, + *Key, + String, + GGameIni + ); +} + +FString UUnrealHelperBPLibrary::RunCommand(FString Cmd, FString Args, FString& StdErr) +{ + FString Result; + int32 ExitCode; + // example (on mac) + // FPlatformProcess::ExecProcess(TEXT("/usr/bin/curl"), TEXT("-XPOST localhost:4443"), &ExitCode, &Result, &StdErr); + // this may be poorly implemented in windows, running sleep instead of blocking on read. + FPlatformProcess::ExecProcess(*Cmd, *Args, &ExitCode, &Result, &StdErr); + return Result; +} + +void UUnrealHelperBPLibrary::ListFiles(FString Directory, FString FileExtension, TArray& Files) +{ + IFileManager& FileManager = IFileManager::Get(); + FPaths::NormalizeDirectoryName(Directory); + FileManager.FindFiles(Files, *Directory, *FileExtension); +} + +void UUnrealHelperBPLibrary::ExportRenderTargetAsBuffer(UTextureRenderTarget2D* RenderTarget, bool& bSuccess, TArray& Buffer) +{ + // From FImageUtils + bSuccess = false; + + if(RenderTarget->GetFormat() != PF_B8G8R8A8) + { + return; + } + + check(RenderTarget != nullptr); + + FRenderTarget* SafeRenderTarget = RenderTarget->GameThread_GetRenderTargetResource(); + + FIntPoint Size = SafeRenderTarget->GetSizeXY(); + + TArray RawData; + EPixelFormat Format = RenderTarget->GetFormat(); + int32 ImageBytes = 32 * Size.X * Size.Y; // CalculateImageBytes(Size.X, Size.Y, 0, Format); + RawData.AddUninitialized(ImageBytes); + bSuccess = SafeRenderTarget->ReadPixelsPtr((FColor*) RawData.GetData()); + + IImageWrapperModule& ImageWrapperModule = FModuleManager::Get().LoadModuleChecked(TEXT("ImageWrapper")); + + TSharedPtr PNGImageWrapper = ImageWrapperModule.CreateImageWrapper(EImageFormat::PNG); + + PNGImageWrapper->SetRaw(RawData.GetData(), RawData.GetAllocatedSize(), Size.X, Size.Y, ERGBFormat::BGRA, 8); + + Buffer = PNGImageWrapper->GetCompressed(100); + + bSuccess = true; + +} + +void UUnrealHelperBPLibrary::GetClipboardAsString(FString& String) +{ + FPlatformApplicationMisc::ClipboardPaste(String); +} + +void UUnrealHelperBPLibrary::MountPak(const FString PakFilename, const FString ProjectFolder, const FString ContentFolder) +{ + // Asset Registry + FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked("AssetRegistry"); + IAssetRegistry& AssetRegistry = AssetRegistryModule.Get(); + + // Virtual Filesystem + const TCHAR* PakTypeName = FPakPlatformFile::GetTypeName(); + + FPlatformFileManager* PlatformFileManager = &FPlatformFileManager::Get(); + IPlatformFile* PlatformFile = PlatformFileManager->FindPlatformFile(PakTypeName); + + FPakPlatformFile* PakPlatformFile = static_cast(PlatformFile); + if (PakPlatformFile == nullptr) + { + PakPlatformFile = static_cast(PlatformFileManager->GetPlatformFile(PakTypeName)); // new FPakPlatformFile(); + PakPlatformFile->Initialize(&PlatformFileManager->GetPlatformFile(), PakTypeName); + PakPlatformFile->InitializeNewAsyncIO(); + PlatformFileManager->SetPlatformFile(*PakPlatformFile); + } + + // Mount Pak at /GRACE// then link appropriately to /Game// + // if(PakFile->GetIsMounted()) UnRegisterMountPoint + + FString StandardFilename(PakFilename); + FPaths::MakeStandardFilename(StandardFilename); + FString BaseFilename = FPaths::GetBaseFilename(StandardFilename); + FString MountPoint = FString::Format(TEXT("/GRACE/{0}/"), {BaseFilename}); + FString IgnoreEngine = FString::Format(TEXT("{0}Engine/"), {MountPoint}); + FString OptionalProjectFolder = ProjectFolder.IsEmpty() ? ProjectFolder : FString::Format(TEXT("{0}/"), {ProjectFolder}); + FString PakContentFolder = FString::Format(TEXT("/{0}Content/{1}/"), {OptionalProjectFolder, ContentFolder}); + FString GraceContentMount = FString::Format(TEXT("{0}{1}Content/{2}/"), {MountPoint, OptionalProjectFolder, ContentFolder}); + FString GameContentFolder = FString::Format(TEXT("/Game/{0}/"), {ContentFolder}); + + if (!FPaths::FileExists(*StandardFilename)) + { + UE_LOG(LogTemp, Warning, TEXT("Could not find pak file: %s"), *StandardFilename); + return; + } + + FPakFile* PakFile = new FPakFile(PakPlatformFile, *StandardFilename, false); + if (!PakFile->IsValid()) + { + UE_LOG(LogTemp, Warning, TEXT("Invalid pak file: %s"), *StandardFilename); + return; + } + + PakFile->SetMountPoint(*MountPoint); + + if(!PakFile->DirectoryExistsInPruned(*GraceContentMount)) + { + UE_LOG(LogTemp, Warning, TEXT("Content folder not found in Pak File: %s"), *GraceContentMount); + return; + } + + if (!PakPlatformFile->Mount(*PakFilename, 0, *MountPoint)) + { + UE_LOG(LogTemp, Warning, TEXT("Failed to mount: %s"), *MountPoint); + return; + } + + FPackageName::RegisterMountPoint(GameContentFolder, GraceContentMount); + TArray< FString > ContentPaths; + ContentPaths.Add(GameContentFolder); + AssetRegistry.ScanPathsSynchronous(ContentPaths); +} \ No newline at end of file diff --git a/UE4-27-0/Plugins/UnrealHelper/Source/UnrealHelper/Public/UnrealHelper.h b/UE4-27-0/Plugins/UnrealHelper/Source/UnrealHelper/Public/UnrealHelper.h new file mode 100644 index 0000000..5020def --- /dev/null +++ b/UE4-27-0/Plugins/UnrealHelper/Source/UnrealHelper/Public/UnrealHelper.h @@ -0,0 +1,12 @@ +#pragma once + +#include "Modules/ModuleManager.h" + +class FUnrealHelperModule : public IModuleInterface +{ +public: + + /** IModuleInterface implementation */ + virtual void StartupModule() override; + virtual void ShutdownModule() override; +}; diff --git a/UE4-27-0/Plugins/UnrealHelper/Source/UnrealHelper/Public/UnrealHelperBPLibrary.h b/UE4-27-0/Plugins/UnrealHelper/Source/UnrealHelper/Public/UnrealHelperBPLibrary.h new file mode 100644 index 0000000..255c8cf --- /dev/null +++ b/UE4-27-0/Plugins/UnrealHelper/Source/UnrealHelper/Public/UnrealHelperBPLibrary.h @@ -0,0 +1,43 @@ +#pragma once + +#include "Kismet/BlueprintFunctionLibrary.h" +#include "Runtime/Core/Public/Misc/ConfigCacheIni.h" +#include "Engine/TextureRenderTarget2D.h" +#include "IImageWrapper.h" +#include "HAL/PlatformApplicationMisc.h" +#include "HAL/FileManager.h" +#include "UnrealHelperBPLibrary.generated.h" + +// class UTextureRenderTarget2D; + +UCLASS() +class UUnrealHelperBPLibrary : public UBlueprintFunctionLibrary +{ + GENERATED_UCLASS_BODY() + + UFUNCTION(BlueprintPure, meta = (DisplayName = "Get String from Game Config", Keywords = "Config"), Category = "Unreal Helper BP Library") + static void GetStringFromGameConfig(const FString Section, const FString Key, bool& Succeeded, FString& String); + + UFUNCTION(BlueprintPure, meta = (DisplayName = "Get Project Version from Game Config", Keywords = "Config"), Category = "Unreal Helper BP Library") + static void GetProjectVersion(FString& ProjectVersion); + + UFUNCTION(BlueprintPure, meta = (DisplayName = "Get Float as String Rounded to Precision", Keywords = "Math"), Category = "Unreal Helper BP Library") + static void GetFloatAsStringWithPrecision(float InFloat, FString& ReturnValue, int32 Precision = 2, bool IncludeLeadingZero = true); + + // Intended to block execution of rendering while it waits. + UFUNCTION(BlueprintCallable, meta = (DisplayName = "Blocking External Command", Keywords = "Shell"), Category = "Unreal Helper BP Library") + static FString RunCommand(FString Cmd, FString Params, FString& StdErr); + + UFUNCTION(BlueprintCallable, meta = (DisplayName = "List Files", Keywords = "File System"), Category = "Unreal Helper BP Library") + static void ListFiles(FString Directory, FString FileExtension, TArray& Files); + + UFUNCTION(BlueprintCallable, meta = (DisplayName = "Export Render Target as Buffer", Keywords = "Media"), Category = "Unreal Helper BP Library") + static void ExportRenderTargetAsBuffer(UTextureRenderTarget2D* RenderTarget, bool& bSuccess, TArray& Buffer); + + UFUNCTION(BlueprintCallable, meta = (DisplayName = "Get Clipboard as String", Keywords = "Clipboard"), Category = "Unreal Helper BP Library") + static void GetClipboardAsString(FString& String); + + UFUNCTION(BlueprintCallable, meta = (DisplayName = "Mount Pak", Keywords = "File System"), Category = "Unreal Helper BP Library") + static void MountPak(const FString PakFilename, const FString ProjectFolder, const FString ContentFolder); + +}; diff --git a/UE4-27-0/Plugins/UnrealHelper/Source/UnrealHelper/UnrealHelper.Build.cs b/UE4-27-0/Plugins/UnrealHelper/Source/UnrealHelper/UnrealHelper.Build.cs new file mode 100644 index 0000000..b60ec40 --- /dev/null +++ b/UE4-27-0/Plugins/UnrealHelper/Source/UnrealHelper/UnrealHelper.Build.cs @@ -0,0 +1,28 @@ +using UnrealBuildTool; + +public class UnrealHelper : ModuleRules +{ + public UnrealHelper(ReadOnlyTargetRules Target) : base(Target) + { + PCHUsage = ModuleRules.PCHUsageMode.UseExplicitOrSharedPCHs; + + PublicDependencyModuleNames.AddRange( + new string[] + { + "Core", + } + ); + + + PrivateDependencyModuleNames.AddRange( + new string[] + { + "ApplicationCore", + "CoreUObject", + "Engine", + "PakFile" + } + ); + + } +} diff --git a/UE4-27-0/Plugins/UnrealHelper/Source/UnrealHttp/Private/UnrealHttp.cpp b/UE4-27-0/Plugins/UnrealHelper/Source/UnrealHttp/Private/UnrealHttp.cpp new file mode 100644 index 0000000..81e9a49 --- /dev/null +++ b/UE4-27-0/Plugins/UnrealHelper/Source/UnrealHttp/Private/UnrealHttp.cpp @@ -0,0 +1,22 @@ +#include "UnrealHttp.h" + +#define LOCTEXT_NAMESPACE "FUnrealHttpModule" + +void FUnrealHttpModule::StartupModule() +{ + // This code will execute after your module is loaded into memory; the exact timing is specified in the .uplugin file per-module + +} + +void FUnrealHttpModule::ShutdownModule() +{ + // This function may be called during shutdown to clean up your module. For modules that support dynamic reloading, + // we call this function before unloading the module. + //UE_LOG("") + //FModuleManager::UnloadModule("WebSockets"); + +} + +#undef LOCTEXT_NAMESPACE + +IMPLEMENT_MODULE(FUnrealHttpModule, UnrealHttp) \ No newline at end of file diff --git a/UE4-27-0/Plugins/UnrealHelper/Source/UnrealHttp/Private/UnrealHttpBPLibrary.cpp b/UE4-27-0/Plugins/UnrealHelper/Source/UnrealHttp/Private/UnrealHttpBPLibrary.cpp new file mode 100644 index 0000000..d8965df --- /dev/null +++ b/UE4-27-0/Plugins/UnrealHelper/Source/UnrealHttp/Private/UnrealHttpBPLibrary.cpp @@ -0,0 +1,176 @@ +#include "UnrealHttpBPLibrary.h" +#include "UnrealHttp.h" +#include "Runtime/Online/HTTP/Public/Http.h" +#include "HAL/IPlatformFileModule.h" + +UUnrealHttpBPLibrary::UUnrealHttpBPLibrary(const FObjectInitializer& ObjectInitializer) +: Super(ObjectInitializer) +{ +} + +void UUnrealHttpBPLibrary::GetHttpFile(const FString Url, const FString Filename, const FHttpOnErrorSignature& OnError, const bool ResumeIfExisting, const int64 ChunkSizeBytes) +{ + FHttpModule& HttpModule = FModuleManager::LoadModuleChecked("HTTP"); + TSharedRef Request = HttpModule.Get().CreateRequest(); + + FString StandardFilename(Filename); + FPaths::MakeStandardFilename(StandardFilename); + + FString Directory = FPaths::GetPath(StandardFilename); + IFileManager* FileManager = &IFileManager::Get(); + + if (FileManager->DirectoryExists(*Directory) == false) + { + UE_LOG(LogTemp, Warning, TEXT("Folder does not exist: %s"), *Directory); + return; + } + + int64 StartRangeAt = 0; + if (ResumeIfExisting && FileManager->FileExists(*StandardFilename) != false) + { + int64 CurrentFileSize = FileManager->FileSize(*StandardFilename); + if(CurrentFileSize > 0) StartRangeAt = CurrentFileSize; + } + + FString BytesRangeString = FString::Printf(TEXT("%lld"), StartRangeAt); + FString ChunkSizeString = ChunkSizeBytes > 0 ? FString::Printf(TEXT("%lld"), StartRangeAt + ChunkSizeBytes - 1) : FString(""); + FString RequestRange = FString::Format(TEXT("bytes={0}-{1}"), {BytesRangeString, ChunkSizeString}); + FString ExpectRange = FString::Format(TEXT("bytes {0}-"), {BytesRangeString, ChunkSizeString}); + + Request->SetURL(Url); + Request->SetVerb("GET"); + + /* + * HTTP Range Requests are a work-around to limit memory use as stream to file is unsupported in FHttpModule. + * The Unreal curl implementation adds "Accept-Encoding: "deflate, gzip" and will transparently decompresses the buffer. + * Servers which support such headers will ignore the Range header. + */ + if(StartRangeAt > 0 || ChunkSizeBytes > 0) + { + Request->SetHeader(TEXT("Range"), RequestRange); + } + bool bAppend = StartRangeAt > 0; + bool bAllowRead = false; + + // progress indicator does not do much for us + if(false) + Request->OnRequestProgress().BindLambda([](FHttpRequestPtr HttpRequest, int32 BytesSent, int32 BytesReceived) { + FHttpResponsePtr HttpResponse = HttpRequest->GetResponse(); + if(!HttpResponse.IsValid()) { + UE_LOG(LogTemp, Warning, TEXT("Invalid HTTP Request")); + return; + } + UE_LOG(LogTemp, Warning, TEXT("Progressing: %d"), BytesReceived); + /* + * The main HTTP client implementation is hard coded to expand its write buffer to the full size of the result: + * FCurlHttpRequest::ReceiveResponseBodyCallback uses FMemory::Memcpy with TotalBytesRead to offset the write location. + * + * We can start writing the file to disk but we can not safely clear its memory buffer. + */ + // TArray Content = HttpResponse->GetContent(); + // + // bool bSuccess = OutputFile->Write(&Content[0], Content.Num()); + // UE_LOG(LogTemp, Warning, TEXT("Current position Request: %d"), Content.Num()); + // We can't clear the buffer as it is marked private + // HttpResponse.TotalBytesRead.SetValue(0); Content.Reset(); + }); + + Request->OnProcessRequestComplete().BindLambda( + [StandardFilename, bAppend, StartRangeAt, ChunkSizeBytes, ExpectRange, Url, Filename, OnError] + (FHttpRequestPtr HttpRequest, FHttpResponsePtr HttpResponse, bool bSucceeded) + { + int32 HttpCode = HttpResponse->GetResponseCode(); + UE_LOG(LogTemp, Warning, TEXT("Final HTTP Status: %d"), HttpCode); + if(!EHttpResponseCodes::IsOk(HttpCode)) { + OnError.ExecuteIfBound(); + return; + } + + FString ContentRange = HttpResponse->GetHeader("Content-Range"); + int64 FullContentLength = 0; + // Assume HttpCode 206 + if(!ContentRange.IsEmpty()) + { + int32 MaxRangeSeparator = 0; + if(ContentRange.FindChar(TEXT('/'), MaxRangeSeparator) && ContentRange.StartsWith(ExpectRange, ESearchCase::CaseSensitive)) { + FString MaxRangePart = ContentRange.RightChop(MaxRangeSeparator+1); + if(MaxRangePart.IsNumeric()) + { + FullContentLength = FCString::Atoi64(*MaxRangePart); + } + else { + OnError.ExecuteIfBound(); + return; + } + } + else { + OnError.ExecuteIfBound(); + return; + } + } + + bool bAllowRead = false; + IFileHandle *OutputFile = IPlatformFile::GetPlatformPhysical().OpenWrite(*StandardFilename, bAppend, bAllowRead); + TArray Content = HttpResponse->GetContent(); + int64 CurrentPosition = StartRangeAt + Content.Num(); + bool bWriteSuccess = OutputFile->Write(&Content[0], Content.Num()); + OutputFile->Flush(true); + delete OutputFile; + + if(!bWriteSuccess) + { + OnError.ExecuteIfBound(); + return; + } + + if(Content.Num() > ChunkSizeBytes) + { + UE_LOG(LogTemp, Warning, TEXT("Range request ignored for chunk size: %d"), ChunkSizeBytes); + } + else if(HttpCode == 206 && FullContentLength > CurrentPosition) + { + bool ContinueWritingOutput = true; + UE_LOG(LogTemp, Warning, TEXT("Ready for Sub-Request: Range %d-%d/%d"), CurrentPosition, FullContentLength); + GetHttpFile(Url, Filename, OnError, ContinueWritingOutput, ChunkSizeBytes); + } + UE_LOG(LogTemp, Warning, TEXT("Finished Request: %d"), Content.Num()); + }); + Request->ProcessRequest(); + // IFileHandler *OutputFile = IPlatformFile::GetPlatformPhysical().DeleteFile(*StandardFilename); +} + +void UUnrealHttpBPLibrary::PutHttpFile(const FString Url, const FString Filename, const FHttpOnErrorSignature& OnError) +{ + FHttpModule& HttpModule = FModuleManager::LoadModuleChecked("HTTP"); + TSharedRef Request = HttpModule.Get().CreateRequest(); + + FString StandardFilename(Filename); + FPaths::MakeStandardFilename(StandardFilename); + + FString Directory = FPaths::GetPath(StandardFilename); + IFileManager* FileManager = &IFileManager::Get(); + + if (FileManager->FileExists(*StandardFilename) == false) + { + UE_LOG(LogTemp, Warning, TEXT("File does not exist: %s"), *Filename); + return; + } + + Request->SetURL(Url); + Request->SetVerb("PUT"); + Request->SetContentAsStreamedFile(StandardFilename); + Request->OnProcessRequestComplete().BindLambda([OnError](FHttpRequestPtr HttpRequest, FHttpResponsePtr HttpResponse, bool bSucceeded) { + if (!HttpResponse.IsValid()) + { + OnError.ExecuteIfBound(); + return; + } + if (!EHttpResponseCodes::IsOk(HttpResponse->GetResponseCode())) { + UE_LOG(LogTemp, Warning, TEXT("Http returned error code: %d"), HttpResponse->GetResponseCode()); + OnError.ExecuteIfBound(); + return; + } + UE_LOG(LogTemp, Warning, TEXT("Finished Request")); + }); + Request->ProcessRequest(); +} diff --git a/UE4-27-0/Plugins/UnrealHelper/Source/UnrealHttp/Public/UnrealHttp.h b/UE4-27-0/Plugins/UnrealHelper/Source/UnrealHttp/Public/UnrealHttp.h new file mode 100644 index 0000000..6d31fa3 --- /dev/null +++ b/UE4-27-0/Plugins/UnrealHelper/Source/UnrealHttp/Public/UnrealHttp.h @@ -0,0 +1,13 @@ +#pragma once + +#include "Modules/ModuleManager.h" +// + +class FUnrealHttpModule : public IModuleInterface +{ +public: + + /** IModuleInterface implementation */ + virtual void StartupModule() override; + virtual void ShutdownModule() override; +}; diff --git a/UE4-27-0/Plugins/UnrealHelper/Source/UnrealHttp/Public/UnrealHttpBPLibrary.h b/UE4-27-0/Plugins/UnrealHelper/Source/UnrealHttp/Public/UnrealHttpBPLibrary.h new file mode 100644 index 0000000..d0f9850 --- /dev/null +++ b/UE4-27-0/Plugins/UnrealHelper/Source/UnrealHttp/Public/UnrealHttpBPLibrary.h @@ -0,0 +1,23 @@ +#pragma once + +#include "Kismet/BlueprintFunctionLibrary.h" +#include "HAL/PlatformApplicationMisc.h" +#include "HAL/FileManager.h" +#include "UnrealHttp.h" +#include "UnrealHttpBPLibrary.generated.h" + +DECLARE_DYNAMIC_DELEGATE(FHttpOnCompleteSignature); +DECLARE_DYNAMIC_DELEGATE(FHttpOnErrorSignature); + +UCLASS() +class UUnrealHttpBPLibrary : public UBlueprintFunctionLibrary +{ + GENERATED_UCLASS_BODY() + + UFUNCTION(BlueprintCallable, meta = (DisplayName = "Get Url as File", Keywords = "Http Url", AutoCreateRefTerm = "OnError"), Category = "Unreal HTTP") + static void GetHttpFile(const FString Url, const FString Filename, const FHttpOnErrorSignature& OnError, const bool Resumable = false, int64 ChunkSizeBytes = 0); + + UFUNCTION(BlueprintCallable, meta = (DisplayName = "Put File as Url", Keywords = "Http Url", AutoCreateRefTerm = "OnError"), Category = "Unreal HTTP") + static void PutHttpFile(const FString Url, const FString Filename, const FHttpOnErrorSignature& OnError); + +}; \ No newline at end of file diff --git a/UE4-27-0/Plugins/UnrealHelper/Source/UnrealHttp/UnrealHttp.Build.cs b/UE4-27-0/Plugins/UnrealHelper/Source/UnrealHttp/UnrealHttp.Build.cs new file mode 100644 index 0000000..abfc4a4 --- /dev/null +++ b/UE4-27-0/Plugins/UnrealHelper/Source/UnrealHttp/UnrealHttp.Build.cs @@ -0,0 +1,27 @@ +using UnrealBuildTool; + +public class UnrealHttp : ModuleRules +{ + public UnrealHttp(ReadOnlyTargetRules Target) : base(Target) + { + PCHUsage = ModuleRules.PCHUsageMode.UseExplicitOrSharedPCHs; + + PublicDependencyModuleNames.AddRange( + new string[] + { + "Core", + } + ); + + + PrivateDependencyModuleNames.AddRange( + new string[] + { + "ApplicationCore", + "CoreUObject", + "Engine", + "HTTP" + } + ); + } +} diff --git a/UE4-27-0/Plugins/UnrealHelper/Source/UnrealJson/Private/UnrealJson.cpp b/UE4-27-0/Plugins/UnrealHelper/Source/UnrealJson/Private/UnrealJson.cpp new file mode 100644 index 0000000..b0e692b --- /dev/null +++ b/UE4-27-0/Plugins/UnrealHelper/Source/UnrealJson/Private/UnrealJson.cpp @@ -0,0 +1,15 @@ +#include "UnrealJson.h" + +#define LOCTEXT_NAMESPACE "FUnrealJsonModule" + +void FUnrealJsonModule::StartupModule() +{ +} + +void FUnrealJsonModule::ShutdownModule() +{ +} + +#undef LOCTEXT_NAMESPACE + +IMPLEMENT_MODULE(FUnrealJsonModule, UnrealJson) diff --git a/UE4-27-0/Plugins/UnrealHelper/Source/UnrealJson/Private/UnrealJsonBPLibrary.cpp b/UE4-27-0/Plugins/UnrealHelper/Source/UnrealJson/Private/UnrealJsonBPLibrary.cpp new file mode 100644 index 0000000..ae7c1d6 --- /dev/null +++ b/UE4-27-0/Plugins/UnrealHelper/Source/UnrealJson/Private/UnrealJsonBPLibrary.cpp @@ -0,0 +1,496 @@ +#include "UnrealJsonBPLibrary.h" +#include "UnrealJson.h" +#include "JsonObjectConverter.h" +#include "JsonObjectWrapper.h" +#include "Serialization/JsonTypes.h" + +UUnrealJsonBPLibrary::UUnrealJsonBPLibrary(const FObjectInitializer& ObjectInitializer) +: Super(ObjectInitializer) +{ + +} + +FString UUnrealJsonBPLibrary::ToJson( + UPARAM(ref) TFieldPath& Wildcard) +{ + check(0); + return FString(TEXT("undefined")); +} + +FString UUnrealJsonBPLibrary::ToJsonSchema( + UPARAM(ref) TFieldPath& Wildcard) +{ + check(0); + return FString(TEXT("undefined")); +} + +bool UUnrealJsonBPLibrary::FromJson( + UPARAM(ref) TFieldPath& Wildcard, + UPARAM(ref) FString& JsonString) +{ + check(0); + return false; +} +void UUnrealJsonBPLibrary::JsonObjectWrapper_ToObject(struct FJsonObjectWrapper JsonWrapper, + UPARAM(ref) TFieldPath& Wildcard +) +{ + check(0); + return; +} + +FString PropertyGetAuthoredName( + FProperty* Property +) +{ + return Property->GetAuthoredName(); +} + +// Serialize a UProperty to a JSON String maintaining key names for structures +void UUnrealJsonBPLibrary::PropertyPtrAsJsonString( + FProperty* Property, + void* Ptr, FString& JsonString, const bool AsSchema) +{ + TSharedRef< TJsonWriter< TCHAR, TCondensedJsonPrintPolicy > > Writer = TJsonWriterFactory >::Create(&JsonString); + + if(FObjectProperty* ObjectProperty = CastField(Property) ) + { + // TODO: PropertyClass may be a result of Cast being called in the blueprint; we want to keep the cast if it's not just a blank UClass. + // UE_LOG(LogTemp, Warning, TEXT("Reflected class %s"), *ObjectProperty->PropertyClass->GetClass()->GetName()); + // This is the blueprint function where the variable is set. + // UE_LOG(LogTemp, Warning, TEXT("Reflected class %s"), *((UObject *) Property)->GetClass()->GetName()); + if(false && !ObjectProperty->HasAnyPropertyFlags(EPropertyFlags::CPF_PersistentInstance)) { + // Return an object reference as a string + TSharedPtr JsonValue = FJsonObjectConverter::UPropertyToJsonValue(ObjectProperty, Ptr, 0, 0, nullptr); + FJsonSerializer::Serialize(JsonValue, FString(), Writer); + } + else + { + FJsonObject JsonObject = FJsonObject(); + // Struct Serialize does not have a flag to maintain authored key names + ObjectAsJsonObject(JsonObject, ObjectProperty, Ptr, AsSchema); + if(AsSchema) + { + FJsonObject TypedJsonObject = FJsonObject(); + FString RefId = FString::Format(TEXT("/GRACE/{0}"), {ObjectProperty->PropertyClass->GetName() }); + TypedJsonObject.SetStringField("$id", RefId); + TypedJsonObject.SetStringField("type", "object"); + TypedJsonObject.SetObjectField("properties", MakeShared(JsonObject)); + FJsonSerializer::Serialize(MakeShared(TypedJsonObject), Writer); + } + else + { + FJsonSerializer::Serialize(MakeShared(JsonObject), Writer); + } + } + } + + else if(FStructProperty* StructProperty = CastField(Property)) + { + FJsonObject JsonObject = FJsonObject(); + StructAsJsonObject(JsonObject, StructProperty, Ptr, AsSchema); + // Struct Serialize does not have a flag to maintain authored key names + if(AsSchema) + { + FJsonObject TypedJsonObject = FJsonObject(); + FString RefId = FString::Format(TEXT("/GRACE/{0}"), {StructProperty->Struct->GetClass()->GetName()}); + TypedJsonObject.SetStringField("$id", RefId); + TypedJsonObject.SetStringField("type", "object"); + TypedJsonObject.SetObjectField("properties", MakeShared(JsonObject)); + FJsonSerializer::Serialize(MakeShared(TypedJsonObject), Writer); + } + else + { + FJsonSerializer::Serialize(MakeShared(JsonObject), Writer); + } + } + else + { + TSharedPtr JsonValue = FJsonObjectConverter::UPropertyToJsonValue(Property, Ptr, 0, 0, nullptr); + FJsonSerializer::Serialize(JsonValue, FString(), Writer); + // WriteCommaIfNeeded does not check for PreviousTokenWritten != EJsonToken::None in WriteValue from JsonWriter.h + JsonString.RemoveFromStart(FString(","), ESearchCase::IgnoreCase); + } +} + +FString UUnrealJsonBPLibrary::GetJsonTypeString(EJson& JsonType) { + switch (JsonType) + { + case EJson::String: return "string"; + case EJson::Number: return "number"; + case EJson::Boolean: return "boolean"; + case EJson::Array: return "string"; + case EJson::Object: return "object"; + // None and Null + default: return "object"; + } +} + +// Convert a structure to JSON +void UUnrealJsonBPLibrary::PropertyAsJsonObject(FJsonObject& JsonObject, + FProperty* Property, + void* ValuePtr, const bool AsSchema) { + FString VariableName = PropertyGetAuthoredName(Property); + if(FArrayProperty *ArrayProperty = CastField(Property)) + { + FScriptArrayHelper Helper(ArrayProperty, ValuePtr); + int32 Len = Helper.Num(); + TArray> ValueJsonArray; + FJsonObject ValueJsonObject = FJsonObject(); + for(int32 Idx = 0; Idx < Len; ++Idx) + { + PropertyAsJsonObject(ValueJsonObject, ArrayProperty->Inner, Helper.GetRawPtr(Idx), AsSchema); + for(auto It = ValueJsonObject.Values.CreateConstIterator(); It; ++It) + { + ValueJsonArray.Add(It.Value()); + } + } + JsonObject.SetArrayField(VariableName, ValueJsonArray); + } + else if(FObjectProperty *ObjectProperty = CastField(Property)) + { + FJsonObject ValueJsonObject = FJsonObject(); + // Soft path references as string + if(false && !ObjectProperty->HasAnyPropertyFlags(EPropertyFlags::CPF_PersistentInstance)) { + TSharedPtr JsonValue = FJsonObjectConverter::UPropertyToJsonValue(ObjectProperty, ValuePtr, 0, 0, nullptr); + JsonObject.Values.Add(VariableName, JsonValue); + return; + } + ObjectAsJsonObject(ValueJsonObject, ObjectProperty, ValuePtr, AsSchema); + if(AsSchema) + { + FJsonObject TypedJsonObject = FJsonObject(); + FString RefId = FString::Format(TEXT("/GRACE/{0}//{1}"), {ObjectProperty->PropertyClass->GetFName().ToString(), ObjectProperty->GetNameCPP()}); + TypedJsonObject.SetStringField("$id", RefId); + TypedJsonObject.SetStringField("type", "object"); + TypedJsonObject.SetObjectField("properties", MakeShared(ValueJsonObject)); + JsonObject.SetObjectField(VariableName, MakeShared(TypedJsonObject)); + } + else + { + JsonObject.SetObjectField(VariableName, MakeShared(ValueJsonObject)); + } + } + else if(FStructProperty *SubProperty = CastField(Property)) + { + FJsonObject ValueJsonObject = FJsonObject(); + StructAsJsonObject(ValueJsonObject, SubProperty, ValuePtr, AsSchema); + if(AsSchema) + { + FJsonObject TypedJsonObject = FJsonObject(); + FString RefId = FString::Format(TEXT("/GRACE/{0}"), {SubProperty->Struct->GetName()}); + TypedJsonObject.SetStringField("$id", RefId); + TypedJsonObject.SetStringField("type", "object"); + TypedJsonObject.SetObjectField("properties", MakeShared(ValueJsonObject)); + JsonObject.SetObjectField(VariableName, MakeShared(TypedJsonObject)); + } + else + { + JsonObject.SetObjectField(VariableName, MakeShared(ValueJsonObject)); + } + } + else // if(CastField(Property) || CastField(Property) || CastField(Property) || CastField(Property) || CastField(Property)) + { + TSharedPtr JsonValue = FJsonObjectConverter::UPropertyToJsonValue(Property, ValuePtr, 0, 0, nullptr); + if (JsonValue.IsValid()) + { + if(AsSchema) + { + FJsonObject TypedJsonObject = FJsonObject(); + TypedJsonObject.SetStringField("type", GetJsonTypeString(JsonValue->Type)); + TypedJsonObject.SetField("example", JsonValue); + FString RefId = FString::Format(TEXT("/Unreal/{0}"), {Property->GetClass()->GetName()}); + TypedJsonObject.SetStringField("$id", RefId); + // Also FNumericProperty->IsEnum() + if(FByteProperty *EnumProperty = CastField(Property)) + { + UEnum* Enum = EnumProperty->GetIntPropertyEnum(); + TArray> ValueJsonArray; + for(int32 EnumIndex = 0; EnumIndex < Enum->NumEnums() - 1; ++EnumIndex) { + ValueJsonArray.Add(MakeShared(Enum->GetAuthoredNameStringByIndex(EnumIndex))); + } + TypedJsonObject.SetArrayField("enum", ValueJsonArray); + // FindObject is an interesting walk + TypedJsonObject.SetField("example", MakeShared(Enum->GetAuthoredNameStringByValue(EnumProperty->GetSignedIntPropertyValue(ValuePtr)))); + TypedJsonObject.SetStringField("$id", FString::Format(TEXT("/GRACE/{0}"), {Enum->GetName()})); + } + if(FEnumProperty *EnumProperty = CastField(Property)) + { + UEnum* Enum = EnumProperty->GetEnum(); + // this may just be for enums with more than 255 elements. + } + JsonObject.SetObjectField(VariableName, MakeShared(TypedJsonObject)); + } + else + { + JsonObject.Values.Add(VariableName, JsonValue); + } + } + } +/* + else + { + UE_LOG(LogTemp, Warning, TEXT("Unserialized JSON reflected class %s"), *Property->GetClass()->GetName()); + UE_LOG(LogTemp, Warning, TEXT("Unserialized JSON reflected name %s"), *Property->GetFName().GetPlainNameString()); + } +*/ + +} + +void UUnrealJsonBPLibrary::ObjectAsJsonObject(FJsonObject& JsonObject, + FObjectProperty* ObjectProperty, + void* ObjectPtr, const bool AsSchema) { + UObject* Object = ObjectProperty->GetObjectPropertyValue(ObjectPtr); + UClass* PropertyClass = Object->GetClass(); + for (TFieldIterator It(PropertyClass); It; ++It) + { + FProperty* Property = *It; + FString VariableName = PropertyGetAuthoredName(Property); + for(int32 Idx = 0; Idx < Property->ArrayDim; Idx++) + { + void* ValuePtr = Property->ContainerPtrToValuePtr((void*) Object, Idx); + PropertyAsJsonObject(JsonObject, Property, ValuePtr, AsSchema); + } + } +} + +void UUnrealJsonBPLibrary::StructAsJsonObject(FJsonObject& JsonObject, + FStructProperty* StructProperty, + void* StructPtr, const bool AsSchema) { + for (TFieldIterator It(StructProperty->Struct); It; ++It) + { + FProperty* Property = *It; + FString VariableName = PropertyGetAuthoredName(Property); + for(int32 Idx = 0; Idx < Property->ArrayDim; Idx++) + { + void* ValuePtr = Property->ContainerPtrToValuePtr(StructPtr, Idx); + PropertyAsJsonObject(JsonObject, Property, ValuePtr, AsSchema); + } + } +} + +// Convert JSON string into a structure +void UUnrealJsonBPLibrary::JsonAsProperty(TSharedPtr JsonObject, + FProperty* Property, + void* ValuePtr) { + FString VariableName = PropertyGetAuthoredName(Property); + if(FArrayProperty *ArrayProperty = CastField(Property)) + { + TArray> ValueJsonArray = JsonObject->Values[VariableName].Get()->AsArray(); + FScriptArrayHelper ValueArray(ArrayProperty, ValuePtr); + ValueArray.Resize(ValueJsonArray.Num()); + FJsonObject ValueJsonObject = FJsonObject(); + for(int32 Idx = 0; Idx < ValueJsonArray.Num(); ++Idx) + { + ValueJsonObject.Values.Add(VariableName, ValueJsonArray[Idx]); + void* SubValuePtr = ValueArray.GetRawPtr(Idx); + JsonAsProperty(MakeShared(ValueJsonObject), ArrayProperty->Inner, SubValuePtr); + } + } + else if(FObjectProperty *ObjectProperty = CastField(Property)) + { + // Allow soft reference strings + if(JsonObject->Values[VariableName].Get()->Type == EJson::String) + { + // The internal code does run: + // if(!ObjectProperty->HasAnyPropertyFlags(EPropertyFlags::CPF_PersistentInstance)) + // ObjectProperty->ImportText(*JsonObject->Values[VariableName].Get()->AsString(), ValuePtr, 0, NULL); + FJsonObjectConverter::JsonValueToUProperty(JsonObject->Values[VariableName], Property, ValuePtr, 0, 0); + } + else + { + TSharedPtr SubObject = JsonObject->Values[VariableName].Get()->AsObject(); + JsonAsObject(SubObject, ObjectProperty, ValuePtr); + } + } + else if(FStructProperty *SubProperty = CastField(Property)) + { + TSharedPtr SubObject = JsonObject->Values[VariableName].Get()->AsObject(); + JsonAsStruct(SubObject, SubProperty, ValuePtr); + } + else + { + FJsonObjectConverter::JsonValueToUProperty(JsonObject->Values[VariableName], Property, ValuePtr, 0, 0); + } +} + +void UUnrealJsonBPLibrary::JsonAsObject(TSharedPtr JsonObject, + FObjectProperty* ObjectProperty, + void* ObjectPtr) { + + UObject* Object = ObjectProperty->GetObjectPropertyValue(ObjectPtr); + // This could be an access violation + UClass* PropertyClass = Object->GetClass(); + + for (TFieldIterator It(PropertyClass); It; ++It) + { + FProperty* Property = *It; + if(!JsonObject->HasField(PropertyGetAuthoredName(Property))) continue; + for(int32 Idx = 0; Idx < Property->ArrayDim; Idx++) + { + void* ValuePtr = Property->ContainerPtrToValuePtr((void*) Object, Idx); + JsonAsProperty(JsonObject, Property, ValuePtr); + } + } +} + +void UUnrealJsonBPLibrary::JsonAsStruct(TSharedPtr JsonObject, + FStructProperty* StructProperty, + void* StructPtr) { + for (TFieldIterator It(StructProperty->Struct); It; ++It) + { + FProperty* Property = *It; + if(!JsonObject->HasField(PropertyGetAuthoredName(Property))) continue; + for(int32 Idx = 0; Idx < Property->ArrayDim; Idx++) + { + void* ValuePtr = Property->ContainerPtrToValuePtr(StructPtr, Idx); + JsonAsProperty(JsonObject, Property, ValuePtr); + } + } +} + +void UUnrealJsonBPLibrary::FromJson_Object(TSharedPtr JsonObject, + FProperty* WildcardProperty, + void* PropertyPtr) { + FObjectProperty* ObjectProperty = CastField(WildcardProperty); + FStructProperty* StructProperty = CastField(WildcardProperty); + if(ObjectProperty) + { + JsonAsObject(JsonObject, ObjectProperty, PropertyPtr); + } + else if(StructProperty) + { + JsonAsStruct(JsonObject, StructProperty, PropertyPtr); + } + else + { + JsonAsProperty(JsonObject, WildcardProperty, PropertyPtr); + } +} + +bool UUnrealJsonBPLibrary::FromJson_Generic( + FProperty* WildcardProperty, + void* PropertyPtr, FString& JsonString) +{ + FObjectProperty* ObjectProperty = CastField(WildcardProperty); + FStructProperty* StructProperty = CastField(WildcardProperty); + FString JsonObjectString = StructProperty || ObjectProperty ? JsonString : FString::Printf(TEXT("{\"%s\": %s}"), *PropertyGetAuthoredName(WildcardProperty), *JsonString); + TSharedRef> Reader = TJsonReaderFactory<>::Create(JsonObjectString); + TSharedPtr JsonObject; + if (FJsonSerializer::Deserialize(Reader, JsonObject) && JsonObject.IsValid()) + { +/* + if(StructProperty && StructProperty->Struct == FJsonObjectWrapper::StaticStruct()) + { + UScriptStruct* Struct = StructProperty->Struct; + UE_LOG(LogTemp, Warning, TEXT("Reflection on %s not implemented"), *Struct->GetStructCPPName()); + return false; + } +*/ + FromJson_Object(JsonObject, WildcardProperty, PropertyPtr); + return true; + } + return false; +} + +FString UUnrealJsonBPLibrary::JsonObjectWrapper_ToString(struct FJsonObjectWrapper JsonWrapper, const bool PrettyPrint) +{ + if(!JsonWrapper.JsonObject.IsValid()) + { + return FString(); + } + FString JsonString; + TSharedRef< TJsonWriter< TCHAR, TCondensedJsonPrintPolicy > > Writer = TJsonWriterFactory >::Create(&JsonString); + FJsonSerializer::Serialize(JsonWrapper.JsonObject.ToSharedRef(), Writer); + return JsonString; +} + +bool UUnrealJsonBPLibrary::JsonObject_WithFieldPath(FJsonObject& JsonObject, const FString& WithFieldPath) { + TArray PathParts; + int32 PathLength = WithFieldPath.ParseIntoArray(PathParts, *FString("."), true); + if(PathLength == 0) + { + return true; + } + if(!JsonObject.HasField(PathParts[0])) + { + return false; + } + const TSharedPtr JsonPtr = MakeShared(JsonObject); + const TSharedPtr* CurrentPointer = &JsonPtr; + for(int32 Idx = 0; Idx < PathLength; Idx++) + { + if(!(*CurrentPointer)->TryGetObjectField(PathParts[Idx], CurrentPointer)) + { + // Allow the ending field to be any field type + if((Idx == PathLength - 1) && (*CurrentPointer)->HasField(PathParts[Idx])) + { + break; + } + else + { + return false; + } + } + } + JsonObject = *(*CurrentPointer).Get(); + return true; +} + +void UUnrealJsonBPLibrary::JsonWrapper_FromString(UPARAM(ref) FString& JsonString, const FString WithFieldPath, EJsonFieldExistsEnum& FieldExists, FJsonObjectWrapper& JsonWrapper) { + TSharedPtr JsonObject(new FJsonObject()); + JsonWrapper.JsonObject = JsonObject; + if(JsonString.Len() > 0) + { + TSharedRef> Reader = TJsonReaderFactory<>::Create(JsonString); + if(FJsonSerializer::Deserialize(Reader, JsonObject) && JsonObject.IsValid()) { + if(WithFieldPath.Len() > 0) + { + FJsonObject Root = *JsonObject.Get(); + if(JsonObject_WithFieldPath(Root, WithFieldPath)) + { + (&JsonWrapper)->JsonObject = MakeShared(Root); + FieldExists = EJsonFieldExistsEnum::Changed; + } + else + { + FieldExists = EJsonFieldExistsEnum::NotChanged; + return; + } + } + else + { + (&JsonWrapper)->JsonObject = JsonObject; + FieldExists = EJsonFieldExistsEnum::Changed; + return; + } + } + else + { + FieldExists = EJsonFieldExistsEnum::NotChanged; + return; + } + } + else + { + FieldExists = EJsonFieldExistsEnum::NotChanged; + return; + } +} + +// undocumented; from source: DeterminesOutputType = "Parameter", DynamicOutputParam="Param" +// UFUNCTION(BlueprintCallable, Category="Utilities", meta=(DeterminesOutputType="Interface", DynamicOutputParam="OutActors")) +// static void GetAllActorsWithInterface(TSubclassOf Interface, TArray& OutActors); +void UUnrealJsonBPLibrary::JsonWrapper_FromField(struct FJsonObjectWrapper InJsonWrapper, FString WithFieldPath, EJsonFieldExistsEnum& FieldExists, FJsonObjectWrapper& OutJsonWrapper) { + // JsonObjectWrapper is poorly defined in UE 4.22 + OutJsonWrapper.JsonObject = InJsonWrapper.JsonObject.IsValid() ? InJsonWrapper.JsonObject : (TSharedPtr) (new FJsonObject()); + FJsonObject Root = *OutJsonWrapper.JsonObject.Get(); + if(JsonObject_WithFieldPath(Root, WithFieldPath)) + { + OutJsonWrapper.JsonObject = MakeShared(Root); + FieldExists = EJsonFieldExistsEnum::Changed; + return; + } + FieldExists = EJsonFieldExistsEnum::NotChanged; + return; +} \ No newline at end of file diff --git a/UE4-27-0/Plugins/UnrealHelper/Source/UnrealJson/Public/UnrealJson.h b/UE4-27-0/Plugins/UnrealHelper/Source/UnrealJson/Public/UnrealJson.h new file mode 100644 index 0000000..5d35b53 --- /dev/null +++ b/UE4-27-0/Plugins/UnrealHelper/Source/UnrealJson/Public/UnrealJson.h @@ -0,0 +1,12 @@ +#pragma once + +#include "Modules/ModuleManager.h" + +class FUnrealJsonModule : public IModuleInterface +{ +public: + + /** IModuleInterface implementation */ + virtual void StartupModule() override; + virtual void ShutdownModule() override; +}; diff --git a/UE4-27-0/Plugins/UnrealHelper/Source/UnrealJson/Public/UnrealJsonBPLibrary.h b/UE4-27-0/Plugins/UnrealHelper/Source/UnrealJson/Public/UnrealJsonBPLibrary.h new file mode 100644 index 0000000..2694816 --- /dev/null +++ b/UE4-27-0/Plugins/UnrealHelper/Source/UnrealJson/Public/UnrealJsonBPLibrary.h @@ -0,0 +1,205 @@ +#pragma once + +#include "Kismet/BlueprintFunctionLibrary.h" +#include "UObject/ObjectMacros.h" +#include "JsonObjectConverter.h" +#include "JsonObjectWrapper.h" +#include "Runtime/Core/Public/Misc/ConfigCacheIni.h" +#include "Runtime/Launch/Resources/Version.h" +#include "Runtime/Json/Public/Policies/CondensedJsonPrintPolicy.h" +#include "UnrealJsonBPLibrary.generated.h" + +UENUM(BlueprintType) +enum class EJsonFieldExistsEnum : uint8 +{ + Changed UMETA(DisplayName = "Changed"), + NotChanged UMETA(DisplayName = "Not Changed"), +}; + +/* + * JSON struct serialization/deserialization + * from mhoelzl/PropertyTest tutorial on gitlab, and reflection tutorial on forums. + */ +UCLASS() +class UUnrealJsonBPLibrary : public UBlueprintFunctionLibrary +{ + GENERATED_UCLASS_BODY() + + /** + * JSON struct serialization/deserialization + * in the style of KismetArrayLibrary.h + */ + + /** + * Convert structure to JSON + * work-around the build in converter because names are mangled: + * - Built-in: FJsonObjectConverter::UStructToJsonObjectString(StructProperty->Struct, StructPtr, JsonString, 0, 0, 0, nullptr, false); + * - Property->GetAuthoredName() should be called, not GetName() + * - There's a normalization which lowercases the first character and treats ID as a special string + * Convert JSON string into a structure + * - JsonObjectToUStruct did not seem to populate correctly + * - FJsonObjectConverter::JsonObjectToUStruct(JsonObject.ToSharedRef(), StructProperty->Struct, StructPtr, 0, 0)) + * - NOTE: Make Structure must be followed by Set Members or Set to create a value usable with pass-by-reference + */ + + /* + // Could be a helper at some point, may be unnecessary. + UFUNCTION(BlueprintPure, meta = (DisplayName = "Class to JSON", Keywords = "Json"), Category = "Unreal Helper BP Library") + static FString ClassToJson(UPARAM(ref) UObject* Object) + { + UE_LOG(LogTemp, Warning, TEXT("Explore class %s"), *Object->GetClass()->GetName()); + FString JsonString = ""; + // PropertyPtrAsJsonString(WildcardProperty, PropertyAddr, JsonString); + return JsonString; + } + */ + + /** + * Serialize from a wildcard property to a JSON string + * + * @param Wildcard The input to serialize to JSON + * @return String containing serializable properties as JSON + */ + UFUNCTION(BlueprintPure, meta = (DisplayName = "Object to JSON", Keywords = "Json", CustomStructureParam = "Wildcard"), Category = "Unreal Helper BP Library", CustomThunk) + static FString ToJson(UPARAM(ref) TFieldPath& Wildcard); + UFUNCTION(BlueprintPure, meta = (DisplayName = "Object to JSON Schema", Keywords = "Json", CustomStructureParam = "Wildcard"), Category = "Unreal Helper BP Library", CustomThunk) + static FString ToJsonSchema(UPARAM(ref) TFieldPath& Wildcard); + static void ObjectAsJsonObject(FJsonObject& JsonObject, FObjectProperty* ObjectProperty, void* ObjectPtr, const bool AsSchema); + static void StructAsJsonObject(FJsonObject& JsonObject, FStructProperty* StructProperty, void* StructPtr, const bool AsSchema); + static void PropertyAsJsonObject(FJsonObject& JsonObject, FProperty* Property, void* ValuePtr, const bool AsSchema); + static FString GetJsonTypeString(EJson& JsonType); + + // FIXME: Property and PropertyPtr should be const + static void PropertyPtrAsJsonString( + FProperty* Property, + void* PropertyPtr, FString& JsonString, const bool AsSchema); + DECLARE_FUNCTION(execToJson) + { + Stack.MostRecentProperty = nullptr; + Stack.StepCompiledIn(NULL); + FProperty* WildcardProperty = CastField(Stack.MostRecentProperty); + void* PropertyAddr = Stack.MostRecentPropertyAddress; + // P_GET_PROPERTY(FBoolProperty, AsSchema); + P_FINISH; + + P_NATIVE_BEGIN; + FString JsonString; + bool AsSchema = false; + PropertyPtrAsJsonString(WildcardProperty, PropertyAddr, JsonString, AsSchema); + *(FString*)RESULT_PARAM = JsonString; + P_NATIVE_END; + } + + // Notes: Do not use, still under development + DECLARE_FUNCTION(execToJsonSchema) + { + Stack.MostRecentProperty = nullptr; + Stack.StepCompiledIn(NULL); + FProperty* WildcardProperty = CastField(Stack.MostRecentProperty); + void* PropertyAddr = Stack.MostRecentPropertyAddress; + P_FINISH; + + P_NATIVE_BEGIN; + FString JsonString; + bool AsSchema = true; + PropertyPtrAsJsonString(WildcardProperty, PropertyAddr, JsonString, AsSchema); + *(FString*)RESULT_PARAM = JsonString; + P_NATIVE_END; + } + + /** + * Deserialize a JSON string into an existing a wildcard property + * + * @param Wildcard The structure or object to populate + * @param JsonString The JSON string to deserialize into the target structure or object + * @return bool True if json was parseable + */ + + UFUNCTION(BlueprintCallable, meta = (DisplayName = "JSON to Object", Keywords = "Json", CustomStructureParam = "Wildcard", AutoCreateRefTerm="Wildcard"), Category = "Unreal Helper BP Library", CustomThunk) + static bool FromJson( + UPARAM(ref) TFieldPath& Wildcard, + UPARAM(ref) FString& JsonString); + static void FromJson_Object(TSharedPtr JsonObject, FProperty* StructProperty, void* StructPtr); + static void JsonAsObject(TSharedPtr JsonObject, FObjectProperty* ObjectProperty, void* ObjectPtr); + static void JsonAsStruct(TSharedPtr JsonObject, FStructProperty* StructProperty, void* StructPtr); + static void JsonAsProperty(TSharedPtr JsonObject, FProperty* Property, void* ValuePtr); + + static bool FromJson_Generic( + FProperty* WildcardProperty, + void* PropertyPtr, FString& JsonString); + DECLARE_FUNCTION(execFromJson) + { + Stack.MostRecentProperty = nullptr; + Stack.StepCompiledIn(NULL); + FProperty* WildcardProperty = CastField(Stack.MostRecentProperty); + void* PropertyAddr = Stack.MostRecentPropertyAddress; + P_GET_PROPERTY_REF(FStrProperty, JsonString); + + P_FINISH; + + P_NATIVE_BEGIN; + // MARK_PROPERTY_DIRTY(Stack.Object, WildcardProperty); + *(bool*)RESULT_PARAM = FromJson_Generic(WildcardProperty, PropertyAddr, JsonString); + P_NATIVE_END; + } + + /** + * Work with JsonWrapper to check if a field exists + * + * @param JsonWrapper (optional) Unreal Json Wrapper over a JsonObject + * @param JsonString (optional) JSON string to deserialize into a Json Wrapper + * @param FieldName A top level field name to The JSON string to look for in the json object + * @param OutJsonWrapper The JsonWrapper to populate when the field was found with the resulting object + * @param FieldExists Enum of Changed or Not Changed based on field name filter and validity + */ + UFUNCTION(BlueprintCallable, meta = (DisplayName = "JsonObjectWrapper From Field", Keywords = "Json", ExpandEnumAsExecs = "FieldExists", AutoCreateRefTerm="JsonString"), Category = "Unreal Helper BP Library") + static void JsonWrapper_FromField(struct FJsonObjectWrapper InJsonWrapper, FString WithFieldPath, EJsonFieldExistsEnum& FieldExists, FJsonObjectWrapper& OutJsonWrapper); + + /** + * Work with JsonWrapper to check if a field exists + * + * @param JsonString JSON string to deserialize into a Json Wrapper + * @param WithFieldPath A top level field name or dot separated field path to traverse in the JSON object + * @param JsonWrapper Blueprint wrapper over a JsonObject + */ + UFUNCTION(BlueprintCallable, meta = (DisplayName = "JsonObjectWrapper From String", Keywords = "Json", ExpandEnumAsExecs = "FieldExists", AutoCreateRefTerm="JsonString"), Category = "Unreal Helper BP Library") + static void JsonWrapper_FromString(UPARAM(ref) FString& JsonString, const FString WithFieldPath, EJsonFieldExistsEnum& FieldExists, FJsonObjectWrapper& JsonWrapper); + + /** + * Work with JsonWrapper to check if a field exists + * + * @param JsonWrapper Unreal Json Wrapper over a JsonObject + * @return FString Serialized JSON as a string + */ + UFUNCTION(BlueprintCallable, meta = (DisplayName = "To String", Keywords = "Json"), Category = "Unreal Helper BP Library") + static FString JsonObjectWrapper_ToString(struct FJsonObjectWrapper JsonWrapper, const bool PrettyPrint=false); + static bool JsonObject_WithFieldPath(FJsonObject& JsonObject, const FString& WithFieldPath); + + /** + * Unwrap a JSON wrapper into an existing a wildcard property + * + * @param Wildcard The structure or object to populate + * @param JsonWrapper The Json Wrapper to unwrap into the target structure or object + * @return void + */ + UFUNCTION(BlueprintCallable, meta = (DisplayName = "To Object", Keywords = "Json", CustomStructureParam = "Wildcard", AutoCreateRefTerm="Wildcard"), Category = "Unreal Helper BP Library", CustomThunk) + static void JsonObjectWrapper_ToObject(struct FJsonObjectWrapper JsonWrapper, + UPARAM(ref) TFieldPath& Wildcard + ); + DECLARE_FUNCTION(execJsonObjectWrapper_ToObject) + { + P_GET_STRUCT(FJsonObjectWrapper, JsonWrapper); + Stack.MostRecentProperty = nullptr; + Stack.StepCompiledIn(NULL); + FProperty* WildcardProperty = CastField(Stack.MostRecentProperty); + void* PropertyAddr = Stack.MostRecentPropertyAddress; + + P_FINISH; + + P_NATIVE_BEGIN; + // MARK_PROPERTY_DIRTY(Stack.Object, WildcardProperty); + FromJson_Object(JsonWrapper.JsonObject, WildcardProperty, PropertyAddr); + P_NATIVE_END; + } + +}; diff --git a/UE4-27-0/Plugins/UnrealHelper/Source/UnrealJson/UnrealJson.Build.cs b/UE4-27-0/Plugins/UnrealHelper/Source/UnrealJson/UnrealJson.Build.cs new file mode 100644 index 0000000..b5c1386 --- /dev/null +++ b/UE4-27-0/Plugins/UnrealHelper/Source/UnrealJson/UnrealJson.Build.cs @@ -0,0 +1,30 @@ +using System.IO; +using UnrealBuildTool; + +public class UnrealJson : ModuleRules +{ + public UnrealJson(ReadOnlyTargetRules Target) : base(Target) + { + PCHUsage = ModuleRules.PCHUsageMode.UseExplicitOrSharedPCHs; + + PublicDependencyModuleNames.AddRange( + new string[] + { + "Core", + } + ); + + + PrivateDependencyModuleNames.AddRange( + new string[] + { + "CoreUObject", + "Engine", + "HTTP", + "Json", + "JsonUtilities" + } + ); + + } +} diff --git a/UE4-27-0/Plugins/UnrealHelper/Source/UnrealMediaOutputSocket/Private/SocketMediaCapture.cpp b/UE4-27-0/Plugins/UnrealHelper/Source/UnrealMediaOutputSocket/Private/SocketMediaCapture.cpp new file mode 100644 index 0000000..1fef81d --- /dev/null +++ b/UE4-27-0/Plugins/UnrealHelper/Source/UnrealMediaOutputSocket/Private/SocketMediaCapture.cpp @@ -0,0 +1,1038 @@ +#include "SocketMediaCapture.h" +#include "Async/Async.h" +#include "Misc/Paths.h" +#include "MediaIOCoreFileWriter.h" +#include "Modules/ModuleManager.h" +#include "Containers/CircularQueue.h" +#if PLATFORM_WINDOWS +#include "Windows/WindowsHWrapper.h" +#endif +// UnrealAudioUtilities seems to have the nicer queue code +// #include "RenderUtils.h" +#include "RenderingThread.h" + +/** + * Write to a target pipe/socket as a consumer thread. + */ +/* Short-hand seemed a lot shorter - but harder to manage async task + Semaphore = FPlatformProcess::GetSynchEventFromPool(false); + DrainTask = FPlatformProcess::GetSynchEventFromPool(true); // called once + AsyncTask(ENamedThreads::AnyBackgroundThreadNormalTask, [this, bHasTcpEndpoint]() { + UE_LOG(LogTemp, Warning, TEXT("Ffmpeg async thread started")); + LockedSendQueue( + bHasTcpEndpoint ? Socket : nullptr, + bHasTcpEndpoint ? nullptr : StdinWritePipe, + StopTaskFlag, PendingFrames, Queue, Semaphore, DrainTask, PacketSize); + UE_LOG(LogTemp, Warning, TEXT("Ffmpeg async thread done")); + }); +*/ + +class UNREALMEDIAOUTPUTSOCKET_API SocketMediaCaptureSink : public FRunnable, FSingleThreadRunnable +{ + +public: + FThreadSafeBool StopTaskFlag; + FThreadSafeCounter PendingFrames; + FEvent *Semaphore = nullptr; + TCircularQueue> *Queue; + FRunnableThread *Thread = nullptr; + uint8 *SingleThreadData = nullptr; + // TQueue> Queue; // = TQueue>(121); + +private: + int32 PacketSize; + FSocket *WriteSocket; + void *WritePipe = nullptr; + +public: + SocketMediaCaptureSink(FSocket *TargetSocket, void *TargetPipe, int32 FramePacketSize) + { + WriteSocket = TargetSocket; + WritePipe = TargetPipe; + PacketSize = FramePacketSize; + } + + ~SocketMediaCaptureSink() + { + UE_LOG(LogTemp, Warning, TEXT("LockedSendQueue destructor")); + StopTaskFlag = true; + WriteSocket = nullptr; + WritePipe = nullptr; + StopListening(); + UE_LOG(LogTemp, Warning, TEXT("Media Capture Sink Destroyed")); + } + + FSingleThreadRunnable *GetSingleThreadInterface() + { + return this; + } + + bool Init() + { + Semaphore = FPlatformProcess::GetSynchEventFromPool(true); + Queue = new TCircularQueue>(511 + 1); + return true; + } + + void Exit() + { + UE_LOG(LogTemp, Warning, TEXT("LockedSendQueue exiting")); + StopTaskFlag = true; + Queue->Empty(); + Semaphore->Trigger(); + // Is this destructor implemented so return synch does not need to? + // delete Semaphore; + FPlatformProcess::ReturnSynchEventToPool(Semaphore); + Semaphore = nullptr; + UE_LOG(LogTemp, Warning, TEXT("LockedSendQueue exit")); + } + + void Stop() + { + UE_LOG(LogTemp, Warning, TEXT("LockedSendQueue stop early")); + if (StopTaskFlag) + { + return; + } + // FIXME: May already be handled via Exit() + StopTaskFlag = true; + Queue->Empty(); + if (Semaphore != nullptr) + { + Semaphore->Trigger(); + } + } + + bool EnqueuePacket(void *InBuffer, bool asAsync) + { + if (StopTaskFlag) + { + return false; + } + if (!asAsync) + { + SingleThreadData = (uint8 *)InBuffer; + Tick(); + } + else + { + // InBuffer is on main thread and needs to be copied out before getting clobbered. + TArray FrameData; + FrameData.AddUninitialized(PacketSize); + FMemory::Memcpy(FrameData.GetData(), InBuffer, PacketSize); + Queue->Enqueue(FrameData); + PendingFrames.Increment(); + int32 MAX_BUFFER_SIZE = 1024 * 1024 * 1024; // 1 gigabyte ram buffer + Semaphore->Trigger(); + if (PendingFrames.GetValue() * PacketSize > MAX_BUFFER_SIZE) + { + UE_LOG(LogTemp, Warning, TEXT("Reached max buffer size %d with pending frames %d"), MAX_BUFFER_SIZE, PendingFrames.GetValue()); + return false; + } + // Seems suspend/unsuspend would be bad form: + // https://docs.microsoft.com/en-us/dotnet/api/system.threading.thread.suspend?view=net-5.0 + // Thread->Suspend(false); + } + return !StopTaskFlag; + } + + // This is convoluted, using EnqueuePacket and Tick. + void Tick() + { + // Single-thread interface + if (WriteSocket != nullptr) + { + int32 BytesLeft = PacketSize; + double SocketTimeoutMs = 100; // 15; // 60fps requires a low timeout + if (!SocketMediaCaptureSink::SocketSend(WriteSocket, ((const uint8 *)SingleThreadData), BytesLeft, SocketTimeoutMs)) + { + UE_LOG(LogTemp, Warning, TEXT("Unexpected connection issue to ffmpeg")); + StopTaskFlag = true; + } + else if (BytesLeft > 0) + { + if (BytesLeft == PacketSize) + { + UE_LOG(LogTemp, Warning, TEXT("Partial packet on pipe")); + } + else + { + UE_LOG(LogTemp, Warning, TEXT("Timeout on pipe: %d"), BytesLeft); + } + } + } + else if (WritePipe != nullptr) + { + int32 BytesLeft = PacketSize; + FThreadSafeBool AlwaysFalse; + if (!SocketMediaCaptureSink::PipeSend(WritePipe, ((const uint8 *)SingleThreadData), BytesLeft, AlwaysFalse)) // , StopTaskFlag)) + { + UE_LOG(LogTemp, Warning, TEXT("Unexpected connection issue to ffmpeg")); + StopTaskFlag = true; + // StopCaptureImpl(false); + } + } + } + + // This is a single frame writer; for debugging we want multiple frames (and a very large file) + // MediaIOCoreFileWriter::WriteRawFile(OutputFilename, reinterpret_cast(InBuffer), Stride * Height); + static bool SocketSend(FSocket *TargetSocket, const uint8 *InBuffer, int32 &BytesLeft, double ReadyTimeoutMs) + { + int32 BytesOffset = 0; + int32 BytesSent = 0; + + while (TargetSocket->Wait(ESocketWaitConditions::WaitForWrite, FTimespan::FromMilliseconds(ReadyTimeoutMs))) + // && BytesLeft > 0 + { + if (BytesLeft == 0) + break; + // UE_LOG(LogTemp, Warning, TEXT("Socket Send Attempt %d at point %d"), BytesLeft, BytesOffset); + TargetSocket->Send((InBuffer) + BytesOffset, BytesLeft, BytesSent); + if (BytesSent == -1) + { + UE_LOG(LogTemp, Warning, TEXT("Socket Send Failure of %d at point %d"), BytesLeft, BytesOffset); + break; + } + BytesLeft -= BytesSent; + BytesOffset += BytesSent; + } + if (BytesSent == -1) + { + return false; + } + return true; + } + + static bool PipeSend(void *TargetPipe, const uint8 *InBuffer, int32 &BytesLeft, FThreadSafeBool &Stop) + { + int32 BytesOffset = 0; + int32 BytesSent = 0; + + while (BytesLeft > 0 && !Stop) + { + if (BytesLeft == 0) + break; + FPlatformProcess::WritePipe(TargetPipe, (InBuffer) + BytesOffset, BytesLeft, &BytesSent); + if (BytesSent == -1) + break; + BytesLeft -= BytesSent; + BytesOffset += BytesSent; + } + + // Clear stdout/stderr or ffmpeg will deadlock + // FString StdoutErr = FPlatformProcess::ReadPipe(TargetPipe); + + if (BytesSent == -1) + { + return false; + } + return true; + } + +public: + bool StartListening() + { + if (Thread != nullptr) + return false; + Thread = FRunnableThread::Create(this, TEXT("USocketMediaCaptureThread"), 128 * 1024, EThreadPriority::TPri_AboveNormal, FPlatformAffinity::GetPoolThreadMask()); + return (Thread != nullptr); + } + + void StopListening() + { + UE_LOG(LogTemp, Warning, TEXT("Stopping thread")); + if (Thread != nullptr) + { + StopTaskFlag = true; + if (Semaphore != nullptr) + { + Semaphore->Trigger(); + } + Thread->WaitForCompletion(); + delete Thread; + Thread = nullptr; + } + } + + uint32 Run() + { + UE_LOG(LogTemp, Warning, TEXT("LockedSendQueue started")); + + TArray InElement; + const uint8 *InBuffer = nullptr; + int32 BytesLeft = 0; + bool SendFailed = false; + const double SocketTimeoutMs = 2000; // This is a limit on delay + + while (!Queue->IsEmpty() || !StopTaskFlag) + { + if (!Queue->Dequeue(InElement)) + { + if (StopTaskFlag) + continue; + Semaphore->Reset(); + Semaphore->Wait(); + continue; + } + InBuffer = InElement.GetData(); + // UE_LOG(LogTemp, Warning, TEXT("LockedSendQueue loop begin")); + + // Bypass the pipe to debug the queue implementation + if (false) + { + PendingFrames.Decrement(); + continue; + } + + BytesLeft = PacketSize; + if (WritePipe != nullptr) + { + // UE_LOG(LogTemp, Warning, TEXT("Send via Pipe")); + if (!PipeSend(WritePipe, InBuffer, BytesLeft, StopTaskFlag)) + { + SendFailed = true; + break; + } + } + + else if (WriteSocket != nullptr) + { + if (!SocketSend(WriteSocket, InBuffer, BytesLeft, SocketTimeoutMs)) + { + SendFailed = true; + break; + } + } + + if (BytesLeft > 0) + { + if (BytesLeft != PacketSize) + { + UE_LOG(LogTemp, Warning, TEXT("Partial packet sent, pipe corrupted")); + SendFailed = true; + break; + } + UE_LOG(LogTemp, Warning, TEXT("Timeout on pipe with frames remaining: %d - packet size: %d"), PendingFrames.GetValue(), PacketSize); + SendFailed = true; + break; + } + + if (SendFailed && WritePipe != nullptr) + { + UE_LOG(LogTemp, Warning, TEXT("FFmpeg pipe failed early")); + StopTaskFlag = true; + } + + else if (SendFailed && WriteSocket != nullptr) + { + ISocketSubsystem *PlatformSockets = ISocketSubsystem::Get(); + ESocketErrors ErrorCode = PlatformSockets->GetLastErrorCode(); + FString ErrorString = FString(PlatformSockets->GetSocketError(ErrorCode)); + if (ErrorString == "SE_ECONNRESET") + { + UE_LOG(LogTemp, Warning, TEXT("FFmpeg failure on port connection %s"), *ErrorString); + } + else if (ErrorString == "SE_EFAULT") + { + UE_LOG(LogTemp, Warning, TEXT("Ffmpeg socket dropped unexpectedly: %s"), *ErrorString); + } + else + { + UE_LOG(LogTemp, Warning, TEXT("Ffmpeg unknown connection issue: %s"), *ErrorString); + } + StopTaskFlag = true; + } + + else + { + PendingFrames.Decrement(); + } + } + + UE_LOG(LogTemp, Warning, TEXT("LockedSendQueue drained")); + + StopTaskFlag = true; + + return 0; + } +}; + +/** + * Media output to write to an off-thread sink, optional spawned process + * off-thread writes to a socket/pipe to allow the next frame to render. + */ + +bool USocketMediaCapture::ValidateMediaOutput() const +{ + USocketMediaOutput *SocketMediaOutput = Cast(MediaOutput); + if (!SocketMediaOutput) + { + UE_LOG(LogTemp, Error, TEXT("Socket Media Capture unable to cast provided Media Output")); + return false; + } + + bool bHasTcpEndpoint = !SocketMediaOutput->NoNetwork; + if (bHasTcpEndpoint) + { + FString IPAddress = FString("127.0.0.1"); + uint32 Port = 4445; + + if (!SocketMediaOutput->IPAddress.IsEmpty()) + { + IPAddress = SocketMediaOutput->IPAddress; + } + + if (SocketMediaOutput->Port > 0) + { + Port = (uint32)SocketMediaOutput->Port; + } + + ISocketSubsystem *PlatformSockets = ISocketSubsystem::Get(); + if (PlatformSockets == nullptr) + { + UE_LOG(LogTemp, Warning, TEXT("Failed to access platform sockets")); + return false; + } + bool bIsValid; + TSharedPtr Endpoint = PlatformSockets->CreateInternetAddr(); + Endpoint->SetIp(*IPAddress, bIsValid); + Endpoint->SetPort(Port); + if (!bIsValid) + { + UE_LOG(LogTemp, Warning, TEXT("Failed to set ip and port")); + return false; + } + // UE_LOG(LogTemp, Warning, TEXT("TCP:connect %s"), *Endpoint->ToString(true)); + } + + return true; +} + +TArray USocketMediaCapture::AsProcessArgs(FIntPoint Size, FFrameRate Framerate, FString IPAddress, uint32 Port) +{ + USocketMediaOutput *SocketMediaOutput = CastChecked(MediaOutput); + + FString inputFormat; + inputFormat = TEXT("-f rawvideo -pixel_format rgba"); + if (SocketMediaOutput->PixelFormat == ESocketMediaOutputPixelFormat::MATTE) + { + inputFormat = TEXT("-f rawvideo -pixel_format gray"); + } + else if (SocketMediaOutput->PixelFormat == ESocketMediaOutputPixelFormat::RGBA) + { + inputFormat = TEXT("-f rawvideo -pixel_format rgba"); + // Untested for A2RGB10 - currently an ffmpeg compile-time: + // -pixel_format x2rgb10 + // still fresh: https://master.gitmailbox.com/ffmpegdev/20220218055012.916556-1-wenbin.chen@intel.com/T/ + } + else if (SocketMediaOutput->PixelFormat == ESocketMediaOutputPixelFormat::UYVY) + { + // Preferred output format as this is an animation + inputFormat = TEXT("-f rawvideo -pixel_format uyvy422"); + } + else if (SocketMediaOutput->PixelFormat == ESocketMediaOutputPixelFormat::V210) + { + // Library of congress notes v210 for analog capture + // incorrect, will lead to double image and flicker: + // inputFormat = TEXT("-f rawvideo -pixel_format yuv422p10le -c:v v210"); + // V210 is the 10-bit standard input for production capture cards in Unreal + inputFormat = TEXT("-f v210"); + } + + FString inputFramerate = FString::Printf(TEXT("%d/%d"), Framerate.Numerator, Framerate.Denominator); + + FString OutputMov = FPaths::ConvertRelativePathToFull(FPaths::Combine(*FPaths::ProjectSavedDir(), *FString("output.mp4"))); + if (!SocketMediaOutput->OutputFilename.IsEmpty()) + { + OutputMov = SocketMediaOutput->OutputFilename; + } + + TArray args; + args.Add(FStringFormatArg(inputFormat)); + args.Add(FStringFormatArg(Size.X)); + args.Add(FStringFormatArg(Size.Y)); + args.Add(FStringFormatArg(inputFramerate)); + args.Add(FStringFormatArg(IPAddress)); + args.Add(FStringFormatArg(Port)); + args.Add(FStringFormatArg(OutputMov)); + + return args; +} + +static FORCEINLINE bool CreatePipeWrite(void *&ReadPipe, void *&WritePipe) +{ +#if PLATFORM_WINDOWS + SECURITY_ATTRIBUTES Attr = {sizeof(SECURITY_ATTRIBUTES), NULL, true}; + + if (!::CreatePipe(&ReadPipe, &WritePipe, &Attr, 0)) + { + return false; + } + + if (!::SetHandleInformation(WritePipe, HANDLE_FLAG_INHERIT, 0)) + { + return false; + } + + return true; +#else + return FPlatformProcess::CreatePipe(ReadPipe, WritePipe); +#endif // PLATFORM_WINDOWS +} + +bool USocketMediaCapture::SpawnProcessBlocking(uint32 *ProcessId, TArray FormatArgs, FString ExpectStr) +{ + USocketMediaOutput *SocketMediaOutput = Cast(MediaOutput); + + // FIXME: Not working out for writing to an anonymous pipe (stdin of ffmpeg) + bool bHasTcpEndpoint = !SocketMediaOutput->NoNetwork; + // This defaults to a null output for reporting input framerate/bandwidth: + FString FfmpegParams = FString::Printf(TEXT("-y -report -vsync 0 -hwaccel auto {0} -video_size {1}x{2} -framerate {3} -i %s -f null {6}"), bHasTcpEndpoint ? TEXT("tcp://{4}:{5}?listen") : TEXT("-")); + + FString ExecutableParamsTemplate = SocketMediaOutput->ExecutableParams.IsEmpty() ? FfmpegParams : SocketMediaOutput->ExecutableParams; + FString Params = FString::Format(*ExecutableParamsTemplate, FormatArgs); + + // FFmpeg specific defaults + +#if PLATFORM_WINDOWS + FString URL = SocketMediaOutput->ExecutablePath.IsEmpty() ? TEXT("C:\\ffmpeg\\bin\\ffmpeg.exe") : SocketMediaOutput->ExecutablePath; +#else + FString URL = SocketMediaOutput->ExecutablePath.IsEmpty() ? TEXT("/usr/local/bin/ffmpeg") : SocketMediaOutput->ExecutablePath; +#endif + + if (!FPlatformProcess::CreatePipe(StderrReadPipe, StderrWritePipe)) + { + UE_LOG(LogTemp, Warning, TEXT("Failed to create stdout pipe")); + return false; + } + + if (!bHasTcpEndpoint) + { + // Needs: bLaunchDetached=True on create proc + if (!CreatePipeWrite(StdinReadPipe, StdinWritePipe)) + { + UE_LOG(LogTemp, Warning, TEXT("Failed to create stdin pipe")); + return false; + } + } + + bool bHasReport = !SocketMediaOutput->NoReport; + if (bHasReport) + { +// UE_LOG(LogTemp, Warning, TEXT("Reporting of my dreams")); +#if PLATFORM_WINDOWS + // The colon in drive paths causes file= to fail on absolute paths + FPlatformMisc::SetEnvironmentVar(TEXT("FFREPORT"), TEXT("file=output.log")); +#else + // OS X PIE does not seem to respect the working folder but absolute paths work + FString OutputLog = FPaths::ConvertRelativePathToFull(FPaths::Combine(*FPaths::ProjectSavedDir(), *FString("output.log"))); + FPlatformMisc::SetEnvironmentVar(TEXT("FFREPORT"), *FString::Printf(TEXT("file=%s"), *OutputLog)); +#endif + } + + UE_LOG(LogTemp, Warning, TEXT("Launch process: %s %s"), *URL, *Params); + + bool bHiddenExecutable = !SocketMediaOutput->NoHiddenExecutable; + int32 PriorityModifier = 2; // High. (2 is Higher) + ProcessHandle = FPlatformProcess::CreateProc(*URL, *Params, !bHasTcpEndpoint || !bHiddenExecutable, bHiddenExecutable, bHiddenExecutable, + ProcessId, PriorityModifier, *FPaths::ProjectSavedDir(), StderrWritePipe, StdinReadPipe); + + if (!ProcessHandle.IsValid()) + { + UE_LOG(LogTemp, Warning, TEXT("Failed to launch process.")); + return false; + } + + if (ExpectStr.IsEmpty()) + { + return true; + } + + // Sleep, because WaitForWrite does not seem to block for ffmpeg readiness in OS X + // and cut ffmpeg a little more slack for gpu probe / other startup activities + for (uint8 Attempts = 0; Attempts < 10; Attempts++) + { + FString TmpStdout = FPlatformProcess::ReadPipe(StderrReadPipe); + if (!TmpStdout.IsEmpty()) + { + if (TmpStdout.Contains(*ExpectStr)) + { + UE_LOG(LogTemp, Warning, TEXT("Process ready via stdout after waiting #%d: %s"), Attempts, *TmpStdout); + break; + } + } + else if (Attempts == 9) + { + UE_LOG(LogTemp, Warning, TEXT("Process stdout not ready, continuing anyway after attempt #%d: %s"), Attempts, *TmpStdout); + } + FPlatformProcess::Sleep(0.05); + } + FPlatformProcess::Sleep(0.1); + return true; +} + +// TODO: See if this is needed - we block anyway as we wait for the process to terminate +// virtual bool HasFinishedProcessing() const; + +// From FunctionalUIScreenshotTest and MediaCapture +void USocketMediaCapture::GetBackbufferInfo(const FSceneViewport *InViewport, EPixelFormat *OutPixelFormat, bool *OutIsSRGB, FIntPoint *Size) +{ + + /* + // Or if an actual texture as provided, as in 4.26 custom capture: + FTextureRenderTargetResource* InTextureRenderTargetResource + SourceTexture = InTextureRenderTargetResource->GetTextureRenderTarget2DResource()->GetTextureRHI(); + */ + + /** + * #if WITH_EDITOR + * if (!IsRunningGame()) + * { + */ + FTexture2DRHIRef SourceTexture = InViewport->GetRenderTargetTexture(); + if (SourceTexture.IsValid()) + { + *OutPixelFormat = SourceTexture->GetFormat(); + *OutIsSRGB = (SourceTexture->GetFlags() & TexCreate_SRGB) == TexCreate_SRGB; + *Size = SourceTexture->GetSizeXY(); + return; + } + + if (!InViewport->GetViewportRHI()) + { + return; + } + + ENQUEUE_RENDER_COMMAND(GetBackbufferFormatCmd) + ( + [InViewport, OutPixelFormat, OutIsSRGB, Size](FRHICommandListImmediate &RHICmdList) + { + FViewportRHIRef ViewportRHI = InViewport->GetViewportRHI(); + if (!ViewportRHI.IsValid()) + { + return; + } + FTexture2DRHIRef BackbufferTexture = RHICmdList.GetViewportBackBuffer(ViewportRHI); + if (!BackbufferTexture.IsValid()) + { + return; + } + *OutPixelFormat = BackbufferTexture->GetFormat(); + *OutIsSRGB = (BackbufferTexture->GetFlags() & TexCreate_SRGB) == TexCreate_SRGB; + *Size = BackbufferTexture->GetSizeXY(); + }); + FlushRenderingCommands(); +} + +// TCP / Process state is better stuffed into the Output class as the Socket / Process connections could be long-lived +// and they also require setup/teardown which have delays. +// However, all MediaOutput classes are reserved in conventional use as parameter stashes and nothing more. + +EPixelFormat USocketMediaCapture::GetPixelFormat() +{ + return SourcePixelFormat; +} + +bool USocketMediaCapture::CaptureSceneViewportImpl(TSharedPtr &InSceneViewport) +{ + + const auto CVar = IConsoleManager::Get().FindTConsoleVariableDataInt(TEXT("r.VSync")); + bool bLockToVsync = CVar->GetValueOnGameThread() != 0; + if (bLockToVsync) + { + UE_LOG(LogTemp, Warning, TEXT("VSync is set to on which may result in reduced frame rate rendering for capture.")); + } + + USocketMediaOutput *SocketMediaOutput = CastChecked(MediaOutput); + OutputPixelFormat = SocketMediaOutput->PixelFormat; // Workaround alpha matte challenges + SocketMediaOutput->SourcePixelFormat = EPixelFormat::PF_B8G8R8A8; // Workaround standalone mode issue + + // Really difficult to get to: InMediaCapture->DesiredOutputSize.X / Y + // seems required when the command line flag -forceres is not in use + // FIntPoint Size = InSceneViewport->GetRenderTargetTextureSizeXY(); + FIntPoint Size = InSceneViewport->GetSizeXY(); + + EPixelFormat PixelFormat = PF_A2B10G10R10; + bool bIsSRGB = false; + // Standalone may have a different buffer than the default backing buffer; force them to meet. + GetBackbufferInfo(&*InSceneViewport, &PixelFormat, &bIsSRGB, &Size); + + if (SocketMediaOutput->PixelFormat == ESocketMediaOutputPixelFormat::MATTE) + { + // PixelFormat = EPixelFormat::PF_B8G8R8A8; + } + + // Pixel format can mean many different things, the enum is overloaded. + SourcePixelFormat = PixelFormat; + + UE_LOG(LogTemp, Warning, TEXT("Enum PixelFormat: %s"), GetPixelFormatString(SourcePixelFormat)); + + // Currently on GetState == EMediaCaptureState::Preparing; + + // SocketMediaOutput->DesiredPixelFormat // Bypass???? + // if (InMediaCapture->DesiredPixelFormat != SourceTexture->GetFormat()) + + bool bHasProcess = !SocketMediaOutput->NoExecutable; + bool bHasReport = !SocketMediaOutput->NoReport; + bool bHasTcpEndpoint = !SocketMediaOutput->NoNetwork; + bool bHiddenExecutable = !SocketMediaOutput->NoHiddenExecutable; + + // Used throughout the methods + bHasAsyncQueue = !SocketMediaOutput->NoAsync && FPlatformProcess::SupportsMultithreading(); + + // For on-cpu bit shifting. + + // One channel of 8 bits per pixel. + if (OutputPixelFormat == ESocketMediaOutputPixelFormat::MATTE) + { + PixelStrideBytes = 4; + PacketSize = Size.X * Size.Y; + OutputPacketBuffer = reinterpret_cast(FMemory::Malloc(PacketSize)); + } + + // RGBA 32 bit word, one pixel + if (OutputPixelFormat == ESocketMediaOutputPixelFormat::RGBA) + { + PixelStrideBytes = 4; + PacketSize = Size.X * Size.Y * PixelStrideBytes; + } + + // UYVY 32 bit word, two pixels + if (OutputPixelFormat == ESocketMediaOutputPixelFormat::UYVY) + { + PixelStrideBytes = 4; + PacketSize = Size.X / 2 * Size.Y * PixelStrideBytes; + } + + // V210 - 128 bit word, 6 pixels - Code from upstream (floor, aligned to 128 bit words) + if (OutputPixelFormat == ESocketMediaOutputPixelFormat::V210) + { + PixelStrideBytes = 16; + PacketSize = ((((Size.X + 47) / 48) * 48) / 6) * Size.Y * PixelStrideBytes; + // When on a 1280 width on window, seeing ceil or an extra padding, e.g: + // (math.ceil((1280 + 47) / 48) * 48) / 6 + } + + FFrameRate Framerate = FApp::GetTimecodeFrameRate(); + + FString IPAddress = FString("127.0.0.1"); + uint32 Port = 4445; + + if (bHasTcpEndpoint) + { + if (!SocketMediaOutput->IPAddress.IsEmpty()) + { + IPAddress = SocketMediaOutput->IPAddress; + } + + if (SocketMediaOutput->Port > 0) + { + Port = (uint32)SocketMediaOutput->Port; + } + } + + // Interprocess pipe, Stdout/Stderr, with Stdin if tcp is disabled + uint32 ProcessId = 0; + if (bHasProcess) + { + FString ExpectFfmpegOutputContains = FString("ffmpeg started on"); + if (!SpawnProcessBlocking(&ProcessId, AsProcessArgs(Size, Framerate, IPAddress, Port), ExpectFfmpegOutputContains)) + { + return false; + } + } + + if (bHasTcpEndpoint) + { + bool bIsValid; + ISocketSubsystem *PlatformSockets = ISocketSubsystem::Get(); + TSharedPtr Endpoint = PlatformSockets->CreateInternetAddr(); + Endpoint->SetIp(*IPAddress, bIsValid); + Endpoint->SetPort(Port); + + FString SocketDesc = FString("Image Stream"); + Socket = FTcpSocketBuilder(SocketDesc).AsBlocking().Build(); + Socket->SetNoDelay(true); + Socket->Connect(*Endpoint); + + // FIXME: Wait may return immediately on OS X, check state too + // for(uint8 Attempts = 0; Attempts < 10; Attempts++) + // if Socket->GetConnectionState() == ESocketConnectionState::SCS_Connected break; + UE_LOG(LogTemp, Warning, TEXT("Waiting for Socket")); + if (Socket->Wait(ESocketWaitConditions::WaitForWrite, FTimespan::FromMilliseconds(30))) + { + UE_LOG(LogTemp, Warning, TEXT("Socket ready")); + } + else + { + UE_LOG(LogTemp, Warning, TEXT("Socket not yet ready, continuing anyway")); + } + } + + if (bHasAsyncQueue) + { + UE_LOG(LogTemp, Warning, TEXT("Booting output buffer")); + SocketMediaCaptureSink *OutputBuffer = new SocketMediaCaptureSink(Socket, StdinWritePipe, PacketSize); + this->Sink = (void *)OutputBuffer; + OutputBuffer->StartListening(); + UE_LOG(LogTemp, Warning, TEXT("Booted output buffer")); + } + + if (bHasProcess) + { + FString StdoutErr = ""; + StdoutErr = FPlatformProcess::ReadPipe(StderrReadPipe); + if (bHasTcpEndpoint && Socket->GetConnectionState() != ESocketConnectionState::SCS_Connected) + { + UE_LOG(LogTemp, Warning, TEXT("FFMpeg Local Tcp Endpoint unreachable")); + } + else + { + if (FPlatformProcess::IsProcRunning(ProcessHandle)) + { + UE_LOG(LogTemp, Warning, TEXT("FFMpeg Boot: %s"), *StdoutErr); + SetState(EMediaCaptureState::Capturing); + return true; + } + UE_LOG(LogTemp, Warning, TEXT("Process stopped unexpectedly")); + } + if (bHasTcpEndpoint) + { + ISocketSubsystem *PlatformSockets = ISocketSubsystem::Get(); + PlatformSockets->DestroySocket(Socket); + } + + UE_LOG(LogTemp, Warning, TEXT("Process failed to boot: %s"), *StdoutErr); + if (FPlatformProcess::IsProcRunning(ProcessHandle)) + { + FPlatformProcess::TerminateProc(ProcessHandle, true); + } + FPlatformProcess::CloseProc(ProcessHandle); + } + else if (bHasTcpEndpoint) + { + if (Socket->GetConnectionState() == ESocketConnectionState::SCS_Connected) + { + SetState(EMediaCaptureState::Capturing); + return true; + } + + ISocketSubsystem *PlatformSockets = ISocketSubsystem::Get(); + PlatformSockets->DestroySocket(Socket); + + if (Socket->GetConnectionState() == ESocketConnectionState::SCS_ConnectionError) + { + UE_LOG(LogTemp, Warning, TEXT("Socket connection error: tcp://%s:%d"), *IPAddress, Port); + } + else + { + UE_LOG(LogTemp, Warning, TEXT("Socket failed unexpectedly: tcp://%s:%d"), *IPAddress, Port); + } + } + + if (OutputPixelFormat == ESocketMediaOutputPixelFormat::MATTE) + { + FMemory::Free(OutputPacketBuffer); + } + + SetState(EMediaCaptureState::Error); + return false; +} + +// NOTES: Event override has to block until complete brecause: +// - Parent StopCapture runs SetState Stopped before calling StopCaptureImpl +// - Seems we are not hooked into SetState(EMediaCaptureState::StopRequested); +// - So we can not ourselves trigger appropriately SetState(EMediaCaptureState::Stopped); + +void USocketMediaCapture::StopCaptureImpl(bool bAllowPendingFrameToBeProcess) +{ + + if (bAllowPendingFrameToBeProcess) + { + UE_LOG(LogTemp, Warning, TEXT("Pending frame before shutdown")); + return; + } + + if (OutputPacketBuffer != nullptr) + { + FMemory::Free(OutputPacketBuffer); + OutputPacketBuffer = nullptr; + } + + UE_LOG(LogTemp, Warning, TEXT("Shutting down process")); + if (Sink != nullptr) + { + UE_LOG(LogTemp, Warning, TEXT("Shutting down locked thread")); + SocketMediaCaptureSink *OutputBuffer = (SocketMediaCaptureSink *)Sink; + + UE_LOG(LogTemp, Warning, TEXT("Waiting on drain thread to complete")); + OutputBuffer->StopListening(); + delete static_cast(Sink); + UE_LOG(LogTemp, Warning, TEXT("Drain thread has completed")); + } + + if (Socket != nullptr) + { + UE_LOG(LogTemp, Warning, TEXT("Shutting down tcp socket: wait time")); + Socket->Wait(ESocketWaitConditions::WaitForWrite, FTimespan::FromMilliseconds(1500)); + UE_LOG(LogTemp, Warning, TEXT("Shutting down tcp socket: shutdown time")); + Socket->Shutdown(ESocketShutdownMode::ReadWrite); + UE_LOG(LogTemp, Warning, TEXT("Shutting down tcp socket: close time")); + Socket->Close(); + ISocketSubsystem *PlatformSockets = ISocketSubsystem::Get(); + PlatformSockets->DestroySocket(Socket); + UE_LOG(LogTemp, Warning, TEXT("Socket closed")); + } + + if (StdinReadPipe != nullptr) + { + UE_LOG(LogTemp, Warning, TEXT("Shutting down process input pipes")); + FPlatformProcess::ClosePipe(StdinReadPipe, StdinWritePipe); + StdinReadPipe = nullptr; + StdinWritePipe = nullptr; + } + + if (StderrReadPipe != nullptr) + { + UE_LOG(LogTemp, Warning, TEXT("Shutting down process output pipes")); + int wait_lock = 0; + while (!FPlatformProcess::ReadPipe(StderrReadPipe).IsEmpty()) + { + // Flush for awhile + // UE_LOG(LogTemp, Warning, TEXT("FFmpeg: %s"), *StdoutErr); + if (wait_lock++ > 10) + { + UE_LOG(LogTemp, Warning, TEXT("Stopped waiting for process output")); + break; + } + } + FPlatformProcess::ClosePipe(StderrReadPipe, StderrWritePipe); + StderrReadPipe = nullptr; + StderrWritePipe = nullptr; + } + + if (ProcessHandle.IsValid()) + { + // FIXME: This has no timeout, could freeze + UE_LOG(LogTemp, Warning, TEXT("Closing process")); + int32 ReturnCode = -1; + // WaitForProc has no timeout - can easily deadlock the process + FPlatformProcess::WaitForProc(ProcessHandle); + FPlatformProcess::GetProcReturnCode(ProcessHandle, &ReturnCode); + FPlatformProcess::CloseProc(ProcessHandle); + if (ReturnCode != 0) + { + UE_LOG(LogTemp, Warning, TEXT("FFmpeg unsuccessful return code %d"), ReturnCode); + } + UE_LOG(LogTemp, Warning, TEXT("Closed proc ffmpeg")); + if (FPlatformProcess::IsProcRunning(ProcessHandle)) + { + FPlatformProcess::TerminateProc(ProcessHandle, true); + UE_LOG(LogTemp, Warning, TEXT("Forced ffmpeg to stop.")); + } + ProcessHandle.Reset(); + // Seems like logging still does not flush to disk; give it a pause. + FPlatformProcess::Sleep(0.05); + UE_LOG(LogTemp, Warning, TEXT("done sleeping, releasing.")); + } +} + +/** + * This is guaranteed to be a single thread, in-order, on the render thread path. + */ + +void USocketMediaCapture::OnFrameCaptured_RenderingThread(const FCaptureBaseData &InBaseData, + TSharedPtr InUserData, + void *InBuffer, int32 Width, int32 Height +#if ENGINE_MAJOR_VERSION > 4 + , + int32 BytesPerRow +#endif +) +{ + if (Sink == nullptr) + { + return; + } + + if (GetState() != EMediaCaptureState::Capturing) + { + return; + } + + SocketMediaCaptureSink *OutputBuffer = (SocketMediaCaptureSink *)Sink; + if (OutputBuffer->StopTaskFlag) + { + UE_LOG(LogTemp, Warning, TEXT("Skipping frame on stopped capture")); + return; + } + + // SourceFrameNumber is available as of 4.24 + // if(InBaseData.SourceFrameNumber < 4) { + // UE_LOG(LogTemp, Warning, TEXT("packet size: %d height x width %d x %d"), PacketSize, Height, Width); + // UE_LOG(LogTemp, Warning, TEXT("Start capture frame: %d with pending %d"), InBaseData.SourceFrameNumber, PendingFrames.GetValue()); + //} + + // Copy out a GRAY8 buffer. ffmpeg has x2rgb10. + // support for a2rgb10 is still cutting edge, with hevc support only in OS X and in nvidia drivers. some othermovement here: + // https://github.com/intel-media-ci/ffmpeg/pull/219 + // https://patchwork.ffmpeg.org/project/ffmpeg/patch/20190910093526.13442-1-zachary.zhou@intel.com/ + // ffmpeg does like to use rgba8, vp9 has support: + // https://stackoverflow.com/questions/66769652/ue4-capture-frame-using-id3d11texture2d-and-convert-to-r8g8b8-bitmap + + if (OutputPixelFormat == ESocketMediaOutputPixelFormat::MATTE) + { + // PixelFormat == PF_FloatRGBA + if (SourcePixelFormat == PF_B8G8R8A8) + { + FColor *PixelBuffer = reinterpret_cast(InBuffer); + for (int32 i = 0; i < PacketSize; i++) + { + *(OutputPacketBuffer + i) = PixelBuffer[i].A; // (uint8) (PixelBuffer[i] & 0x000000ff); + } + } + else if (SourcePixelFormat == PF_A2B10G10R10) + { + // Assuming directx, this is likely: DXGI_FORMAT_R10G10B10A2_UNORM + // a 2-bit UNORM represents 0.0f, 1/3, 2/3, and 1.0f. + // use uint32* for portability. + // https://docs.microsoft.com/en-us/windows/win32/api/dxgiformat/ne-dxgiformat-dxgi_format + uint32 *PixelBuffer = reinterpret_cast(InBuffer); + for (int32 i = 0; i < PacketSize; i++) + { + // uint8 alpha = (uint8)((PixelBuffer[i] << 30) >> 24); + uint8 alpha = (uint8)((PixelBuffer[i] >> 30) << 6); + *(OutputPacketBuffer + i) = alpha | (alpha >> 2) | (alpha >> 4) | (alpha >> 6); + } + } + + if (!OutputBuffer->EnqueuePacket((void *)OutputPacketBuffer, bHasAsyncQueue)) + { + SetState(EMediaCaptureState::Error); + } + return; + } + + int32 FramePacketSize = 4 * Width * Height; + if (FramePacketSize != PacketSize) + { + // Resulting stream will be out of alignment - 4.24 downscales in PIE giving wrong size to StartCaptureImpl. + // In practice we may want to reinitialize our output pipe to our actual texture instead of our hope at initialization + // Width is set via private parent properties: + // InMediaCapture->DesiredOutputSize.X hitting the ReadyFrame->ReadbackTexture + UE_LOG(LogTemp, Warning, TEXT("packet re-size: from %d to %d"), PacketSize, FramePacketSize); + // UE_LOG(LogTemp, Warning, TEXT("Frame size: %d x %d @ %d == %d"), Width, Height, PixelStrideBytes, PacketSize); + PacketSize = FramePacketSize; + } + + if (StderrReadPipe != nullptr) + { + // Flush subprocess stderr/stdout or it will deadlock when the buffer fills + FPlatformProcess::ReadPipe(StderrReadPipe); + } + + if (!OutputBuffer->EnqueuePacket(InBuffer, bHasAsyncQueue)) + { + SetState(EMediaCaptureState::Error); + } +} diff --git a/UE4-27-0/Plugins/UnrealHelper/Source/UnrealMediaOutputSocket/Private/SocketMediaOutput.cpp b/UE4-27-0/Plugins/UnrealHelper/Source/UnrealMediaOutputSocket/Private/SocketMediaOutput.cpp new file mode 100644 index 0000000..b29bce3 --- /dev/null +++ b/UE4-27-0/Plugins/UnrealHelper/Source/UnrealMediaOutputSocket/Private/SocketMediaOutput.cpp @@ -0,0 +1,167 @@ +#include "SocketMediaOutput.h" +#include "SocketMediaCapture.h" + +#include "Misc/Paths.h" +#include "UnrealEngine.h" + +// Really not fun methods of finding the viewport + +#include "Slate/SceneViewport.h" +#include "Engine/GameEngine.h" +#if WITH_EDITOR +#include "Editor.h" +#include "Editor/EditorEngine.h" +#include "Engine/Engine.h" +#include "Engine/EngineTypes.h" +#endif + +// MediaCaptureDetails was stashed in MediaOutput.cpp +USocketMediaOutput::USocketMediaOutput() + : Super() +{ +} + +bool USocketMediaOutput::FindSceneViewportAndLevel(TSharedPtr &OutSceneViewport) const +{ +#if WITH_EDITOR + if (GIsEditor) + { + for (const FWorldContext &Context : GEngine->GetWorldContexts()) + { + if (Context.WorldType == EWorldType::PIE) + { + // UEditorEngine::GetPrivateStaticClass missing from link; plsfix + // UEditorEngine* EditorEngine = CastChecked(GEngine); + UEditorEngine *EditorEngine = (UEditorEngine *)(GEngine); + FSlatePlayInEditorInfo &Info = EditorEngine->SlatePlayInEditorMap.FindChecked(Context.ContextHandle); + if (Info.SlatePlayInEditorWindowViewport.IsValid()) + { + OutSceneViewport = Info.SlatePlayInEditorWindowViewport; + return true; + } + } + } + return false; + } + else +#endif + { + UGameEngine *GameEngine = CastChecked(GEngine); + OutSceneViewport = GameEngine->SceneViewport; + return true; + } +} + +// TODO: Add initialize option to check for resize, look into resize options +bool USocketMediaOutput::Validate(FString &OutFailureReason) const +{ + return true; +} + +/* + * Hard coded the logic: + * Uncertain as to why UMediaOutput::RequestCaptureSourceSize (FIntPoint::ZeroValue) + * did not fall back to FoundSceneViewport->GetSizeXY + * e.g. (GetRequestedSize() == UMediaOutput::RequestCaptureSourceSize) + */ +FIntPoint USocketMediaOutput::GetRequestedSize() const +{ + TSharedPtr FoundSceneViewport; + if (FindSceneViewportAndLevel(FoundSceneViewport)) + { + FIntPoint Size = FoundSceneViewport->GetRenderTargetTextureSizeXY(); + // MediaCapture should have handled this, but does not seem to in 4.22 + // FIntPoint SizeIsh = FoundSceneViewport->GetSizeXY(); + return Size; + } + // assert(FIntPoint::ZeroValue == UMediaOutput::RequestCaptureSourceSize); + return FIntPoint::ZeroValue; +} + +EPixelFormat USocketMediaOutput::GetRequestedPixelFormat() const +{ + // Trying again for matte processing in a2rgb10 + // if(PixelFormat == ESocketMediaOutputPixelFormat::MATTE) + //{ + // return EPixelFormat::PF_B8G8R8A8; + // } + + // ValidateSceneViewport seems to require this: + if (SourcePixelFormat == EPixelFormat::PF_Unknown) + { + static const auto CVarDefaultBackBufferPixelFormat = IConsoleManager::Get().FindTConsoleVariableDataInt(TEXT("r.DefaultBackBufferPixelFormat")); + return EDefaultBackBufferPixelFormat::Convert2PixelFormat(EDefaultBackBufferPixelFormat::FromInt(CVarDefaultBackBufferPixelFormat->GetValueOnAnyThread())); + } + + // Then CaptureSceneViewportImpl is run; which can update our Pixel Format to the correct one? + + // return EPixelFormat::PF_B8G8R8A8; + // return EPixelFormat::PF_A2B10G10R10; + /* + if(SourcePixelFormat == EPixelFormat::PF_Unknown) + { + // CaptureImpl is not set - this is all one big workaround + SourcePixelFormat = ((USocketMediaCapture*) CaptureImpl)->GetPixelFormat(); + } + */ + return SourcePixelFormat; + // This worked nicely except that Standalone mode can switch from rgb10 to rgb8 +} + +EMediaCaptureConversionOperation USocketMediaOutput::GetConversionOperation(EMediaCaptureSourceType InSourceType) const +{ + static const auto CVarDefaultBackBufferPixelFormat = IConsoleManager::Get().FindTConsoleVariableDataInt(TEXT("r.DefaultBackBufferPixelFormat")); + EPixelFormat SceneTargetFormat = EDefaultBackBufferPixelFormat::Convert2PixelFormat(EDefaultBackBufferPixelFormat::FromInt(CVarDefaultBackBufferPixelFormat->GetValueOnAnyThread())); + + // Same as the PNG / EXR encoder + if (PixelFormat == ESocketMediaOutputPixelFormat::MATTE) + { + return EMediaCaptureConversionOperation::INVERT_ALPHA; + } + if (PixelFormat == ESocketMediaOutputPixelFormat::RGBA) + { + return EMediaCaptureConversionOperation::NONE; + } + // This triggers a pixel shader to compress/alter the format, e.g. rgba to uyuv. + if (SceneTargetFormat == EPixelFormat::PF_A2B10G10R10) + { + if (PixelFormat == ESocketMediaOutputPixelFormat::V210) + { + UE_LOG(LogTemp, Warning, TEXT("V210 Capture Format")); + return EMediaCaptureConversionOperation::RGB10_TO_YUVv210_10BIT; + } + UE_LOG(LogTemp, Warning, TEXT("UYVY Capture Format")); + return EMediaCaptureConversionOperation::RGBA8_TO_YUV_8BIT; + // UE 4.23+ EMediaCaptureConversionOperation::CUSTOM + } + if (SceneTargetFormat == EPixelFormat::PF_B8G8R8A8) + { + if (PixelFormat == ESocketMediaOutputPixelFormat::V210) + { + UE_LOG(LogTemp, Warning, TEXT("V210 Capture Format Selected but Backing is RGBA8 - Falling to UYUV")); + } + return EMediaCaptureConversionOperation::RGBA8_TO_YUV_8BIT; + } + return EMediaCaptureConversionOperation::NONE; +} + +template +static FString EnumToString(const FString &enumName, const T value) +{ + UEnum *pEnum = FindObject(ANY_PACKAGE, *enumName); + return *(pEnum ? pEnum->GetNameStringByIndex(static_cast(value)) : "null"); +} + +UMediaCapture *USocketMediaOutput::CreateMediaCaptureImpl() +{ + if (PixelFormat == ESocketMediaOutputPixelFormat::MATTE) + { + DesiredPixelFormat = EPixelFormat::PF_B8G8R8A8; + } + UMediaCapture *Result = NewObject(); + if (Result) + { + Result->SetMediaOutput(this); + } + return Result; +} diff --git a/UE4-27-0/Plugins/UnrealHelper/Source/UnrealMediaOutputSocket/Private/UnrealMediaOutputSocket.cpp b/UE4-27-0/Plugins/UnrealHelper/Source/UnrealMediaOutputSocket/Private/UnrealMediaOutputSocket.cpp new file mode 100644 index 0000000..a10cce8 --- /dev/null +++ b/UE4-27-0/Plugins/UnrealHelper/Source/UnrealMediaOutputSocket/Private/UnrealMediaOutputSocket.cpp @@ -0,0 +1,18 @@ +#include "UnrealMediaOutputSocket.h" + +#define LOCTEXT_NAMESPACE "FUnrealMediaOutputSocketModule" + +void FUnrealMediaOutputSocketModule::StartupModule() +{ + // This code will execute after your module is loaded into memory; the exact timing is specified in the .uplugin file per-module +} + +void FUnrealMediaOutputSocketModule::ShutdownModule() +{ + // This function may be called during shutdown to clean up your module. For modules that support dynamic reloading, + // we call this function before unloading the module. +} + +#undef LOCTEXT_NAMESPACE + +IMPLEMENT_MODULE(FUnrealMediaOutputSocketModule, UnrealMediaOutputSocket) diff --git a/UE4-27-0/Plugins/UnrealHelper/Source/UnrealMediaOutputSocket/Public/SocketMediaCapture.h b/UE4-27-0/Plugins/UnrealHelper/Source/UnrealMediaOutputSocket/Public/SocketMediaCapture.h new file mode 100644 index 0000000..01099e0 --- /dev/null +++ b/UE4-27-0/Plugins/UnrealHelper/Source/UnrealMediaOutputSocket/Public/SocketMediaCapture.h @@ -0,0 +1,64 @@ +#pragma once + +#include "MediaCapture.h" +#include "Slate/SceneViewport.h" +#include "Runtime/Core/Public/GenericPlatform/GenericPlatformProcess.h" +#include "Runtime/Core/Public/GenericPlatform/GenericPlatformMisc.h" +#include "Runtime/Launch/Resources/Version.h" +#include "Runtime/Networking/Public/Common/TcpSocketBuilder.h" +#include "IPAddress.h" +#include "Sockets.h" +#include "SocketSubsystem.h" +#include "SocketMediaOutput.h" +#include "SocketMediaCapture.generated.h" + +UCLASS(BlueprintType) +class UNREALMEDIAOUTPUTSOCKET_API USocketMediaCapture : public UMediaCapture +{ + GENERATED_BODY() +protected: + virtual bool ValidateMediaOutput() const; + virtual bool CaptureSceneViewportImpl(TSharedPtr &InSceneViewport) override; + virtual void StopCaptureImpl(bool bAllowPendingFrameToBeProcess) override; + virtual void OnFrameCaptured_RenderingThread(const FCaptureBaseData &InBaseData, + TSharedPtr InUserData, + void *InBuffer, int32 Width, int32 Height +#if ENGINE_MAJOR_VERSION > 4 + , + int32 BytesPerRow +#endif + ) override; + +public: + EPixelFormat GetPixelFormat(); + +private: + EPixelFormat SourcePixelFormat; + ESocketMediaOutputPixelFormat OutputPixelFormat; + uint8 *OutputPacketBuffer = nullptr; + + void GetBackbufferInfo(const FSceneViewport *InViewport, EPixelFormat *OutPixelFormat, bool *OutIsSRGB, FIntPoint *Size); + + uint8 PixelStrideBytes; + // Still getting a different Width*Height from Standalone mode. + int32 PacketSize = 0; + + // Subprocess, Stderr and Stdout + FProcHandle ProcessHandle; + void *StderrReadPipe = nullptr; + void *StderrWritePipe = nullptr; + + // Stdin (vs TCP socket) + void *StdinReadPipe = nullptr; + void *StdinWritePipe = nullptr; + + TArray AsProcessArgs(FIntPoint Size, FFrameRate Framerate, FString IPAddress, uint32 Port); + bool SpawnProcessBlocking(uint32 *ProcessId, TArray FormatArgs, FString ExpectStr); + + // TCP Socket + FSocket *Socket; + + // Copy frame data from rendering thread + bool bHasAsyncQueue = false; + void *Sink = nullptr; +}; diff --git a/UE4-27-0/Plugins/UnrealHelper/Source/UnrealMediaOutputSocket/Public/SocketMediaOutput.h b/UE4-27-0/Plugins/UnrealHelper/Source/UnrealMediaOutputSocket/Public/SocketMediaOutput.h new file mode 100644 index 0000000..0784a4c --- /dev/null +++ b/UE4-27-0/Plugins/UnrealHelper/Source/UnrealMediaOutputSocket/Public/SocketMediaOutput.h @@ -0,0 +1,103 @@ +#pragma once + +#include "FileMediaOutput.h" +#include "Engine/RendererSettings.h" +#include "Slate/SceneViewport.h" +#include "SocketMediaOutput.generated.h" + +/* + * UYVY422 is two pixels packed into 4 bytes + * - Apple: 2yuv + * - FFmpeg: -pixel_format uyvy422 + * - Unreal: RGBA8_TO_YUV_8BIT + * + * V210 is four pixels packed into 16 bites + * - Apple: V210 + * - FFmpeg: -pixel_format yuv422p10le -c:v v210 + * -pix_fmt yuv422p10le -vcodec v210 + * - Unreal: RGB10_TO_YUVv210_10BIT + * + * RGBA is one pixel in 4 bytes with format by back buffer + * Unreal: PF_A2B10G10R10 + * - Nvidia: NV_ENC_BUFFER_FORMAT_ABGR10 + * - FFmpeg: -pix_fmt x2rgb10 + * - Apple: R210 + * - Kona: R10K + * Unreal: PF_B8G8R8A8 + * - Nvidia: NV_ENC_BUFFER_FORMAT_ABGR + * - FFmpeg: -pixel_format bgra + * For ffmpeg see ffmpeg -pix_fmts + */ + +UENUM(BlueprintType) +enum class ESocketMediaOutputPixelFormat : uint8 +{ + UYVY UMETA(DisplayName = "UYVY"), + V210 UMETA(DisplayName = "V210"), + RGBA UMETA(DisplayName = "RGBA"), + MATTE UMETA(DisplayName = "MATTE") +}; + +/** + * Output information for a socket media capture. + */ +UCLASS(BlueprintType) +class UNREALMEDIAOUTPUTSOCKET_API USocketMediaOutput : public UMediaOutput +{ + GENERATED_BODY() +public: + USocketMediaOutput(); + +public: + UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Network", meta = (DisplayName = "IP Address", MakeStructureDefaultValue = "127.0.0.1")) + FString IPAddress; + + UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Network", meta = (ClampMin = "0", ClampMax = "65535", MakeStructureDefaultValue = "4445")) + int32 Port; + + UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Process", meta = (DisplayName = "No Background Process")) + bool NoExecutable; + + UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Process", meta = (DisplayName = "Show Background Process Window")) + bool NoHiddenExecutable; + + UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Process", meta = (DisplayName = "No FFMpeg Report")) + bool NoReport; + + UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Process", meta = (DisplayName = "No TCP Network Pipe")) + bool NoNetwork; + + UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Process", meta = (DisplayName = "No Async Queue")) + bool NoAsync; + + UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Process", meta = (MakeStructureDefaultValue = "output.mkv")) + FString OutputFilename; + + UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Process") + FString ExecutablePath; + + UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Process") + FString ExecutableParams; + + UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Media", meta = (MakeStructureDefaultValue = "UYUV")) + ESocketMediaOutputPixelFormat PixelFormat; + + // Kind of broken: + EPixelFormat DesiredPixelFormat; + +public: + // primary viewport to match GetRequestedSize to back buffer resolution + bool FindSceneViewportAndLevel(TSharedPtr &OutSceneViewport) const; + virtual bool Validate(FString &FailureReason) const override; + virtual FIntPoint GetRequestedSize() const override; + virtual EPixelFormat GetRequestedPixelFormat() const override; + virtual EMediaCaptureConversionOperation GetConversionOperation(EMediaCaptureSourceType InSourceType) const override; + +protected: + virtual UMediaCapture *CreateMediaCaptureImpl() override; + UMediaCapture *CreateMediaCapture(); + +public: + // Work-around the pixel format check in MediaCapture + EPixelFormat SourcePixelFormat; +}; diff --git a/UE4-27-0/Plugins/UnrealHelper/Source/UnrealMediaOutputSocket/Public/UnrealMediaOutputSocket.h b/UE4-27-0/Plugins/UnrealHelper/Source/UnrealMediaOutputSocket/Public/UnrealMediaOutputSocket.h new file mode 100644 index 0000000..02e7212 --- /dev/null +++ b/UE4-27-0/Plugins/UnrealHelper/Source/UnrealMediaOutputSocket/Public/UnrealMediaOutputSocket.h @@ -0,0 +1,13 @@ +#pragma once + +#include "CoreMinimal.h" +#include "Modules/ModuleManager.h" + +class FUnrealMediaOutputSocketModule : public IModuleInterface +{ +public: + + /** IModuleInterface implementation */ + virtual void StartupModule() override; + virtual void ShutdownModule() override; +}; diff --git a/UE4-27-0/Plugins/UnrealHelper/Source/UnrealMediaOutputSocket/UnrealMediaOutputSocket.Build.cs b/UE4-27-0/Plugins/UnrealHelper/Source/UnrealMediaOutputSocket/UnrealMediaOutputSocket.Build.cs new file mode 100644 index 0000000..c14a9ee --- /dev/null +++ b/UE4-27-0/Plugins/UnrealHelper/Source/UnrealMediaOutputSocket/UnrealMediaOutputSocket.Build.cs @@ -0,0 +1,28 @@ +using UnrealBuildTool; + +public class UnrealMediaOutputSocket : ModuleRules +{ + public UnrealMediaOutputSocket(ReadOnlyTargetRules Target) : base(Target) + { + PCHUsage = ModuleRules.PCHUsageMode.UseExplicitOrSharedPCHs; + + PublicDependencyModuleNames.AddRange( + new string[] + { + "Core", + "MediaIOCore", + "RenderCore", + "RHI", + "Networking", + "Sockets" + } + ); + PrivateDependencyModuleNames.AddRange( + new string[] + { + "CoreUObject", + "Engine", + } + ); + } +} diff --git a/UE4-27-0/Plugins/UnrealHelper/Source/UnrealWebSockets/Private/UnrealWebSocket.cpp b/UE4-27-0/Plugins/UnrealHelper/Source/UnrealWebSockets/Private/UnrealWebSocket.cpp new file mode 100644 index 0000000..506c8d6 --- /dev/null +++ b/UE4-27-0/Plugins/UnrealHelper/Source/UnrealWebSockets/Private/UnrealWebSocket.cpp @@ -0,0 +1,112 @@ +#include "UnrealWebSocket.h" + +UUnrealWebSocket::UUnrealWebSocket(const FObjectInitializer& ObjectInitializer) +: Super(ObjectInitializer) +{ + WebSocket = nullptr; + ReadyState = EWebSocketReadyState::CLOSED; +} + + +void UUnrealWebSocket::PostInitProperties() +{ + Super::PostInitProperties(); + FWebSocketsModule& Module = FModuleManager::LoadModuleChecked(TEXT("WebSockets")); + Protocols.Add(TEXT("ws")); + Protocols.Add(TEXT("wss")); +} + +void UUnrealWebSocket::PostLoad() +{ + Super::PostLoad(); + +} + +void UUnrealWebSocket::Connect(const FString& Url) +{ + WebSocket = FWebSocketsModule::Get().CreateWebSocket(Url, Protocols); + + // https://html.spec.whatwg.org/multipage/web-sockets.html#the-websocket-interface + if (WebSocket.IsValid()) + { + ReadyState = EWebSocketReadyState::CONNECTING; + + WebSocket->OnConnected().AddWeakLambda(this, [this]() -> void { + + ReadyState = EWebSocketReadyState::OPEN; + OnOpen.Broadcast(); + + }); + + WebSocket->OnConnectionError().AddWeakLambda(this, [this](const FString& Error) -> void { + + ReadyState = EWebSocketReadyState::CLOSED; + OnError.Broadcast(Error); + + }); + + WebSocket->OnClosed().AddWeakLambda(this, [this](int32 StatusCode, const FString Reason, bool bWasClean) -> void { + + ReadyState = EWebSocketReadyState::CLOSED; + OnClosed.Broadcast(Reason); + Cleanup(); + + }); + + WebSocket->OnMessage().AddWeakLambda(this, [this](const FString& Message) -> void { + + OnMessage.Broadcast(Message); + + }); + + WebSocket->Connect(); + } + else + { + ReadyState = EWebSocketReadyState::CLOSED; + OnError.Broadcast(FString("Unable to Create WebSocket")); + } + +} + +void UUnrealWebSocket::Close(int32 Code, const FString& Reason) +{ + if (WebSocket.IsValid() && WebSocket->IsConnected()) + { + ReadyState = EWebSocketReadyState::CLOSING; + WebSocket->Close(); + } +} + +void UUnrealWebSocket::Send(const FString& Data) +{ + if (WebSocket.IsValid() && WebSocket->IsConnected()) + { + WebSocket->Send(Data); + } +} + +void UUnrealWebSocket::SendBuffer(const TArray& Data) +{ + if (WebSocket.IsValid() && WebSocket->IsConnected()) + { + WebSocket->Send(Data.GetData(), Data.GetAllocatedSize(), true); + } +} + +void UUnrealWebSocket::Cleanup() +{ + if (WebSocket.IsValid()) + { + WebSocket->OnConnected().RemoveAll(this); + WebSocket->OnConnectionError().RemoveAll(this); + WebSocket->OnMessage().RemoveAll(this); + WebSocket->OnClosed().RemoveAll(this); + WebSocket.Reset(); + } +} + +void UUnrealWebSocket::BeginDestroy() +{ + Super::BeginDestroy(); +} diff --git a/UE4-27-0/Plugins/UnrealHelper/Source/UnrealWebSockets/Private/UnrealWebSockets.cpp b/UE4-27-0/Plugins/UnrealHelper/Source/UnrealWebSockets/Private/UnrealWebSockets.cpp new file mode 100644 index 0000000..8d3c050 --- /dev/null +++ b/UE4-27-0/Plugins/UnrealHelper/Source/UnrealWebSockets/Private/UnrealWebSockets.cpp @@ -0,0 +1,24 @@ +// Copyright 2020 Disney Direct-to-Consumer and International. All Rights Reserved. + +#include "UnrealWebSockets.h" + +#define LOCTEXT_NAMESPACE "FUnrealWebSocketsModule" + +void FUnrealWebSocketsModule::StartupModule() +{ + // This code will execute after your module is loaded into memory; the exact timing is specified in the .uplugin file per-module + +} + +void FUnrealWebSocketsModule::ShutdownModule() +{ + // This function may be called during shutdown to clean up your module. For modules that support dynamic reloading, + // we call this function before unloading the module. + //UE_LOG("") + //FModuleManager::UnloadModule("WebSockets"); + +} + +#undef LOCTEXT_NAMESPACE + +IMPLEMENT_MODULE(FUnrealWebSocketsModule, UnrealWebSockets) \ No newline at end of file diff --git a/UE4-27-0/Plugins/UnrealHelper/Source/UnrealWebSockets/Private/UnrealWebSocketsBPLibrary.cpp b/UE4-27-0/Plugins/UnrealHelper/Source/UnrealWebSockets/Private/UnrealWebSocketsBPLibrary.cpp new file mode 100644 index 0000000..8d9a736 --- /dev/null +++ b/UE4-27-0/Plugins/UnrealHelper/Source/UnrealWebSockets/Private/UnrealWebSocketsBPLibrary.cpp @@ -0,0 +1,14 @@ +#include "UnrealWebSocketsBPLibrary.h" +#include "UnrealWebSockets.h" + +UUnrealWebSocketsBPLibrary::UUnrealWebSocketsBPLibrary(const FObjectInitializer& ObjectInitializer) +: Super(ObjectInitializer) +{ + +} +void UUnrealWebSocketsBPLibrary::CreateWebSocket(UObject* Owner, const FString Url, UUnrealWebSocket*& WebSocket) +{ + WebSocket = NewObject(Owner); + WebSocket->Connect(Url); +} + diff --git a/UE4-27-0/Plugins/UnrealHelper/Source/UnrealWebSockets/Public/UnrealWebSocket.h b/UE4-27-0/Plugins/UnrealHelper/Source/UnrealWebSockets/Public/UnrealWebSocket.h new file mode 100644 index 0000000..2316c7d --- /dev/null +++ b/UE4-27-0/Plugins/UnrealHelper/Source/UnrealWebSockets/Public/UnrealWebSocket.h @@ -0,0 +1,77 @@ +#pragma once + +#include "CoreMinimal.h" +#include "UObject/NoExportTypes.h" +#include "Modules/ModuleManager.h" +#include "WebSocketsModule.h" +#include "IWebSocket.h" +#include "UnrealWebSocket.generated.h" + +/** + * + */ +// Compromise between Unreal and WebSocket API + +DECLARE_DYNAMIC_MULTICAST_DELEGATE(FWebSocketOnOpenSignature); +DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FWebSocketOnClosedSignature, const FString&, Reason); +DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FWebSocketOnErrorSignature, const FString&, error); +DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FWebSocketOnMessageSignature, const FString&, data); + +UENUM(BlueprintType) +enum class EWebSocketReadyState : uint8 { + CONNECTING UMETA(DisplayName = "Connecting"), + OPEN UMETA(DisplayName = "Open"), + CLOSING UMETA(DisplayName = "Closing"), + CLOSED UMETA(DisplayName = "Closed") +}; + +UCLASS(BlueprintType) +class UNREALWEBSOCKETS_API UUnrealWebSocket : public UObject +{ + GENERATED_BODY() +private: + + TArray Protocols; + + UUnrealWebSocket(const FObjectInitializer& ObjectInitializer); + + virtual void PostInitProperties() override; + + virtual void PostLoad() override; + + virtual void BeginDestroy() override; + +public: + + TSharedPtr WebSocket; + + void Cleanup(); + + void Connect(const FString& Url); + + UPROPERTY(BlueprintReadOnly) + EWebSocketReadyState ReadyState; + + UFUNCTION(BlueprintCallable, meta = (DisplayName = "Close", Keywords = "Close", AutoCreateRefTerm="Reason"), Category = "WebSocket") + void Close(int32 Code = 1000, const FString& Reason = ""); + + UFUNCTION(BlueprintCallable, meta = (DisplayName = "Send", Keywords = "Send"), Category = "WebSocket") + void Send(const FString& Data); + + UFUNCTION(BlueprintCallable, meta = (DisplayName = "Send Buffer", Keywords = "Send"), Category = "WebSocket") + void SendBuffer(const TArray& Data); + + UPROPERTY(BlueprintAssignable, Category = "Unreal WebSockets") + FWebSocketOnClosedSignature OnClosed; + + UPROPERTY(BlueprintAssignable, Category = "Unreal WebSockets") + FWebSocketOnErrorSignature OnError; + + UPROPERTY(BlueprintAssignable, Category = "Unreal WebSockets") + FWebSocketOnOpenSignature OnOpen; + + UPROPERTY(BlueprintAssignable, Category = "Unreal WebSockets") + FWebSocketOnMessageSignature OnMessage; + + +}; diff --git a/UE4-27-0/Plugins/UnrealHelper/Source/UnrealWebSockets/Public/UnrealWebSockets.h b/UE4-27-0/Plugins/UnrealHelper/Source/UnrealWebSockets/Public/UnrealWebSockets.h new file mode 100644 index 0000000..c6a25df --- /dev/null +++ b/UE4-27-0/Plugins/UnrealHelper/Source/UnrealWebSockets/Public/UnrealWebSockets.h @@ -0,0 +1,13 @@ +#pragma once + +#include "Modules/ModuleManager.h" +// + +class FUnrealWebSocketsModule : public IModuleInterface +{ +public: + + /** IModuleInterface implementation */ + virtual void StartupModule() override; + virtual void ShutdownModule() override; +}; diff --git a/UE4-27-0/Plugins/UnrealHelper/Source/UnrealWebSockets/Public/UnrealWebSocketsBPLibrary.h b/UE4-27-0/Plugins/UnrealHelper/Source/UnrealWebSockets/Public/UnrealWebSocketsBPLibrary.h new file mode 100644 index 0000000..eae90f1 --- /dev/null +++ b/UE4-27-0/Plugins/UnrealHelper/Source/UnrealWebSockets/Public/UnrealWebSocketsBPLibrary.h @@ -0,0 +1,15 @@ +#pragma once + +#include "Kismet/BlueprintFunctionLibrary.h" +#include "UnrealWebSocket.h" +#include "UnrealWebSocketsBPLibrary.generated.h" + +UCLASS() +class UUnrealWebSocketsBPLibrary : public UBlueprintFunctionLibrary +{ + GENERATED_UCLASS_BODY() + + UFUNCTION(BlueprintCallable, meta = (DisplayName = "Create WebSocket", Keywords = "Create", DefaultToSelf="Owner", HidePin="Owner"), Category = "Unreal WebSockets") + static void CreateWebSocket(UObject* Owner, const FString Url, UUnrealWebSocket*& WebSocket); + +}; diff --git a/UE4-27-0/Plugins/UnrealHelper/Source/UnrealWebSockets/UnrealWebSockets.Build.cs b/UE4-27-0/Plugins/UnrealHelper/Source/UnrealWebSockets/UnrealWebSockets.Build.cs new file mode 100644 index 0000000..347068d --- /dev/null +++ b/UE4-27-0/Plugins/UnrealHelper/Source/UnrealWebSockets/UnrealWebSockets.Build.cs @@ -0,0 +1,32 @@ +using UnrealBuildTool; + +public class UnrealWebSockets : ModuleRules +{ + public UnrealWebSockets(ReadOnlyTargetRules Target) : base(Target) + { + PCHUsage = ModuleRules.PCHUsageMode.UseExplicitOrSharedPCHs; + + PublicDependencyModuleNames.AddRange( + new string[] + { + "Core", + "WebSockets", + } + ); + + + PrivateDependencyModuleNames.AddRange( + new string[] + { + "CoreUObject", + "Engine", + "Slate", + "SlateCore", + "HTTP", + "Json", + "JsonUtilities", + "WebSockets", + } + ); + } +} diff --git a/UE4-27-0/Plugins/UnrealHelper/UnrealHelper.uplugin b/UE4-27-0/Plugins/UnrealHelper/UnrealHelper.uplugin new file mode 100644 index 0000000..9699f18 --- /dev/null +++ b/UE4-27-0/Plugins/UnrealHelper/UnrealHelper.uplugin @@ -0,0 +1,54 @@ +{ + "FileVersion": 3, + "Version": 1, + "VersionName": "0.1", + "FriendlyName": "UnrealHelper", + "Description": "Blueprint extensions to built-in Unreal methods", + "Category": "Blueprints", + "CreatedBy": "", + "CreatedByURL": "", + "DocsURL": "", + "MarketplaceURL": "", + "SupportURL": "", + "CanContainContent": false, + "IsBetaVersion": true, + "IsExperimentalVersion": false, + "Installed": false, + "Modules": [ + { + "Name": "UnrealHelper", + "Type": "Runtime", + "LoadingPhase": "PreLoadingScreen" + }, + { + "Name": "UnrealHttp", + "Type": "Runtime", + "LoadingPhase": "PreLoadingScreen" + }, + { + "Name": "UnrealJson", + "Type": "Runtime", + "LoadingPhase": "PreLoadingScreen" + }, + { + "Name": "UnrealMediaOutputSocket", + "Type": "Runtime", + "LoadingPhase": "PreLoadingScreen" + }, + { + "Name": "UnrealWebSockets", + "Type": "Runtime", + "LoadingPhase": "PreLoadingScreen" + } + ], + "Plugins": [ + { + "Name": "MediaIOFramework", + "Enabled": true + }, + { + "Name": "HardwareEncoders", + "Enabled": true + } + ] +} diff --git a/UE4-27-0/README.md b/UE4-27-0/README.md new file mode 100644 index 0000000..e6846ed --- /dev/null +++ b/UE4-27-0/README.md @@ -0,0 +1,11 @@ +# VictoryPlugin Tests + +These Blueprint tests show how to use the VictoryHelper plugin and bring in some automated testing. + +Four submodules are being developed using this method: +- Json - This provides object to JSON and JSON string to object features. +- WebSockets - Exposes the websockets API based on the standard browser api +- Http - Providing streaming HTTP PUT and HTTP GET methods for larger files. +- MediaOutputSocket - Using the capture API to stream real-time frames to a console program, with FFMpeg as the default target + +These four submodules were originally developed by Charles Pritchard at the Walt Disney Company and approved for submission to the VictoryPlugin repository under its open source license. They may be submitted to Epic's Unreal Engine repository once modified for inclusion into the Unreal Engine project. \ No newline at end of file diff --git a/UE4-27-0/UnrealHelper.uproject b/UE4-27-0/UnrealHelper.uproject new file mode 100644 index 0000000..7844e5a --- /dev/null +++ b/UE4-27-0/UnrealHelper.uproject @@ -0,0 +1,16 @@ +{ + "FileVersion": 3, + "EngineAssociation": "4.27", + "Category": "", + "Description": "", + "Plugins": [ + { + "Name": "MediaFrameworkUtilities", + "Enabled": true + }, + { + "Name": "MediaIOFramework", + "Enabled": true + } + ] +} \ No newline at end of file