First commit

This commit is contained in:
2025-09-04 16:16:17 +03:00
parent 25a1a8d36a
commit ae934d9718
506 changed files with 15576 additions and 2 deletions

View File

@@ -0,0 +1,153 @@
/*
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 <Components/NDIBroadcastComponent.h>
UNDIBroadcastComponent::UNDIBroadcastComponent(const FObjectInitializer& ObjectInitializer) : Super(ObjectInitializer)
{}
/**
Initialize this component with the media source required for sending NDI audio, video, and metadata.
Returns false, if the MediaSource is already been set. This is usually the case when this component is
initialized in Blueprints.
*/
bool UNDIBroadcastComponent::Initialize(UNDIMediaSender* InMediaSource)
{
// is the media source already set?
if (this->NDIMediaSource == nullptr && InMediaSource != nullptr)
{
// we passed validation, so set the media source
this->NDIMediaSource = InMediaSource;
}
// did we pass validation
return InMediaSource != nullptr && InMediaSource == NDIMediaSource;
}
/**
Attempts to start broadcasting audio, video, and metadata via the 'NDIMediaSource' associated with this object
@param ErrorMessage The error message received when the media source is unable to start broadcasting
@result Indicates whether this object successfully started broadcasting
*/
bool UNDIBroadcastComponent::StartBroadcasting(FString& ErrorMessage)
{
// validate the Media Source object
if (IsValid(NDIMediaSource))
{
// call the media source implementation of the function
NDIMediaSource->Initialize(nullptr);
// the underlying functionality is always return 'true'
return true;
}
// We have no media source to broadcast
ErrorMessage = TEXT("No Media Source present to broadcast");
// looks like we don't have a media source to broadcast
return false;
}
/**
Changes the name of the sender object as seen on the network for remote connections
@param InSourceName The new name of the source to be identified as on the network
*/
void UNDIBroadcastComponent::ChangeSourceName(const FString& InSourceName)
{
// validate the Media Source object
if (IsValid(NDIMediaSource))
{
// call the media source implementation of the function
NDIMediaSource->ChangeSourceName(InSourceName);
}
}
/**
Attempts to change the Broadcast information associated with this media object
@param InConfiguration The new configuration to broadcast
*/
void UNDIBroadcastComponent::ChangeBroadcastConfiguration(const FNDIBroadcastConfiguration& InConfiguration)
{
// validate the Media Source object
if (IsValid(NDIMediaSource))
{
// call the media source implementation of the function
NDIMediaSource->ChangeBroadcastConfiguration(InConfiguration);
}
}
/**
Attempts to change the RenderTarget used in sending video frames over NDI
@param BroadcastTexture The texture to use as video, while broadcasting over NDI
*/
void UNDIBroadcastComponent::ChangeBroadcastTexture(UTextureRenderTarget2D* BroadcastTexture)
{
// validate the Media Source object
if (IsValid(NDIMediaSource))
{
// call the media source implementation of the function
NDIMediaSource->ChangeVideoTexture(BroadcastTexture);
}
}
/**
Determines the current tally information. If you specify a timeout then it will wait until it has
changed, otherwise it will simply poll it and return the current tally immediately
@param IsOnPreview - A state indicating whether this source in on preview of a receiver
@param IsOnProgram - A state indicating whether this source is on program of a receiver
*/
void UNDIBroadcastComponent::GetTallyInformation(bool& IsOnPreview, bool& IsOnProgram)
{
// Initialize the properties
IsOnPreview = false;
IsOnProgram = false;
// validate the Media Source object
if (IsValid(NDIMediaSource))
{
// call the media source implementation of the function
NDIMediaSource->GetTallyInformation(IsOnPreview, IsOnProgram, 0);
}
}
/**
Gets the current number of receivers connected to this source. This can be used to avoid rendering
when nothing is connected to the video source. which can significantly improve the efficiency if
you want to make a lot of sources available on the network
@param Result The total number of connected receivers attached to the broadcast of this object
*/
void UNDIBroadcastComponent::GetNumberOfConnections(int32& Result)
{
// Initialize the property
Result = 0;
// validate the Media Source object
if (IsValid(NDIMediaSource))
{
// call the media source implementation of the function
NDIMediaSource->GetNumberOfConnections(Result);
}
}
/**
Attempts to immediately stop sending frames over NDI to any connected receivers
*/
void UNDIBroadcastComponent::StopBroadcasting()
{
// validate the Media Source object
if (IsValid(NDIMediaSource))
{
// call the media source implementation of the function
NDIMediaSource->Shutdown();
}
}

View File

@@ -0,0 +1,104 @@
/*
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 <Components/NDIFinderComponent.h>
#include <Services/NDIFinderService.h>
UNDIFinderComponent::UNDIFinderComponent(const FObjectInitializer& ObjectInitializer) : Super(ObjectInitializer) {}
void UNDIFinderComponent::BeginPlay()
{
Super::BeginPlay();
// Provide some sense of thread-safety
FScopeLock Lock(&CollectionSyncContext);
// Update the NetworkSourceCollection with some sources which that the service has already found
FNDIFinderService::UpdateSourceCollection(NetworkSourceCollection);
// Ensure that we are subscribed to the collection changed notification so we can handle it locally
FNDIFinderService::EventOnNDISourceCollectionChanged.AddUObject(
this, &UNDIFinderComponent::OnNetworkSourceCollectionChangedEvent);
}
void UNDIFinderComponent::EndPlay(const EEndPlayReason::Type EndPlayReason)
{
Super::EndPlay(EndPlayReason);
// Provide some sense of thread-safety
FScopeLock Lock(&CollectionSyncContext);
// Empty the source collection
this->NetworkSourceCollection.Empty(0);
// Ensure that we are no longer subscribed to collection change notifications
FNDIFinderService::EventOnNDISourceCollectionChanged.RemoveAll(this);
}
/**
An Event handler for when the NDI Finder Service notifies listeners that changes have been
detected in the network source collection
*/
void UNDIFinderComponent::OnNetworkSourceCollectionChangedEvent()
{
// Since we don't poll the NDIFinderService for network sources, we subscribe to the change notification.
// Now we need to update the Network Source Collection, but we need to do it in a thread-safe way.
FScopeLock Lock(&CollectionSyncContext);
// Check to determine if something actually changed within the collection. We don't want to trigger
// notifications unnecessarily.
if (FNDIFinderService::UpdateSourceCollection(NetworkSourceCollection))
{
// Trigger the blueprint handling of the situation.
this->OnNetworkSourcesChangedEvent();
// If any listeners have subscribed broadcast any collection changes
if (this->OnNetworkSourcesChanged.IsBound())
this->OnNetworkSourcesChanged.Broadcast(this);
}
}
/**
Attempts to find a network source by the supplied name.
@param ConnectionInformation An existing source information structure which contains the source name
@param InSourceName A string value representing the name of the source to find
@result A value indicating whether a source with the supplied name was found
*/
const bool UNDIFinderComponent::FindNetworkSourceByName(FNDIConnectionInformation& ConnectionInformation,
FString InSourceName)
{
// Lock the Collection so that we are working with a solid collection of items
FScopeLock Lock(&CollectionSyncContext);
// Ensure we Reset the SourceInformation
ConnectionInformation.Reset();
for (const auto& connectionInfo : NetworkSourceCollection)
{
if (InSourceName.Equals(connectionInfo.SourceName, ESearchCase::IgnoreCase))
{
ConnectionInformation = connectionInfo;
return true;
}
}
return false;
}
/**
Returns the current collection of sources found on the network
*/
const TArray<FNDIConnectionInformation> UNDIFinderComponent::GetNetworkSources()
{
// Lock the current source collection
FScopeLock Lock(&CollectionSyncContext);
// return the source collection
return this->NetworkSourceCollection;
}

