Added ndi receiver and sdi outputs

This commit is contained in:
mchara40
2025-08-25 13:16:29 +03:00
parent 11b8be19f6
commit d046828313
125 changed files with 14757 additions and 0 deletions

View File

@@ -0,0 +1,404 @@
/*
Copyright (C) 2024 Vizrt NDI AB. All rights reserved.
This file and its use within a Product is bound by the terms of NDI SDK license that was provided
as part of the NDI SDK. For more information, please review the license and the NDI SDK documentation.
*/
#include <Services/NDIConnectionService.h>
#include <UObject/UObjectGlobals.h>
#include <UObject/Package.h>
#include <Misc/CoreDelegates.h>
#include <NDIIOPluginSettings.h>
#include <Objects/Media/NDIMediaSender.h>
#include <Framework/Application/SlateApplication.h>
#include <Misc/EngineVersionComparison.h>
#include <Engine/Engine.h>
#include <TextureResource.h>
#if WITH_EDITOR
#include <Editor.h>
#endif
/** Define Global Accessors */
FNDIConnectionServiceSendVideoEvent FNDIConnectionService::EventOnSendVideoFrame;
TMap<USoundSubmix*, FNDIConnectionServiceSendAudioEvent> FNDIConnectionService::SubmixSendAudioFrameEvents;
FCriticalSection FNDIConnectionService::AudioSyncContext;
FCriticalSection FNDIConnectionService::RenderSyncContext;
/** ************************ **/
/**
Constructs a new instance of this object
*/
FNDIConnectionService::FNDIConnectionService() {}
// Begin the service
bool FNDIConnectionService::Start()
{
if (!bIsInitialized)
{
bIsInitialized = true;
// Define some basic properties
FNDIBroadcastConfiguration Configuration;
FString BroadcastName = TEXT("Unreal Engine");
EObjectFlags Flags = RF_Public | RF_Standalone | RF_Transient | RF_MarkAsNative;
bool bBeginBroadcastOnPlay = false;
// Load the plugin settings for broadcasting the active viewport
if (auto* CoreSettings = NewObject<UNDIIOPluginSettings>())
{
// Define the configuration properties
Configuration.FrameRate = CoreSettings->BroadcastRate;
Configuration.FrameSize = FIntPoint(FMath::Clamp(CoreSettings->PreferredFrameSize.X, 240, 3840),
FMath::Clamp(CoreSettings->PreferredFrameSize.Y, 240, 3840));
// Set the broadcast name
BroadcastName = CoreSettings->ApplicationStreamName;
bBeginBroadcastOnPlay = CoreSettings->bBeginBroadcastOnPlay;
// clean-up the settings object
CoreSettings->ConditionalBeginDestroy();
CoreSettings = nullptr;
}
/** Construct the Active Viewport video texture */
this->VideoTexture = NewObject<UTextureRenderTarget2D>(
GetTransientPackage(), UTextureRenderTarget2D::StaticClass(), TEXT("NDIViewportVideoTexture"), Flags);
/** Construct the active viewport sender */
this->ActiveViewportSender = NewObject<UNDIMediaSender>(GetTransientPackage(), UNDIMediaSender::StaticClass(),
TEXT("NDIViewportSender"), Flags);
VideoTexture->UpdateResource();
// Update the active viewport sender, with the properties defined in the settings configuration
this->ActiveViewportSender->ChangeSourceName(BroadcastName);
this->ActiveViewportSender->ChangeVideoTexture(VideoTexture);
this->ActiveViewportSender->ChangeBroadcastConfiguration(Configuration);
// Hook into the core for the end of frame handlers
FCoreDelegates::OnEndFrameRT.AddRaw(this, &FNDIConnectionService::OnEndRenderFrame);
if (!GIsEditor)
{
FCoreDelegates::OnPostEngineInit.AddRaw(this, &FNDIConnectionService::OnPostEngineInit);
FCoreDelegates::OnEnginePreExit.AddRaw(this, &FNDIConnectionService::OnEnginePreExit);
if (bBeginBroadcastOnPlay)
BeginBroadcastingActiveViewport();
}
#if WITH_EDITOR
else
{
FEditorDelegates::PostPIEStarted.AddLambda([this](const bool Success) {
if (auto* CoreSettings = NewObject<UNDIIOPluginSettings>())
{
if (CoreSettings->bBeginBroadcastOnPlay == true)
BeginBroadcastingActiveViewport();
else
BeginAudioCapture();
// clean-up the settings object
CoreSettings->ConditionalBeginDestroy();
CoreSettings = nullptr;
}
bIsInPIEMode = true;
});
FEditorDelegates::PrePIEEnded.AddLambda([this](const bool Success) { StopBroadcastingActiveViewport(); });
}
#endif
}
return true;
}
// Stop the service
void FNDIConnectionService::Shutdown()
{
// Wait for the sync context locks
FScopeLock AudioLock(&AudioSyncContext);
FScopeLock RenderLock(&RenderSyncContext);
// reset the initialization properties
bIsInitialized = false;
StopAudioCapture();
// unbind our handlers for our frame events
FCoreDelegates::OnEndFrame.RemoveAll(this);
FCoreDelegates::OnEndFrameRT.RemoveAll(this);
// Cleanup the broadcasting of the active viewport
StopBroadcastingActiveViewport();
}
// Handler for when the render thread frame has ended
void FNDIConnectionService::OnEndRenderFrame()
{
FScopeLock Lock(&RenderSyncContext);
if (bIsInitialized)
{
int64 ticks = FDateTime::Now().GetTimeOfDay().GetTicks();
if (FNDIConnectionService::EventOnSendVideoFrame.IsBound())
{
FNDIConnectionService::EventOnSendVideoFrame.Broadcast(ticks);
}
}
}
void FNDIConnectionService::BeginAudioCapture()
{
if (bIsInitialized)
{
if (!bIsAudioInitialized)
{
if (GEngine)
{
FAudioDeviceHandle AudioDevice = GEngine->GetActiveAudioDevice();
if (AudioDevice.IsValid())
{
#if (ENGINE_MAJOR_VERSION > 5) || ((ENGINE_MAJOR_VERSION == 5) && (ENGINE_MINOR_VERSION >= 4)) // 5.4 or later
for (auto& SendAudioEvent : SubmixSendAudioFrameEvents)
{
if (SendAudioEvent.Key == nullptr)
AudioDevice->RegisterSubmixBufferListener(AsShared(), AudioDevice->GetMainSubmixObject());
else
AudioDevice->RegisterSubmixBufferListener(AsShared(), *SendAudioEvent.Key);
}
#else
AudioDevice->RegisterSubmixBufferListener(this);
#endif
bIsAudioInitialized = true;
}
}
}
}
}
void FNDIConnectionService::StopAudioCapture()
{
if (bIsAudioInitialized)
{
if (GEngine)
{
FAudioDeviceHandle AudioDevice = GEngine->GetActiveAudioDevice();
if (AudioDevice)
{
#if (ENGINE_MAJOR_VERSION > 5) || ((ENGINE_MAJOR_VERSION == 5) && (ENGINE_MINOR_VERSION >= 4)) // 5.4 or later
for (auto& SendAudioEvent : SubmixSendAudioFrameEvents)
{
if (SendAudioEvent.Key == nullptr)
AudioDevice->UnregisterSubmixBufferListener(AsShared(), AudioDevice->GetMainSubmixObject());
else
AudioDevice->UnregisterSubmixBufferListener(AsShared(), *SendAudioEvent.Key);
}
#else
AudioDevice->UnregisterSubmixBufferListener(this);
#endif
}
}
bIsAudioInitialized = false;
}
}
void FNDIConnectionService::OnPostEngineInit()
{
BeginAudioCapture();
}
void FNDIConnectionService::OnEnginePreExit()
{
StopAudioCapture();
}
bool FNDIConnectionService::BeginBroadcastingActiveViewport()
{
if (!bIsBroadcastingActiveViewport && IsValid(ActiveViewportSender))
{
// Load the plugin settings for broadcasting the active viewport
if (auto* CoreSettings = NewObject<UNDIIOPluginSettings>())
{
// Define some basic properties
FNDIBroadcastConfiguration Configuration;
FString BroadcastName = TEXT("Unreal Engine");
// Define the configuration properties
Configuration.FrameRate = CoreSettings->BroadcastRate;
Configuration.FrameSize = FIntPoint(FMath::Clamp(CoreSettings->PreferredFrameSize.X, 240, 3840),
FMath::Clamp(CoreSettings->PreferredFrameSize.Y, 240, 3840));
// Set the broadcast name
BroadcastName = CoreSettings->ApplicationStreamName;
// clean-up the settings object
CoreSettings->ConditionalBeginDestroy();
CoreSettings = nullptr;
// Update the active viewport sender, with the properties defined in the settings configuration
this->ActiveViewportSender->ChangeSourceName(BroadcastName);
this->ActiveViewportSender->ChangeBroadcastConfiguration(Configuration);
}
// we don't want to perform the linear conversion for the active viewport,
// since it's already had the conversion completed by the engine before passing to the sender
ActiveViewportSender->PerformLinearTosRGBConversion(false);
// Do not enable PTZ capabilities for active viewport sender
ActiveViewportSender->EnablePTZ(false);
// Initialize the sender, this will automatically start rendering output via NDI
ActiveViewportSender->Initialize(nullptr);
// We've initialized the active viewport
bIsBroadcastingActiveViewport = true;
// However we need to update the 'Video Texture' to the active viewport back buffer...
FSlateApplication::Get().GetRenderer()->OnPreResizeWindowBackBuffer().AddRaw(
this, &FNDIConnectionService::OnActiveViewportBackbufferPreResize);
FSlateApplication::Get().GetRenderer()->OnBackBufferReadyToPresent().AddRaw(
this, &FNDIConnectionService::OnActiveViewportBackbufferReadyToPresent);
BeginAudioCapture();
}
// always return true
return true;
}
// Handler for when the active viewport back buffer has been resized
void FNDIConnectionService::OnActiveViewportBackbufferPreResize(void* Backbuffer)
{
check(IsInGameThread());
// Ensure we have a valid video texture
FTextureResource* TextureResource = GetVideoTextureResource();
if (TextureResource != nullptr)
{
FRenderCommandFence Fence;
TextureResource->TextureRHI.SafeRelease();
this->ActiveViewportSender->ChangeVideoTexture(VideoTexture);
ENQUEUE_RENDER_COMMAND(FlushRHIThreadToUpdateTextureRenderTargetReference)(
[this](FRHICommandListImmediate& RHICmdList)
{
RHIUpdateTextureReference(VideoTexture->TextureReference.TextureReferenceRHI, nullptr);
RHICmdList.ImmediateFlush(EImmediateFlushType::FlushRHIThread);
});
// Wait for render thread to finish, so that renderthread texture references are updated
Fence.BeginFence();
Fence.Wait();
}
}
// Handler for when the back buffer is read to present to the end user
void FNDIConnectionService::OnActiveViewportBackbufferReadyToPresent(SWindow& Window,
const FTextureRHIRef& Backbuffer)
{
if (Window.GetType() == EWindowType::GameWindow || (Window.IsRegularWindow() && IsRunningInPIE()))
{
FTextureResource* TextureResource = GetVideoTextureResource();
if (TextureResource != nullptr)
{
// Lets improve the performance a bit
if (TextureResource->TextureRHI != Backbuffer)
{
TextureResource->TextureRHI = (FTextureRHIRef&)Backbuffer;
this->ActiveViewportSender->ChangeVideoTexture(VideoTexture);
RHIUpdateTextureReference(VideoTexture->TextureReference.TextureReferenceRHI, Backbuffer);
}
}
}
}
void FNDIConnectionService::StopBroadcastingActiveViewport()
{
// Wait for the sync context locks
FScopeLock RenderLock(&RenderSyncContext);
// reset the initialization properties
bIsInPIEMode = false;
StopAudioCapture();
// Ensure that if the active viewport sender is active, that we shut it down
if (IsValid(this->ActiveViewportSender))
{
FSlateApplication::Get().GetRenderer()->OnPreResizeWindowBackBuffer().RemoveAll(this);
FSlateApplication::Get().GetRenderer()->OnBackBufferReadyToPresent().RemoveAll(this);
// shutdown the active viewport sender (just in case it was activated)
this->ActiveViewportSender->Shutdown();
// reset the broadcasting flag, so that we can restart the broadcast later
this->bIsBroadcastingActiveViewport = false;
FTextureResource* TextureResource = GetVideoTextureResource();
if (TextureResource != nullptr)
{
TextureResource->TextureRHI.SafeRelease();
this->ActiveViewportSender->ChangeVideoTexture(VideoTexture);
}
}
}
FTextureResource* FNDIConnectionService::GetVideoTextureResource() const
{
if(IsValid(this->VideoTexture))
return this->VideoTexture->GetResource();
return nullptr;
}
void FNDIConnectionService::OnNewSubmixBuffer(const USoundSubmix* OwningSubmix, float* AudioData, int32 NumSamples, int32 NumChannels, const int32 SampleRate, double AudioClock)
{
if (NumSamples > 0)
{
FScopeLock Lock(&AudioSyncContext);
if (bIsAudioInitialized)
{
int64 ticks = FDateTime::Now().GetTimeOfDay().GetTicks();
#if (ENGINE_MAJOR_VERSION > 5) || ((ENGINE_MAJOR_VERSION == 5) && (ENGINE_MINOR_VERSION >= 4)) // 5.4 or later
FAudioDeviceHandle AudioDevice = GEngine->GetActiveAudioDevice();
if (&AudioDevice->GetMainSubmixObject() == OwningSubmix)
OwningSubmix = nullptr;
#else
OwningSubmix = nullptr;
#endif
FNDIConnectionServiceSendAudioEvent* SendAudioEvent = SubmixSendAudioFrameEvents.Find(OwningSubmix);
if (SendAudioEvent)
{
if (SendAudioEvent->IsBound())
{
SendAudioEvent->Broadcast(ticks, AudioData, NumSamples, NumChannels, SampleRate, AudioClock);
}
}
}
}
}
#if (ENGINE_MAJOR_VERSION > 5) || ((ENGINE_MAJOR_VERSION == 5) && (ENGINE_MINOR_VERSION >= 4)) // 5.4 or later
const FString& FNDIConnectionService::GetListenerName() const
{
static const FString ListenerName(TEXT("NDIConnectionServiceListener"));
return ListenerName;
}
#endif

