Files
UnrealEngine/Engine/Plugins/Animation/LiveLinkHub/Source/LiveLinkHubMessaging/Private/LiveLinkHubControlChannel.h
Brandyn / Techy fcc1b09210 init
2026-04-04 15:40:51 -05:00

360 lines
13 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#pragma once
#include "IMessageContext.h"
#include "LiveLinkHubConnectionManager.h"
#include "ILiveLinkHubMessagingModule.h"
#include "LiveLinkHubMessageBusSource.h"
#include "MessageEndpoint.h"
#include "MessageEndpointBuilder.h"
#include "Misc/EngineVersion.h"
#if WITH_EDITOR
#include "LevelEditor.h"
#include "UnrealEdMisc.h"
#endif
/**
* Utility class that acts as a bridge between a LiveLinkHub Provider and a UE Client (or LLH instance) while handling backwards compatibility with previous versions of LLH Provider.
* The control channel can be set in Global mode, where it acts as a
*/
class FLiveLinkHubControlChannel : public TSharedFromThis<FLiveLinkHubControlChannel>
{
public:
/** Defines the operational mode of control channel. */
enum class EChannelMode : uint8
{
Global, // Channel is owned by LiveLinkHubMessagingModule and handles all control messages directly.
Source // Channel is owned by a LiveLinkHubMessageBusSource to support the V1 Discovery Protocol.
};
/** What mode this channel is acting in. */
EChannelMode ChannelMode;
public:
FLiveLinkHubControlChannel(EChannelMode InChannelMode)
: ChannelMode(InChannelMode)
{
}
~FLiveLinkHubControlChannel()
{
Shutdown();
}
/** Initializes the endpoint (when in Global mode) and registers callbacks. */
void Initialize(FGuid SourceId = FGuid())
{
if (bInitialized)
{
return;
}
if (ChannelMode == EChannelMode::Global)
{
FMessageEndpoint::Builder EndpointBuilder = FMessageEndpoint::Builder(TEXT("LiveLinkHubControlChannel"));
InitializeMessageEndpoint(EndpointBuilder);
Endpoint = EndpointBuilder;
if (Endpoint)
{
// For backwards compatibility with < 5.7
Endpoint->Subscribe<FLiveLinkHubDiscoveryMessage>();
}
}
#if WITH_EDITOR
if (FLevelEditorModule* LevelEditorModule = FModuleManager::GetModulePtr<FLevelEditorModule>("LevelEditor"))
{
LevelEditorModule->OnMapChanged().AddRaw(this, &FLiveLinkHubControlChannel::OnMapChanged);
}
#endif
CachedSourceId = SourceId;
bInitialized = true;
}
/** Overrides the endpoint, allows LLHMessageBusSource to pass messages using its own endpoint. */
void SetEndpoint(TSharedPtr<FMessageEndpoint> InEndpoint)
{
Endpoint = MoveTemp(InEndpoint);
}
/** Get the address of the MessageEndpoint held by this channel. */
FMessageAddress GetAddress() const
{
FMessageAddress Address;
if (Endpoint)
{
Address = Endpoint->GetAddress();
}
return Address;
}
/** Registers the necessary handlers with the endpoint builder. */
void InitializeMessageEndpoint(FMessageEndpoint::Builder& Builder)
{
// We need to dispatch these calls on the game thread, but the LiveLinkMessageBusSource receives data on AnyThread, so we use this lambda to dispatch those correctly.
auto GameThreadDispatch = [this] <typename MessageType> (const MessageType & Message, const TSharedRef<IMessageContext, ESPMode::ThreadSafe>&Context) mutable
{
if (IsInGameThread())
{
Handle<MessageType>(Message, Context);
}
else
{
TWeakPtr<FLiveLinkHubControlChannel> Self = AsShared();
ExecuteOnGameThread(UE_SOURCE_LOCATION, [Self, Message, Context]() mutable
{
if (TSharedPtr<FLiveLinkHubControlChannel> Receiver = Self.Pin())
{
Receiver->Handle<MessageType>(Message, Context);
}
});
}
};
Builder.Handling<FLiveLinkHubDiscoveryMessage>(GameThreadDispatch)
.Handling<FLiveLinkHubCustomTimeStepSettings>(GameThreadDispatch)
.Handling<FLiveLinkHubTimecodeSettings>(GameThreadDispatch)
.Handling<FLiveLinkHubDisconnectMessage>(GameThreadDispatch)
.WithCatchall(
[this, GameThreadDispatch]
(const TSharedRef<IMessageContext, ESPMode::ThreadSafe>& InContext) mutable
{
const UScriptStruct* MessageTypeInfo = InContext->GetMessageTypeInfo().Get();
// Handle subclasses of aux request.
if (MessageTypeInfo->IsChildOf(FLiveLinkHubAuxChannelRequestMessage::StaticStruct()))
{
GameThreadDispatch(*(const FLiveLinkHubAuxChannelRequestMessage*)InContext->GetMessage(), InContext);
}
})
;
}
/** Informs a LLH instance that this endpoint exists in order to create an entry in their MessageBus AddressBook. */
void SendBeacon(const FMessageAddress& RemoteAddress) const
{
FLiveLinkHubMessagingModule& Module = FModuleManager::Get().GetModuleChecked<FLiveLinkHubMessagingModule>("LiveLinkHubMessaging");
ELiveLinkTopologyMode Mode = Module.GetHostTopologyMode();
FString CurrentLevelName;
if (GWorld)
{
CurrentLevelName = GWorld->GetName();
}
Endpoint->Send(FMessageEndpoint::MakeMessage<FLiveLinkHubBeaconMessage>(Mode, Hostname, FApp::GetProjectName(), MoveTemp(CurrentLevelName)), EMessageFlags::Reliable, {}, nullptr, { RemoteAddress }, FTimespan::Zero(), FDateTime::MaxValue());
}
/**
* Called by FLiveLinkHubMessageBusSource to ensure we disconnect the remote provider when that source is shutdown.
* @note We can't use the LiveLink OnSourceRemoved because the source ptr would be invalid by then.
*/
void OnSourceShutdown(const TSharedPtr<FLiveLinkHubMessageBusSource>& InSource)
{
if (!InSource)
{
return;
}
const FGuid SourceId = InSource->GetSourceId();
if (!DisconnectingSources.Contains(SourceId))
{
UE_LOG(LogLiveLinkHubMessaging, Verbose, TEXT("MessageBusSource(%s) : Sending disconnect message to %s"), *InSource->GetAddress().ToString(), *InSource->GetConnectionAddress().ToString());
FLiveLinkHubDisconnectMessage Message{ InSource->GetSourceType().ToString(), InSource->GetSourceMachineName().ToString(), SourceId };
Endpoint->Send(FMessageEndpoint::MakeMessage<FLiveLinkHubDisconnectMessage>(MoveTemp(Message)), EMessageFlags::None, {}, nullptr, { InSource->GetConnectionAddress() }, FTimespan::Zero(), FDateTime::MaxValue());
DisconnectingSources.Add(SourceId);
TrackedProviders.Remove(InSource->GetConnectionAddress());
}
}
/** Gather information about this client to put in a client info struct. */
FLiveLinkClientInfoMessage CreateLiveLinkClientInfo() const
{
FLiveLinkClientInfoMessage ClientInfo;
FString CurrentLevelName;
if (GWorld && GWorld->GetCurrentLevel())
{
CurrentLevelName = GWorld->GetName();
}
// todo: Distinguish between UE and UEFN.
ClientInfo.LongName = FString::Printf(TEXT("%s - %s %s"), TEXT("UE"), *FEngineVersion::Current().ToString(EVersionComponent::Patch), FPlatformProcess::ComputerName());
ClientInfo.Status = ELiveLinkClientStatus::Connected;
ClientInfo.Hostname = Hostname;
ClientInfo.ProjectName = FApp::GetProjectName();
ClientInfo.CurrentLevel = CurrentLevelName;
FLiveLinkHubMessagingModule& Module = FModuleManager::Get().GetModuleChecked<FLiveLinkHubMessagingModule>("LiveLinkHubMessaging");
ELiveLinkTopologyMode Mode = Module.GetHostTopologyMode();
// Only populate this field if this is a Hub.
if (Mode == ELiveLinkTopologyMode::Hub)
{
ClientInfo.LiveLinkInstanceName = Module.GetInstanceId().ToString();
ClientInfo.TopologyMode = Mode;
}
ClientInfo.LiveLinkVersion = ILiveLinkClient::LIVELINK_VERSION;
return ClientInfo;
}
/** Returning false from this delegate will respond to the sender with FLiveLinkHubAuxChannelRejectMessage. */
using FAuxRequestDelegate = TTSDelegate<bool(const FLiveLinkHubAuxChannelRequestMessage& InMessage, const TSharedRef<IMessageContext, ESPMode::ThreadSafe>& InContext)>;
FAuxRequestDelegate OnAuxRequest;
private:
/** Unregister callbacks. */
void Shutdown()
{
#if WITH_EDITOR
if (FLevelEditorModule* LevelEditorModule = FModuleManager::GetModulePtr<FLevelEditorModule>("LevelEditor"))
{
LevelEditorModule->OnMapChanged().RemoveAll(this);
}
#endif
}
/** Templated method to dispatch messages using template specialization. */
template <typename MessageType>
void Handle(const MessageType& Message, const TSharedRef<IMessageContext, ESPMode::ThreadSafe>& Context)
{
checkf(false, TEXT("Handler not implemented for %s."), *MessageType::StaticClass()->GetName());
}
/** Handle message telling this source to disconnect. */
template <>
void Handle(const FLiveLinkHubDisconnectMessage& Message, const TSharedRef<IMessageContext, ESPMode::ThreadSafe>& Context)
{
FGuid SourceId = Message.SourceGuid;
if (!SourceId.IsValid())
{
// <5.7, Disconnect Message didn't specify source.
SourceId = CachedSourceId;
}
DisconnectingSources.Add(SourceId);
ILiveLinkClient* Client = &IModularFeatures::Get().GetModularFeature<ILiveLinkClient>(ILiveLinkClient::ModularFeatureName);
Client->RemoveSource(SourceId);
TrackedProviders.Remove(Context->GetSender());
}
/** Handle a custom time step settings message and update the engine's custom time step settings accordingly. */
template <>
void Handle(const FLiveLinkHubCustomTimeStepSettings& Message, const TSharedRef<IMessageContext, ESPMode::ThreadSafe>& Context)
{
Message.AssignCustomTimeStepToEngine();
}
/** Handle a timecode settings message and update the engine's timecode settings accordingly. */
template <>
void Handle(const FLiveLinkHubTimecodeSettings& Message, const TSharedRef<IMessageContext, ESPMode::ThreadSafe>& Context)
{
Message.AssignTimecodeSettingsAsProviderToEngine();
}
/** Handle a FLiveLinkHubDiscoveryMessage settings message and update the engine's timecode settings accordingly. */
template <>
void Handle(const FLiveLinkHubDiscoveryMessage& Message, const TSharedRef<IMessageContext, ESPMode::ThreadSafe>& Context)
{
if (ChannelMode != EChannelMode::Global)
{
// This message should only be handled by the global (LLHMessagingModule) control channel.
return;
}
if (TrackedProviders.Contains(Context->GetSender()))
{
UE_LOG(LogLiveLinkHubMessaging, Display, TEXT("Ignoring discovery request from %s because we're already tracking it."), *Context->GetSender().ToString());
return;
}
// Before this annotation was added, LiveLinkHub would automatically be added, so we need to keep the previous behavior if we discovered an older LiveLinkHub instance.
ELiveLinkHubAutoConnectMode AutoConnectMode = ELiveLinkHubAutoConnectMode::Disabled;
if (const FString* AutoConnectModeAnnotation = Context->GetAnnotations().Find(FLiveLinkHubMessageAnnotation::AutoConnectModeAnnotation))
{
int64 AutoConnectModeValue = StaticEnum<ELiveLinkHubAutoConnectMode>()->GetValueByName(**AutoConnectModeAnnotation);
if (AutoConnectModeValue != INDEX_NONE)
{
AutoConnectMode = (ELiveLinkHubAutoConnectMode)AutoConnectModeValue;
}
}
FLiveLinkHubMessagingModule& Module = FModuleManager::Get().GetModuleChecked<FLiveLinkHubMessagingModule>("LiveLinkHubMessaging");
ELiveLinkTopologyMode Mode = Module.GetHostTopologyMode();
bool bShouldConnect = GetDefault<ULiveLinkHubMessagingSettings>()->CanReceiveFrom(Mode, Message.Mode)
&& LiveLinkHubConnectionManager::CanConnectTo(Message.MachineName, *Context, Module.GetInstanceId());
if (bShouldConnect)
{
const double MachineTimeOffset = LiveLinkMessageBusHelper::CalculateProviderMachineOffset(Message.CreationTime, Context);
// Only create a sub control channel for Sources talking to a LLH instance that's built before 5.7.
TSharedPtr<FLiveLinkHubMessageBusSource> LiveLinkSource = MakeShared<FLiveLinkHubMessageBusSource>(FText::FromString(Message.ProviderName), FText::FromString(Message.MachineName), Context->GetSender(), MachineTimeOffset, Message.DiscoveryProtocolVersion);
ILiveLinkClient& Client = IModularFeatures::Get().GetModularFeature<ILiveLinkClient>(ILiveLinkClient::ModularFeatureName);
FGuid SourceId = Client.AddSource(LiveLinkSource);
Module.OnConnectionEstablished().Broadcast(SourceId);
TrackedProviders.Add(Context->GetSender());
}
}
/** Handle aux channel requests by delegating to the provided callback. */
template<>
void Handle(const FLiveLinkHubAuxChannelRequestMessage& InMessage, const TSharedRef<IMessageContext, ESPMode::ThreadSafe>& InContext)
{
if (OnAuxRequest.IsBound())
{
const bool bHandled = OnAuxRequest.Execute(InMessage, InContext);
if (bHandled)
{
return;
}
}
// Request not handled; respond with reject.
FLiveLinkHubAuxChannelRejectMessage* RejectMessage = FMessageEndpoint::MakeMessage<FLiveLinkHubAuxChannelRejectMessage>();
RejectMessage->ChannelId = InMessage.ChannelId;
Endpoint->Send(RejectMessage, EMessageFlags::Reliable, nullptr, { InContext->GetSender() }, FTimespan::Zero(), FDateTime::MaxValue());
}
#if WITH_EDITOR
/** Handler called on map changed to update the livelink hub. */
void OnMapChanged(UWorld* World, EMapChangeType ChangeType)
{
Endpoint->Send(FMessageEndpoint::MakeMessage<FLiveLinkClientInfoMessage>(CreateLiveLinkClientInfo()), EMessageFlags::None, {}, nullptr, TrackedProviders, FTimespan::Zero(), FDateTime::MaxValue());
}
#endif
private:
/** MessageBus endpoint used to transmit control messages. */
TSharedPtr<FMessageEndpoint> Endpoint;
/** Track sources in the process of disconnecting. */
TSet<FGuid> DisconnectingSources;
/** Name of this machine. */
FString Hostname = FPlatformProcess::ComputerName();
/** List of providers we've discovered. */
TArray<FMessageAddress> TrackedProviders;
/** When acting in Source mode, this is the ID of the source that owns this channel. */
FGuid CachedSourceId;
/** Flag to prevent initializing channel that's already initialized. */
bool bInitialized = false;
};