View File

@@ -0,0 +1,471 @@
/*
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 <Components/NDIPTZControllerComponent.h>
#include <GameFramework/Actor.h>
#include <Structures/NDIXml.h>
/**
Parsers for PTZ metadata
*/
class NDIXmlElementParser_ntk_ptz_pan_tilt_speed : public NDIXmlElementParser
{
public:
NDIXmlElementParser_ntk_ptz_pan_tilt_speed(UPTZController* PTZControllerIn)
: PTZController(PTZControllerIn)
{}
virtual bool ProcessOpen(const TCHAR* ElementName, const TCHAR* ElementData)
{
PanSpeed = 0.0;
TiltSpeed = 0.0;
return true;
}
virtual bool ProcessAttribute(const TCHAR* AttributeName, const TCHAR* AttributeValue) override
{
if(FCString::Strcmp(TEXT("pan_speed"), AttributeName) == 0)
{
PanSpeed = FCString::Atod(AttributeValue);
}
else if(FCString::Strcmp(TEXT("tilt_speed"), AttributeName) == 0)
{
TiltSpeed = FCString::Atod(AttributeValue);
}
return true;
}
virtual bool ProcessClose(const TCHAR* ElementName) override
{
PTZController->SetPTZPanTiltSpeed(PanSpeed, TiltSpeed);
return true;
}
protected:
UPTZController* PTZController;
double PanSpeed { 0.0 };
double TiltSpeed { 0.0 };
};
class NDIXmlElementParser_ntk_ptz_zoom_speed : public NDIXmlElementParser
{
public:
NDIXmlElementParser_ntk_ptz_zoom_speed(UPTZController* PTZControllerIn)
: PTZController(PTZControllerIn)
{}
virtual bool ProcessOpen(const TCHAR* ElementName, const TCHAR* ElementData)
{
ZoomSpeed = 0.0;
return true;
}
virtual bool ProcessAttribute(const TCHAR* AttributeName, const TCHAR* AttributeValue) override
{
if(FCString::Strcmp(TEXT("zoom_speed"), AttributeName) == 0)
{
ZoomSpeed = FCString::Atod(AttributeValue);
}
return true;
}
virtual bool ProcessClose(const TCHAR* ElementName) override
{
PTZController->SetPTZZoomSpeed(ZoomSpeed);
return true;
}
protected:
UPTZController* PTZController;
double ZoomSpeed { 0.0 };
};
class NDIXmlElementParser_ntk_ptz_focus : public NDIXmlElementParser
{
public:
NDIXmlElementParser_ntk_ptz_focus(UPTZController* PTZControllerIn)
: PTZController(PTZControllerIn)
{}
virtual bool ProcessOpen(const TCHAR* ElementName, const TCHAR* ElementData)
{
AutoMode = true;
Distance = 0.5;
return true;
}
virtual bool ProcessAttribute(const TCHAR* AttributeName, const TCHAR* AttributeValue) override
{
if(FCString::Strcmp(TEXT("mode"), AttributeName) == 0)
{
if(FCString::Strcmp(TEXT("manual"), AttributeValue) == 0)
AutoMode = false;
}
else if(FCString::Strcmp(TEXT("distance"), AttributeName) == 0)
{
Distance = FCString::Atod(AttributeValue);
}
return true;
}
virtual bool ProcessClose(const TCHAR* ElementName) override
{
PTZController->SetPTZFocus(AutoMode, Distance);
return true;
}
protected:
UPTZController* PTZController;
bool AutoMode { true };
double Distance { 0.5 };
};
class NDIXmlElementParser_ntk_ptz_store_preset : public NDIXmlElementParser
{
public:
NDIXmlElementParser_ntk_ptz_store_preset(UPTZController* PTZControllerIn)
: PTZController(PTZControllerIn)
{}
virtual bool ProcessOpen(const TCHAR* ElementName, const TCHAR* ElementData)
{
StoreIndex = -1;
return true;
}
virtual bool ProcessAttribute(const TCHAR* AttributeName, const TCHAR* AttributeValue) override
{
if(FCString::Strcmp(TEXT("index"), AttributeName) == 0)
{
StoreIndex = FCString::Atoi(AttributeValue);
}
return true;
}
virtual bool ProcessClose(const TCHAR* ElementName) override
{
if(StoreIndex >= 0)
{
PTZController->StorePTZState(StoreIndex);
}
return true;
}
protected:
UPTZController* PTZController;
int StoreIndex { -1 };
};
class NDIXmlElementParser_ntk_ptz_recall_preset : public NDIXmlElementParser
{
public:
NDIXmlElementParser_ntk_ptz_recall_preset(UPTZController* PTZControllerIn)
: PTZController(PTZControllerIn)
{}
virtual bool ProcessOpen(const TCHAR* ElementName, const TCHAR* ElementData)
{
RecallIndex = -1;
return true;
}
virtual bool ProcessAttribute(const TCHAR* AttributeName, const TCHAR* AttributeValue) override
{
if(FCString::Strcmp(TEXT("index"), AttributeName) == 0)
{
RecallIndex = FCString::Atoi(AttributeValue);
}
return true;
}
virtual bool ProcessClose(const TCHAR* ElementName) override
{
if(RecallIndex >= 0)
{
PTZController->RecallPTZState(RecallIndex);
}
return true;
}
protected:
UPTZController* PTZController;
int RecallIndex { -1 };
};
/**
PTZ controller component
*/
UPTZController::UPTZController()
{
this->bWantsInitializeComponent = true;
this->PrimaryComponentTick.bAllowTickOnDedicatedServer = false;
this->PrimaryComponentTick.bCanEverTick = true;
this->PrimaryComponentTick.bHighPriority = true;
this->PrimaryComponentTick.bRunOnAnyThread = false;
this->PrimaryComponentTick.bStartWithTickEnabled = true;
this->PrimaryComponentTick.bTickEvenWhenPaused = true;
this->NDIMetadataParser = MakeShareable(new NDIXmlParser());
this->NDIMetadataParser->AddElementParser("ntk_ptz_pan_tilt_speed", MakeShareable(new NDIXmlElementParser_ntk_ptz_pan_tilt_speed(this)));
this->NDIMetadataParser->AddElementParser("ntk_ptz_zoom_speed", MakeShareable(new NDIXmlElementParser_ntk_ptz_zoom_speed(this)));
this->NDIMetadataParser->AddElementParser("ntk_ptz_focus", MakeShareable(new NDIXmlElementParser_ntk_ptz_focus(this)));
this->NDIMetadataParser->AddElementParser("ntk_ptz_store_preset", MakeShareable(new NDIXmlElementParser_ntk_ptz_store_preset(this)));
this->NDIMetadataParser->AddElementParser("ntk_ptz_recall_preset", MakeShareable(new NDIXmlElementParser_ntk_ptz_recall_preset(this)));
}
UPTZController::~UPTZController()
{}
void UPTZController::InitializeComponent()
{
Super::InitializeComponent();
if (IsValid(NDIMediaSource))
{
// Ensure the PTZ controller is subscribed to the sender receiving metadata
this->NDIMediaSource->OnSenderMetaDataReceived.RemoveAll(this);
this->NDIMediaSource->OnSenderMetaDataReceived.AddDynamic(this, &UPTZController::ReceiveMetaDataFromSender);
}
}
bool UPTZController::Initialize(UNDIMediaSender* InMediaSource)
{
// is the media source already set?
if (this->NDIMediaSource == nullptr && InMediaSource != nullptr)
{
// we passed validation, so set the media source
this->NDIMediaSource = InMediaSource;
// validate the Media Source object
if (IsValid(NDIMediaSource))
{
// Ensure the PTZ controller is subscribed to the sender receiving metadata
this->NDIMediaSource->OnSenderMetaDataReceived.RemoveAll(this);
this->NDIMediaSource->OnSenderMetaDataReceived.AddDynamic(this, &UPTZController::ReceiveMetaDataFromSender);
}
}
// did we pass validation
return InMediaSource != nullptr && InMediaSource == NDIMediaSource;
}
void UPTZController::SetPTZPanTiltSpeed(float PanSpeed, float TiltSpeed)
{
PTZPanSpeed = PanSpeed;
PTZTiltSpeed = TiltSpeed;
OnPTZPanTiltSpeed.Broadcast(PanSpeed, TiltSpeed);
}
void UPTZController::SetPTZZoomSpeed(float ZoomSpeed)
{
PTZZoomSpeed = ZoomSpeed;
OnPTZZoomSpeed.Broadcast(ZoomSpeed);
}
void UPTZController::SetPTZFocus(bool AutoMode, float Distance)
{
FPTZState PTZState = GetPTZStateFromUE();
PTZState.FocusDistance = Distance;
PTZState.bAutoFocus = AutoMode;
SetPTZStateToUE(PTZState);
OnPTZFocus.Broadcast(AutoMode, Distance);
}
void UPTZController::StorePTZState(int Index)
{
if((Index >= 0) && (Index < 256))
{
FPTZState PTZState = GetPTZStateFromUE();
if(Index >= PTZStoredStates.Num())
PTZStoredStates.SetNum(Index+1);
PTZStoredStates[Index] = PTZState;
OnPTZStore.Broadcast(Index);
}
}
void UPTZController::RecallPTZState(int Index)
{
if((Index >= 0) && (Index < PTZStoredStates.Num()))
{
if(PTZRecallEasing > 0)
{
PTZStateInterp.PTZTargetState = PTZStoredStates[Index];
PTZStateInterp.EasingDuration = PTZRecallEasing;
PTZStateInterp.EasingRemaining = PTZStateInterp.EasingDuration;
}
else
{
SetPTZStateToUE(PTZStoredStates[Index]);
}
}
OnPTZRecall.Broadcast(Index);
}
FPTZState UPTZController::GetPTZStateFromUE() const
{
AActor* OwnerActor = GetOwner();
IPTZControllableInterface* ControllableObject = Cast<IPTZControllableInterface>(OwnerActor);
if (ControllableObject != nullptr)
{
return ControllableObject->GetPTZStateFromUE();
}
else
{
FPTZState PTZState;
FTransform Transform = OwnerActor->GetActorTransform();
FVector Euler = Transform.GetRotation().Euler();
PTZState.Pan = FMath::DegreesToRadians(Euler[2]);
PTZState.Tilt = FMath::DegreesToRadians(Euler[1]);
Transform.SetRotation(FQuat::MakeFromEuler(FVector(Euler[0], 0.f, 0.f)));
PTZState.CameraTransform = Transform;
return PTZState;
}
}
void UPTZController::SetPTZStateToUE(const FPTZState& PTZState)
{
if (EnablePTZ == true)
{
AActor* OwnerActor = GetOwner();
IPTZControllableInterface* ControllableObject = Cast<IPTZControllableInterface>(OwnerActor);
if (ControllableObject != nullptr)
{
ControllableObject->SetPTZStateToUE(PTZState);
}
else
{
FTransform Transform = PTZState.CameraTransform;
FVector Euler = Transform.GetRotation().Euler();
float Pitch = FMath::RadiansToDegrees(PTZState.Tilt);
float Yaw = FMath::RadiansToDegrees(PTZState.Pan);
Transform.SetRotation(FQuat::MakeFromEuler(FVector(Euler[0], Pitch, Yaw)));
OwnerActor->SetActorTransform(Transform);
}
}
}
void UPTZController::TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction)
{
Super::TickComponent(DeltaTime, TickType, ThisTickFunction);
bool bUpdatePTZ = false;
if(PTZStateInterp.EasingRemaining > 0)
bUpdatePTZ = true;
if((PTZPanSpeed != 0) || (PTZTiltSpeed != 0) || (PTZZoomSpeed != 0))
bUpdatePTZ = true;
if(bUpdatePTZ)
{
FPTZState PTZState = GetPTZStateFromUE();
if(PTZStateInterp.EasingRemaining > 0)
{
float EasingDelta = FMath::Min(PTZStateInterp.EasingRemaining, DeltaTime);
/** Interpolate from 0 to 1 using polynomial:
I(F) = a*F^3 + b*F^2 + c*F + d
with constraints:
Start and end points: I(0) = 0, I(1) = 1
Smooth stop at end: I'(1) = 0 (velocity)
I''(1) = 0 (acceleration)
Solve to get:
a = 1, b = -3, c = 3, d = 0
I(F) = F^3 - 3*F^2 + 3*F
*/
float EasingFrac = (PTZStateInterp.EasingRemaining > 0) ? (EasingDelta / PTZStateInterp.EasingRemaining) : 1;
float EasingInterp = EasingFrac*EasingFrac*EasingFrac - 3*EasingFrac*EasingFrac + 3*EasingFrac;
PTZState.Pan = PTZState.Pan * (1 - EasingInterp) + PTZStateInterp.PTZTargetState.Pan * EasingInterp;
PTZState.Tilt = PTZState.Tilt * (1 - EasingInterp) + PTZStateInterp.PTZTargetState.Tilt * EasingInterp;
PTZState.FieldOfView = PTZState.FieldOfView * (1 - EasingInterp) + PTZStateInterp.PTZTargetState.FieldOfView * EasingInterp;
PTZState.FocusDistance = PTZState.FocusDistance * (1 - EasingInterp) + PTZStateInterp.PTZTargetState.FocusDistance * EasingInterp;
PTZState.CameraTransform.BlendWith(PTZStateInterp.PTZTargetState.CameraTransform, EasingInterp);
PTZStateInterp.EasingRemaining -= EasingDelta;
}
PTZState.FieldOfView -= FMath::RadiansToDegrees(PTZZoomSpeed) * DeltaTime;
if(PTZWithFoVLimit)
{
PTZState.FieldOfView = FMath::Clamp(PTZState.FieldOfView, PTZFoVMinLimit, PTZFoVMaxLimit);
}
PTZState.FieldOfView = FMath::Clamp(PTZState.FieldOfView, 5.f, 170.f);
float MovementScale = PTZState.FieldOfView / 90.f;
PTZState.Pan += PTZPanSpeed * DeltaTime * MovementScale * (bPTZPanInvert ? -1 : 1);
PTZState.Pan = FMath::Fmod(PTZState.Pan, 2*PI);
if(PTZWithPanLimit)
{
PTZState.Pan = FMath::Clamp(PTZState.Pan, FMath::DegreesToRadians(PTZPanMinLimit), FMath::DegreesToRadians(PTZPanMaxLimit));
}
PTZState.Tilt += PTZTiltSpeed * DeltaTime * MovementScale * (bPTZTiltInvert ? -1 : 1);
PTZState.Tilt = FMath::Fmod(PTZState.Tilt, 2*PI);
if(PTZWithTiltLimit)
{
PTZState.Tilt = FMath::Clamp(PTZState.Tilt, FMath::DegreesToRadians(PTZTiltMinLimit), FMath::DegreesToRadians(PTZTiltMaxLimit));
}
SetPTZStateToUE(PTZState);
}
}
void UPTZController::ReceiveMetaDataFromSender(UNDIMediaSender* Sender, FString Data)
{
FText OutErrorMessage;
int32 OutErrorLineNumber;
FFastXml::ParseXmlFile(this->NDIMetadataParser.Get(),
nullptr, // XmlFilePath
Data.GetCharArray().GetData(), // XmlFileContents
nullptr, // FeedbackContext
false, // bShowSlowTaskDialog
false, // bShowCancelButton
OutErrorMessage, // OutErrorMessage
OutErrorLineNumber // OutErrorLineNumber
);
}

View File

@@ -0,0 +1,126 @@
/*
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 <Components/NDIReceiverComponent.h>
UNDIReceiverComponent::UNDIReceiverComponent(const FObjectInitializer& ObjectInitializer) : Super(ObjectInitializer) {}
/**
Initialize this component with the media source required for receiving NDI audio, video, and metadata.
Returns false, if the MediaSource is already been set. This is usually the case when this component is
initialized in Blueprints.
*/
bool UNDIReceiverComponent::Initialize(UNDIMediaReceiver* InMediaSource)
{
if (this->NDIMediaSource == nullptr && InMediaSource != nullptr)
{
this->NDIMediaSource = InMediaSource;
}
return InMediaSource != nullptr && InMediaSource == NDIMediaSource;
}
/**
Begin receiving NDI audio, video, and metadata frames
*/
bool UNDIReceiverComponent::StartReceiver(const FNDIConnectionInformation& InConnectionInformation)
{
if (IsValid(this->NDIMediaSource))
{
// Call to the Media Source's function to initialize (hopefully with valid connection information)
if (NDIMediaSource->Initialize(InConnectionInformation, UNDIMediaReceiver::EUsage::Standalone))
{
// FNDIConnectionService::RegisterReceiver(this->NDIMediaSource);
return true;
}
}
return false;
}
/**
Attempt to change the connection for which to get audio, video, and metadata frame from
*/
void UNDIReceiverComponent::ChangeConnection(const FNDIConnectionInformation& InConnectionInformation)
{
// Ensure a valid source to change the connection on
if (IsValid(this->NDIMediaSource))
{
// Call the underlying function
NDIMediaSource->ChangeConnection(InConnectionInformation);
}
}
/**
This will add a metadata frame and return immediately, having scheduled the frame asynchronously
*/
void UNDIReceiverComponent::SendMetadataFrame(const FString& metadata)
{
// Ensure a valid source to send metadata from
if (IsValid(this->NDIMediaSource))
{
// Call the underlying function
NDIMediaSource->SendMetadataFrame(metadata);
}
}
/**
This will setup the up-stream tally notifications. If no streams are connected, it will automatically send
the tally state upon connection
*/
void UNDIReceiverComponent::SendTallyInformation(const bool& IsOnPreview, const bool& IsOnProgram)
{
if (IsValid(this->NDIMediaSource))
{
NDIMediaSource->SendTallyInformation(IsOnPreview, IsOnProgram);
}
}
/**
Attempts to stop receiving audio, video, and metadata frame from the connected source
*/
void UNDIReceiverComponent::ShutdownReceiver()
{
if (IsValid(this->NDIMediaSource))
{
NDIMediaSource->Shutdown();
}
}
/**
Returns the current framerate of the connected source
*/
FFrameRate UNDIReceiverComponent::GetCurrentFrameRate() const
{
return IsValid(NDIMediaSource) ? NDIMediaSource->GetCurrentFrameRate() : FFrameRate(60, 1);
}
/**
Returns the current timecode of the connected source
*/
FTimecode UNDIReceiverComponent::GetCurrentTimecode() const
{
return IsValid(NDIMediaSource)
? NDIMediaSource->GetCurrentTimecode()
: FTimecode::FromTimespan(FTimespan::FromMilliseconds(0.0), FFrameRate(60, 1), false, true);
}
/**
Returns the current connection information of the connected source
*/
FNDIConnectionInformation UNDIReceiverComponent::GetCurrentConnectionInformation() const
{
return IsValid(NDIMediaSource) ? NDIMediaSource->GetCurrentConnectionInformation() : FNDIConnectionInformation();
}
/**
Returns the current performance data of the receiver while connected to the source
*/
FNDIReceiverPerformanceData UNDIReceiverComponent::GetPerformanceData() const
{
return IsValid(NDIMediaSource) ? NDIMediaSource->GetPerformanceData() : FNDIReceiverPerformanceData();
}

View File

@@ -0,0 +1,340 @@
/*
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 <Components/NDITriCasterExtComponent.h>
#include <Structures/NDIXml.h>
#include <Misc/EngineVersionComparison.h>
#include <EngineUtils.h>
/**
Parsers for TriCasterExt metadata
*/
class NDIXmlElementParser_tricaster_ext : public NDIXmlElementParser
{
public:
NDIXmlElementParser_tricaster_ext(UTriCasterExtComponent* TriCasterExtComponentIn)
: TriCasterExtComponent(TriCasterExtComponentIn)
{}
virtual bool ProcessOpen(const TCHAR* ElementName, const TCHAR* ElementData)
{
TCData.Value = FString();
TCData.KeyValues.Empty();
return true;
}
virtual bool ProcessAttribute(const TCHAR* AttributeName, const TCHAR* AttributeValue) override
{
if(FCString::Strcmp(TEXT("name"), AttributeName) == 0)
{}
else if(FCString::Strcmp(TEXT("value"), AttributeName) == 0)
{
TCData.Value = FString(AttributeValue);
}
else
{
TCData.KeyValues.Add(FName(AttributeName), FString(AttributeValue));
}
return true;
}
virtual bool ProcessClose(const TCHAR* ElementName) override
{
if(TCData.Value == "ndiio")
{
FString* ActorNamePtr = TCData.KeyValues.Find("actor");
FString* PropertyNamePtr = TCData.KeyValues.Find("property");
FString* PropertyValueStrPtr = TCData.KeyValues.Find("propertyvalue");
FString* ComponentNamePtr = TCData.KeyValues.Find("component");
FString* EasingDurationPtr = TCData.KeyValues.Find("easing");
if((ActorNamePtr != nullptr) && (PropertyNamePtr != nullptr) && (PropertyValueStrPtr != nullptr))
{
FString PropertyBaseName, PropertyElementName;
if(!PropertyNamePtr->Split(TEXT(":"), &PropertyBaseName, &PropertyElementName))
PropertyBaseName = *PropertyNamePtr;
FTimespan EasingDuration = 0;
if(EasingDurationPtr != nullptr)
{
double Seconds = FCString::Atod(**EasingDurationPtr);
EasingDuration = FTimespan::FromSeconds(Seconds);
}
for(TActorIterator<AActor> ActorItr(TriCasterExtComponent->GetWorld()); ActorItr; ++ActorItr)
{
AActor* Actor = *ActorItr;
if(Actor->GetName() == *ActorNamePtr)
{
UObject* FoundObject = nullptr;
FProperty* FoundProperty = nullptr;
if(ComponentNamePtr != nullptr)
{
TInlineComponentArray<UActorComponent*> PrimComponents;
Actor->GetComponents(PrimComponents, true);
for(auto& CompIt : PrimComponents)
{
if(CompIt->GetName() == *ComponentNamePtr)
{
FProperty* Property = CompIt->GetClass()->FindPropertyByName(*PropertyBaseName);
if(Property)
{
FoundObject = CompIt;
FoundProperty = Property;
break;
}
}
}
}
else
{
FProperty* ActorProperty = Actor->GetClass()->FindPropertyByName(*PropertyBaseName);
if(ActorProperty)
{
FoundObject = Actor;
FoundProperty = ActorProperty;
}
else
{
TInlineComponentArray<UActorComponent*> PrimComponents;
Actor->GetComponents(PrimComponents, true);
for(auto& CompIt : PrimComponents)
{
FProperty* CompProperty = CompIt->GetClass()->FindPropertyByName(*PropertyBaseName);
if(CompProperty)
{
FoundObject = CompIt;
FoundProperty = CompProperty;
break;
}
}
}
}
if(FoundObject && FoundProperty)
{
TriCasterExtComponent->TriCasterExt(Actor, FoundObject, FoundProperty, PropertyElementName, *PropertyValueStrPtr, EasingDuration);
break;
}
}
}
}
}
TriCasterExtComponent->TriCasterExtCustom(TCData);
return true;
}
protected:
UTriCasterExtComponent* TriCasterExtComponent;
FTriCasterExt TCData;
};
// <tricaster_ext name="net1" value="ndiio" actor="LightSource" property="Intensity" propertyvalue="1.234" />
// <tricaster_ext name="net1" value="ndiio" actor="LightSource" component="LightComponent0" property="Intensity" propertyvalue="1.234" />
// <tricaster_ext name="net1" value="ndiio" actor="LightSource" property="RelativeLocation" propertyvalue="(X=1,Y=2,Z=3)" />
// <tricaster_ext name="net1" value="ndiio" actor="LightSource" property="RelativeLocation" propertyvalue="(X=1)" />
// <tricaster_ext name="net1" value="ndiio" actor="LightSource" property="RelativeLocation:Y" propertyvalue="2" easing="5.3"/>
UTriCasterExtComponent::UTriCasterExtComponent()
{
this->bWantsInitializeComponent = true;
this->PrimaryComponentTick.bAllowTickOnDedicatedServer = false;
this->PrimaryComponentTick.bCanEverTick = true;
this->PrimaryComponentTick.bHighPriority = true;
this->PrimaryComponentTick.bRunOnAnyThread = false;
this->PrimaryComponentTick.bStartWithTickEnabled = true;
this->PrimaryComponentTick.bTickEvenWhenPaused = true;
this->NDIMetadataParser = MakeShareable(new NDIXmlParser());
NDIMetadataParser->AddElementParser("tricaster_ext", MakeShareable(new NDIXmlElementParser_tricaster_ext(this)));
}
UTriCasterExtComponent::~UTriCasterExtComponent()
{}
void UTriCasterExtComponent::InitializeComponent()
{
Super::InitializeComponent();
if (IsValid(NDIMediaSource))
{
// Ensure the TriCasterExt component is subscribed to the sender receiving metadata
this->NDIMediaSource->OnSenderMetaDataReceived.RemoveAll(this);
this->NDIMediaSource->OnSenderMetaDataReceived.AddDynamic(this, &UTriCasterExtComponent::ReceiveMetaDataFromSender);
}
}
bool UTriCasterExtComponent::Initialize(UNDIMediaSender* InMediaSource)
{
// is the media source already set?
if (this->NDIMediaSource == nullptr && InMediaSource != nullptr)
{
// we passed validation, so set the media source
this->NDIMediaSource = InMediaSource;
// validate the Media Source object
if (IsValid(NDIMediaSource))
{
// Ensure the TriCasterExt component is subscribed to the sender receiving metadata
this->NDIMediaSource->OnSenderMetaDataReceived.RemoveAll(this);
this->NDIMediaSource->OnSenderMetaDataReceived.AddDynamic(this, &UTriCasterExtComponent::ReceiveMetaDataFromSender);
}
}
// did we pass validation
return InMediaSource != nullptr && InMediaSource == NDIMediaSource;
}
void UTriCasterExtComponent::TriCasterExt(AActor* Actor, UObject* Object, FProperty* Property, FString PropertyElementName, FString PropertyValueStr, FTimespan EasingDuration)
{
if(Actor && Object && Property)
{
FTriCasterExtInterp Interp;
Interp.Actor = Actor;
Interp.Object = Object;
Interp.Property = Property;
Interp.PropertyElementName = PropertyElementName;
Interp.PropertyValueStr = PropertyValueStr;
Interp.EasingDuration = EasingDuration.GetTotalSeconds();
Interp.EasingRemaining = Interp.EasingDuration;
TriCasterExtInterp.Add(Interp);
}
OnTriCasterExt.Broadcast(Actor, Object, PropertyElementName, PropertyValueStr, EasingDuration);
}
void UTriCasterExtComponent::TriCasterExtCustom(const FTriCasterExt& TCData)
{
OnTriCasterExtCustom.Broadcast(TCData);
}
void UTriCasterExtComponent::TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction)
{
Super::TickComponent(DeltaTime, TickType, ThisTickFunction);
for(int32 i = 0; i < TriCasterExtInterp.Num(); ++i)
{
FTriCasterExtInterp& Interp = TriCasterExtInterp[i];
float EasingDelta = FMath::Min(Interp.EasingRemaining, DeltaTime);
void* Data = Interp.Property->ContainerPtrToValuePtr<void>(Interp.Object);
if(Data)
{
bool Done = false;
#if WITH_EDITOR
Interp.Object->PreEditChange(Interp.Property);
Interp.Actor->PreEditChange(Interp.Property);
#endif
if(FNumericProperty* NumericProperty = CastField<FNumericProperty>(Interp.Property))
{
double PropertyValue = NumericProperty->GetFloatingPointPropertyValue(Data);
double TargetValue = FCString::Atod(*Interp.PropertyValueStr);
double EasingFrac = (Interp.EasingRemaining > 0) ? (EasingDelta / Interp.EasingRemaining) : 1;
double EasingInterp = 3*EasingFrac - 3*EasingFrac*EasingFrac + EasingFrac*EasingFrac*EasingFrac;
double NewValue = PropertyValue * (1 - EasingInterp) + TargetValue * EasingInterp;
NumericProperty->SetFloatingPointPropertyValue(Data, NewValue);
Done = true;
}
else if(FStructProperty* StructProperty = CastField<FStructProperty>(Interp.Property))
{
FProperty* FieldProperty = FindFProperty<FProperty>(StructProperty->Struct, *(Interp.PropertyElementName));
if(FNumericProperty* StructNumericProperty = CastField<FNumericProperty>(FieldProperty))
{
void* FieldData = FieldProperty->ContainerPtrToValuePtr<void>(Data);
double PropertyValue = StructNumericProperty->GetFloatingPointPropertyValue(FieldData);
double TargetValue = FCString::Atod(*Interp.PropertyValueStr);
double EasingFrac = (Interp.EasingRemaining > 0) ? (EasingDelta / Interp.EasingRemaining) : 1;
double EasingInterp = 3*EasingFrac - 3*EasingFrac*EasingFrac + EasingFrac*EasingFrac*EasingFrac;
double NewValue = PropertyValue * (1 - EasingInterp) + TargetValue * EasingInterp;
StructNumericProperty->SetFloatingPointPropertyValue(FieldData, NewValue);
Done = true;
}
}
if(!Done)
{
FString ImportText;
if(!Interp.PropertyElementName.IsEmpty())
ImportText = "(" + Interp.PropertyElementName + "=" + Interp.PropertyValueStr + ")";
else
ImportText = Interp.PropertyValueStr;
Interp.Property->ImportText_Direct(*ImportText, Data, Interp.Object, 0);
}
UActorComponent* ActorComponent = Cast<UActorComponent>(Interp.Object);
if(ActorComponent)
{
if((Interp.Property->GetFName() == TEXT("RelativeLocation")) ||
(Interp.Property->GetFName() == TEXT("RelativeRotation")) ||
(Interp.Property->GetFName() == TEXT("RelativeScale3D")))
{
ActorComponent->UpdateComponentToWorld();
}
}
#if (ENGINE_MAJOR_VERSION == 5) && (ENGINE_MINOR_VERSION < 3) // Before 5.3
if(Interp.Property->HasAnyPropertyFlags(CPF_Interp))
Interp.Object->PostInterpChange(Interp.Property);
#endif
#if WITH_EDITOR
TArray<const UObject*> ModifiedObjects;
ModifiedObjects.Add(Interp.Actor);
FPropertyChangedEvent PropertyChangedEvent(Interp.Property, EPropertyChangeType::ValueSet, MakeArrayView(ModifiedObjects));
FEditPropertyChain PropertyChain;
PropertyChain.AddHead(Interp.Property);
FPropertyChangedChainEvent PropertyChangedChainEvent(PropertyChain, PropertyChangedEvent);
Interp.Object->PostEditChangeChainProperty(PropertyChangedChainEvent);
Interp.Actor->PostEditChangeChainProperty(PropertyChangedChainEvent);
#endif
}
Interp.EasingRemaining -= EasingDelta;
if(Interp.EasingRemaining == 0)
TriCasterExtInterp.RemoveAtSwap(i);
}
}
void UTriCasterExtComponent::ReceiveMetaDataFromSender(UNDIMediaSender* Sender, FString Data)
{
FText OutErrorMessage;
int32 OutErrorLineNumber;
FFastXml::ParseXmlFile(this->NDIMetadataParser.Get(),
nullptr, // XmlFilePath
Data.GetCharArray().GetData(), // XmlFileContents
nullptr, // FeedbackContext
false, // bShowSlowTaskDialog
false, // bShowCancelButton
OutErrorMessage, // OutErrorMessage
OutErrorLineNumber // OutErrorLineNumber
);
}

View File

@@ -0,0 +1,264 @@
/*
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 <Components/NDIViewportCaptureComponent.h>
#include <Rendering/RenderingCommon.h>
#include <SceneView.h>
#include <SceneViewExtension.h>
#include <CanvasTypes.h>
#include <EngineModule.h>
#include <LegacyScreenPercentageDriver.h>
#include <RenderResource.h>
#include <UnrealClient.h>
#include <Engine/Engine.h>
#include <EngineUtils.h>
#include <Misc/CoreDelegates.h>
#include <Engine/TextureRenderTarget2D.h>
#include <UObject/Package.h>
UNDIViewportCaptureComponent::UNDIViewportCaptureComponent(const FObjectInitializer& ObjectInitializer)
: Super(ObjectInitializer)
{
this->bWantsInitializeComponent = true;
this->CaptureSource = ESceneCaptureSource::SCS_FinalToneCurveHDR;
this->PostProcessSettings.bOverride_DepthOfFieldFocalDistance = true;
this->PostProcessSettings.DepthOfFieldFocalDistance = 10000.f;
}
UNDIViewportCaptureComponent::~UNDIViewportCaptureComponent()
{}
void UNDIViewportCaptureComponent::InitializeComponent()
{
Super::InitializeComponent();
// validate the Media Source object
if (IsValid(NDIMediaSource))
{
// define default capture values
const auto& capture_size = !bOverrideBroadcastSettings ? NDIMediaSource->GetFrameSize() : CaptureSize;
const auto& capture_rate = !bOverrideBroadcastSettings ? NDIMediaSource->GetFrameRate() : CaptureRate;
// change the capture sizes as necessary
ChangeCaptureSettings(capture_size, capture_rate);
// ensure we are subscribed to the broadcast configuration changed event
this->NDIMediaSource->OnBroadcastConfigurationChanged.RemoveAll(this);
this->NDIMediaSource->OnBroadcastConfigurationChanged.AddDynamic(
this, &UNDIViewportCaptureComponent::OnBroadcastConfigurationChanged);
}
}
void UNDIViewportCaptureComponent::UninitializeComponent()
{
if (IsValid(NDIMediaSource))
{
if (IsValid(TextureTarget))
{
NDIMediaSource->ChangeVideoTexture(nullptr);
}
}
Super::UninitializeComponent();
}
bool UNDIViewportCaptureComponent::Initialize(UNDIMediaSender* InMediaSource)
{
// is the media source already set?
if (this->NDIMediaSource == nullptr && InMediaSource != nullptr)
{
// we passed validation, so set the media source
this->NDIMediaSource = InMediaSource;
// validate the Media Source object
if (IsValid(NDIMediaSource))
{
// define default capture values
const auto& capture_size = !bOverrideBroadcastSettings ? NDIMediaSource->GetFrameSize() : CaptureSize;
const auto& capture_rate = !bOverrideBroadcastSettings ? NDIMediaSource->GetFrameRate() : CaptureRate;
// change the capture sizes as necessary
ChangeCaptureSettings(capture_size, capture_rate);
// ensure we are subscribed to the broadcast configuration changed event
this->NDIMediaSource->OnBroadcastConfigurationChanged.RemoveAll(this);
this->NDIMediaSource->OnBroadcastConfigurationChanged.AddDynamic(
this, &UNDIViewportCaptureComponent::OnBroadcastConfigurationChanged);
}
}
// did we pass validation
return InMediaSource != nullptr && InMediaSource == NDIMediaSource;
}
/**
Changes the name of the sender object as seen on the network for remote connections
@param InSourceName The new name of the source to be identified as on the network
*/
void UNDIViewportCaptureComponent::ChangeSourceName(const FString& InSourceName)
{
// validate the Media Source object
if (IsValid(NDIMediaSource))
{
// call the media source implementation of the function
NDIMediaSource->ChangeSourceName(InSourceName);
}
}
/**
Attempts to change the Broadcast information associated with this media object
@param InConfiguration The new configuration to broadcast
*/
void UNDIViewportCaptureComponent::ChangeBroadcastConfiguration(const FNDIBroadcastConfiguration& InConfiguration)
{
// validate the Media Source object
if (IsValid(NDIMediaSource))
{
// call the media source implementation of the function
NDIMediaSource->ChangeBroadcastConfiguration(InConfiguration);
}
}
/**
Attempts to change the RenderTarget used in sending video frames over NDI
@param BroadcastTexture The texture to use as video, while broadcasting over NDI
*/
void UNDIViewportCaptureComponent::ChangeBroadcastTexture(UTextureRenderTarget2D* BroadcastTexture)
{
// ensure we have some thread-safety
FScopeLock Lock(&UpdateRenderContext);
this->TextureTarget = BroadcastTexture;
}
/**
Change the capture settings of the viewport capture
@param InCaptureSize The Capture size of the frame to capture of the viewport
@param InCaptureRate A framerate at which to capture frames of the viewport
*/
void UNDIViewportCaptureComponent::ChangeCaptureSettings(FIntPoint InCaptureSize, FFrameRate InCaptureRate)
{
// clamp our viewport capture size
int32 capture_width = FMath::Max(InCaptureSize.X, 64);
int32 capture_height = FMath::Max(InCaptureSize.Y, 64);
// set the capture size
this->CaptureSize = FIntPoint(capture_width, capture_height);
// set the capture rate
this->CaptureRate = InCaptureRate;
// clamp the maximum capture rate to something reasonable
float capture_rate_max = 1 / 1000.0f;
float capture_rate = CaptureRate.Denominator / (float)CaptureRate.Numerator;
// set the primary tick interval to the sensible capture rate
this->PrimaryComponentTick.TickInterval = capture_rate >= capture_rate_max ? capture_rate : -1.0f;
// ensure we have some thread-safety
FScopeLock Lock(&UpdateRenderContext);
if (!IsValid(this->TextureTarget))
{
this->TextureTarget = NewObject<UTextureRenderTarget2D>(
GetTransientPackage(), UTextureRenderTarget2D::StaticClass(), NAME_None, RF_Transient | RF_MarkAsNative);
this->TextureTarget->UpdateResource();
}
this->TextureTarget->ResizeTarget(this->CaptureSize.X, this->CaptureSize.Y);
}
/**
Determines the current tally information. If you specify a timeout then it will wait until it has
changed, otherwise it will simply poll it and return the current tally immediately
@param IsOnPreview - A state indicating whether this source in on preview of a receiver
@param IsOnProgram - A state indicating whether this source is on program of a receiver
*/
void UNDIViewportCaptureComponent::GetTallyInformation(bool& IsOnPreview, bool& IsOnProgram)
{
// Initialize the properties
IsOnPreview = false;
IsOnProgram = false;
// validate the Media Source object
if (IsValid(NDIMediaSource))
{
// call the media source implementation of the function
NDIMediaSource->GetTallyInformation(IsOnPreview, IsOnProgram, 0);
}
}
/**
Gets the current number of receivers connected to this source. This can be used to avoid rendering
when nothing is connected to the video source. which can significantly improve the efficiency if
you want to make a lot of sources available on the network
@param Result The total number of connected receivers attached to the broadcast of this object
*/
void UNDIViewportCaptureComponent::GetNumberOfConnections(int32& Result)
{
// Initialize the property
Result = 0;
// validate the Media Source object
if (IsValid(NDIMediaSource))
{
// call the media source implementation of the function
NDIMediaSource->GetNumberOfConnections(Result);
}
}
#if (ENGINE_MAJOR_VERSION > 5) || ((ENGINE_MAJOR_VERSION == 5) && (ENGINE_MINOR_VERSION >= 6)) // 5.6 or later
void UNDIViewportCaptureComponent::UpdateSceneCaptureContents(FSceneInterface* Scene, ISceneRenderBuilder& SceneRenderBuilder)
#else
void UNDIViewportCaptureComponent::UpdateSceneCaptureContents(FSceneInterface* Scene)
#endif
{
// ensure we have some thread-safety
FScopeLock Lock(&UpdateRenderContext);
if (TextureTarget == nullptr)
return;
if (IsValid(NDIMediaSource))
{
NDIMediaSource->ChangeVideoTexture(TextureTarget);
// Some capture sources treat alpha as opacity, some sources use transparency.
// Alpha in NDI is opacity. Reverse the alpha mapping to always get opacity.
bool flip_alpha = (CaptureSource == SCS_SceneColorHDR) || (CaptureSource == SCS_SceneColorHDRNoAlpha) ||
(CaptureSource == SCS_SceneDepth) || (CaptureSource == SCS_Normal) ||
(CaptureSource == SCS_BaseColor);
if (flip_alpha == false)
NDIMediaSource->ChangeAlphaRemap(AlphaMin, AlphaMax);
else
NDIMediaSource->ChangeAlphaRemap(AlphaMax, AlphaMin);
// Do the actual capturing
#if (ENGINE_MAJOR_VERSION > 5) || ((ENGINE_MAJOR_VERSION == 5) && (ENGINE_MINOR_VERSION >= 6)) // 5.6 or later
Super::UpdateSceneCaptureContents(Scene, SceneRenderBuilder);
#else
Super::UpdateSceneCaptureContents(Scene);
#endif
}
}
void UNDIViewportCaptureComponent::OnBroadcastConfigurationChanged(UNDIMediaSender* Sender)
{
// If we are not overriding the broadcast settings and the sender is valid
if (!bOverrideBroadcastSettings && IsValid(Sender))
{
// change the capture sizes as necessary
ChangeCaptureSettings(Sender->GetFrameSize(), Sender->GetFrameRate());
}
}