// Fill out your copyright notice in the Description page of Project Settings. #include "DTFluxRemoteSubsystem.h" #include "DTFluxRemoteSubsystem.h" #include "DTFluxGeneralSettings.h" #include "DTFluxRemoteModule.h" #include "DTFluxRemoteModule.h" #include "HttpServerModule.h" #include "IHttpRouter.h" #include "Rundown/AvaRundown.h" #include "Json.h" #include "JsonObjectConverter.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")); #if WITH_EDITOR // S'abonner aux changements de settings if (UDTFluxGeneralSettings* Settings = GetMutableDefault()) { SettingsRundownChangedHandle = Settings->OnRemoteRundownChanged.AddUObject( this, &UDTFluxRemoteSubsystem::OnSettingsRundownChanged ); } #endif LoadRundownFromSettings(); // Auto-start server (optionnel) StartHTTPServer(63350); } void UDTFluxRemoteSubsystem::Deinitialize() { StopHTTPServer(); #if WITH_EDITOR // Se désabonner du delegate if (UDTFluxGeneralSettings* Settings = GetMutableDefault()) { 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:")); 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::ResetPendingTitleData() { bHasPendingTitleRequest = false; } void UDTFluxRemoteSubsystem::ResetPendingBibData() { bHasPendingTitleBibRequest = false; } void UDTFluxRemoteSubsystem::SetupRoutes() { if (!HttpRouter.IsValid()) { return; } // Route: POST /dtflux/api/v1/title TitleRouteHandle = HttpRouter->BindRoute( FHttpPath(TEXT("/dtflux/api/v1/title")), 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")), EHttpServerRequestVerbs::VERB_PUT, FHttpRequestHandler::CreateUObject(this, &UDTFluxRemoteSubsystem::HandleTitleBibRequest) ); // Route: POST /dtflux/api/v1/commands CommandsRouteHandle = HttpRouter->BindRoute( FHttpPath(TEXT("/dtflux/api/v1/commands")), 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 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); if (RemotedRundown && RemotedRundown->IsValidLowLevel()) { PendingTitleData = TitleData; bHasPendingTitleRequest = true; UE_LOG(logDTFluxRemote, Log, TEXT("Playing page %i"), TitleData.RundownPageId); RemotedRundown->PlayPage(TitleData.RundownPageId, EAvaRundownPagePlayType::PlayFromStart); } else { UE_LOG(logDTFluxRemote, Warning, TEXT("No rundown loaded")); } }); 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 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 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); if (RemotedRundown && RemotedRundown->IsValidLowLevel()) { RemotedRundown->StopPage(CommandData.RundownPageId, EAvaRundownPageStopOptions::None, false); UE_LOG(logDTFluxRemote, Log, TEXT("Stoping page %i"), CommandData.RundownPageId); } else { UE_LOG(logDTFluxRemote, Warning, TEXT("No rundown loaded")); } }); OnComplete(FHttpServerResponse::Create(CreateErrorResponse(TEXT("OK")), TEXT("application/json"))); return true; } TSharedPtr UDTFluxRemoteSubsystem::ParseJsonFromRequest(const FHttpServerRequest& Request) { // Get request body TArray Body = Request.Body; FString JsonString; if (Body.Num() > 0) { // Ajouter un null terminator si nécessaire if (Body.Last() != 0) { Body.Add(0); } JsonString = FString(UTF8_TO_TCHAR(reinterpret_cast(Body.GetData()))); } UE_LOG(logDTFluxRemote, Verbose, TEXT("Received JSON: %s"), *JsonString); // Parse JSON TSharedPtr JsonObject; TSharedRef> 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 ResponseObject = MakeShareable(new FJsonObject); ResponseObject->SetBoolField(TEXT("success"), true); ResponseObject->SetStringField(TEXT("message"), Message); ResponseObject->SetStringField(TEXT("timestamp"), FDateTime::Now().ToIso8601()); FString OutputString; TSharedRef> Writer = TJsonWriterFactory<>::Create(&OutputString); FJsonSerializer::Serialize(ResponseObject.ToSharedRef(), Writer); return OutputString; } FString UDTFluxRemoteSubsystem::CreateErrorResponse(const FString& Error, int32 Code) { TSharedPtr 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> Writer = TJsonWriterFactory<>::Create(&OutputString); FJsonSerializer::Serialize(ResponseObject.ToSharedRef(), Writer); return OutputString; } bool UDTFluxRemoteSubsystem::ParseTitleData(const TSharedPtr& JsonObject, FDTFluxRemoteTitleData& OutData) { if (!JsonObject.IsValid()) { UE_LOG(logDTFluxRemote, Error, TEXT("Invalid JSON object for %s"), TEXT("FDTFluxRemoteTitleData")); return false; } if (FJsonObjectConverter::JsonObjectToUStruct(JsonObject.ToSharedRef(), &OutData)) { UE_LOG(logDTFluxRemote, Log, TEXT("Successfully parsed %s"), TEXT("FDTFluxRemoteTitleData")); return true; } UE_LOG(logDTFluxRemote, Error, TEXT("Failed to convert JSON to %s struct"),TEXT("FDTFluxRemoteTitleData")); return false; } bool UDTFluxRemoteSubsystem::ParseTitleBibData(const TSharedPtr& JsonObject, FDTFluxRemoteBibData& OutData) { if (!JsonObject.IsValid()) { UE_LOG(logDTFluxRemote, Error, TEXT("Invalid JSON object for %s"), TEXT("FDTFluxRemoteBibData")); return false; } if (FJsonObjectConverter::JsonObjectToUStruct(JsonObject.ToSharedRef(), &OutData)) { UE_LOG(logDTFluxRemote, Log, TEXT("Successfully parsed %s"), TEXT("FDTFluxRemoteBibData")); return true; } UE_LOG(logDTFluxRemote, Error, TEXT("Failed to convert JSON to %s struct"),TEXT("FDTFluxRemoteBibData")); return false; } bool UDTFluxRemoteSubsystem::ParseCommandData(const TSharedPtr& JsonObject, FDTFluxRemoteCommandData& OutData) { if (!JsonObject.IsValid()) { UE_LOG(logDTFluxRemote, Error, TEXT("Invalid JSON object for %s"), TEXT("FDTFluxRemoteCommandData")); return false; } if (FJsonObjectConverter::JsonObjectToUStruct(JsonObject.ToSharedRef(), &OutData)) { UE_LOG(logDTFluxRemote, Log, TEXT("Successfully parsed %s"), TEXT("FDTFluxRemoteCommandData")); return true; } UE_LOG(logDTFluxRemote, Error, TEXT("Failed to convert JSON to %s struct"),TEXT("FDTFluxRemoteCommandData")); return false; } void UDTFluxRemoteSubsystem::UnloadCurrentRundown() { if (RemotedRundown) { UE_LOG(logDTFluxRemote, Log, TEXT("Unloading current rundown")); // Ici vous pouvez ajouter une logique de nettoyage si nécessaire // Par exemple : RemotedRundown->StopAllPages(); RemotedRundown = nullptr; } } void UDTFluxRemoteSubsystem::LoadRundownFromSettings() { const UDTFluxGeneralSettings* Settings = GetDefault(); if (!Settings) { UE_LOG(logDTFluxRemote, Warning, TEXT("Cannot access DTFlux settings")); return; } TSoftObjectPtr RundownAsset = Settings->RemoteTargetRundown; if (RundownAsset.IsNull()) { UE_LOG(logDTFluxRemote, Log, TEXT("No rundown specified in settings")); UnloadCurrentRundown(); return; } 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(); RundownAsset = RundownAsset.LoadSynchronous(); // Charger le nouveau rundown if ( RundownAsset.IsValid()) { 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()); } LoadRundown(RundownAsset); } bool UDTFluxRemoteSubsystem::LoadRundown(const TSoftObjectPtr& RundownAsset) { if (RundownAsset.IsNull()) { UE_LOG(logDTFluxRemote, Warning, TEXT("Cannot load rundown: asset is null")); UnloadCurrentRundown(); return false; } // Charger le rundown de manière synchrone UAvaRundown* LoadedRundown = RundownAsset.LoadSynchronous(); // Vérifier si le rundown est déjà chargé if (RemotedRundown && RemotedRundown == LoadedRundown) { UE_LOG(logDTFluxRemote, Log, TEXT("Rundown already loaded: %s"), *RundownAsset.ToString()); return true; } // Décharger l'ancien rundown d'abord UnloadCurrentRundown(); // Assigner le nouveau rundown RemotedRundown = LoadedRundown; // Vérifier que le chargement a réussi if (RemotedRundown && RemotedRundown->IsValidLowLevel()) { RemotedRundown->InitializePlaybackContext(); UE_LOG(logDTFluxRemote, Log, TEXT("Successfully loaded rundown: %s"), *RundownAsset.ToString()); return true; } else { UE_LOG(logDTFluxRemote, Error, TEXT("Failed to load rundown: %s"), *RundownAsset.ToString()); RemotedRundown = nullptr; return false; } } #if WITH_EDITOR void UDTFluxRemoteSubsystem::OnSettingsRundownChanged(const TSoftObjectPtr& NewRundown) { } #endif // Manual processing functions for testing bool UDTFluxRemoteSubsystem::ProcessTitleData(const FString& JsonString) { TSharedPtr JsonObject; TSharedRef> 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 JsonObject; TSharedRef> 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 JsonObject; TSharedRef> 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; }