View File

@@ -0,0 +1,232 @@
/*
Copyright (C) 2024 Vizrt NDI AB. All rights reserved.
This file and its use within a Product is bound by the terms of NDI SDK license that was provided
as part of the NDI SDK. For more information, please review the license and the NDI SDK documentation.
*/
#include <Services/NDIFinderService.h>
#include <Async/Async.h>
#include <Misc/EngineVersionComparison.h>
#include <NDIIOPluginAPI.h>
/** Define Global Accessors */
static NDIlib_find_instance_t NDI_FIND_INSTANCE = nullptr;
static FCriticalSection NDI_FIND_SYNC_CONTEXT;
FNDIFinderService::FNDISourceCollectionChangedEvent FNDIFinderService::EventOnNDISourceCollectionChanged;
TArray<FNDIConnectionInformation> FNDIFinderService::NetworkSourceCollection = TArray<FNDIConnectionInformation>();
/** ************************ **/
FNDIFinderService::FNDIFinderService()
{
if (NDI_FIND_INSTANCE == nullptr)
{
FScopeLock Lock(&NDI_FIND_SYNC_CONTEXT);
NDI_FIND_INSTANCE = NDIlib_find_create_v2(nullptr);
}
}
// Begin the service
bool FNDIFinderService::Start()
{
if (!bIsThreadRunning && p_RunnableThread == nullptr)
{
if (NDI_FIND_INSTANCE != nullptr)
{
this->bIsThreadRunning = true;
p_RunnableThread = FRunnableThread::Create(this, TEXT("FNDIFinderService_Tick"), 0, TPri_BelowNormal);
return bIsThreadRunning = p_RunnableThread != nullptr;
}
}
return false;
}
/** FRunnable Interface implementation for 'Init' */
bool FNDIFinderService::Init()
{
return NDI_FIND_INSTANCE != nullptr;
}
/** FRunnable Interface implementation for 'Stop' */
uint32 FNDIFinderService::Run()
{
static const uint32 find_wait_time = 500;
if (NDI_FIND_INSTANCE == nullptr)
return 0;
// Only update when we are suppose to run
while (bIsThreadRunning)
{
// Wait up to 'find_wait_time' (in milliseconds) to determine whether new sources have been added
if (!NDIlib_find_wait_for_sources(NDI_FIND_INSTANCE, find_wait_time))
{
// alright the source collection has stopped updating, did we change the network source collection?
if (UpdateNetworkSourceCollection())
{
// Broadcast the even on the game thread for thread safety purposes
AsyncTask(ENamedThreads::GameThread, []() {
if (FNDIFinderService::EventOnNDISourceCollectionChanged.IsBound())
FNDIFinderService::EventOnNDISourceCollectionChanged.Broadcast();
});
}
}
}
// return success
return 1;
}
/** FRunnable Interface implementation for 'Run' */
void FNDIFinderService::Shutdown()
{
if (p_RunnableThread != nullptr)
{
this->bIsThreadRunning = false;
p_RunnableThread->WaitForCompletion();
p_RunnableThread = nullptr;
}
// Ensure we unload the finder instance
if (NDI_FIND_INSTANCE != nullptr)
NDIlib_find_destroy(NDI_FIND_INSTANCE);
}
// Stop the service
void FNDIFinderService::Stop()
{
Shutdown();
}
bool FNDIFinderService::UpdateNetworkSourceCollection()
{
uint32 no_sources = 0;
bool bHasCollectionChanged = false;
if (NDI_FIND_INSTANCE != nullptr)
{
const NDIlib_source_t* p_sources = NDIlib_find_get_current_sources(NDI_FIND_INSTANCE, &no_sources);
// Change Scope
{
FScopeLock lock(&NDI_FIND_SYNC_CONTEXT);
bHasCollectionChanged = FNDIFinderService::NetworkSourceCollection.Num() != no_sources;
if (no_sources > 0 && p_sources != nullptr)
{
uint32 CurrentSourceCount = NetworkSourceCollection.Num();
for (uint32 iter = 0; iter < no_sources; iter++)
{
if (iter >= CurrentSourceCount)
{
NetworkSourceCollection.Add(FNDIConnectionInformation());
}
const NDIlib_source_t* SourceInformation = &p_sources[iter];
FNDIConnectionInformation* CollectionSource = &NetworkSourceCollection[iter];
bHasCollectionChanged |= SourceInformation->p_url_address != CollectionSource->Url;
CollectionSource->Url = SourceInformation->p_url_address;
CollectionSource->SourceName = SourceInformation->p_ndi_name;
FString SourceName = SourceInformation->p_ndi_name;
SourceName.Split(TEXT(" "), &CollectionSource->MachineName, &CollectionSource->StreamName);
// Now that the MachineName and StreamName have been split, cleanup the stream name
CollectionSource->StreamName.RemoveFromStart("(");
CollectionSource->StreamName.RemoveFromEnd(")");
}
if (CurrentSourceCount > no_sources)
{
#if (ENGINE_MAJOR_VERSION > 5) || ((ENGINE_MAJOR_VERSION == 5) && (ENGINE_MINOR_VERSION >= 5)) // 5.5 or later
NetworkSourceCollection.RemoveAt(no_sources, CurrentSourceCount - no_sources, EAllowShrinking::Yes);
#else
NetworkSourceCollection.RemoveAt(no_sources, CurrentSourceCount - no_sources, true);
#endif
bHasCollectionChanged = true;
}
}
else if (NetworkSourceCollection.Num() > 0)
{
NetworkSourceCollection.Empty();
bHasCollectionChanged = true;
}
bHasCollectionChanged |= NetworkSourceCollection.Num() != no_sources;
}
}
return bHasCollectionChanged;
}
/** Call to update an existing collection of network sources to match the current collection */
bool FNDIFinderService::UpdateSourceCollection(TArray<FNDIConnectionInformation>& InSourceCollection)
{
bool bHasCollectionChanged = false;
{
FScopeLock Lock(&NDI_FIND_SYNC_CONTEXT);
const uint32& no_sources = NetworkSourceCollection.Num();
bHasCollectionChanged = InSourceCollection.Num() != no_sources;
if (no_sources > 0)
{
uint32 CurrentSourceCount = InSourceCollection.Num();
for (uint32 iter = 0; iter < no_sources; iter++)
{
if (iter >= CurrentSourceCount)
{
InSourceCollection.Add(FNDIConnectionInformation());
CurrentSourceCount = InSourceCollection.Num();
}
FNDIConnectionInformation* CollectionSource = &InSourceCollection[iter];
const FNDIConnectionInformation* SourceInformation = &NetworkSourceCollection[iter];
bHasCollectionChanged |= SourceInformation->Url != CollectionSource->Url;
CollectionSource->Url = SourceInformation->Url;
CollectionSource->SourceName = SourceInformation->SourceName;
CollectionSource->MachineName = SourceInformation->MachineName;
CollectionSource->StreamName = SourceInformation->StreamName;
}
if (CurrentSourceCount > no_sources)
{
#if (ENGINE_MAJOR_VERSION > 5) || ((ENGINE_MAJOR_VERSION == 5) && (ENGINE_MINOR_VERSION >= 5)) // 5.5 or later
InSourceCollection.RemoveAt(no_sources, CurrentSourceCount - no_sources, EAllowShrinking::Yes);
#else
InSourceCollection.RemoveAt(no_sources, CurrentSourceCount - no_sources, true);
#endif
bHasCollectionChanged = true;
}
}
else if (InSourceCollection.Num() > 0)
{
InSourceCollection.Empty();
bHasCollectionChanged = true;
}
}
return bHasCollectionChanged;
}
/** Get the available sources on the network */
const TArray<FNDIConnectionInformation> FNDIFinderService::GetNetworkSourceCollection()
{
FScopeLock Lock(&NDI_FIND_SYNC_CONTEXT);
return FNDIFinderService::NetworkSourceCollection;
}