Files
DTFluxAPI/Source/DTFluxRemote/Private/DTFluxRemoteSubsystem.cpp

458 lines
14 KiB
C++
Raw Normal View History

// Fill out your copyright notice in the Description page of Project Settings.
#include "DTFluxRemoteSubsystem.h"
#include "DTFluxRemoteSubsystem.h"
2025-07-18 01:10:00 +02:00
#include "DTFluxGeneralSettings.h"
#include "DTFluxRemoteModule.h"
#include "DTFluxRemoteModule.h"
#include "HttpServerModule.h"
#include "IHttpRouter.h"
2025-07-18 01:10:00 +02:00
#include "Rundown/AvaRundown.h"
#include "Json.h"
#include "Engine/Engine.h"
#include "Misc/DateTime.h"
void UDTFluxRemoteSubsystem::Initialize(FSubsystemCollectionBase& Collection)
{
Super::Initialize(Collection);
UE_LOG(logDTFluxRemote, Log, TEXT("DTFlux API Subsystem Initialized"));
2025-07-18 01:10:00 +02:00
#if WITH_EDITOR
// S'abonner aux changements de settings
if (UDTFluxGeneralSettings* Settings = GetMutableDefault<UDTFluxGeneralSettings>())
{
SettingsRundownChangedHandle = Settings->OnRemoteRundownChanged.AddUObject(
this, &UDTFluxRemoteSubsystem::OnSettingsRundownChanged
);
}
#endif
LoadRundownFromSettings();
// Auto-start server (optionnel)
StartHTTPServer(63350);
}
void UDTFluxRemoteSubsystem::Deinitialize()
{
StopHTTPServer();
2025-07-18 01:10:00 +02:00
2025-07-18 01:10:00 +02:00
#if WITH_EDITOR
// Se désabonner du delegate
if (UDTFluxGeneralSettings* Settings = GetMutableDefault<UDTFluxGeneralSettings>())
{
if (SettingsRundownChangedHandle.IsValid())
{
Settings->OnRemoteRundownChanged.Remove(SettingsRundownChangedHandle);
SettingsRundownChangedHandle.Reset();
}
}
#endif
// Décharger proprement le rundown
UnloadCurrentRundown();
UE_LOG(logDTFluxRemote, Log, TEXT("DTFlux API Subsystem Deinitialized"));
Super::Deinitialize();
}
bool UDTFluxRemoteSubsystem::StartHTTPServer(int32 Port)
{
if (bServerRunning)
{
UE_LOG(logDTFluxRemote, Warning, TEXT("HTTP Server already running on port %d"), ServerPort);
return true;
}
ServerPort = Port;
// Get HTTP Server Module
FHttpServerModule& HttpServerModule = FHttpServerModule::Get();
// Create router
HttpRouter = HttpServerModule.GetHttpRouter(ServerPort);
if (!HttpRouter.IsValid())
{
UE_LOG(logDTFluxRemote, Error, TEXT("Failed to create HTTP router for port %d"), ServerPort);
return false;
}
// Setup routes
SetupRoutes();
// Start listening
HttpServerModule.StartAllListeners();
bServerRunning = true;
UE_LOG(logDTFluxRemote, Log, TEXT("DTFlux HTTP API Server started on port %d"), ServerPort);
UE_LOG(logDTFluxRemote, Log, TEXT("Base URL: http://localhost:%d/dtflux/api/v1"), ServerPort);
UE_LOG(logDTFluxRemote, Log, TEXT("Available routes:"));
2025-07-18 01:10:00 +02:00
UE_LOG(logDTFluxRemote, Log, TEXT(" PUT /dtflux/api/v1/title"));
UE_LOG(logDTFluxRemote, Log, TEXT(" PUT /dtflux/api/v1/title-bib"));
UE_LOG(logDTFluxRemote, Log, TEXT(" PUT /dtflux/api/v1/commands"));
return true;
}
void UDTFluxRemoteSubsystem::StopHTTPServer()
{
if (!bServerRunning)
{
return;
}
// Remove route handlers
if (HttpRouter.IsValid())
{
HttpRouter->UnbindRoute(TitleRouteHandle);
HttpRouter->UnbindRoute(TitleBibRouteHandle);
HttpRouter->UnbindRoute(CommandsRouteHandle);
}
// Stop server
FHttpServerModule& HttpServerModule = FHttpServerModule::Get();
HttpServerModule.StopAllListeners();
HttpRouter.Reset();
bServerRunning = false;
UE_LOG(logDTFluxRemote, Log, TEXT("DTFlux HTTP API Server stopped"));
}
bool UDTFluxRemoteSubsystem::IsHTTPServerRunning() const
{
return bServerRunning;
}
void UDTFluxRemoteSubsystem::SetupRoutes()
{
if (!HttpRouter.IsValid())
{
return;
}
// Route: POST /dtflux/api/v1/title
TitleRouteHandle = HttpRouter->BindRoute(
FHttpPath(TEXT("/dtflux/api/v1/title")),
2025-07-18 01:10:00 +02:00
EHttpServerRequestVerbs::VERB_PUT,
FHttpRequestHandler::CreateUObject(this, &UDTFluxRemoteSubsystem::HandleTitleRequest)
);
// Route: POST /dtflux/api/v1/title-bib
TitleBibRouteHandle = HttpRouter->BindRoute(
FHttpPath(TEXT("/dtflux/api/v1/title-bib")),
2025-07-18 01:10:00 +02:00
EHttpServerRequestVerbs::VERB_PUT,
FHttpRequestHandler::CreateUObject(this, &UDTFluxRemoteSubsystem::HandleTitleBibRequest)
2025-07-18 01:10:00 +02:00
);
// Route: POST /dtflux/api/v1/commands
CommandsRouteHandle = HttpRouter->BindRoute(
FHttpPath(TEXT("/dtflux/api/v1/commands")),
2025-07-18 01:10:00 +02:00
EHttpServerRequestVerbs::VERB_PUT,
FHttpRequestHandler::CreateUObject(this, &UDTFluxRemoteSubsystem::HandleCommandsRequest)
);
UE_LOG(logDTFluxRemote, Log, TEXT("HTTP routes configured successfully"));
}
bool UDTFluxRemoteSubsystem::HandleTitleRequest(const FHttpServerRequest& Request, const FHttpResultCallback& OnComplete)
{
UE_LOG(logDTFluxRemote, Log, TEXT("Received Title request"));
// Parse JSON
TSharedPtr<FJsonObject> JsonObject = ParseJsonFromRequest(Request);
if (!JsonObject.IsValid())
{
OnComplete(FHttpServerResponse::Create(CreateErrorResponse(TEXT("Invalid JSON")), TEXT("application/json")));
return true;
}
// Parse title data
FDTFluxRemoteTitleData TitleData;
if (!ParseTitleData(JsonObject, TitleData))
{
OnComplete(FHttpServerResponse::Create(CreateErrorResponse(TEXT("Invalid title data format")), TEXT("application/json")));
return true;
}
// Broadcast event (execute on Game Thread)
AsyncTask(ENamedThreads::GameThread, [this, TitleData]()
{
OnTitleReceived.Broadcast(TitleData);
});
// Send success response
OnComplete(FHttpServerResponse::Create(CreateSuccessResponse(TEXT("Title data received")), TEXT("application/json")));
return true;
}
bool UDTFluxRemoteSubsystem::HandleTitleBibRequest(const FHttpServerRequest& Request, const FHttpResultCallback& OnComplete)
{
UE_LOG(logDTFluxRemote, Log, TEXT("Received Title-Bib request"));
TSharedPtr<FJsonObject> JsonObject = ParseJsonFromRequest(Request);
if (!JsonObject.IsValid())
{
OnComplete(FHttpServerResponse::Create(CreateErrorResponse(TEXT("Invalid JSON")), TEXT("application/json")));
return true;
}
FDTFluxRemoteBibData BibData;
if (!ParseTitleBibData(JsonObject, BibData))
{
OnComplete(FHttpServerResponse::Create(CreateErrorResponse(TEXT("Invalid title-bib data format")), TEXT("application/json")));
return true;
}
AsyncTask(ENamedThreads::GameThread, [this, BibData]()
{
OnTitleBibReceived.Broadcast(BibData);
});
OnComplete(FHttpServerResponse::Create(CreateSuccessResponse(TEXT("Title-bib data received")), TEXT("application/json")));
return true;
}
bool UDTFluxRemoteSubsystem::HandleCommandsRequest(const FHttpServerRequest& Request, const FHttpResultCallback& OnComplete)
{
UE_LOG(logDTFluxRemote, Log, TEXT("Received Commands request"));
TSharedPtr<FJsonObject> JsonObject = ParseJsonFromRequest(Request);
if (!JsonObject.IsValid())
{
OnComplete(FHttpServerResponse::Create(CreateErrorResponse(TEXT("Invalid JSON")), TEXT("application/json")));
return true;
}
FDTFluxRemoteCommandData CommandData;
if (!ParseCommandData(JsonObject, CommandData))
{
OnComplete(FHttpServerResponse::Create(CreateErrorResponse(TEXT("Invalid command data format")), TEXT("application/json")));
return true;
}
AsyncTask(ENamedThreads::GameThread, [this, CommandData]()
{
OnCommandReceived.Broadcast(CommandData);
});
OnComplete(FHttpServerResponse::Create(CreateSuccessResponse(TEXT("Command data received")), TEXT("application/json")));
return true;
}
TSharedPtr<FJsonObject> UDTFluxRemoteSubsystem::ParseJsonFromRequest(const FHttpServerRequest& Request)
{
// Get request body
TArray<uint8> Body = Request.Body;
FString JsonString = FString(UTF8_TO_TCHAR(reinterpret_cast<const char*>(Body.GetData())));
UE_LOG(logDTFluxRemote, Verbose, TEXT("Received JSON: %s"), *JsonString);
// Parse JSON
TSharedPtr<FJsonObject> JsonObject;
TSharedRef<TJsonReader<>> Reader = TJsonReaderFactory<>::Create(JsonString);
if (!FJsonSerializer::Deserialize(Reader, JsonObject) || !JsonObject.IsValid())
{
UE_LOG(logDTFluxRemote, Error, TEXT("Failed to parse JSON: %s"), *JsonString);
return nullptr;
}
return JsonObject;
}
FString UDTFluxRemoteSubsystem::CreateSuccessResponse(const FString& Message)
{
TSharedPtr<FJsonObject> ResponseObject = MakeShareable(new FJsonObject);
ResponseObject->SetBoolField(TEXT("success"), true);
ResponseObject->SetStringField(TEXT("message"), Message);
ResponseObject->SetStringField(TEXT("timestamp"), FDateTime::Now().ToIso8601());
FString OutputString;
TSharedRef<TJsonWriter<>> Writer = TJsonWriterFactory<>::Create(&OutputString);
FJsonSerializer::Serialize(ResponseObject.ToSharedRef(), Writer);
return OutputString;
}
FString UDTFluxRemoteSubsystem::CreateErrorResponse(const FString& Error, int32 Code)
{
TSharedPtr<FJsonObject> ResponseObject = MakeShareable(new FJsonObject);
ResponseObject->SetBoolField(TEXT("success"), false);
ResponseObject->SetStringField(TEXT("error"), Error);
ResponseObject->SetNumberField(TEXT("code"), Code);
ResponseObject->SetStringField(TEXT("timestamp"), FDateTime::Now().ToIso8601());
FString OutputString;
TSharedRef<TJsonWriter<>> Writer = TJsonWriterFactory<>::Create(&OutputString);
FJsonSerializer::Serialize(ResponseObject.ToSharedRef(), Writer);
return OutputString;
}
bool UDTFluxRemoteSubsystem::ParseTitleData(const TSharedPtr<FJsonObject>& JsonObject, FDTFluxRemoteTitleData& OutData)
{
if (!JsonObject.IsValid())
{
return false;
}
// Parse title fields
JsonObject->TryGetStringField(TEXT("LastName"), OutData.LastName);
JsonObject->TryGetStringField(TEXT("FirsName"), OutData.FirstName);
JsonObject->TryGetStringField(TEXT("Function1"), OutData.Function1);
JsonObject->TryGetStringField(TEXT("Function2"), OutData.Function2);
UE_LOG(logDTFluxRemote, Log, TEXT("Parsed Title Data - LastName: %s, FirstName: %s"), *OutData.LastName, *OutData.FirstName);
return true;
}
bool UDTFluxRemoteSubsystem::ParseTitleBibData(const TSharedPtr<FJsonObject>& JsonObject, FDTFluxRemoteBibData& OutData)
{
if (!JsonObject.IsValid())
{
return false;
}
JsonObject->TryGetNumberField(TEXT("Bib"), OutData.Bib);
UE_LOG(logDTFluxRemote, Log, TEXT("Parsed Title-Bib Data - Bib: %i"), OutData.Bib);
return true;
}
bool UDTFluxRemoteSubsystem::ParseCommandData(const TSharedPtr<FJsonObject>& JsonObject, FDTFluxRemoteCommandData& OutData)
{
if (!JsonObject.IsValid())
{
return false;
}
JsonObject->TryGetNumberField(TEXT("type"), OutData.Type);
JsonObject->TryGetStringField(TEXT("Data"), OutData.Data);
UE_LOG(logDTFluxRemote, Log, TEXT("Parsed Command Data - Command Type: %i, Data: %s"),
OutData.Type, *OutData.Data);
return true;
}
2025-07-18 01:10:00 +02:00
void UDTFluxRemoteSubsystem::UnloadCurrentRundown()
{
}
void UDTFluxRemoteSubsystem::LoadRundownFromSettings()
{
const UDTFluxGeneralSettings* Settings = GetDefault<UDTFluxGeneralSettings>();
if (!Settings)
{
UE_LOG(logDTFluxRemote, Warning, TEXT("Cannot access DTFlux settings"));
return;
}
TSoftObjectPtr<UAvaRundown> RundownAsset = Settings->RemoteTargetRundown;
if (RundownAsset.IsNull())
{
UE_LOG(logDTFluxRemote, Log, TEXT("No rundown specified in settings"));
UnloadCurrentRundown();
return;
}
// Si c'est le même rundown, pas besoin de recharger
if (RemotedRundown && RemotedRundown == RundownAsset.LoadSynchronous())
{
UE_LOG(logDTFluxRemote, Log, TEXT("Rundown already loaded: %s"), *RundownAsset.ToString());
return;
}
// Décharger l'ancien rundown d'abord
UnloadCurrentRundown();
// Charger le nouveau rundown
if (LoadRundown(RundownAsset))
{
UE_LOG(logDTFluxRemote, Log, TEXT("Successfully loaded rundown from settings: %s"), *RundownAsset.ToString());
}
else
{
UE_LOG(logDTFluxRemote, Error, TEXT("Failed to load rundown from settings: %s"), *RundownAsset.ToString());
}
}
#if WITH_EDITOR
void UDTFluxRemoteSubsystem::OnSettingsRundownChanged(const TSoftObjectPtr<UAvaRundown>& NewRundown)
{
}
#endif
// Manual processing functions for testing
bool UDTFluxRemoteSubsystem::ProcessTitleData(const FString& JsonString)
{
TSharedPtr<FJsonObject> JsonObject;
TSharedRef<TJsonReader<>> Reader = TJsonReaderFactory<>::Create(JsonString);
if (!FJsonSerializer::Deserialize(Reader, JsonObject) || !JsonObject.IsValid())
{
return false;
}
FDTFluxRemoteTitleData TitleData;
if (ParseTitleData(JsonObject, TitleData))
{
OnTitleReceived.Broadcast(TitleData);
return true;
}
return false;
}
bool UDTFluxRemoteSubsystem::ProcessTitleBibData(const FString& JsonString)
{
TSharedPtr<FJsonObject> JsonObject;
TSharedRef<TJsonReader<>> Reader = TJsonReaderFactory<>::Create(JsonString);
if (!FJsonSerializer::Deserialize(Reader, JsonObject) || !JsonObject.IsValid())
{
return false;
}
FDTFluxRemoteBibData TitleBibData;
if (ParseTitleBibData(JsonObject, TitleBibData))
{
OnTitleBibReceived.Broadcast(TitleBibData);
return true;
}
return false;
}
bool UDTFluxRemoteSubsystem::ProcessCommandData(const FString& JsonString)
{
TSharedPtr<FJsonObject> JsonObject;
TSharedRef<TJsonReader<>> Reader = TJsonReaderFactory<>::Create(JsonString);
if (!FJsonSerializer::Deserialize(Reader, JsonObject) || !JsonObject.IsValid())
{
return false;
}
FDTFluxRemoteCommandData CommandData;
if (ParseCommandData(JsonObject, CommandData))
{
OnCommandReceived.Broadcast(CommandData);
return true;
}
return false;
2025-07-18 01:10:00 +02:00
}
void UDTFluxRemoteSubsystem::LoadRundown