Hello,
I’m currently delving into the utilization of built-in HTTP classes within Unreal Engine, such as FHttpServerModule, IHttpRouter, FHttpServerResponse
.
I’ve developed an engine plugin tasked with capturing generated frames at a certain frequency, for example, 25 frames per second. These frames are stored as const TArray64<uint8> &ImgData
. Additionally, the plugin operates an HTTP server. My objective is to stream this frame data as MJPEG to any visitor accessing http://127.0.0.1:8000/stream
.
Below, I’ve included snippets of the relevant code.
The problem.
I’m encountering difficulties understanding how to manage client connections to the HTTP server and how to continuously send responses to all connected clients. Despite extensive searches and examination of Unreal Engine sources, I haven’t found a satisfactory solution. The documentation for such modules is quite sparse. As far as I can tell, when I send a response using the following code:
TUniquePtr<FHttpServerResponse> Response = FHttpServerResponse::Create(ResponseContent, ContentType);
OnComplete(MoveTemp(Response));
the response is sent, but the connection is closed immediately. However, I need to maintain continuous responses until the client disconnects. When I try to execute the above code in a while(true) loop app crashes on second pass.
Any guidance or advice would be greatly appreciated!
Here are some excerpts from my HTTP server code. I acknowledge that there are issues present:
void AHttpServer::BeginPlay()
{
StartServer();
Super::BeginPlay();
}
void AHttpServer::StartServer()
{
if (ServerPort <= 0)
{
UE_LOG(LogTemp, Error, TEXT("Could not start HttpServer, port number must be greater than zero!"));
return;
}
FHttpServerModule &httpServerModule = FHttpServerModule::Get();
TSharedPtr<IHttpRouter> httpRouter = httpServerModule.GetHttpRouter(ServerPort);
if (httpRouter.IsValid())
{
httpRouter->BindRoute(FHttpPath(HttpPathSTREAM), EHttpServerRequestVerbs::VERB_GET,
[this](const FHttpServerRequest &Request, const FHttpResultCallback &OnComplete)
{ return RequestSTREAM(Request, OnComplete); });
httpServerModule.StartAllListeners();
_isServerStarted = true;
UE_LOG(LogTemp, Log, TEXT("Web server started on port = %d"), ServerPort);
}
else
{
_isServerStarted = false;
UE_LOG(LogTemp, Error, TEXT("Could not start web server on port = %d"), ServerPort);
}
}
void AHttpServer::UpdateJpegData(const TArray64<uint8> &NewJpegData)
{
JpegData = NewJpegData;
}
bool AHttpServer::RequestSTREAM(const FHttpServerRequest &Request, const FHttpResultCallback &OnComplete)
{
// Define the content type as MJPEG
FString ContentType = "multipart/x-mixed-replace; boundary=--frame";
// Start the response with the appropriate headers
FString ResponseContent = FString::Printf(TEXT("HTTP/1.1 200 OK\r\nContent-Type: %s\r\n\r\n"), *ContentType);
// Send each JPEG frame in a loop
while (true)
{
// Add the boundary delimiter
ResponseContent += "--frame\r\n";
// Add the content type for each frame
ResponseContent += "Content-Type: image/jpeg\r\n\r\n";
// Append the JPEG data
ResponseContent.Append(reinterpret_cast<const char *>(JpegData.GetData()), JpegData.Num());
// Add a newline after each frame
ResponseContent += "\r\n";
// Send the response asynchronously
TUniquePtr<FHttpServerResponse> Response = FHttpServerResponse::Create(ResponseContent, ContentType);
// Here I get the crash on second pass of the loop
OnComplete(MoveTemp(Response));
// Delay between frames (adjust as needed)
FPlatformProcess::Sleep(1.0f / 25.0f);
}
TUniquePtr<FHttpServerResponse> response = FHttpServerResponse::Create(TEXT("HttpServer STREAM"), TEXT("text/html"));
OnComplete(MoveTemp(response));
return true;
}
In my main class of the plugin in BeginPlay()
I create server:
HttpServer = (AHttpServer *)GetWorld()->SpawnActor(AHttpServer::StaticClass());
And here is the Tick
function where I update frame in HttpServer
class:
void AStreamManager::Tick(float DeltaTime)
{
Super::Tick(DeltaTime);
if (!RenderRequestQueue.IsEmpty())
{
// Peek the next RenderRequest from queue
FRenderRequestStreamStruct *nextRenderRequest = nullptr;
RenderRequestQueue.Peek(nextRenderRequest);
if (nextRenderRequest)
{ // nullptr check
if (nextRenderRequest->RenderFence.IsFenceComplete())
{
// Check if rendering is done, indicated by RenderFence
// Load the image wrapper module
IImageWrapperModule &ImageWrapperModule = FModuleManager::LoadModuleChecked<IImageWrapperModule>(FName("ImageWrapper"));
// Prepare data to be JPEG
static TSharedPtr<IImageWrapper> imageWrapper = ImageWrapperModule.CreateImageWrapper(EImageFormat::JPEG); // EImageFormat::JPEG
imageWrapper->SetRaw(nextRenderRequest->Image.GetData(), nextRenderRequest->Image.GetAllocatedSize(), FrameWidth, FrameHeight, ERGBFormat::BGRA, 8);
const TArray64<uint8> &ImgData = imageWrapper->GetCompressed(0);
HttpServer->UpdateJpegData(ImgData);
ImgCounter += 1;
// Delete the first element from RenderQueue
RenderRequestQueue.Pop();
delete nextRenderRequest;
}
}
}
}