170 changed files with 1234 additions and 1118 deletions
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -0,0 +1,216 @@ |
|||||
|
// Fill out your copyright notice in the Description page of Project Settings.
|
||||
|
|
||||
|
|
||||
|
#include "n8n/n8n_connector.h" |
||||
|
|
||||
|
#include "Async/Async.h" |
||||
|
#include "Dom/JsonValue.h" |
||||
|
#include "HttpModule.h" |
||||
|
#include "Interfaces/IHttpResponse.h" |
||||
|
#include "Serialization/JsonSerializer.h" |
||||
|
|
||||
|
namespace |
||||
|
{ |
||||
|
static FString HttpRequestStatusToString(EHttpRequestStatus::Type Status) |
||||
|
{ |
||||
|
switch (Status) |
||||
|
{ |
||||
|
case EHttpRequestStatus::NotStarted: return TEXT("NotStarted"); |
||||
|
case EHttpRequestStatus::Processing: return TEXT("Processing"); |
||||
|
case EHttpRequestStatus::Failed: return TEXT("Failed"); |
||||
|
//case EHttpRequestStatus::Failed_ConnectionError: return TEXT("Failed_ConnectionError");
|
||||
|
case EHttpRequestStatus::Succeeded: return TEXT("Succeeded"); |
||||
|
default: return TEXT("Unknown"); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
Un8n_connector::Un8n_connector() |
||||
|
{ |
||||
|
} |
||||
|
|
||||
|
void Un8n_connector::PostInitProperties() |
||||
|
{ |
||||
|
Super::PostInitProperties(); |
||||
|
EnsureRooted(); |
||||
|
} |
||||
|
|
||||
|
void Un8n_connector::BeginDestroy() |
||||
|
{ |
||||
|
DeinitN8NConnector(); |
||||
|
|
||||
|
if (!HasAnyFlags(RF_ClassDefaultObject) && IsRooted()) |
||||
|
{ |
||||
|
RemoveFromRoot(); |
||||
|
} |
||||
|
|
||||
|
Super::BeginDestroy(); |
||||
|
} |
||||
|
|
||||
|
void Un8n_connector::EnsureRooted() |
||||
|
{ |
||||
|
if (!HasAnyFlags(RF_ClassDefaultObject) && !IsRooted()) |
||||
|
{ |
||||
|
AddToRoot(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
int32 Un8n_connector::StartWorkflow(const FString& payload) |
||||
|
{ |
||||
|
EnsureRooted(); |
||||
|
|
||||
|
if (N8NURL.IsEmpty()) |
||||
|
{ |
||||
|
BroadcastError(INDEX_NONE, TEXT("StartWorkflow: url is empty")); |
||||
|
return INDEX_NONE; |
||||
|
} |
||||
|
|
||||
|
int32 RequestId; |
||||
|
{ |
||||
|
FScopeLock Lock(&ActiveRequestsCriticalSection); |
||||
|
RequestId = NextRequestId++; |
||||
|
} |
||||
|
|
||||
|
TSharedRef<IHttpRequest, ESPMode::ThreadSafe> HttpRequest = FHttpModule::Get().CreateRequest(); |
||||
|
HttpRequest->SetURL(N8NURL); |
||||
|
HttpRequest->SetVerb(TEXT("POST")); |
||||
|
HttpRequest->SetHeader(TEXT("Content-Type"), TEXT("application/json; charset=utf-8")); |
||||
|
|
||||
|
// Validate payload as JSON (allow object or array)
|
||||
|
{ |
||||
|
TSharedPtr<FJsonValue> RootValue; |
||||
|
TSharedRef<TJsonReader<>> Reader = TJsonReaderFactory<>::Create(payload); |
||||
|
if (!FJsonSerializer::Deserialize(Reader, RootValue) || !RootValue.IsValid()) |
||||
|
{ |
||||
|
BroadcastError(INDEX_NONE, TEXT("StartWorkflow: payload is not valid JSON")); |
||||
|
} |
||||
|
if (RootValue->Type != EJson::Object && RootValue->Type != EJson::Array) |
||||
|
{ |
||||
|
BroadcastError(INDEX_NONE, TEXT("StartWorkflow: payload i JSON object or array")); |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
HttpRequest->SetHeader(TEXT("Accept"), TEXT("application/json")); |
||||
|
HttpRequest->SetContentAsString(payload); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
HttpRequest->SetTimeout(N8NTimeout); |
||||
|
|
||||
|
HttpRequest->OnProcessRequestComplete().BindLambda( |
||||
|
[this, RequestId](FHttpRequestPtr Request, FHttpResponsePtr Response, bool bWasSuccessful) |
||||
|
{ |
||||
|
HandleRequestComplete(RequestId, Request, Response, bWasSuccessful); |
||||
|
}); |
||||
|
|
||||
|
{ |
||||
|
FScopeLock Lock(&ActiveRequestsCriticalSection); |
||||
|
ActiveRequestsById.Add(RequestId, HttpRequest); |
||||
|
} |
||||
|
|
||||
|
if (!HttpRequest->ProcessRequest()) |
||||
|
{ |
||||
|
{ |
||||
|
FScopeLock Lock(&ActiveRequestsCriticalSection); |
||||
|
ActiveRequestsById.Remove(RequestId); |
||||
|
} |
||||
|
BroadcastError(RequestId, TEXT("StartWorkflow: failed to start HTTP request")); |
||||
|
return RequestId; |
||||
|
} |
||||
|
|
||||
|
return RequestId; |
||||
|
} |
||||
|
|
||||
|
void Un8n_connector::InitN8NConnector(const FString& url, float timeout) |
||||
|
{ |
||||
|
N8NURL = url; |
||||
|
N8NTimeout = timeout; |
||||
|
} |
||||
|
|
||||
|
void Un8n_connector::DeinitN8NConnector() |
||||
|
{ |
||||
|
ClearN8NConnector(); |
||||
|
|
||||
|
if (IsRooted()) |
||||
|
{ |
||||
|
RemoveFromRoot(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
void Un8n_connector::ClearN8NConnector() |
||||
|
{ |
||||
|
TArray<TSharedPtr<IHttpRequest, ESPMode::ThreadSafe>> RequestsToCancel; |
||||
|
{ |
||||
|
FScopeLock Lock(&ActiveRequestsCriticalSection); |
||||
|
RequestGenerationId++; |
||||
|
ActiveRequestsById.GenerateValueArray(RequestsToCancel); |
||||
|
ActiveRequestsById.Empty(); |
||||
|
} |
||||
|
|
||||
|
for (const TSharedPtr<IHttpRequest, ESPMode::ThreadSafe>& Req : RequestsToCancel) |
||||
|
{ |
||||
|
if (Req.IsValid()) |
||||
|
{ |
||||
|
Req->CancelRequest(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
void Un8n_connector::HandleRequestComplete(int32 RequestId, FHttpRequestPtr Request, FHttpResponsePtr Response, bool bWasSuccessful) |
||||
|
{ |
||||
|
{ |
||||
|
FScopeLock Lock(&ActiveRequestsCriticalSection); |
||||
|
ActiveRequestsById.Remove(RequestId); |
||||
|
} |
||||
|
|
||||
|
if (!bWasSuccessful || !Response.IsValid()) |
||||
|
{ |
||||
|
const FString Url = Request.IsValid() ? Request->GetURL() : FString(TEXT("<null request>")); |
||||
|
const FString Status = Request.IsValid() ? HttpRequestStatusToString(Request->GetStatus()) : FString(TEXT("<unknown>")); |
||||
|
BroadcastError(RequestId, FString::Printf(TEXT("n8n webhook request failed (no HTTP response). url=%s status=%s"), *Url, *Status)); |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
const int32 Code = Response->GetResponseCode(); |
||||
|
const FString Body = Response->GetContentAsString(); |
||||
|
|
||||
|
if (Code >= 200 && Code < 300) |
||||
|
{ |
||||
|
BroadcastResult(RequestId, Body); |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
BroadcastError(RequestId, FString::Printf(TEXT("HTTP %d: %s"), Code, *Body)); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
void Un8n_connector::BroadcastError(int32 RequestId, const FString& ErrorMessage) |
||||
|
{ |
||||
|
if (IsInGameThread()) |
||||
|
{ |
||||
|
OnN8NError.Broadcast(RequestId, ErrorMessage); |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
const FString Copy = ErrorMessage; |
||||
|
AsyncTask(ENamedThreads::GameThread, [this, RequestId, Copy]() |
||||
|
{ |
||||
|
OnN8NError.Broadcast(RequestId, Copy); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
void Un8n_connector::BroadcastResult(int32 RequestId, const FString& Payload) |
||||
|
{ |
||||
|
if (IsInGameThread()) |
||||
|
{ |
||||
|
OnN8NResult.Broadcast(RequestId, Payload); |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
const FString Copy = Payload; |
||||
|
AsyncTask(ENamedThreads::GameThread, [this, RequestId, Copy]() |
||||
|
{ |
||||
|
OnN8NResult.Broadcast(RequestId, Copy); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
@ -0,0 +1,62 @@ |
|||||
|
// Fill out your copyright notice in the Description page of Project Settings.
|
||||
|
|
||||
|
#pragma once |
||||
|
|
||||
|
#include "CoreMinimal.h" |
||||
|
#include "UObject/NoExportTypes.h" |
||||
|
#include "Http.h" |
||||
|
#include "n8n_connector.generated.h" |
||||
|
|
||||
|
DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FOnN8NError, int32, RequestId, const FString&, ErrorMessage); |
||||
|
DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FOnN8NResult, int32, RequestId, const FString&, Payload); |
||||
|
|
||||
|
/**
|
||||
|
* |
||||
|
*/ |
||||
|
UCLASS(Blueprintable, BlueprintType) |
||||
|
class AVATARCORE_AI_API Un8n_connector : public UObject |
||||
|
{ |
||||
|
GENERATED_BODY() |
||||
|
|
||||
|
public: |
||||
|
Un8n_connector(); |
||||
|
|
||||
|
UPROPERTY(BlueprintAssignable, Category = "AvatarCoreAI|n8n|Events") |
||||
|
FOnN8NError OnN8NError; |
||||
|
|
||||
|
UPROPERTY(BlueprintAssignable, Category = "AvatarCoreAI|n8n|Events") |
||||
|
FOnN8NResult OnN8NResult; |
||||
|
|
||||
|
// Blueprint Functions - Lifecycle
|
||||
|
UFUNCTION(BlueprintCallable, Category = "AvatarCoreAI|n8n") |
||||
|
int32 StartWorkflow(const FString& payload); |
||||
|
|
||||
|
UFUNCTION(BlueprintCallable, Category = "AvatarCoreAI|n8n") |
||||
|
void InitN8NConnector(const FString& url, float timeout = 60); |
||||
|
|
||||
|
UFUNCTION(BlueprintCallable, Category = "AvatarCoreAI|n8n") |
||||
|
void DeinitN8NConnector(); |
||||
|
|
||||
|
UFUNCTION(BlueprintCallable, Category = "AvatarCoreAI|n8n") |
||||
|
void ClearN8NConnector(); |
||||
|
|
||||
|
protected: |
||||
|
virtual void PostInitProperties() override; |
||||
|
virtual void BeginDestroy() override; |
||||
|
|
||||
|
private: |
||||
|
void HandleRequestComplete(int32 RequestId, FHttpRequestPtr Request, FHttpResponsePtr Response, bool bWasSuccessful); |
||||
|
void BroadcastError(int32 RequestId, const FString& ErrorMessage); |
||||
|
void BroadcastResult(int32 RequestId, const FString& Payload); |
||||
|
void EnsureRooted(); |
||||
|
|
||||
|
FString N8NURL; |
||||
|
float N8NTimeout = 60; |
||||
|
|
||||
|
private: |
||||
|
FCriticalSection ActiveRequestsCriticalSection; |
||||
|
TMap<int32, TSharedPtr<IHttpRequest, ESPMode::ThreadSafe>> ActiveRequestsById; |
||||
|
|
||||
|
int32 NextRequestId = 1; |
||||
|
uint32 RequestGenerationId = 0; |
||||
|
}; |
||||
@ -1,24 +0,0 @@ |
|||||
{ |
|
||||
"FileVersion": 3, |
|
||||
"Version": 1, |
|
||||
"VersionName": "1.0", |
|
||||
"FriendlyName": "AvatarCore_FaceDetector", |
|
||||
"Description": "Tracking faces in webcame streams", |
|
||||
"Category": "Other", |
|
||||
"CreatedBy": "b.ReX", |
|
||||
"CreatedByURL": "", |
|
||||
"DocsURL": "", |
|
||||
"MarketplaceURL": "", |
|
||||
"SupportURL": "", |
|
||||
"CanContainContent": true, |
|
||||
"IsBetaVersion": false, |
|
||||
"IsExperimentalVersion": false, |
|
||||
"Installed": false, |
|
||||
"Modules": [ |
|
||||
{ |
|
||||
"Name": "AvatarCore_FaceDetector", |
|
||||
"Type": "Runtime", |
|
||||
"LoadingPhase": "Default" |
|
||||
} |
|
||||
] |
|
||||
} |
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -1,60 +0,0 @@ |
|||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
|
||||
|
|
||||
using System.IO; |
|
||||
using UnrealBuildTool; |
|
||||
|
|
||||
public class AvatarCore_FaceDetector : ModuleRules |
|
||||
{ |
|
||||
public AvatarCore_FaceDetector(ReadOnlyTargetRules Target) : base(Target) |
|
||||
{ |
|
||||
PCHUsage = ModuleRules.PCHUsageMode.UseExplicitOrSharedPCHs; |
|
||||
|
|
||||
PublicIncludePaths.AddRange( |
|
||||
new string[] { |
|
||||
// ... add public include paths required here ...
|
|
||||
} |
|
||||
); |
|
||||
|
|
||||
|
|
||||
PrivateIncludePaths.AddRange( |
|
||||
new string[] { |
|
||||
// ... add other private include paths required here ...
|
|
||||
} |
|
||||
); |
|
||||
|
|
||||
|
|
||||
PublicDependencyModuleNames.AddRange( |
|
||||
new string[] |
|
||||
{ |
|
||||
"Core", |
|
||||
// ... add other public dependencies that you statically link with here ...
|
|
||||
} |
|
||||
); |
|
||||
|
|
||||
|
|
||||
PrivateDependencyModuleNames.AddRange( |
|
||||
new string[] |
|
||||
{ |
|
||||
"CoreUObject", |
|
||||
"Engine", |
|
||||
"Projects", |
|
||||
"Json", |
|
||||
"JsonUtilities", |
|
||||
"Slate", |
|
||||
"SlateCore", |
|
||||
// ... add private dependencies that you statically link with here ...
|
|
||||
} |
|
||||
); |
|
||||
|
|
||||
|
|
||||
DynamicallyLoadedModuleNames.AddRange( |
|
||||
new string[] |
|
||||
{ |
|
||||
// ... add any modules that your module loads dynamically here ...
|
|
||||
} |
|
||||
); |
|
||||
|
|
||||
RuntimeDependencies.Add(Path.Combine(ModuleDirectory, "..", "ThirdParty", "FaceDetector.exe")); |
|
||||
RuntimeDependencies.Add(Path.Combine(ModuleDirectory, "..", "ThirdParty", "face_landmarker.task")); |
|
||||
} |
|
||||
} |
|
||||
@ -1,20 +0,0 @@ |
|||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
|
||||
|
|
||||
#include "AvatarCore_FaceDetector.h" |
|
||||
|
|
||||
#define LOCTEXT_NAMESPACE "FAvatarCore_FaceDetectorModule" |
|
||||
|
|
||||
void FAvatarCore_FaceDetectorModule::StartupModule() |
|
||||
{ |
|
||||
// This code will execute after your module is loaded into memory; the exact timing is specified in the .uplugin file per-module
|
|
||||
} |
|
||||
|
|
||||
void FAvatarCore_FaceDetectorModule::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(FAvatarCore_FaceDetectorModule, AvatarCore_FaceDetector) |
|
||||
@ -1,307 +0,0 @@ |
|||||
#include "AvatarFaceDetectorSubsystem.h" |
|
||||
|
|
||||
#include "Async/Async.h" |
|
||||
#include "Interfaces/IPluginManager.h" |
|
||||
#include "Misc/Paths.h" |
|
||||
#include "Serialization/JsonReader.h" |
|
||||
#include "Serialization/JsonSerializer.h" |
|
||||
|
|
||||
bool UAvatarFaceDetectorSubsystem::InitializeFaceDetector(const FFaceDetectorSettings& Settings) |
|
||||
{ |
|
||||
ShutdownFaceDetector(); |
|
||||
return StartProcess(Settings); |
|
||||
} |
|
||||
|
|
||||
void UAvatarFaceDetectorSubsystem::ShutdownFaceDetector() |
|
||||
{ |
|
||||
StopProcess(); |
|
||||
} |
|
||||
|
|
||||
bool UAvatarFaceDetectorSubsystem::IsFaceDetectorRunning() const |
|
||||
{ |
|
||||
FProcHandle HandleCopy = ProcHandle; |
|
||||
return HandleCopy.IsValid() && FPlatformProcess::IsProcRunning(HandleCopy); |
|
||||
} |
|
||||
|
|
||||
void UAvatarFaceDetectorSubsystem::Deinitialize() |
|
||||
{ |
|
||||
ShutdownFaceDetector(); |
|
||||
Super::Deinitialize(); |
|
||||
} |
|
||||
|
|
||||
FString UAvatarFaceDetectorSubsystem::ResolveThirdPartyFilePath(const FString& FileName) const |
|
||||
{ |
|
||||
const TSharedPtr<IPlugin> Plugin = IPluginManager::Get().FindPlugin(TEXT("AvatarCore_FaceDetector")); |
|
||||
if (!Plugin.IsValid()) |
|
||||
{ |
|
||||
return FString(); |
|
||||
} |
|
||||
|
|
||||
const FString ThirdPartyDir = FPaths::Combine(Plugin->GetBaseDir(), TEXT("Source"), TEXT("ThirdParty")); |
|
||||
return FPaths::Combine(ThirdPartyDir, FileName); |
|
||||
} |
|
||||
|
|
||||
bool UAvatarFaceDetectorSubsystem::StartProcess(const FFaceDetectorSettings& Settings) |
|
||||
{ |
|
||||
FScopeLock Lock(&StateCriticalSection); |
|
||||
|
|
||||
const FString ExePath = ResolveThirdPartyFilePath(TEXT("FaceDetector.exe")); |
|
||||
if (ExePath.IsEmpty() || !FPaths::FileExists(ExePath)) |
|
||||
{ |
|
||||
AsyncTask(ENamedThreads::GameThread, [this]() |
|
||||
{ |
|
||||
OnFaceDetectorError.Broadcast(TEXT("FaceDetector.exe not found (expected under Plugin/Source/ThirdParty).")); |
|
||||
}); |
|
||||
return false; |
|
||||
} |
|
||||
|
|
||||
FString Args; |
|
||||
if (!Settings.WebcamName.IsEmpty()) |
|
||||
{ |
|
||||
Args += FString::Printf(TEXT(" --device \"%s\""), *Settings.WebcamName); |
|
||||
} |
|
||||
if (!Settings.ModelName.IsEmpty()) |
|
||||
{ |
|
||||
const FString ModelPath = ResolveThirdPartyFilePath(Settings.ModelName); |
|
||||
Args += FString::Printf(TEXT(" --model \"%s\""), *ModelPath); |
|
||||
} |
|
||||
Args += FString::Printf(TEXT(" --num_faces %d"), Settings.NumFaces); |
|
||||
if (Settings.bDebugMode) |
|
||||
{ |
|
||||
Args += TEXT(" --debugMode"); |
|
||||
} |
|
||||
|
|
||||
Args += FString::Printf(TEXT(" --fps_limit %d"), Settings.FrameRateLimit); |
|
||||
|
|
||||
FPlatformProcess::CreatePipe(ReadPipe, WritePipe); |
|
||||
|
|
||||
bStopRequested = false; |
|
||||
StdoutRemainder.Reset(); |
|
||||
|
|
||||
ProcHandle = FPlatformProcess::CreateProc( |
|
||||
*ExePath, |
|
||||
*Args, |
|
||||
false, |
|
||||
true, |
|
||||
true, |
|
||||
nullptr, |
|
||||
0, |
|
||||
nullptr, |
|
||||
WritePipe, |
|
||||
ReadPipe); |
|
||||
|
|
||||
if (!ProcHandle.IsValid()) |
|
||||
{ |
|
||||
FPlatformProcess::ClosePipe(ReadPipe, WritePipe); |
|
||||
ReadPipe = nullptr; |
|
||||
WritePipe = nullptr; |
|
||||
|
|
||||
AsyncTask(ENamedThreads::GameThread, [this]() |
|
||||
{ |
|
||||
OnFaceDetectorError.Broadcast(TEXT("Failed to launch FaceDetector.exe")); |
|
||||
}); |
|
||||
return false; |
|
||||
} |
|
||||
|
|
||||
ReaderFuture = Async(EAsyncExecution::Thread, [this]() |
|
||||
{ |
|
||||
ReaderThreadMain(); |
|
||||
}); |
|
||||
|
|
||||
return true; |
|
||||
} |
|
||||
|
|
||||
void UAvatarFaceDetectorSubsystem::StopProcess() |
|
||||
{ |
|
||||
TFuture<void> LocalFuture; |
|
||||
FProcHandle LocalProc; |
|
||||
void* LocalReadPipe = nullptr; |
|
||||
void* LocalWritePipe = nullptr; |
|
||||
|
|
||||
{ |
|
||||
FScopeLock Lock(&StateCriticalSection); |
|
||||
|
|
||||
bStopRequested = true; |
|
||||
LocalFuture = MoveTemp(ReaderFuture); |
|
||||
LocalProc = ProcHandle; |
|
||||
LocalReadPipe = ReadPipe; |
|
||||
LocalWritePipe = WritePipe; |
|
||||
|
|
||||
ProcHandle.Reset(); |
|
||||
ReadPipe = nullptr; |
|
||||
WritePipe = nullptr; |
|
||||
StdoutRemainder.Reset(); |
|
||||
} |
|
||||
|
|
||||
if (LocalProc.IsValid()) |
|
||||
{ |
|
||||
FPlatformProcess::TerminateProc(LocalProc, true); |
|
||||
FPlatformProcess::CloseProc(LocalProc); |
|
||||
} |
|
||||
|
|
||||
if (LocalFuture.IsValid()) |
|
||||
{ |
|
||||
LocalFuture.Wait(); |
|
||||
} |
|
||||
|
|
||||
if (LocalReadPipe != nullptr || LocalWritePipe != nullptr) |
|
||||
{ |
|
||||
FPlatformProcess::ClosePipe(LocalReadPipe, LocalWritePipe); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
void UAvatarFaceDetectorSubsystem::ReaderThreadMain() |
|
||||
{ |
|
||||
while (!bStopRequested) |
|
||||
{ |
|
||||
FString NewOutput; |
|
||||
{ |
|
||||
FScopeLock Lock(&StateCriticalSection); |
|
||||
if (ReadPipe == nullptr) |
|
||||
{ |
|
||||
break; |
|
||||
} |
|
||||
NewOutput = FPlatformProcess::ReadPipe(ReadPipe); |
|
||||
} |
|
||||
|
|
||||
if (NewOutput.IsEmpty()) |
|
||||
{ |
|
||||
if (!IsFaceDetectorRunning()) |
|
||||
{ |
|
||||
break; |
|
||||
} |
|
||||
FPlatformProcess::Sleep(0.01f); |
|
||||
continue; |
|
||||
} |
|
||||
|
|
||||
FString Combined; |
|
||||
{ |
|
||||
FScopeLock Lock(&StateCriticalSection); |
|
||||
Combined = StdoutRemainder + NewOutput; |
|
||||
StdoutRemainder.Reset(); |
|
||||
} |
|
||||
|
|
||||
TArray<FString> Lines; |
|
||||
Combined.ParseIntoArrayLines(Lines, false); |
|
||||
|
|
||||
const bool bEndsWithNewline = Combined.EndsWith(TEXT("\n")) || Combined.EndsWith(TEXT("\r\n")); |
|
||||
if (!bEndsWithNewline && Lines.Num() > 0) |
|
||||
{ |
|
||||
FScopeLock Lock(&StateCriticalSection); |
|
||||
StdoutRemainder = Lines.Pop(); |
|
||||
} |
|
||||
|
|
||||
for (const FString& Line : Lines) |
|
||||
{ |
|
||||
const FString Trimmed = Line.TrimStartAndEnd(); |
|
||||
if (!Trimmed.IsEmpty()) |
|
||||
{ |
|
||||
HandleLineOnWorkerThread(Trimmed); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
if (!bStopRequested) |
|
||||
{ |
|
||||
AsyncTask(ENamedThreads::GameThread, [this]() |
|
||||
{ |
|
||||
OnFaceDetectorError.Broadcast(TEXT("FaceDetector process stopped.")); |
|
||||
}); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
void UAvatarFaceDetectorSubsystem::HandleLineOnWorkerThread(const FString& Line) |
|
||||
{ |
|
||||
TSharedPtr<FJsonObject> Root; |
|
||||
TSharedRef<TJsonReader<>> Reader = TJsonReaderFactory<>::Create(Line); |
|
||||
if (!FJsonSerializer::Deserialize(Reader, Root) || !Root.IsValid()) |
|
||||
{ |
|
||||
AsyncTask(ENamedThreads::GameThread, [this, Line]() |
|
||||
{ |
|
||||
OnFaceDetectorLog.Broadcast(Line); |
|
||||
}); |
|
||||
return; |
|
||||
} |
|
||||
|
|
||||
FString Type; |
|
||||
if (!Root->TryGetStringField(TEXT("type"), Type)) |
|
||||
{ |
|
||||
AsyncTask(ENamedThreads::GameThread, [this, Line]() |
|
||||
{ |
|
||||
OnFaceDetectorLog.Broadcast(Line); |
|
||||
}); |
|
||||
return; |
|
||||
} |
|
||||
|
|
||||
if (Type.Equals(TEXT("error"), ESearchCase::IgnoreCase)) |
|
||||
{ |
|
||||
FString Message; |
|
||||
Root->TryGetStringField(TEXT("message"), Message); |
|
||||
AsyncTask(ENamedThreads::GameThread, [this, Message]() |
|
||||
{ |
|
||||
OnFaceDetectorError.Broadcast(Message); |
|
||||
}); |
|
||||
return; |
|
||||
} |
|
||||
|
|
||||
if (Type.Equals(TEXT("log"), ESearchCase::IgnoreCase)) |
|
||||
{ |
|
||||
FString Message; |
|
||||
Root->TryGetStringField(TEXT("message"), Message); |
|
||||
AsyncTask(ENamedThreads::GameThread, [this, Message]() |
|
||||
{ |
|
||||
OnFaceDetectorLog.Broadcast(Message); |
|
||||
}); |
|
||||
return; |
|
||||
} |
|
||||
|
|
||||
if (Type.Equals(TEXT("faces"), ESearchCase::IgnoreCase)) |
|
||||
{ |
|
||||
TArray<FDetectedFace> Faces; |
|
||||
|
|
||||
const TArray<TSharedPtr<FJsonValue>>* FacesJson = nullptr; |
|
||||
if (Root->TryGetArrayField(TEXT("faces"), FacesJson) && FacesJson != nullptr) |
|
||||
{ |
|
||||
Faces.Reserve(FacesJson->Num()); |
|
||||
for (const TSharedPtr<FJsonValue>& FaceVal : *FacesJson) |
|
||||
{ |
|
||||
if (!FaceVal.IsValid() || FaceVal->Type != EJson::Object) |
|
||||
{ |
|
||||
continue; |
|
||||
} |
|
||||
const TSharedPtr<FJsonObject> FaceObj = FaceVal->AsObject(); |
|
||||
if (!FaceObj.IsValid()) |
|
||||
{ |
|
||||
continue; |
|
||||
} |
|
||||
|
|
||||
double X = 0.0; |
|
||||
double Y = 0.0; |
|
||||
double IdNum = (double)INDEX_NONE; |
|
||||
bool bActive = false; |
|
||||
|
|
||||
FaceObj->TryGetNumberField(TEXT("x"), X); |
|
||||
FaceObj->TryGetNumberField(TEXT("y"), Y); |
|
||||
FaceObj->TryGetNumberField(TEXT("id"), IdNum); |
|
||||
FaceObj->TryGetBoolField(TEXT("active"), bActive); |
|
||||
|
|
||||
FDetectedFace Face; |
|
||||
Face.Position = FVector2D((float)X, (float)Y); |
|
||||
Face.Id = (int32)IdNum; |
|
||||
Face.TrackingLost = !bActive; |
|
||||
Faces.Add(Face); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
AsyncTask(ENamedThreads::GameThread, [this, Faces = MoveTemp(Faces)]() mutable |
|
||||
{ |
|
||||
OnFaceDetectorUpdate.Broadcast(Faces); |
|
||||
}); |
|
||||
return; |
|
||||
} |
|
||||
|
|
||||
AsyncTask(ENamedThreads::GameThread, [this, Line]() |
|
||||
{ |
|
||||
OnFaceDetectorLog.Broadcast(Line); |
|
||||
}); |
|
||||
} |
|
||||
@ -1,14 +0,0 @@ |
|||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
|
||||
|
|
||||
#pragma once |
|
||||
|
|
||||
#include "Modules/ModuleManager.h" |
|
||||
|
|
||||
class FAvatarCore_FaceDetectorModule : public IModuleInterface |
|
||||
{ |
|
||||
public: |
|
||||
|
|
||||
/** IModuleInterface implementation */ |
|
||||
virtual void StartupModule() override; |
|
||||
virtual void ShutdownModule() override; |
|
||||
}; |
|
||||
@ -1,56 +0,0 @@ |
|||||
#pragma once |
|
||||
|
|
||||
#include "CoreMinimal.h" |
|
||||
#include "HAL/PlatformProcess.h" |
|
||||
#include "Async/Future.h" |
|
||||
#include "Subsystems/GameInstanceSubsystem.h" |
|
||||
#include "FaceDetectorTypes.h" |
|
||||
#include "AvatarFaceDetectorSubsystem.generated.h" |
|
||||
|
|
||||
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FFaceDetectorErrorSig, const FString&, ErrorMessage); |
|
||||
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FFaceDetectorLogSig, const FString&, LogMessage); |
|
||||
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FFaceDetectorUpdateSig, const TArray<FDetectedFace>&, Faces); |
|
||||
|
|
||||
UCLASS() |
|
||||
class AVATARCORE_FACEDETECTOR_API UAvatarFaceDetectorSubsystem : public UGameInstanceSubsystem |
|
||||
{ |
|
||||
GENERATED_BODY() |
|
||||
|
|
||||
public: |
|
||||
UFUNCTION(BlueprintCallable, Category="FaceDetector") |
|
||||
bool InitializeFaceDetector(const FFaceDetectorSettings& Settings); |
|
||||
|
|
||||
UFUNCTION(BlueprintCallable, Category="FaceDetector") |
|
||||
void ShutdownFaceDetector(); |
|
||||
|
|
||||
UFUNCTION(BlueprintCallable, Category="FaceDetector") |
|
||||
bool IsFaceDetectorRunning() const; |
|
||||
|
|
||||
UPROPERTY(BlueprintAssignable, Category="FaceDetector") |
|
||||
FFaceDetectorErrorSig OnFaceDetectorError; |
|
||||
|
|
||||
UPROPERTY(BlueprintAssignable, Category="FaceDetector") |
|
||||
FFaceDetectorLogSig OnFaceDetectorLog; |
|
||||
|
|
||||
UPROPERTY(BlueprintAssignable, Category="FaceDetector") |
|
||||
FFaceDetectorUpdateSig OnFaceDetectorUpdate; |
|
||||
|
|
||||
virtual void Deinitialize() override; |
|
||||
|
|
||||
private: |
|
||||
bool StartProcess(const FFaceDetectorSettings& Settings); |
|
||||
void StopProcess(); |
|
||||
void ReaderThreadMain(); |
|
||||
void HandleLineOnWorkerThread(const FString& Line); |
|
||||
|
|
||||
FString ResolveThirdPartyFilePath(const FString& FileName) const; |
|
||||
|
|
||||
private: |
|
||||
FProcHandle ProcHandle; |
|
||||
void* ReadPipe = nullptr; |
|
||||
void* WritePipe = nullptr; |
|
||||
TAtomic<bool> bStopRequested = false; |
|
||||
TFuture<void> ReaderFuture; |
|
||||
FCriticalSection StateCriticalSection; |
|
||||
FString StdoutRemainder; |
|
||||
}; |
|
||||
@ -1,45 +0,0 @@ |
|||||
#pragma once |
|
||||
|
|
||||
#include "CoreMinimal.h" |
|
||||
#include "FaceDetectorTypes.generated.h" |
|
||||
|
|
||||
USTRUCT(BlueprintType) |
|
||||
struct FDetectedFace |
|
||||
{ |
|
||||
GENERATED_BODY() |
|
||||
|
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="FaceDetector") |
|
||||
FVector2D Position = FVector2D::ZeroVector; |
|
||||
|
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="FaceDetector") |
|
||||
int32 Id = INDEX_NONE; |
|
||||
|
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="FaceDetector") |
|
||||
bool TrackingLost = false; |
|
||||
}; |
|
||||
|
|
||||
USTRUCT(BlueprintType) |
|
||||
struct FFaceDetectorSettings |
|
||||
{ |
|
||||
GENERATED_BODY() |
|
||||
|
|
||||
//Name of the webcam (pick first if ambigious)
|
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="FaceDetector") |
|
||||
FString WebcamName; |
|
||||
|
|
||||
//Create a DebugWindow to see detected faces
|
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="FaceDetector") |
|
||||
bool bDebugMode = false; |
|
||||
|
|
||||
//Override Model (full path to modelfile)
|
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="FaceDetector") |
|
||||
FString ModelName; |
|
||||
|
|
||||
//Limit Framerate of Videostream (0 to disable)
|
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="FaceDetector") |
|
||||
int32 FrameRateLimit = 24; |
|
||||
|
|
||||
//Max faces to track
|
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="FaceDetector") |
|
||||
int32 NumFaces = 5; |
|
||||
}; |
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue