// Copyright Epic Games, Inc. All Rights Reserved. #include "LiveLinkFaceSource.h" #include "Async/Async.h" #include "Common/UdpSocketBuilder.h" #include "Common/UdpSocketReceiver.h" #include "InterpolationProcessor/LiveLinkBasicFrameInterpolateProcessor.h" #include "Sockets.h" #include "UObject/Package.h" #include "Roles/LiveLinkBasicRole.h" #include "EngineAnalytics.h" #include "Engine/Engine.h" #include "LiveLinkFaceSourceSettings.h" #include "LiveLinkFacePacket.h" #include "LiveLinkFaceSubjectSettings.h" #include "LiveLinkFaceSourceDefaults.h" DEFINE_LOG_CATEGORY(LogLiveLinkFaceSource); #define LOCTEXT_NAMESPACE "FLiveLinkFaceSource" FLiveLinkFaceSource::FLiveLinkFaceSource(const FString& InConnectionString) : Status(Disconnected) , ConnectionString(InConnectionString) , ControlValueScalingFactor(UINT16_MAX) { // If analytics shutdowns while source is running, because editor was closed, ensure analytics are still sent. // It would be too late to try to send them in Stop function. #if WITH_EDITOR AnalyticsShutdownHandler = FEngineAnalytics::OnShutdownEngineAnalytics.AddRaw(this, &FLiveLinkFaceSource::SendAnalytics); #endif } FLiveLinkFaceSource::~FLiveLinkFaceSource() { #if WITH_EDITOR FEngineAnalytics::OnShutdownEngineAnalytics.Remove(AnalyticsShutdownHandler); #endif UE_LOG(LogLiveLinkFaceSource, Verbose, TEXT("Destroying Source")) Stop(); } void FLiveLinkFaceSource::ReceiveClient(ILiveLinkClient* InClient, FGuid InSourceGuid) { LiveLinkClient = InClient; LiveLinkClient->OnLiveLinkSubjectAdded().AddSP(this, &FLiveLinkFaceSource::SubjectAdded); SourceGuid = InSourceGuid; } bool FLiveLinkFaceSource::IsSourceStillValid() const { return Status == Connected; } bool FLiveLinkFaceSource::RequestSourceShutdown() { Stop(); return true; } FText FLiveLinkFaceSource::GetSourceType() const { return LOCTEXT("SourceType", "Live Link Face"); } FText FLiveLinkFaceSource::GetSourceMachineName() const { return Control.IsValid() ? Control->GetServerName() : FText(); } FText FLiveLinkFaceSource::GetSourceStatus() const { switch (Status) { case Disconnected: return LOCTEXT("DisconnectedSourceStatus", "Disconnected"); case Connecting: return LOCTEXT("ConnectingSourceStatus", "Connecting"); case Connected: return LOCTEXT("ConnectedSourceStatus", "Connected"); default: return LOCTEXT("UnknownSourceStatus", "Unknown"); } } TSubclassOf FLiveLinkFaceSource::GetSettingsClass() const { return ULiveLinkFaceSourceSettings::StaticClass(); } void FLiveLinkFaceSource::InitializeSettings(ULiveLinkSourceSettings* InSettings) { ULiveLinkFaceSourceSettings* LiveLinkFaceSourceSettings = Cast(InSettings); LiveLinkFaceSourceSettings->Init(this, ConnectionString); InitUdpReceiver(); // Only connect to the server if the address is valid. // This should only be the case if we have loaded a preset and populated the settings with a valid connection string. if (!LiveLinkFaceSourceSettings->IsAddressValid()) { return; } Connect(LiveLinkFaceSourceSettings); } void FLiveLinkFaceSource::Connect(const ULiveLinkFaceSourceSettings* InSettings) { // Remove all subjects in case we are switching to a different server. for (FLiveLinkSubjectKey LiveLinkSubjectKey : GetSubjects()) { LiveLinkClient->RemoveSubject_AnyThread(LiveLinkSubjectKey); } RemoteSubject.Reset(); const FString& Host = InSettings->GetAddress(); const uint16 Port = InSettings->GetPort(); CustomSubjectName = InSettings->GetSubjectName(); UE_LOG(LogLiveLinkFaceSource, Display, TEXT("Connecting to Host %s Port: %d"), *Host, Port) FLiveLinkFaceControl::FOnSelectRemoteSubject OnSelectRemoteSubjectDelegate = FLiveLinkFaceControl::FOnSelectRemoteSubject::CreateSP(this, &FLiveLinkFaceSource::OnSelectRemoteSubject); FLiveLinkFaceControl::FOnStreamingStarted OnStreamingStartedDelegate = FLiveLinkFaceControl::FOnStreamingStarted::CreateSP(this, &FLiveLinkFaceSource::OnStreamingStarted); Control = MakeUnique( SourceGuid, Host, Port, UdpSocket->GetPortNo(), MoveTemp(OnSelectRemoteSubjectDelegate), MoveTemp(OnStreamingStartedDelegate)); Control->Start(); Status = Connecting; } void FLiveLinkFaceSource::Stop() { UE_LOG(LogLiveLinkFaceSource, Verbose, TEXT("Stopping")); SendAnalytics(); if (UdpSocket.IsValid()) { UdpSocket->Close(); UdpSocket.Reset(); } if (UdpReceiver.IsValid()) { UdpReceiver->Stop(); UdpReceiver.Reset(); } if (Control.IsValid()) { Control->Stop(); Control.Reset(); } Status = Disconnected; } bool FLiveLinkFaceSource::InitUdpReceiver() { // In reality the packets are between 1KB and 2KB constexpr int32 ReceiveBufferSize = 2048; FSocket* Socket = FUdpSocketBuilder(TEXT("Live Link Face Source UDP Socket")) .AsNonBlocking() .AsReusable() .WithReceiveBufferSize(ReceiveBufferSize) .BoundToAddress(FIPv4Address::Any) .Build(); if (!Socket) { UE_LOG(LogLiveLinkFaceSource, Error, TEXT("Failed to create UDP socket")); return false; } UdpSocket.Reset(Socket); UdpReceiver = MakeUnique(Socket, FTimespan::FromMilliseconds(100), TEXT("FLiveLinkFaceSource-UdpReceiver")); UdpReceiver->OnDataReceived().BindSP(this, &FLiveLinkFaceSource::OnDataReceived); UdpReceiver->Start(); return true; } void FLiveLinkFaceSource::OnDataReceived(const FArrayReaderPtr& InPayload, const FIPv4Endpoint& InEndpoint) { UE_LOG(LogLiveLinkFaceSource, VeryVerbose, TEXT("Read %lld bytes from %s"), InPayload->TotalSize(), *InEndpoint.ToString()); FLiveLinkFacePacket Packet; const bool bReadResult = Packet.Read(InPayload); if (bReadResult == false) { UE_LOG(LogLiveLinkFaceSource, Error, TEXT("Error reading payload")); return; } if (Status != Connected) { UE_LOG(LogLiveLinkFaceSource, Verbose, TEXT("Received valid data but the source is not yet in a connected state")); return; } ProcessPacket(Packet); } const FLiveLinkFaceControl::FRemoteSubject FLiveLinkFaceSource::OnSelectRemoteSubject( const FLiveLinkFaceControl::FRemoteSubjects& InRemoteSubjects) { check(!InRemoteSubjects.IsEmpty()) // Right now we only support a single subject, so we will select the first result FLiveLinkFaceControl::FRemoteSubject SelectedSubject = InRemoteSubjects[0]; // If we have specified a subject name in the source UI then override the subject name in the remote subject. // This will cause the remote subject name to be updated when streaming is started. if (!CustomSubjectName.IsEmpty()) { SelectedSubject.Name = CustomSubjectName; } else { // If we're loading from a preset we should already have at least one subject, if so we will use that as the remote subject name. TArray ExistingSubjects = GetSubjects(); if (!ExistingSubjects.IsEmpty()) { SelectedSubject.Name = ExistingSubjects[0].SubjectName.ToString(); } } return SelectedSubject; } void FLiveLinkFaceSource::OnStreamingStarted(const FLiveLinkFaceControl::FRemoteSubject& InRemoteSubject) { Status = Connected; RemoteSubject = MakeShared(InRemoteSubject); bSendAnalytics = true; ProcessingStarted = FPlatformTime::Seconds(); NumAnimationFrames = 0; AnalyticsItems.Reset(); const FString Platform = Control->GetServerPlatform().ToString(); if (Platform == "iOS" || Platform == "iPadOS") { AnalyticsItems.Add(TEXT("DeviceType"), "Live Link Face " + Platform); } else if (Platform.StartsWith("Android")) { AnalyticsItems.Add(TEXT("DeviceType"), "Live Link Face Android"); } else { AnalyticsItems.Add(TEXT("DeviceType"), "Live Link Face Unknown (" + Platform + ")"); } AnalyticsItems.Add(TEXT("DeviceModel"), Control->GetServerModel().ToString()); AsyncTask(ENamedThreads::GameThread, [this, InRemoteSubject]() { const FLiveLinkSubjectKey& LiveLinkSubjectKey = FLiveLinkSubjectKey(SourceGuid, *InRemoteSubject.Name); UE_LOG(LogLiveLinkFaceSource, Verbose, TEXT("Streaming started for subject '%s' with %d control values."), *LiveLinkSubjectKey.SubjectName.ToString(), InRemoteSubject.PropertyNames.Num()) // Check if this Live Link subject already exists with this key which may be the case when loading a pre-set const bool bLiveLinkSubjectExists = GetSubjects().ContainsByPredicate([LiveLinkSubjectKey](const FLiveLinkSubjectKey& ExistingKey) { return ExistingKey == LiveLinkSubjectKey; }); if (!bLiveLinkSubjectExists) { // Create Live Link Subject const TSubclassOf Role = ULiveLinkBasicRole::StaticClass(); ULiveLinkFaceSubjectSettings* SubjectSettings = NewObject(GetTransientPackage(), ULiveLinkFaceSubjectSettings::StaticClass()); SubjectSettings->InterpolationProcessor = NewObject(SubjectSettings); SubjectSettings->Role = Role; const ULiveLinkFaceSourceDefaults* DefaultSettings = GetDefault(); SubjectSettings->bHeadOrientation = DefaultSettings->bHeadOrientation; SubjectSettings->bHeadTranslation = DefaultSettings->bHeadTranslation; FLiveLinkSubjectPreset Preset; Preset.Key = LiveLinkSubjectKey; Preset.Role = Role; Preset.Settings = SubjectSettings; Preset.bEnabled = true; if (!LiveLinkClient->CreateSubject(Preset)) { UE_LOG(LogLiveLinkFaceSource, Warning, TEXT("Failed to create subject")) } } else { // Push static data to existing subject. // The subject may have been created via a preset. PushStaticData(LiveLinkSubjectKey, InRemoteSubject.PropertyNames); } }); } void FLiveLinkFaceSource::ProcessPacket(const FLiveLinkFacePacket& InPacket) { const FString SubjectId = InPacket.GetSubjectId(); if (!RemoteSubject.IsValid()) { UE_LOG(LogLiveLinkFaceSource, Warning, TEXT("Received packet but no remote subject is set")); return; } if (RemoteSubject->Id != InPacket.GetSubjectId()) { UE_LOG(LogLiveLinkFaceSource, Warning, TEXT("Received packet for unknown subject id: %s"), *SubjectId); return; } const FLiveLinkSubjectKey LiveLinkSubjectKey = FLiveLinkSubjectKey(SourceGuid, *RemoteSubject->Name); const FString SubjectName = LiveLinkSubjectKey.SubjectName.ToString(); // Check incoming control values count matches our static data const TArray ControlValues = InPacket.GetControlValues(); const uint32 ControlValueCount = ControlValues.Num(); const uint32 ExpectedControlValueCount = RemoteSubject->PropertyNames.Num(); if (ControlValueCount != ExpectedControlValueCount) { UE_LOG(LogLiveLinkFaceSource, Warning, TEXT("Received an unexpected number of control values for subject '%s'. Received %d Expected %d"), *SubjectName, ControlValueCount, ExpectedControlValueCount); Control->RestartConnection(); return; } // Does static data exist for this subject key? // If not or if the static data is invalid, we are unable to process this packet. const FLiveLinkStaticDataStruct* SubjectStaticData = LiveLinkClient->GetSubjectStaticData_AnyThread(LiveLinkSubjectKey); if (SubjectStaticData == nullptr || !SubjectStaticData->IsValid()) { UE_LOG(LogLiveLinkFaceSource, Verbose, TEXT("Packet received for subject '%s' but static data has not yet been set."), *SubjectName); return; } // Push frame data FLiveLinkFrameDataStruct FrameDataStruct = FLiveLinkFrameDataStruct(FLiveLinkBaseFrameData::StaticStruct()); FLiveLinkBaseFrameData& FrameData = *FrameDataStruct.Cast(); FrameData.MetaData.SceneTime = InPacket.GetQualifiedFrameTime(); PopulatePropertyValues(InPacket.GetControlValues(), FrameData.PropertyValues); // Pass on the provided head pose property values if enabled ULiveLinkFaceSubjectSettings* SubjectSettings = Cast(LiveLinkClient->GetSubjectSettings(LiveLinkSubjectKey)); TArray HeadPose = InPacket.GetHeadPose(); constexpr int32 HeadPoseValueCount = FLiveLinkFacePacket::HeadPoseValueCount; if (HeadPose.Num() != HeadPoseValueCount) { UE_LOG(LogLiveLinkFaceSource, Error, TEXT("Expected %d head pose values in packet but received %d."), HeadPoseValueCount, HeadPose.Num()) return; } const bool bHeadOrientation = SubjectSettings->bHeadOrientation; const bool bHeadTranslation = SubjectSettings->bHeadTranslation; // If either head orientation or translation is enabled then set HeadControlSwitch to 1.0 so that we drive the head movement in the rig. // Otherwise we should set the value to 0.0 as we are not providing any head movement information. FrameData.PropertyValues.Add(bHeadOrientation || bHeadTranslation); // HeadControlSwitch // 0 - Roll // 1 - Pitch // 2 - Yaw // 3 - X // 4 - Y // 5 - Z if (bHeadOrientation) { FrameData.PropertyValues.Add(HeadPose[0]); // Roll FrameData.PropertyValues.Add(HeadPose[1]); // Pitch FrameData.PropertyValues.Add(HeadPose[2]); // Yaw } else { FrameData.PropertyValues.Add(0); // Roll FrameData.PropertyValues.Add(0); // Pitch FrameData.PropertyValues.Add(0); // Yaw } int32 HeadPoseMode = 0; if (bHeadTranslation) { HeadPoseMode = 1; FrameData.PropertyValues.Add(HeadPose[3]); // X FrameData.PropertyValues.Add(HeadPose[4]); // Y FrameData.PropertyValues.Add(HeadPose[5]); // Z } else { FrameData.PropertyValues.Add(0); // X FrameData.PropertyValues.Add(0); // Y FrameData.PropertyValues.Add(0); // Z } FrameData.PropertyValues.Add(RemoteSubject->AnimationVersion); // MHFDSVersion FrameData.PropertyValues.Add(1); // DisableFaceOverride // Provide the head pose mode value as expected by the head translation preprocessor. FrameData.MetaData.StringMetaData.Add("HeadPoseMode", FString::Printf(TEXT("%i"), HeadPoseMode)); SubjectSettings->PreProcess(*(*SubjectStaticData).Cast(), FrameData); LiveLinkClient->PushSubjectFrameData_AnyThread(LiveLinkSubjectKey, MoveTemp(FrameDataStruct)); NumAnimationFrames++; AnalyticsItems.Add(TEXT("HeadTranslation"), LexToString(bHeadTranslation)); AnalyticsItems.Add(TEXT("HeadOrientation"), LexToString(bHeadOrientation)); AnalyticsItems.Add(TEXT("HasCalibrationNeutral"), LexToString(!SubjectSettings->NeutralFrame.IsEmpty())); AnalyticsItems.Add(TEXT("HasHeadTranslationNeutral"), LexToString(SubjectSettings->NeutralHeadTranslation.Length() > 0)); } void FLiveLinkFaceSource::PopulatePropertyValues(const TArray& InControlValues, TArray& OutPropertyValues) { OutPropertyValues.Reset(InControlValues.Num()); for (const uint16 ControlValue : InControlValues) { OutPropertyValues.Add(float(ControlValue) / ControlValueScalingFactor); } } TArray FLiveLinkFaceSource::GetSubjects() const { return LiveLinkClient->GetSubjects(true, true).FilterByPredicate([this](const FLiveLinkSubjectKey& Key) { return Key.Source == SourceGuid; }); } void FLiveLinkFaceSource::SubjectAdded(FLiveLinkSubjectKey InSubjectKey) { if (InSubjectKey.Source != SourceGuid) { // This could be called for subjects created by other sources so we really don't want to do anything with those or react at all. return; } // Ensure that all subjects created via the live link source set bIsLiveProcessing to true. // This counts for new sources and those created via a preset. ULiveLinkFaceSubjectSettings* SubjectSettings = Cast(LiveLinkClient->GetSubjectSettings(InSubjectKey)); SubjectSettings->bIsLiveProcessing = true; // If the subject is added as part of a preset this method (SubjectAdded) will be called before we've gathered remote subject information. // In this case the OnStreamingStarted method will push the static data to the existing subject. // For new sources this method (SubjectAdded) will be called *after* remote subject data has been gathered, // in which case we need to push static data at this point. if (RemoteSubject.IsValid()) { PushStaticData(InSubjectKey, RemoteSubject->PropertyNames); } } void FLiveLinkFaceSource::PushStaticData(const FLiveLinkSubjectKey& InSubjectKey, const TArray InPropertyNames) { FLiveLinkStaticDataStruct StaticDataStruct(FLiveLinkBaseStaticData::StaticStruct()); FLiveLinkBaseStaticData& StaticData = *StaticDataStruct.Cast(); StaticData.PropertyNames = InPropertyNames; // Add Head Pose Property Names StaticData.PropertyNames.Add("HeadControlSwitch"); StaticData.PropertyNames.Add("HeadRoll"); StaticData.PropertyNames.Add("HeadPitch"); StaticData.PropertyNames.Add("HeadYaw"); StaticData.PropertyNames.Add("HeadTranslationX"); StaticData.PropertyNames.Add("HeadTranslationY"); StaticData.PropertyNames.Add("HeadTranslationZ"); StaticData.PropertyNames.Add("MHFDSVersion"); StaticData.PropertyNames.Add("DisableFaceOverride"); LiveLinkClient->PushSubjectStaticData_AnyThread(InSubjectKey, ULiveLinkBasicRole::StaticClass(), MoveTemp(StaticDataStruct)); } void FLiveLinkFaceSource::SendAnalytics() { if (bSendAnalytics && GEngine->AreEditorAnalyticsEnabled() && FEngineAnalytics::IsAvailable()) { bSendAnalytics = false; AnalyticsItems.Add(TEXT("NumAnimationFrames"), LexToString(NumAnimationFrames)); AnalyticsItems.Add(TEXT("Duration"), LexToString(FPlatformTime::Seconds() - ProcessingStarted)); TArray AnalyticsEvents; for (const TPair& AnalyticsItem : AnalyticsItems) { AnalyticsEvents.Add(FAnalyticsEventAttribute(AnalyticsItem.Key, AnalyticsItem.Value)); } #if 0 for (const FAnalyticsEventAttribute& Attr : AnalyticsEvents) { UE_LOG(LogTemp, Warning, TEXT("[%s] [%s]"), *Attr.GetName(), *Attr.GetValue()); } #else FEngineAnalytics::GetProvider().RecordEvent(TEXT("Editor.MetaHumanLiveLinkPlugin.ProcessInfo"), AnalyticsEvents); #endif } } #undef LOCTEXT_NAMESPACE