// Copyright Epic Games, Inc. All Rights Reserved. #include "AjaMediaCapture.h" #include "AJALib.h" #include "AjaDeviceProvider.h" #include "AjaMediaOutput.h" #include "AjaMediaOutputModule.h" #include "GPUTextureTransferModule.h" #include "Engine/Engine.h" #include "HAL/Event.h" #include "HAL/IConsoleManager.h" #include "IAjaMediaOutputModule.h" #include "IAjaMediaModule.h" #include "MediaIOCoreDefinitions.h" #include "MediaIOCoreFileWriter.h" #include "MediaIOCoreSubsystem.h" #include "Misc/ScopeLock.h" #include "Slate/SceneViewport.h" #include "Widgets/SViewport.h" #if WITH_EDITOR #include "AnalyticsEventAttribute.h" #include "Editor.h" #include "EngineAnalytics.h" #include "Interfaces/IMainFrameModule.h" #endif static FAutoConsoleVariableDeprecated CVarAjaEnableGPUDirect_Deprecated(TEXT("Aja.EnableGPUDirect"), TEXT("MediaIO.EnableGPUDirect"), TEXT("5.6")); /* namespace AjaMediaCaptureDevice *****************************************************************************/ namespace AjaMediaCaptureDevice { struct FAjaMediaEncodeOptions { FAjaMediaEncodeOptions(const int32 InWidth, const int32 InHeight, const EAjaMediaOutputPixelFormat InEncodePixelFormat, bool bInUseKey) { switch (InEncodePixelFormat) { case EAjaMediaOutputPixelFormat::PF_8BIT_YUV: if (bInUseKey) { Initialize(InWidth * 4, InWidth, EMediaIOCoreEncodePixelFormat::CharBGRA, TEXT("Aja_Input_8_RGBA")); break; } else { Initialize(InWidth * 4, InWidth * 2, EMediaIOCoreEncodePixelFormat::CharUYVY, TEXT("Aja_Input_8_YUV")); break; } case EAjaMediaOutputPixelFormat::PF_10BIT_YUV: if (bInUseKey) { Initialize(InWidth * 4, InWidth, EMediaIOCoreEncodePixelFormat::A2B10G10R10, TEXT("Aja_Input_10_RGBA")); break; } else { Initialize(InWidth * 16, InWidth * 6, EMediaIOCoreEncodePixelFormat::YUVv210, TEXT("Aja_Input_10_YUV")); break; } default: checkNoEntry(); } } void Initialize(const uint32 InStride, const uint32 InTimeEncodeWidth, const EMediaIOCoreEncodePixelFormat InEncodePixelFormat, const FString InOutputFilename) { Stride = InStride; TimeEncodeWidth = InTimeEncodeWidth; EncodePixelFormat = InEncodePixelFormat; OutputFilename = InOutputFilename; } uint32 Stride; uint32 TimeEncodeWidth; EMediaIOCoreEncodePixelFormat EncodePixelFormat; FString OutputFilename; }; AJA::FTimecode ConvertToAJATimecode(const FTimecode& InTimecode, float InEngineFPS, float InAjaFPS) { const float Divider = InEngineFPS / InAjaFPS; AJA::FTimecode Timecode; Timecode.Hours = InTimecode.Hours; Timecode.Minutes = InTimecode.Minutes; Timecode.Seconds = InTimecode.Seconds; Timecode.Frames = int32(float(InTimecode.Frames) / Divider); return Timecode; } } namespace AjaMediaCaptureUtils { AJA::FAjaHDROptions MakeAjaHDRMetadata(const FAjaMediaHDROptions& HDROptions) { AJA::FAjaHDROptions HDRMetadata; HDRMetadata.Gamut = (AJA::EAjaHDRMetadataGamut) HDROptions.Gamut; HDRMetadata.EOTF = (AJA::EAjaHDRMetadataEOTF) HDROptions.EOTF; return HDRMetadata; } AJA::ETransportType ConvertTransportType(const EMediaIOTransportType TransportType, const EMediaIOQuadLinkTransportType QuadTransportType) { switch (TransportType) { case EMediaIOTransportType::SingleLink: return AJA::ETransportType::TT_SdiSingle; case EMediaIOTransportType::DualLink: return AJA::ETransportType::TT_SdiDual; case EMediaIOTransportType::QuadLink: return QuadTransportType == EMediaIOQuadLinkTransportType::SquareDivision ? AJA::ETransportType::TT_SdiQuadSQ : AJA::ETransportType::TT_SdiQuadTSI; case EMediaIOTransportType::HDMI: return AJA::ETransportType::TT_Hdmi; default: checkNoEntry(); return AJA::ETransportType::TT_SdiSingle; } } AJA::EPixelFormat ConvertPixelFormat(EAjaMediaOutputPixelFormat PixelFormat, bool bUseKey) { switch (PixelFormat) { case EAjaMediaOutputPixelFormat::PF_8BIT_YUV: return bUseKey ? AJA::EPixelFormat::PF_8BIT_ARGB : AJA::EPixelFormat::PF_8BIT_YCBCR; case EAjaMediaOutputPixelFormat::PF_10BIT_YUV: return bUseKey ? AJA::EPixelFormat::PF_10BIT_RGB : AJA::EPixelFormat::PF_10BIT_YCBCR; default: return AJA::EPixelFormat::PF_8BIT_YCBCR; } } AJA::ETimecodeFormat ConvertTimecode(EMediaIOTimecodeFormat TimecodeFormat) { switch (TimecodeFormat) { case EMediaIOTimecodeFormat::None: return AJA::ETimecodeFormat::TCF_None; case EMediaIOTimecodeFormat::LTC: return AJA::ETimecodeFormat::TCF_LTC; case EMediaIOTimecodeFormat::VITC: return AJA::ETimecodeFormat::TCF_VITC1; default: return AJA::ETimecodeFormat::TCF_None; } } AJA::EAJAReferenceType Convert(EMediaIOReferenceType OutputReference) { switch(OutputReference) { case EMediaIOReferenceType::External: return AJA::EAJAReferenceType::EAJA_REFERENCETYPE_EXTERNAL; case EMediaIOReferenceType::Input: return AJA::EAJAReferenceType::EAJA_REFERENCETYPE_INPUT; default: return AJA::EAJAReferenceType::EAJA_REFERENCETYPE_FREERUN; } } } #if WITH_EDITOR namespace AjaMediaCaptureAnalytics { /** * @EventName MediaFramework.AjaCaptureStarted * @Trigger Triggered when a Aja capture of the viewport or render target is started. * @Type Client * @Owner MediaIO Team */ void SendCaptureEvent(const FIntPoint& Resolution, const FFrameRate FrameRate, const FString& CaptureType) { if (FEngineAnalytics::IsAvailable()) { TArray EventAttributes; EventAttributes.Add(FAnalyticsEventAttribute(TEXT("CaptureType"), CaptureType)); EventAttributes.Add(FAnalyticsEventAttribute(TEXT("ResolutionWidth"), FString::Printf(TEXT("%d"), Resolution.X))); EventAttributes.Add(FAnalyticsEventAttribute(TEXT("ResolutionHeight"), FString::Printf(TEXT("%d"), Resolution.Y))); EventAttributes.Add(FAnalyticsEventAttribute(TEXT("FrameRate"), FrameRate.ToPrettyText().ToString())); FEngineAnalytics::GetProvider().RecordEvent(TEXT("MediaFramework.AjaCaptureStarted"), EventAttributes); } } } #endif bool bAjaWriteInputRawDataCmdEnable = false; static FAutoConsoleCommand AjaWriteInputRawDataCmd( TEXT("Aja.WriteInputRawData"), TEXT("Write Aja raw input buffer to file."), FConsoleCommandDelegate::CreateLambda([]() { bAjaWriteInputRawDataCmdEnable = true; }) ); ///* FAjaOutputCallback definition //*****************************************************************************/ struct UAjaMediaCapture::FAjaOutputCallback : public AJA::IAJAInputOutputChannelCallbackInterface { virtual void OnInitializationCompleted(bool bSucceed) override; virtual bool OnRequestInputBuffer(const AJA::AJARequestInputBufferData& RequestBuffer, AJA::AJARequestedInputBufferData& OutRequestedBuffer) override; virtual bool OnInputFrameReceived(const AJA::AJAInputFrameData& InInputFrame, const AJA::AJAAncillaryFrameData& InAncillaryFrame, const AJA::AJAAudioFrameData& AudioFrame, const AJA::AJAVideoFrameData& VideoFrame) override; virtual bool OnOutputFrameCopied(const AJA::AJAOutputFrameData& InFrameData) override; virtual void OnOutputFrameStarted() override; virtual void OnCompletion(bool bSucceed) override; UAjaMediaCapture* Owner; /** Last frame drop count to detect count */ uint64 LastFrameDropCount = 0; uint64 PreviousDroppedCount = 0; }; ///* FAjaOutputCallback definition //*****************************************************************************/ struct UAjaMediaCapture::FAJAOutputChannel : public AJA::AJAOutputChannel { FAJAOutputChannel() = default; }; ///* UAjaMediaCapture implementation //*****************************************************************************/ UAjaMediaCapture::UAjaMediaCapture() : bWaitForSyncEvent(false) , bLogDropFrame(false) , bEncodeTimecodeInTexel(false) , PixelFormat(EAjaMediaOutputPixelFormat::PF_8BIT_YUV) , UseKey(false) , bSavedIgnoreTextureAlpha(false) , bIgnoreTextureAlphaChanged(false) , FrameRate(30, 1) , WakeUpEvent(nullptr) { if (!HasAnyFlags(RF_ClassDefaultObject | RF_ArchetypeObject)) { if (FGPUTextureTransferModule::Get().IsInitialized()) { TextureTransfer = FGPUTextureTransferModule::Get().GetTextureTransfer(); } #if WITH_EDITOR if (GEditor) { // In editor, an asset re-save dialog can prevent AJA from cleaning up in the regular PreExit callback, // So we have to do our cleanup before the regular callback is called. IMainFrameModule& MainFrame = FModuleManager::LoadModuleChecked("MainFrame"); CanCloseEditorDelegateHandle = MainFrame.RegisterCanCloseEditor(IMainFrameModule::FMainFrameCanCloseEditor::CreateUObject(this, &UAjaMediaCapture::CleanupPreEditorExit)); } #else FCoreDelegates::OnEnginePreExit.AddUObject(this, &UAjaMediaCapture::OnEnginePreExit); #endif } } UAjaMediaCapture::~UAjaMediaCapture() { #if WITH_EDITOR if (IMainFrameModule* MainFrame = FModuleManager::GetModulePtr("MainFrame")) { MainFrame->UnregisterCanCloseEditor(CanCloseEditorDelegateHandle); } #else FCoreDelegates::OnEnginePreExit.RemoveAll(this); #endif } bool UAjaMediaCapture::ValidateMediaOutput() const { UAjaMediaOutput* AjaMediaOutput = Cast(MediaOutput); if (!AjaMediaOutput) { UE_LOG(LogAjaMediaOutput, Error, TEXT("Can not start the capture. MediaSource's class is not supported.")); return false; } return true; } bool UAjaMediaCapture::InitializeCapture() { UAjaMediaOutput* AjaMediaSource = CastChecked(MediaOutput); if (FGPUTextureTransferModule::Get().IsEnabled() && !FGPUTextureTransferModule::Get().IsInitialized()) { // This will trigger an initialization of GPUDirect. TextureTransfer = FGPUTextureTransferModule::Get().GetTextureTransfer(); } const bool bResult = InitAJA(AjaMediaSource); if (GEngine) { GEngine->GetEngineSubsystem()->OnBufferReceived_AudioThread().AddUObject(this, &UAjaMediaCapture::OnAudioBufferReceived_AudioThread); } if (bResult) { #if WITH_EDITOR AjaMediaCaptureAnalytics::SendCaptureEvent(AjaMediaSource->GetRequestedSize(), FrameRate, GetCaptureSourceType()); #endif } return bResult; } bool UAjaMediaCapture::PostInitializeCaptureViewport(TSharedPtr& InSceneViewport) { ApplyViewportTextureAlpha(InSceneViewport); return true; } bool UAjaMediaCapture::UpdateSceneViewportImpl(TSharedPtr& InSceneViewport) { RestoreViewportTextureAlpha(GetCapturingSceneViewport()); ApplyViewportTextureAlpha(InSceneViewport); return true; } bool UAjaMediaCapture::UpdateRenderTargetImpl(UTextureRenderTarget2D* InRenderTarget) { RestoreViewportTextureAlpha(GetCapturingSceneViewport()); return true; } bool UAjaMediaCapture::UpdateAudioDeviceImpl(const FAudioDeviceHandle& InAudioDeviceHandle) { return CreateAudioOutput(InAudioDeviceHandle, Cast(MediaOutput)); } void UAjaMediaCapture::StopCaptureImpl(bool bAllowPendingFrameToBeProcess) { if (!bAllowPendingFrameToBeProcess) { { // Prevent the rendering thread from copying while we are stopping the capture. FScopeLock ScopeLock(&CopyingCriticalSection); ENQUEUE_RENDER_COMMAND(AjaMediaCaptureInitialize)( [this](FRHICommandListImmediate& RHICmdList) mutable { // Unregister texture before closing channel. if (ShouldCaptureRHIResource()) { for (FTextureRHIRef& Texture : TexturesToRelease) { TextureTransfer->UnregisterTexture(Texture->GetTexture2D()); } TexturesToRelease.Reset(); } if (OutputChannel) { // Close the aja channel in the another thread. OutputChannel->Uninitialize(); OutputChannel.Reset(); OutputCallback.Reset(); } }); if (WakeUpEvent) { FPlatformProcess::ReturnSynchEventToPool(WakeUpEvent); WakeUpEvent = nullptr; } } if (GEngine) { UMediaIOCoreSubsystem* SubSystem = GEngine->GetEngineSubsystem(); if (SubSystem) { SubSystem->OnBufferReceived_AudioThread().RemoveAll(this); } } AudioOutput.Reset(); RestoreViewportTextureAlpha(GetCapturingSceneViewport()); } } bool UAjaMediaCapture::ShouldCaptureRHIResource() const { return TextureTransfer && FGPUTextureTransferModule::Get().IsEnabled(); } void UAjaMediaCapture::ApplyViewportTextureAlpha(TSharedPtr InSceneViewport) { if (InSceneViewport.IsValid()) { TSharedPtr Widget(InSceneViewport->GetViewportWidget().Pin()); if (Widget.IsValid()) { bSavedIgnoreTextureAlpha = Widget->GetIgnoreTextureAlpha(); UAjaMediaOutput* AjaMediaSource = CastChecked(MediaOutput); if (AjaMediaSource->OutputConfiguration.OutputType == EMediaIOOutputType::FillAndKey) { if (bSavedIgnoreTextureAlpha) { bIgnoreTextureAlphaChanged = true; Widget->SetIgnoreTextureAlpha(false); } } } } } void UAjaMediaCapture::RestoreViewportTextureAlpha(TSharedPtr InSceneViewport) { // restore the ignore texture alpha state if (bIgnoreTextureAlphaChanged) { if (InSceneViewport.IsValid()) { TSharedPtr Widget(InSceneViewport->GetViewportWidget().Pin()); if (Widget.IsValid()) { Widget->SetIgnoreTextureAlpha(bSavedIgnoreTextureAlpha); } } bIgnoreTextureAlphaChanged = false; } } bool UAjaMediaCapture::CleanupPreEditorExit() { OnEnginePreExit(); return true; } void UAjaMediaCapture::OnEnginePreExit() { if (OutputChannel) { // Close the aja channel in the another thread. OutputChannel->Uninitialize(); OutputChannel.Reset(); OutputCallback.Reset(); } } bool UAjaMediaCapture::HasFinishedProcessing() const { return Super::HasFinishedProcessing() || !OutputChannel; } const FMatrix& UAjaMediaCapture::GetRGBToYUVConversionMatrix() const { if (const UAjaMediaOutput* AjaOutput = Cast(MediaOutput)) { switch(AjaOutput->HDROptions.Gamut) { case EAjaHDRMetadataGamut::Rec709: return MediaShaders::RgbToYuvRec709Scaled; case EAjaHDRMetadataGamut::Rec2020: return MediaShaders::RgbToYuvRec2020Scaled; default: checkNoEntry(); return MediaShaders::RgbToYuvRec709Scaled; } } return Super::GetRGBToYUVConversionMatrix(); } void UAjaMediaCapture::WaitForGPU(FRHITexture* InRHITexture) { if (TextureTransfer) { TextureTransfer->WaitForGPU(InRHITexture); } } bool UAjaMediaCapture::InitAJA(UAjaMediaOutput* InAjaMediaOutput) { check(InAjaMediaOutput); IAjaMediaModule& MediaModule = FModuleManager::LoadModuleChecked(TEXT("AjaMedia")); if (!MediaModule.CanBeUsed()) { UE_LOG(LogAjaMediaOutput, Warning, TEXT("The AjaMediaCapture can't open MediaOutput '%s' because Aja card cannot be used. Are you in a Commandlet? You may override this behavior by launching with -ForceAjaUsage"), *InAjaMediaOutput->GetName()); return false; } // Init general settings bWaitForSyncEvent = InAjaMediaOutput->bWaitForSyncEvent; bLogDropFrame = InAjaMediaOutput->bLogDropFrame; bEncodeTimecodeInTexel = InAjaMediaOutput->bEncodeTimecodeInTexel; FrameRate = InAjaMediaOutput->GetRequestedFrameRate(); PortName = FAjaDeviceProvider().ToText(InAjaMediaOutput->OutputConfiguration.MediaConfiguration.MediaConnection).ToString(); if (ShouldCaptureRHIResource()) { if (InAjaMediaOutput->OutputConfiguration.MediaConfiguration.MediaMode.Standard != EMediaIOStandardType::Progressive) { UE_LOG(LogAjaMediaOutput, Warning, TEXT("GPU DMA is not supported with interlaced, defaulting to regular path.")); TextureTransfer.Reset(); } } // Init Device options AJA::AJADeviceOptions DeviceOptions(InAjaMediaOutput->OutputConfiguration.MediaConfiguration.MediaConnection.Device.DeviceIdentifier); OutputCallback = MakePimpl(); OutputCallback->Owner = this; AJA::AJAVideoFormats::VideoFormatDescriptor Descriptor = AJA::AJAVideoFormats::GetVideoFormat(InAjaMediaOutput->OutputConfiguration.MediaConfiguration.MediaMode.DeviceModeIdentifier); // Init Channel options AJA::AJAInputOutputChannelOptions ChannelOptions(TEXT("ViewportOutput"), InAjaMediaOutput->OutputConfiguration.MediaConfiguration.MediaConnection.PortIdentifier); ChannelOptions.CallbackInterface = OutputCallback.Get(); ChannelOptions.bOutput = true; ChannelOptions.NumberOfAudioChannel = static_cast(InAjaMediaOutput->NumOutputAudioChannels); ChannelOptions.SynchronizeChannelIndex = InAjaMediaOutput->OutputConfiguration.ReferencePortIdentifier; ChannelOptions.KeyChannelIndex = InAjaMediaOutput->OutputConfiguration.KeyPortIdentifier; ChannelOptions.OutputNumberOfBuffers = InAjaMediaOutput->NumberOfAJABuffers; ChannelOptions.VideoFormatIndex = InAjaMediaOutput->OutputConfiguration.MediaConfiguration.MediaMode.DeviceModeIdentifier; ChannelOptions.bUseAutoCirculating = InAjaMediaOutput->bOutputWithAutoCirculating; ChannelOptions.bUseKey = InAjaMediaOutput->OutputConfiguration.OutputType == EMediaIOOutputType::FillAndKey; // must be RGBA to support Fill+Key ChannelOptions.bUseAncillary = false; ChannelOptions.bUseAudio = InAjaMediaOutput->bOutputAudio; ChannelOptions.bUseVideo = true; ChannelOptions.bOutputInterlacedFieldsTimecodeNeedToMatch = InAjaMediaOutput->bInterlacedFieldsTimecodeNeedToMatch && Descriptor.bIsInterlacedStandard && InAjaMediaOutput->TimecodeFormat != EMediaIOTimecodeFormat::None; ChannelOptions.bOutputInterlaceAsProgressive = InAjaMediaOutput->bOutputInterlaceAsProgressive; ChannelOptions.bDisplayWarningIfDropFrames = bLogDropFrame; ChannelOptions.bConvertOutputLevelAToB = InAjaMediaOutput->bOutputIn3GLevelB && Descriptor.bIsVideoFormatA; ChannelOptions.TransportType = AjaMediaCaptureUtils::ConvertTransportType(InAjaMediaOutput->OutputConfiguration.MediaConfiguration.MediaConnection.TransportType, InAjaMediaOutput->OutputConfiguration.MediaConfiguration.MediaConnection.QuadTransportType); ChannelOptions.PixelFormat = AjaMediaCaptureUtils::ConvertPixelFormat(InAjaMediaOutput->PixelFormat, ChannelOptions.bUseKey); ChannelOptions.TimecodeFormat = AjaMediaCaptureUtils::ConvertTimecode(InAjaMediaOutput->TimecodeFormat); ChannelOptions.OutputReferenceType = AjaMediaCaptureUtils::Convert(InAjaMediaOutput->OutputConfiguration.OutputReference); ChannelOptions.bUseGPUDMA = ShouldCaptureRHIResource(); ChannelOptions.bDirectlyWriteAudio = InAjaMediaOutput->bOutputAudioOnAudioThread; ChannelOptions.HDROptions = AjaMediaCaptureUtils::MakeAjaHDRMetadata(InAjaMediaOutput->HDROptions); bOutputAudio = InAjaMediaOutput->bOutputAudio; bDirectlyWriteAudio = InAjaMediaOutput->bOutputAudioOnAudioThread; if (bOutputAudio) { if (!CreateAudioOutput(AudioDeviceHandle, InAjaMediaOutput)) { UE_LOG(LogAjaMediaOutput, Error, TEXT("Failed to initialize audio output.")); } } if (GEngine) { NumInputChannels = GEngine->GetEngineSubsystem()->GetNumAudioInputChannels(); NumOutputChannels = static_cast(InAjaMediaOutput->NumOutputAudioChannels); } PixelFormat = InAjaMediaOutput->PixelFormat; UseKey = ChannelOptions.bUseKey; OutputChannel = MakePimpl(); if (!OutputChannel->Initialize(DeviceOptions, ChannelOptions)) { UE_LOG(LogAjaMediaOutput, Warning, TEXT("The AJA output port for '%s' could not be opened."), *InAjaMediaOutput->GetName()); OutputChannel.Reset(); OutputCallback.Reset(); return false; } if (bWaitForSyncEvent) { const auto CVar = IConsoleManager::Get().FindTConsoleVariableDataInt(TEXT("r.VSync")); bool bLockToVsync = CVar->GetValueOnGameThread() != 0; if (bLockToVsync) { UE_LOG(LogAjaMediaOutput, Warning, TEXT("The Engine use VSync and something wants to wait for the sync event. This may break the \"gen-lock\".")); } const bool bIsManualReset = false; WakeUpEvent = FPlatformProcess::GetSynchEventFromPool(bIsManualReset); } if (ShouldCaptureRHIResource()) { UE_LOG(LogAjaMediaOutput, Display, TEXT("Aja capture started using GPU Direct")); } return true; } bool UAjaMediaCapture::CreateAudioOutput(const FAudioDeviceHandle& InAudioDeviceHandle, const UAjaMediaOutput* InAjaMediaOutput) { if (GEngine && bOutputAudio && InAjaMediaOutput) { UMediaIOCoreSubsystem::FCreateAudioOutputArgs Args; Args.NumOutputChannels = static_cast(InAjaMediaOutput->NumOutputAudioChannels); Args.TargetFrameRate = FrameRate; Args.MaxSampleLatency = Align(InAjaMediaOutput->AudioBufferSize, 4); Args.OutputSampleRate = static_cast(InAjaMediaOutput->AudioSampleRate); Args.AudioDeviceHandle = InAudioDeviceHandle; AudioOutput = GEngine->GetEngineSubsystem()->CreateAudioOutput(Args); return AudioOutput.IsValid(); } return false; } void UAjaMediaCapture::OnFrameCaptured_RenderingThread(const FCaptureBaseData& InBaseData, TSharedPtr InUserData, void* InBuffer, int32 Width, int32 Height, int32 BytesPerRow) { TRACE_CPUPROFILER_EVENT_SCOPE(UAjaMediaCapture::OnFrameCaptured_RenderingThread); FMediaCaptureResourceData InResourceData; InResourceData.Buffer = InBuffer; InResourceData.Width = Width; InResourceData.Height = Height; InResourceData.BytesPerRow = BytesPerRow; OnFrameCapturedInternal_AnyThread(InBaseData, InUserData, MoveTemp(InResourceData)); } void UAjaMediaCapture::OnRHIResourceCaptured_RenderingThread(FRHICommandListImmediate& /*RHICmdList*/, const FCaptureBaseData& InBaseData, TSharedPtr InUserData, FTextureRHIRef InTexture) { OnRHIResourceCaptured_AnyThread(InBaseData, InUserData, InTexture); } void UAjaMediaCapture::LockDMATexture_RenderThread(FTextureRHIRef InTexture) { if (ShouldCaptureRHIResource()) { if (!TexturesToRelease.Contains(InTexture)) { TexturesToRelease.Add(InTexture); FRHITexture* Texture = InTexture->GetTexture2D(); UE::GPUTextureTransfer::FRegisterDMATextureArgs Args; Args.RHITexture = Texture; Args.RHIResourceMemory = nullptr; // = Texture->GetNativeResource(); todo: VulkanTexture->Surface->GetAllocationHandle for Vulkan Args.Width = Texture->GetDesc().GetSize().X; Args.Height = Texture->GetDesc().GetSize().Y; if (Args.RHITexture->GetFormat() == EPixelFormat::PF_B8G8R8A8) { Args.PixelFormat = UE::GPUTextureTransfer::EPixelFormat::PF_8Bit; Args.Stride = Args.Width * 4; } else if (Args.RHITexture->GetFormat() == EPixelFormat::PF_R32G32B32A32_UINT) { Args.PixelFormat = UE::GPUTextureTransfer::EPixelFormat::PF_10Bit; Args.Stride = Args.Width * 16; } else if (Args.RHITexture->GetFormat() == EPixelFormat::PF_A2B10G10R10) { // RGB 10 bit can be considered as 8 PF_8 bits by GPUDirect. Args.PixelFormat = UE::GPUTextureTransfer::EPixelFormat::PF_8Bit; Args.Stride = Args.Width * 4; } else { checkf(false, TEXT("Format not supported")); } TextureTransfer->RegisterTexture(Args); } TextureTransfer->LockTexture(InTexture->GetTexture2D()); } } void UAjaMediaCapture::UnlockDMATexture_RenderThread(FTextureRHIRef InTexture) { if (ShouldCaptureRHIResource()) { TRACE_CPUPROFILER_EVENT_SCOPE(UAjaMediaCapture::OnFrameCaptured_RenderingThread::UnlockDMATexture); TextureTransfer->UnlockTexture(InTexture->GetTexture2D()); } } bool UAjaMediaCapture::SupportsAnyThreadCapture() const { return true; } void UAjaMediaCapture::OnFrameCaptured_AnyThread(const FCaptureBaseData& InBaseData, TSharedPtr InUserData, const FMediaCaptureResourceData& InResourceData) { TRACE_CPUPROFILER_EVENT_SCOPE(UAjaMediaCapture::OnFrameCaptured_AnyThread); OnFrameCapturedInternal_AnyThread(InBaseData, InUserData, InResourceData); } void UAjaMediaCapture::WaitForSync_AnyThread() const { if (bWaitForSyncEvent) { if (WakeUpEvent && GetState() != EMediaCaptureState::Error) // Could be shutdown in a middle of a frame { WakeUpEvent->Wait(); } } } void UAjaMediaCapture::OutputAudio_AnyThread(const AJA::AJAOutputFrameBufferData& FrameBuffer) const { if (bOutputAudio && !bDirectlyWriteAudio) { TRACE_CPUPROFILER_EVENT_SCOPE(UAjaMediaCapture::OnFrameCaptured_RenderingThread::SetAudio); // Take a local copy of the audio output in case it is switched from the main thread. const TSharedPtr LocalAudioOutput = AudioOutput; if (!LocalAudioOutput) { return; } const int32 NumSamplesToPull = FMath::RoundToInt32(48000.f * LocalAudioOutput->NumInputChannels / FrameRate.AsDecimal()); TArray AudioSamples = LocalAudioOutput->GetAudioSamples(NumSamplesToPull); OutputChannel->SetAudioFrameData(FrameBuffer, reinterpret_cast(AudioSamples.GetData()), AudioSamples.Num() * sizeof(int32)); } } /* namespace IAJAInputCallbackInterface implementation // This is called from the AJA thread. There's a lock inside AJA to prevent this object from dying while in this thread. *****************************************************************************/ void UAjaMediaCapture::FAjaOutputCallback::OnInitializationCompleted(bool bSucceed) { check(Owner); if (Owner->GetState() != EMediaCaptureState::Stopped) { Owner->SetState(bSucceed ? EMediaCaptureState::Capturing : EMediaCaptureState::Error); } if (Owner->WakeUpEvent) { Owner->WakeUpEvent->Trigger(); } } bool UAjaMediaCapture::FAjaOutputCallback::OnOutputFrameCopied(const AJA::AJAOutputFrameData& InFrameData) { const uint32 FrameDropCount = InFrameData.FramesDropped; if (Owner->bLogDropFrame) { if (FrameDropCount > LastFrameDropCount) { PreviousDroppedCount += FrameDropCount - LastFrameDropCount; static const int32 NumMaxFrameBeforeWarning = 50; if (PreviousDroppedCount % NumMaxFrameBeforeWarning == 0) { UE_LOG(LogAjaMediaOutput, Warning, TEXT("Loosing frames on AJA output %s. The current count is %d."), *Owner->PortName, PreviousDroppedCount); } } else if (PreviousDroppedCount > 0) { UE_LOG(LogAjaMediaOutput, Warning, TEXT("Lost %d frames on AJA output %s. Frame rate may be too slow."), PreviousDroppedCount, *Owner->PortName); PreviousDroppedCount = 0; } } LastFrameDropCount = FrameDropCount; return true; } void UAjaMediaCapture::FAjaOutputCallback::OnOutputFrameStarted() { if (Owner->WakeUpEvent) { Owner->WakeUpEvent->Trigger(); } } void UAjaMediaCapture::FAjaOutputCallback::OnCompletion(bool bSucceed) { if (!bSucceed) { Owner->SetState(EMediaCaptureState::Error); } if (Owner->WakeUpEvent) { Owner->WakeUpEvent->Trigger(); } } bool UAjaMediaCapture::FAjaOutputCallback::OnRequestInputBuffer(const AJA::AJARequestInputBufferData& RequestBuffer, AJA::AJARequestedInputBufferData& OutRequestedBuffer) { check(false); return false; } bool UAjaMediaCapture::FAjaOutputCallback::OnInputFrameReceived(const AJA::AJAInputFrameData& InInputFrame, const AJA::AJAAncillaryFrameData& InAncillaryFrame, const AJA::AJAAudioFrameData& AudioFrame, const AJA::AJAVideoFrameData& VideoFrame) { check(false); return false; } void UAjaMediaCapture::OnRHIResourceCaptured_AnyThread(const FCaptureBaseData& InBaseData, TSharedPtr InUserData, FTextureRHIRef InTexture) { TRACE_CPUPROFILER_EVENT_SCOPE(UAjaMediaCapture::OnFrameCaptured_AnyThread); // Prevent the rendering thread from copying while we are stopping the capture. FScopeLock ScopeLock(&CopyingCriticalSection); if (OutputChannel) { const AJA::FTimecode Timecode = AjaMediaCaptureDevice::ConvertToAJATimecode(InBaseData.SourceFrameTimecode, InBaseData.SourceFrameTimecodeFramerate.AsDecimal(), FrameRate.AsDecimal()); AJA::AJAOutputFrameBufferData FrameBuffer; FrameBuffer.Timecode = Timecode; FrameBuffer.FrameIdentifier = InBaseData.SourceFrameNumber; bool bSetVideoResult = false; { TRACE_CPUPROFILER_EVENT_SCOPE(UAjaMediaCapture::OnFrameCaptured_RenderingThread::SetVideo_GPUDirect); bSetVideoResult = OutputChannel->SetVideoFrameData(FrameBuffer, InTexture->GetTexture2D()); } // If the set video call fails, that means we probably didn't find an available frame to write to, // so don't pop from the audio buffer since we would lose these samples in the SetAudioFrameData call. if (bSetVideoResult) { OutputAudio_AnyThread(FrameBuffer); } WaitForSync_AnyThread(); } else if (GetState() != EMediaCaptureState::Stopped) { SetState(EMediaCaptureState::Error); } } void UAjaMediaCapture::OnFrameCapturedInternal_AnyThread(const FCaptureBaseData& InBaseData, TSharedPtr InUserData, const FMediaCaptureResourceData& InResourceData) { // Prevent this thread from copying while we are stopping the capture. FScopeLock ScopeLock(&CopyingCriticalSection); if (OutputChannel) { const AJA::FTimecode Timecode = AjaMediaCaptureDevice::ConvertToAJATimecode(InBaseData.SourceFrameTimecode, InBaseData.SourceFrameTimecodeFramerate.AsDecimal(), FrameRate.AsDecimal()); const AjaMediaCaptureDevice::FAjaMediaEncodeOptions EncodeOptions(InResourceData.Width, InResourceData.Height, PixelFormat, UseKey); if (bEncodeTimecodeInTexel) { const FMediaIOCoreEncodeTime EncodeTime(EncodeOptions.EncodePixelFormat, InResourceData.Buffer, EncodeOptions.Stride, EncodeOptions.TimeEncodeWidth, InResourceData.Height); EncodeTime.Render(Timecode.Hours, Timecode.Minutes, Timecode.Seconds, Timecode.Frames); } AJA::AJAOutputFrameBufferData FrameBuffer; FrameBuffer.Timecode = Timecode; FrameBuffer.FrameIdentifier = InBaseData.SourceFrameNumber; // This will most likely be wrong when using the AnyThread callback FrameBuffer.bEvenFrame = GFrameCounterRenderThread % 2 == 0; bool bSetVideoResult = false; { TRACE_CPUPROFILER_EVENT_SCOPE(UAjaMediaCapture::OnFrameCaptured::SetVideo); bSetVideoResult = OutputChannel->SetVideoFrameData(FrameBuffer, reinterpret_cast(InResourceData.Buffer), EncodeOptions.Stride * InResourceData.Height); } // If the set video call fails, that means we probably didn't find an available frame to write to, // so don't pop from the audio buffer since we would lose these samples in the SetAudioFrameData call. if (bSetVideoResult) { OutputAudio_AnyThread(FrameBuffer); } if (bAjaWriteInputRawDataCmdEnable) { MediaIOCoreFileWriter::WriteRawFile(EncodeOptions.OutputFilename, reinterpret_cast(InResourceData.Buffer), EncodeOptions.Stride * InResourceData.Height); bAjaWriteInputRawDataCmdEnable = false; } WaitForSync_AnyThread(); } else if (GetState() != EMediaCaptureState::Stopped) { SetState(EMediaCaptureState::Error); } } void UAjaMediaCapture::OnAudioBufferReceived_AudioThread(Audio::FDeviceId DeviceId, float* Data, int32 NumSamples) const { TRACE_CPUPROFILER_EVENT_SCOPE(UAjaMediaCapture::AudioBufferReceived); if (bOutputAudio && OutputChannel) { if (ensure(NumInputChannels != 0)) { const TArray ConvertedSamples = FMediaIOAudioOutput::ConvertAndUpmixBuffer(MakeArrayView(Data, NumSamples), NumInputChannels, NumOutputChannels); OutputChannel->DMAWriteAudio(reinterpret_cast(ConvertedSamples.GetData()), ConvertedSamples.Num() * sizeof(int32)); } } }