// Copyright Epic Games, Inc. All Rights Reserved. #include "MetaHumanLocalLiveLinkSourceBlueprint.h" #include "MetaHumanVideoLiveLinkSource.h" #include "MetaHumanVideoLiveLinkSourceSettings.h" #include "MetaHumanVideoLiveLinkSubjectSettings.h" #include "MetaHumanAudioLiveLinkSource.h" #include "MetaHumanAudioLiveLinkSourceSettings.h" #include "MetaHumanAudioLiveLinkSubjectSettings.h" #include "MetaHumanPipelineMediaPlayerNode.h" #include "MediaCaptureSupport.h" #include "MediaBundle.h" #include "MediaPlayer.h" #include "MediaPlayerFacade.h" #include "UObject/Package.h" #include "HAL/RunnableThread.h" #include "AssetRegistry/AssetData.h" #include "AssetRegistry/AssetRegistryModule.h" #include "Features/IModularFeatures.h" #include "UObject/GCObject.h" // A class for querying track and format info of media. Given a device it will fill in a track list. // Additionally, if a specific track is specified it will fill in a format list for that track. class FMediaPlayerQuery : public FRunnable, public FGCObject, public TSharedFromThis { public: FMediaPlayerQuery(const FMetaHumanLiveLinkVideoDevice& InVideoDevice, const FMetaHumanLiveLinkVideoTrack& InVideoTrack) { VideoDevice = InVideoDevice; VideoTrack = InVideoTrack; bIsVideo = true; } FMediaPlayerQuery(const FMetaHumanLiveLinkAudioDevice& InAudioDevice, const FMetaHumanLiveLinkAudioTrack& InAudioTrack) { AudioDevice = InAudioDevice; AudioTrack = InAudioTrack; bIsVideo = false; } ~FMediaPlayerQuery() { if (MediaPlayer) { MediaPlayer->Close(); } } void Start(bool bInFormatsFiltered, float InTimeout) { bFormatsFiltered = bInFormatsFiltered; Timeout = InTimeout; bTimedOut = false; bOpened = false; const FString Url = bIsVideo ? VideoDevice.Url : AudioDevice.Url; if (Url.IsEmpty()) { return; } else if (Url.StartsWith(UE::MetaHuman::Pipeline::FMediaPlayerNode::BundleURL)) { // Add dummy track and format for media bundle cases. This makes the logic of any // combo boxes these are typically wired up to much easier since there is then // a direct parallel between regular devices (eg webcam) and media bundles. // It then only requires minimal and simple special case handling to hide the track // and format combo boxes for the media bundle case so that these dummy values are never shown. if (bIsVideo) { FMetaHumanLiveLinkVideoTrack Track; Track.VideoDevice = VideoDevice; Track.Name = "Unknown"; VideoTracks.Add(Track); FMetaHumanLiveLinkVideoFormat Format; Format.VideoTrack = Track; Format.Name = "Unknown"; VideoFormats.Add(Format); } else { FMetaHumanLiveLinkAudioTrack Track; Track.AudioDevice = AudioDevice; Track.Name = "Unknown"; AudioTracks.Add(Track); FMetaHumanLiveLinkAudioFormat Format; Format.AudioTrack = Track; Format.Name = "Unknown"; AudioFormats.Add(Format); } } else { MediaPlayer = NewObject(GetTransientPackage()); check(MediaPlayer); MediaPlayer->OnMediaEvent().AddSP(this, &FMediaPlayerQuery::OnMediaPlayerEvent); MediaPlayer->PlayOnOpen = false; MediaPlayer->GetPlayerFacade()->SetAreEventsSafeForAnyThread(true); MediaPlayer->OpenUrl(Url); // async call, picked up in OnMediaPlayerEvent function below TUniquePtr Thread; Thread.Reset(FRunnableThread::Create(this, TEXT("FMediaPlayerQuery"), 0, TPri_BelowNormal)); Thread->WaitForCompletion(); } } virtual uint32 Run() override { const double Start = FPlatformTime::Seconds(); while (FPlatformTime::Seconds() < Start + Timeout) { MediaPlayer->GetPlayerFacade()->TickInput(FTimespan(100), FTimespan(100)); // arbitrary tick interval if (bOpened) { break; } FPlatformProcess::Sleep(0.01f); } bTimedOut = !bOpened; return 0; } void OnMediaPlayerEvent(EMediaEvent InEvent) { // Dont rely on InEvent == EMediaEvent::MediaOpened in this function, EMediaEvent::MediaOpenFailed // may also suffice for our needs here to just list tracks/format. // We get the failed case for the BRIO camera which has a strange video track 0 (a MSN audio track). // Without the codec for that we can get the "failed" case even though that track wont be used in practice. // Video track 1 contains all the useable formats for the BRIO. // One solution would be to install the codec, but that would be a step required of all end-users // and is a codec thats never needed in practice. Better to treat the "MediaOpenFailed" more // as a warning and carry on. Error handling when you actually select a video track/format and // attempt to play it will catch any real errors. if (InEvent == EMediaEvent::MediaOpened || InEvent == EMediaEvent::MediaOpenFailed) { const int32 NumTracks = MediaPlayer->GetNumTracks(bIsVideo ? EMediaPlayerTrack::Video : EMediaPlayerTrack::Audio); for (int32 TrackIndex = 0; TrackIndex < NumTracks; ++TrackIndex) { if (bIsVideo) { FMetaHumanLiveLinkVideoTrack Track; Track.Index = TrackIndex; Track.Name = FString::FromInt(TrackIndex); Track.VideoDevice = VideoDevice; VideoTracks.Add(Track); } else { FMetaHumanLiveLinkAudioTrack Track; Track.Index = TrackIndex; Track.Name = FString::FromInt(TrackIndex); Track.AudioDevice = AudioDevice; AudioTracks.Add(Track); } } if (bIsVideo && !VideoTrack.Name.IsEmpty()) // Need to fill in video track format list? { const int32 Track = VideoTrack.Index; const int32 NumTrackFormats = MediaPlayer->GetNumTrackFormats(EMediaPlayerTrack::Video, Track); for (int32 TrackFormat = 0; TrackFormat < NumTrackFormats; ++TrackFormat) { FMetaHumanLiveLinkVideoFormat Format; Format.Index = TrackFormat; Format.Resolution = MediaPlayer->GetVideoTrackDimensions(Track, TrackFormat); Format.FrameRate = MediaPlayer->GetVideoTrackFrameRate(Track, TrackFormat); Format.Type = MediaPlayer->GetVideoTrackType(Track, TrackFormat); if (!bFormatsFiltered || ((Format.Type == TEXT("NV12") || Format.Type == TEXT("YUY2") || Format.Type == TEXT("UYVY") || Format.Type == TEXT("BGRA")) && Format.Resolution.X > 500 && Format.Resolution.Y > 500 && Format.FrameRate >= 24)) { Format.Name = FString::Printf(TEXT("%d: %s %ix%i"), TrackFormat, *Format.Type, Format.Resolution.X, Format.Resolution.Y); const int32 IntFrameRate = FMath::RoundToInt(Format.FrameRate); if (FMath::Abs(Format.FrameRate - IntFrameRate) > 0.0001) { Format.Name += FString::Printf(TEXT(" %.2f fps"), Format.FrameRate); } else { Format.Name += FString::Printf(TEXT(" %i fps"), IntFrameRate); } Format.VideoTrack = VideoTrack; VideoFormats.Add(Format); } } VideoFormats.Sort([](const FMetaHumanLiveLinkVideoFormat& InItem1, const FMetaHumanLiveLinkVideoFormat& InItem2) { // Sort first by fps, then res, then type if (InItem1.FrameRate == InItem2.FrameRate) { if (InItem1.Resolution == InItem2.Resolution) { return InItem1.Type > InItem2.Type; } else { return InItem1.Resolution.Size() > InItem2.Resolution.Size(); } } else { return InItem1.FrameRate > InItem2.FrameRate; } }); } else if (!bIsVideo && !AudioTrack.Name.IsEmpty()) // Need to fill in audio track format list? { const int32 Track = AudioTrack.Index; const int32 NumTrackFormats = MediaPlayer->GetNumTrackFormats(EMediaPlayerTrack::Audio, Track); for (int32 TrackFormat = 0; TrackFormat < NumTrackFormats; ++TrackFormat) { FMetaHumanLiveLinkAudioFormat Format; Format.Index = TrackFormat; Format.NumChannels = MediaPlayer->GetAudioTrackChannels(Track, TrackFormat); Format.SampleRate = MediaPlayer->GetAudioTrackSampleRate(Track, TrackFormat); Format.Type = MediaPlayer->GetAudioTrackType(Track, TrackFormat); Format.Name = FString::Printf(TEXT("%d: %s %i channels @ %i Hz"), TrackFormat, *Format.Type, Format.NumChannels, Format.SampleRate); Format.AudioTrack = AudioTrack; AudioFormats.Add(Format); } } bOpened = true; } } virtual void AddReferencedObjects(FReferenceCollector& InCollector) override { InCollector.AddReferencedObject(MediaPlayer); } virtual FString GetReferencerName() const override { return TEXT("FMediaPlayerQuery"); } TObjectPtr MediaPlayer; FMetaHumanLiveLinkVideoDevice VideoDevice; FMetaHumanLiveLinkVideoTrack VideoTrack; FMetaHumanLiveLinkAudioDevice AudioDevice; FMetaHumanLiveLinkAudioTrack AudioTrack; bool bIsVideo = true; bool bFormatsFiltered = true; float Timeout = 5; bool bTimedOut = false; bool bOpened = false; TArray VideoTracks; TArray VideoFormats; TArray AudioTracks; TArray AudioFormats; }; static void AddDevice(TArray InDeviceInfo, bool bInIncludeMediaBundles, TFunction InAddFunction) { for (const FMediaCaptureDeviceInfo& DeviceInfo : InDeviceInfo) { InAddFunction(DeviceInfo.DisplayName.ToString(), DeviceInfo.Url, false /* IsMediaBundle */); } if (bInIncludeMediaBundles) { const IAssetRegistry& AssetRegistry = IAssetRegistry::GetChecked(); TArray MediaBundles; AssetRegistry.GetAssetsByClass(UMediaBundle::StaticClass()->GetClassPathName(), MediaBundles); for (const FAssetData& MediaBundle : MediaBundles) { UObject* Bundle = MediaBundle.GetAsset(); if (Bundle) { InAddFunction(Bundle->GetName(), UE::MetaHuman::Pipeline::FMediaPlayerNode::BundleURL + Bundle->GetPathName(), true /* IsMediaBundle */); } } } } void UMetaHumanLocalLiveLinkSourceBlueprint::GetVideoDevices(TArray& OutVideoDevices, bool bInIncludeMediaBundles) { TArray VideoDeviceInfos; MediaCaptureSupport::EnumerateVideoCaptureDevices(VideoDeviceInfos); AddDevice(VideoDeviceInfos, bInIncludeMediaBundles, [&OutVideoDevices](const FString& InName, const FString& InUrl, bool bInIsMediaBundle) { FMetaHumanLiveLinkVideoDevice VideoDevice; VideoDevice.Name = InName; VideoDevice.Url = InUrl; VideoDevice.IsMediaBundle = bInIsMediaBundle; OutVideoDevices.Add(VideoDevice); }); } void UMetaHumanLocalLiveLinkSourceBlueprint::GetVideoTracks(const FMetaHumanLiveLinkVideoDevice& InVideoDevice, TArray& OutVideoTracks, bool& bOutTimedOut, float InTimeout) { TSharedPtr MediaPlayerQuery = MakeShared(InVideoDevice, FMetaHumanLiveLinkVideoTrack()); MediaPlayerQuery->Start(true /* bInFormatsFiltered, N/A here */, InTimeout); OutVideoTracks = MediaPlayerQuery->VideoTracks; bOutTimedOut = MediaPlayerQuery->bTimedOut; } void UMetaHumanLocalLiveLinkSourceBlueprint::GetVideoFormats(const FMetaHumanLiveLinkVideoTrack& InVideoTrack, TArray& OutVideoFormats, bool& bOutTimedOut, bool bInFilterFormats, float InTimeout) { TSharedPtr MediaPlayerQuery = MakeShared(InVideoTrack.VideoDevice, InVideoTrack); MediaPlayerQuery->Start(bInFilterFormats, InTimeout); OutVideoFormats = MediaPlayerQuery->VideoFormats; bOutTimedOut = MediaPlayerQuery->bTimedOut; } void UMetaHumanLocalLiveLinkSourceBlueprint::CreateVideoSource(FLiveLinkSourceHandle& OutVideoSource, bool& bOutSucceeded) { if (!IModularFeatures::Get().IsModularFeatureAvailable(ILiveLinkClient::ModularFeatureName)) { bOutSucceeded = false; return; } TSharedPtr VideoSource = MakeShared(); ILiveLinkClient& LiveLinkClient = IModularFeatures::Get().GetModularFeature(ILiveLinkClient::ModularFeatureName); const FGuid GUID = LiveLinkClient.AddSource(VideoSource); OutVideoSource.SetSourcePointer(VideoSource); bOutSucceeded = GUID.IsValid(); } void UMetaHumanLocalLiveLinkSourceBlueprint::CreateVideoSubject(const FLiveLinkSourceHandle& InVideoSource, const FMetaHumanLiveLinkVideoFormat& InVideoFormat, const FString& InSubjectName, FLiveLinkSubjectKey& OutVideoSubject, bool& bOutSucceeded, float InStartTimeout, float InFormatWaitTime, float InSampleTimeout) { if (!IModularFeatures::Get().IsModularFeatureAvailable(ILiveLinkClient::ModularFeatureName)) { bOutSucceeded = false; return; } ILiveLinkClient& LiveLinkClient = IModularFeatures::Get().GetModularFeature(ILiveLinkClient::ModularFeatureName); if (InVideoSource.SourcePointer && InVideoSource.SourcePointer->GetSourceType().ToString() == FMetaHumanVideoLiveLinkSource::SourceType.ToString()) { FMetaHumanVideoLiveLinkSource* VideoSource = (FMetaHumanVideoLiveLinkSource*) InVideoSource.SourcePointer.Get(); UMetaHumanVideoLiveLinkSourceSettings* VideoSourceSettings = Cast(LiveLinkClient.GetSourceSettings(VideoSource->GetSourceGuid())); if (VideoSourceSettings) { UMetaHumanVideoLiveLinkSubjectSettings* VideoSubjectSettings = NewObject(GetTransientPackage()); VideoSubjectSettings->MediaSourceCreateParams.VideoName = InVideoFormat.VideoTrack.VideoDevice.Name; VideoSubjectSettings->MediaSourceCreateParams.VideoURL = InVideoFormat.VideoTrack.VideoDevice.Url; VideoSubjectSettings->MediaSourceCreateParams.VideoTrack = InVideoFormat.VideoTrack.Index; VideoSubjectSettings->MediaSourceCreateParams.VideoTrackFormat = InVideoFormat.Index; VideoSubjectSettings->MediaSourceCreateParams.VideoTrackFormatName = InVideoFormat.Name; VideoSubjectSettings->MediaSourceCreateParams.StartTimeout = InStartTimeout; VideoSubjectSettings->MediaSourceCreateParams.FormatWaitTime = InFormatWaitTime; VideoSubjectSettings->MediaSourceCreateParams.SampleTimeout = InSampleTimeout; VideoSubjectSettings->Setup(); OutVideoSubject = VideoSourceSettings->RequestSubjectCreation(InSubjectName, VideoSubjectSettings); } } bOutSucceeded = OutVideoSubject.Source.IsValid(); } void UMetaHumanLocalLiveLinkSourceBlueprint::GetAudioDevices(TArray& OutAudioDevices, bool bInIncludeMediaBundles) { TArray AudioDeviceInfos; MediaCaptureSupport::EnumerateAudioCaptureDevices(AudioDeviceInfos); AddDevice(AudioDeviceInfos, bInIncludeMediaBundles, [&OutAudioDevices](const FString& InName, const FString& InUrl, bool bInIsMediaBundle) { FMetaHumanLiveLinkAudioDevice AudioDevice; AudioDevice.Name = InName; AudioDevice.Url = InUrl; AudioDevice.IsMediaBundle = bInIsMediaBundle; OutAudioDevices.Add(AudioDevice); }); } void UMetaHumanLocalLiveLinkSourceBlueprint::GetAudioTracks(const FMetaHumanLiveLinkAudioDevice& InAudioDevice, TArray& OutAudioTracks, bool& bOutTimedOut, float InTimeout) { TSharedPtr MediaPlayerQuery = MakeShared(InAudioDevice, FMetaHumanLiveLinkAudioTrack()); MediaPlayerQuery->Start(true /* bInFormatsFiltered, N/A here */, InTimeout); OutAudioTracks = MediaPlayerQuery->AudioTracks; bOutTimedOut = MediaPlayerQuery->bTimedOut; } void UMetaHumanLocalLiveLinkSourceBlueprint::GetAudioFormats(const FMetaHumanLiveLinkAudioTrack& InAudioTrack, TArray& OutAudioFormats, bool& bOutTimedOut, float InTimeout) { TSharedPtr MediaPlayerQuery = MakeShared(InAudioTrack.AudioDevice, InAudioTrack); MediaPlayerQuery->Start(true /* bInFormatsFiltered, N/A here */, InTimeout); OutAudioFormats = MediaPlayerQuery->AudioFormats; bOutTimedOut = MediaPlayerQuery->bTimedOut; } void UMetaHumanLocalLiveLinkSourceBlueprint::CreateAudioSource(FLiveLinkSourceHandle& OutAudioSource, bool& bOutSucceeded) { if (!IModularFeatures::Get().IsModularFeatureAvailable(ILiveLinkClient::ModularFeatureName)) { bOutSucceeded = false; return; } TSharedPtr AudioSource = MakeShared(); ILiveLinkClient& LiveLinkClient = IModularFeatures::Get().GetModularFeature(ILiveLinkClient::ModularFeatureName); const FGuid GUID = LiveLinkClient.AddSource(AudioSource); OutAudioSource.SetSourcePointer(AudioSource); bOutSucceeded = GUID.IsValid(); } void UMetaHumanLocalLiveLinkSourceBlueprint::CreateAudioSubject(const FLiveLinkSourceHandle& InAudioSource, const FMetaHumanLiveLinkAudioFormat& InAudioFormat, const FString& InSubjectName, FLiveLinkSubjectKey& OutAudioSubject, bool& bOutSucceeded, float InStartTimeout, float InFormatWaitTime, float InSampleTimeout) { if (!IModularFeatures::Get().IsModularFeatureAvailable(ILiveLinkClient::ModularFeatureName)) { bOutSucceeded = false; return; } ILiveLinkClient& LiveLinkClient = IModularFeatures::Get().GetModularFeature(ILiveLinkClient::ModularFeatureName); if (InAudioSource.SourcePointer && InAudioSource.SourcePointer->GetSourceType().ToString() == FMetaHumanAudioLiveLinkSource::SourceType.ToString()) { FMetaHumanAudioLiveLinkSource* AudioSource = (FMetaHumanAudioLiveLinkSource*) InAudioSource.SourcePointer.Get(); UMetaHumanAudioLiveLinkSourceSettings* AudioSourceSettings = Cast(LiveLinkClient.GetSourceSettings(AudioSource->GetSourceGuid())); if (AudioSourceSettings) { UMetaHumanAudioLiveLinkSubjectSettings* AudioSubjectSettings = NewObject(GetTransientPackage()); AudioSubjectSettings->MediaSourceCreateParams.AudioName = InAudioFormat.AudioTrack.AudioDevice.Name; AudioSubjectSettings->MediaSourceCreateParams.AudioURL = InAudioFormat.AudioTrack.AudioDevice.Url; AudioSubjectSettings->MediaSourceCreateParams.AudioTrack = InAudioFormat.AudioTrack.Index; AudioSubjectSettings->MediaSourceCreateParams.AudioTrackFormat = InAudioFormat.Index; AudioSubjectSettings->MediaSourceCreateParams.AudioTrackFormatName = InAudioFormat.Name; AudioSubjectSettings->MediaSourceCreateParams.StartTimeout = InStartTimeout; AudioSubjectSettings->MediaSourceCreateParams.FormatWaitTime = InFormatWaitTime; AudioSubjectSettings->MediaSourceCreateParams.SampleTimeout = InSampleTimeout; AudioSubjectSettings->Setup(); OutAudioSubject = AudioSourceSettings->RequestSubjectCreation(InSubjectName, AudioSubjectSettings); } } bOutSucceeded = OutAudioSubject.Source.IsValid(); } void UMetaHumanLocalLiveLinkSourceBlueprint::GetSubjectSettings(const FLiveLinkSubjectKey& InSubject, UObject*& OutSettings) { if (!IModularFeatures::Get().IsModularFeatureAvailable(ILiveLinkClient::ModularFeatureName)) { OutSettings = nullptr; return; } ILiveLinkClient& LiveLinkClient = IModularFeatures::Get().GetModularFeature(ILiveLinkClient::ModularFeatureName); OutSettings = LiveLinkClient.GetSubjectSettings(InSubject); }