// Copyright Epic Games, Inc. All Rights Reserved. #include "GameplayMediaEncoder.h" #include "Engine/GameEngine.h" #include "HAL/IConsoleManager.h" #include "Framework/Application/SlateApplication.h" #include "Modules/ModuleManager.h" #include "RendererInterface.h" #include "ScreenRendering.h" #include "ShaderCore.h" #include "PipelineStateCache.h" #include "ProfilingDebugging/CsvProfiler.h" #include "IbmLiveStreaming.h" #include "VideoEncoderFactory.h" #include "VideoEncoderInput.h" #include "ClearQuad.h" #include "CommonRenderResources.h" #include "AudioEncoderFactory.h" DEFINE_LOG_CATEGORY(GameplayMediaEncoder); CSV_DEFINE_CATEGORY(GameplayMediaEncoder, true); // right now we support only 48KHz audio sample rate as it's the only config UE4 seems to output // WMF AAC encoder supports also 44100Hz so its support can be easily added const uint32 HardcodedAudioSamplerate = 48000; // for now we downsample to stereo. WMF AAC encoder also supports 6 (5.1) channels // so it can be added too const uint32 HardcodedAudioNumChannels = 2; // currently neither IVideoRecordingSystem neither HighlightFeature APIs allow to configure // audio stream parameters const uint32 HardcodedAudioBitrate = 192000; // currently neither IVideoRecordingSystem neither HighlightFeature APIs allow to configure // video stream parameters #if PLATFORM_WINDOWS const uint32 HardcodedVideoFPS = 60; #else const uint32 HardcodedVideoFPS = 30; #endif const uint32 HardcodedVideoBitrate = 20000000; const uint32 MinVideoBitrate = 1000000; const uint32 MaxVideoBitrate = 20000000; const uint32 MinVideoFPS = 10; const uint32 MaxVideoFPS = 60; const uint32 MaxWidth = 1920; const uint32 MaxHeight = 1080; PRAGMA_DISABLE_DEPRECATION_WARNINGS FAutoConsoleCommand GameplayMediaEncoderInitialize(TEXT("GameplayMediaEncoder.Initialize"), TEXT("Constructs the audio/video encoding objects. Does not start encoding"), FConsoleCommandDelegate::CreateStatic(&FGameplayMediaEncoder::InitializeCmd)); FAutoConsoleCommand GameplayMediaEncoderStart(TEXT("GameplayMediaEncoder.Start"), TEXT("Starts encoding"), FConsoleCommandDelegate::CreateStatic(&FGameplayMediaEncoder::StartCmd)); FAutoConsoleCommand GameplayMediaEncoderStop(TEXT("GameplayMediaEncoder.Stop"), TEXT("Stops encoding"), FConsoleCommandDelegate::CreateStatic(&FGameplayMediaEncoder::StopCmd)); FAutoConsoleCommand GameplayMediaEncoderShutdown(TEXT("GameplayMediaEncoder.Shutdown"), TEXT("Releases all systems."), FConsoleCommandDelegate::CreateStatic(&FGameplayMediaEncoder::ShutdownCmd)); PRAGMA_ENABLE_DEPRECATION_WARNINGS ////////////////////////////////////////////////////////////////////////// // // FGameplayMediaEncoder // ////////////////////////////////////////////////////////////////////////// PRAGMA_DISABLE_DEPRECATION_WARNINGS TSharedPtr FGameplayMediaEncoder::Singleton = { }; PRAGMA_ENABLE_DEPRECATION_WARNINGS PRAGMA_DISABLE_DEPRECATION_WARNINGS FGameplayMediaEncoder* FGameplayMediaEncoder::Get() PRAGMA_ENABLE_DEPRECATION_WARNINGS { // Constructed and captured as a thread safe shared pointer as this is required by the audio submix listener interface if(!Singleton.IsValid()) { PRAGMA_DISABLE_DEPRECATION_WARNINGS Singleton = MakeShared(); PRAGMA_ENABLE_DEPRECATION_WARNINGS } return Singleton.Get(); } PRAGMA_DISABLE_DEPRECATION_WARNINGS FGameplayMediaEncoder::FGameplayMediaEncoder() {} PRAGMA_ENABLE_DEPRECATION_WARNINGS PRAGMA_DISABLE_DEPRECATION_WARNINGS FGameplayMediaEncoder::~FGameplayMediaEncoder() { bIsInDestructor = true; Shutdown(); } PRAGMA_ENABLE_DEPRECATION_WARNINGS bool FGameplayMediaEncoder::RegisterListener(IGameplayMediaEncoderListener* Listener) { check(IsInGameThread()); ListenersCS.Lock(); bool bFirstListener = Listeners.Num() == 0; ListenersCS.Unlock(); if(bFirstListener) { UE_LOG(GameplayMediaEncoder, Log, TEXT("Registering the first listener")); if(!Start()) { return false; } } FScopeLock Lock(&ListenersCS); Listeners.AddUnique(Listener); return true; } void FGameplayMediaEncoder::UnregisterListener(IGameplayMediaEncoderListener* Listener) { check(IsInGameThread()); ListenersCS.Lock(); Listeners.Remove(Listener); bool bAnyListenersLeft = Listeners.Num() > 0; ListenersCS.Unlock(); if(bAnyListenersLeft == false) { UE_LOG(GameplayMediaEncoder, Log, TEXT("Unregistered the last listener")); Stop(); } } bool FGameplayMediaEncoder::Initialize() { MemoryCheckpoint("Initial"); if(VideoEncoder) { UE_LOG(GameplayMediaEncoder, Log, TEXT("Already initialized")); return true; } // If some error occurs, call Shutdown to cleanup bool bIsOk = false; ON_SCOPE_EXIT { if(!bIsOk) { Shutdown(); } }; // // Audio // PRAGMA_DISABLE_DEPRECATION_WARNINGS AVEncoder::FAudioEncoderFactory* AudioEncoderFactory = AVEncoder::FAudioEncoderFactory::FindFactory("aac"); PRAGMA_ENABLE_DEPRECATION_WARNINGS if(!AudioEncoderFactory) { UE_LOG(GameplayMediaEncoder, Error, TEXT("No audio encoder for aac found")); return false; } PRAGMA_DISABLE_DEPRECATION_WARNINGS AudioEncoder = AudioEncoderFactory->CreateEncoder("aac"); PRAGMA_ENABLE_DEPRECATION_WARNINGS if(!AudioEncoder) { UE_LOG(GameplayMediaEncoder, Error, TEXT("Could not create audio encoder")); return false; } PRAGMA_DISABLE_DEPRECATION_WARNINGS AVEncoder::FAudioConfig AudioConfig; AudioConfig.Samplerate = HardcodedAudioSamplerate; AudioConfig.NumChannels = HardcodedAudioNumChannels; AudioConfig.Bitrate = HardcodedAudioBitrate; if(!AudioEncoder->Initialize(AudioConfig)) { UE_LOG(GameplayMediaEncoder, Error, TEXT("Could not initialize audio encoder")); return false; } AudioEncoder->RegisterListener(*this); PRAGMA_ENABLE_DEPRECATION_WARNINGS MemoryCheckpoint("Audio encoder initialized"); // // Video // PRAGMA_DISABLE_DEPRECATION_WARNINGS VideoConfig.Codec = "h264"; VideoConfig.Height = VideoConfig.Width = VideoConfig.Framerate = VideoConfig.Bitrate = 0; FParse::Value(FCommandLine::Get(), TEXT("GameplayMediaEncoder.ResY="), VideoConfig.Height); UE_LOG(GameplayMediaEncoder, Log, TEXT("GameplayMediaEncoder.ResY = %d"), VideoConfig.Height); if(VideoConfig.Height == 0 || VideoConfig.Height == 720) { VideoConfig.Width = 1280; VideoConfig.Height = 720; } else if(VideoConfig.Height == 1080) { VideoConfig.Width = 1920; VideoConfig.Height = 1080; } PRAGMA_ENABLE_DEPRECATION_WARNINGS else { UE_LOG(GameplayMediaEncoder, Fatal, TEXT("GameplayMediaEncoder.ResY can only have a value of 720 or 1080")); return false; } // Specifying 0 will completely disable frame skipping (therefore encoding as many frames as possible) PRAGMA_DISABLE_DEPRECATION_WARNINGS FParse::Value(FCommandLine::Get(), TEXT("GameplayMediaEncoder.FPS="), VideoConfig.Framerate); if(VideoConfig.Framerate == 0) PRAGMA_ENABLE_DEPRECATION_WARNINGS { // Note : When disabling frame skipping, we lie to the encoder when initializing. // We still specify a framerate, but then feed frames without skipping PRAGMA_DISABLE_DEPRECATION_WARNINGS VideoConfig.Framerate = HardcodedVideoFPS; PRAGMA_ENABLE_DEPRECATION_WARNINGS bDoFrameSkipping = false; UE_LOG(GameplayMediaEncoder, Log, TEXT("Uncapping FPS")); } else { PRAGMA_DISABLE_DEPRECATION_WARNINGS VideoConfig.Framerate = FMath::Clamp(VideoConfig.Framerate, (uint32)MinVideoFPS, (uint32)MaxVideoFPS); UE_LOG(GameplayMediaEncoder, Log, TEXT("Capping FPS %u"), VideoConfig.Framerate); PRAGMA_ENABLE_DEPRECATION_WARNINGS bDoFrameSkipping = true; } PRAGMA_DISABLE_DEPRECATION_WARNINGS VideoConfig.Bitrate = HardcodedVideoBitrate; FParse::Value(FCommandLine::Get(), TEXT("GameplayMediaEncoder.Bitrate="), VideoConfig.Bitrate); VideoConfig.Bitrate = FMath::Clamp(VideoConfig.Bitrate, (uint32)MinVideoBitrate, (uint32)MaxVideoBitrate); AVEncoder::FVideoEncoder::FLayerConfig videoInit; videoInit.Width = VideoConfig.Width; videoInit.Height = VideoConfig.Height; videoInit.MaxBitrate = MaxVideoBitrate; videoInit.TargetBitrate = VideoConfig.Bitrate; videoInit.MaxFramerate = VideoConfig.Framerate; PRAGMA_ENABLE_DEPRECATION_WARNINGS if(GDynamicRHI) { const ERHIInterfaceType RHIType = RHIGetInterfaceType(); #if PLATFORM_DESKTOP && !PLATFORM_APPLE if (RHIType == ERHIInterfaceType::D3D11) { PRAGMA_DISABLE_DEPRECATION_WARNINGS VideoEncoderInput = AVEncoder::FVideoEncoderInput::CreateForD3D11(GDynamicRHI->RHIGetNativeDevice(), true, IsRHIDeviceAMD()); PRAGMA_ENABLE_DEPRECATION_WARNINGS } else if (RHIType == ERHIInterfaceType::D3D12) { PRAGMA_DISABLE_DEPRECATION_WARNINGS VideoEncoderInput = AVEncoder::FVideoEncoderInput::CreateForD3D12(GDynamicRHI->RHIGetNativeDevice(), true, IsRHIDeviceNVIDIA()); PRAGMA_ENABLE_DEPRECATION_WARNINGS } else if (RHIType == ERHIInterfaceType::Vulkan) { PRAGMA_DISABLE_DEPRECATION_WARNINGS VideoEncoderInput = AVEncoder::FVideoEncoderInput::CreateForVulkan(GDynamicRHI->RHIGetNativeDevice(), true); PRAGMA_ENABLE_DEPRECATION_WARNINGS } else #endif { UE_LOG(GameplayMediaEncoder, Error, TEXT("Video encoding is not supported with the current Platform/RHI combo.")); return false; } } PRAGMA_DISABLE_DEPRECATION_WARNINGS const TArray& AvailableEncodersInfo = AVEncoder::FVideoEncoderFactory::Get().GetAvailable(); PRAGMA_ENABLE_DEPRECATION_WARNINGS if (AvailableEncodersInfo.Num() == 0) { UE_LOG(GameplayMediaEncoder, Error, TEXT("No video encoders found. Check if relevent encoder plugins have been enabled for this project.")); return false; } for (const auto& EncoderInfo : AvailableEncodersInfo) { PRAGMA_DISABLE_DEPRECATION_WARNINGS if (EncoderInfo.CodecType == AVEncoder::ECodecType::H264) { VideoEncoder = AVEncoder::FVideoEncoderFactory::Get().Create(EncoderInfo.ID, VideoEncoderInput, videoInit); } PRAGMA_ENABLE_DEPRECATION_WARNINGS } if (!VideoEncoder) { UE_LOG(GameplayMediaEncoder, Error, TEXT("No H264 video encoder found. Check if relevent encoder plugins have been enabled for this project.")); return false; } PRAGMA_DISABLE_DEPRECATION_WARNINGS VideoEncoder->SetOnEncodedPacket([this](uint32 LayerIndex, const TSharedPtr Frame, const AVEncoder::FCodecPacket& Packet) { OnEncodedVideoFrame(LayerIndex, Frame, Packet); }); PRAGMA_ENABLE_DEPRECATION_WARNINGS if(!VideoEncoder) { UE_LOG(GameplayMediaEncoder, Fatal, TEXT("Creating video encoder failed.")); return false; } MemoryCheckpoint("Video encoder initialized"); bIsOk = true; // So Shutdown is not called due to the ON_SCOPE_EXIT return true; } bool FGameplayMediaEncoder::Start() { if(StartTime != 0) { UE_LOG(GameplayMediaEncoder, Log, TEXT("Already running")); return true; } if(!VideoEncoder) { UE_LOG(GameplayMediaEncoder, Log, TEXT("Not initialized yet , so also performing a Intialize()")); if(!Initialize()) { return false; } } StartTime = FTimespan::FromSeconds(FPlatformTime::Seconds()); AudioClock = 0; NumCapturedFrames = 0; // // subscribe to engine delegates for audio output and back buffer // FAudioDeviceHandle AudioDevice = GEngine->GetMainAudioDevice(); if(AudioDevice) { bAudioFormatChecked = false; AudioDevice->RegisterSubmixBufferListener(AsShared(), AudioDevice->GetMainSubmixObject()); } PRAGMA_DISABLE_DEPRECATION_WARNINGS FSlateApplication::Get().GetRenderer()->OnBackBufferReadyToPresent().AddRaw(this, &FGameplayMediaEncoder::OnFrameBufferReady); PRAGMA_ENABLE_DEPRECATION_WARNINGS return true; } void FGameplayMediaEncoder::Stop() { check(IsInGameThread()); if(StartTime == 0) { UE_LOG(GameplayMediaEncoder, Log, TEXT("Not running")); return; } if(UGameEngine* GameEngine = Cast(GEngine)) { if(FAudioDevice* AudioDevice = GameEngine->GetMainAudioDeviceRaw()) { // AsShared() cannot be called in the destructor of the ref counted object! if(!bIsInDestructor) { AudioDevice->UnregisterSubmixBufferListener(AsShared(), AudioDevice->GetMainSubmixObject()); } } if(FSlateApplication::IsInitialized()) { FSlateApplication::Get().GetRenderer()->OnBackBufferReadyToPresent().RemoveAll(this); } } StartTime = 0; AudioClock = 0; } PRAGMA_DISABLE_DEPRECATION_WARNINGS AVEncoder::FAudioConfig FGameplayMediaEncoder::GetAudioConfig() const PRAGMA_ENABLE_DEPRECATION_WARNINGS { PRAGMA_DISABLE_DEPRECATION_WARNINGS if(AudioEncoder) { auto codec = AudioEncoder->GetType(); auto config = AudioEncoder->GetConfig(); return {codec, config.Samplerate, config.NumChannels, config.Bitrate}; } else { return {}; } PRAGMA_ENABLE_DEPRECATION_WARNINGS } void FGameplayMediaEncoder::Shutdown() { if(StartTime != 0) { UE_LOG(GameplayMediaEncoder, Log, TEXT("Currently running, so also performing a Stop()")); Stop(); } { FScopeLock Lock(&AudioProcessingCS); if(AudioEncoder) { // AudioEncoder->Reset(); PRAGMA_DISABLE_DEPRECATION_WARNINGS AudioEncoder->Shutdown(); PRAGMA_ENABLE_DEPRECATION_WARNINGS AudioEncoder.Reset(); } } { FScopeLock Lock(&VideoProcessingCS); if(VideoEncoder) { PRAGMA_DISABLE_DEPRECATION_WARNINGS VideoEncoder->Shutdown(); PRAGMA_ENABLE_DEPRECATION_WARNINGS VideoEncoder.Reset(); BackBuffers.Empty(); } } } FTimespan FGameplayMediaEncoder::GetMediaTimestamp() const { return FTimespan::FromSeconds(FPlatformTime::Seconds()) - StartTime; } PRAGMA_DISABLE_DEPRECATION_WARNINGS const FString& FGameplayMediaEncoder::GetListenerName() const PRAGMA_ENABLE_DEPRECATION_WARNINGS { static const FString& ListenerName = TEXT("GameplayMediaEncoderListener"); return ListenerName; } void FGameplayMediaEncoder::OnNewSubmixBuffer(const USoundSubmix* OwningSubmix, float* AudioData, int32 NumSamples, int32 NumChannels, const int32 SampleRate, double /*AudioClock*/) { CSV_SCOPED_TIMING_STAT(GameplayMediaEncoder, OnNewSubmixBuffer); if(SampleRate != HardcodedAudioSamplerate) { // Only report the problem once if(!bAudioFormatChecked) { bAudioFormatChecked = true; UE_LOG(GameplayMediaEncoder, Error, TEXT("Audio SampleRate needs to be %d HZ, current value is %d. VideoRecordingSystem won't record audio"), HardcodedAudioSamplerate, SampleRate); } return; } ProcessAudioFrame(AudioData, NumSamples, NumChannels, SampleRate); } void FGameplayMediaEncoder::OnFrameBufferReady(SWindow& SlateWindow, const FTextureRHIRef& FrameBuffer) { CSV_SCOPED_TIMING_STAT(GameplayMediaEncoder, OnBackBufferReady); check(IsInRenderingThread()); ProcessVideoFrame(FrameBuffer); } void FGameplayMediaEncoder::FloatToPCM16(float const* floatSamples, int32 numSamples, TArray& out) const { out.Reset(numSamples); out.AddZeroed(numSamples); float const* ptr = floatSamples; for(auto&& sample : out) { int32 N = *ptr >= 0 ? FMath::TruncToInt32(*ptr * int32(MAX_int16)) : FMath::TruncToInt32(*ptr * (int32(MAX_int16) + 1)); sample = static_cast(FMath::Clamp(N, int32(MIN_int16), int32(MAX_int16))); ptr++; } } void FGameplayMediaEncoder::ProcessAudioFrame(const float* AudioData, int32 NumSamples, int32 NumChannels, int32 SampleRate) { // Don't encode audio encoder is not setup or destroyed. if(!AudioEncoder.IsValid()) { return; } //// convert to PCM data // TArray conversionBuffer; // FloatToPCM16(AudioData, NumSamples, conversionBuffer); //// add to any remainder data // PCM16.Append(conversionBuffer); //// encode the 10ms blocks // size_t const encodeBlockSize = AudioConfig.Samplerate / 100 * AudioConfig.NumChannels; // int32 bufferSize = PCM16.Num(); // auto timestamp = GetMediaTimestamp().GetTicks(); // auto bufferStart = PCM16.GetData(); // while (bufferSize >= encodeBlockSize) //{ // rtc::Buffer encoded; // auto encodedInfo = AudioEncoder->Encode(timestamp, { bufferStart, encodeBlockSize }, &encoded); // if (encodedInfo.encoded_bytes > 0 || encodedInfo.send_even_if_empty) // OnEncodedAudioFrame(encodedInfo, &encoded); // timestamp += 10 * ETimespan::TicksPerMillisecond; // bufferStart += encodeBlockSize; // bufferSize -= encodeBlockSize; //} //// move the remainder to the start of the buffer // int32 const remainderIdx = bufferStart - PCM16.GetData(); // for (int32 i = 0; i < bufferSize; ++i) //{ // PCM16[i] = PCM16[remainderIdx + i]; //} // PCM16.SetNum(bufferSize, EAllowShrinking::No); Audio::AlignedFloatBuffer InData; InData.Append(AudioData, NumSamples); Audio::TSampleBuffer FloatBuffer(InData, NumChannels, SampleRate); // Mix to stereo if required, since PixelStreaming only accept stereo at the moment if(FloatBuffer.GetNumChannels() != HardcodedAudioNumChannels) { FloatBuffer.MixBufferToChannels(HardcodedAudioNumChannels); } // Adjust the AudioClock if for some reason it falls behind real time. This can happen if the game spikes, or if we break into the debugger. FTimespan Now = GetMediaTimestamp(); if(AudioClock < Now.GetTotalSeconds()) { UE_LOG(GameplayMediaEncoder, Warning, TEXT("Audio clock falling behind real time clock by %.3f seconds. Ajusting audio clock"), Now.GetTotalSeconds() - AudioClock); // Put it slightly ahead of the real time clock AudioClock = Now.GetTotalSeconds() + (FloatBuffer.GetSampleDuration() / 2); } // Convert to signed PCM 16-bits // PCM16.Reset(FloatBuffer.GetNumSamples()); // PCM16.AddZeroed(FloatBuffer.GetNumSamples()); // int blockSize = AudioConfig.Samplerate / 100 * AudioConfig.NumChannels; // PCM16.Reset(blockSize); // PCM16.AddZeroed(blockSize); // const float* Ptr = reinterpret_cast(FloatBuffer.GetData()); // auto timestamp = GetMediaTimestamp().GetTotalMilliseconds(); // for (int i = 0; i < NumSamples * NumChannels; ++i) //{ // int u; // for (u = 0; u < blockSize && i < NumSamples; ++u, ++i, ++Ptr) // { // int32 N = *Ptr >= 0 ? *Ptr * int32(MAX_int16) : *Ptr * (int32(MAX_int16) + 1); // PCM16[u] = static_cast(FMath::Clamp(N, int32(MIN_int16), int32(MAX_int16))); // } // rtc::Buffer encoded; // auto encodedInfo = AudioEncoder->Encode(timestamp, { PCM16.GetData(), static_cast(u) }, &encoded); // OnEncodedAudioFrame(encodedInfo, &encoded); // timestamp += 10; //} // for (int16& S : PCM16) //{ // int32 N = *Ptr >= 0 ? *Ptr * int32(MAX_int16) : *Ptr * (int32(MAX_int16) + 1); // S = static_cast(FMath::Clamp(N, int32(MIN_int16), int32(MAX_int16))); // Ptr++; //} ////rtc::ArrayView audio(PCM16.GetData(), FloatBuffer.GetNumSamples()); // rtc::ArrayView audio(PCM16.GetData(), blockSize); // auto testSampleRate = AudioEncoder->SampleRateHz(); // auto testNumChannels = AudioEncoder->NumChannels(); // check(testSampleRate == AudioConfig.Samplerate); // check(testNumChannels == AudioConfig.NumChannels); // auto timestamp = GetMediaTimestamp().GetTotalMilliseconds(); // auto encodedInfo = AudioEncoder->Encode(timestamp, audio, &encoded); // OnEncodedAudioFrame(encodedInfo, &encoded); PRAGMA_DISABLE_DEPRECATION_WARNINGS AVEncoder::FAudioFrame Frame; Frame.Timestamp = FTimespan::FromSeconds(AudioClock); Frame.Duration = FTimespan::FromSeconds(FloatBuffer.GetSampleDuration()); FloatBuffer.Clamp(); Frame.Data = FloatBuffer; AudioEncoder->Encode(Frame); PRAGMA_ENABLE_DEPRECATION_WARNINGS AudioClock += FloatBuffer.GetSampleDuration(); } void FGameplayMediaEncoder::ProcessVideoFrame(const FTextureRHIRef& FrameBuffer) { // Early exit is video encoder is not valid because it is not setup or has been destroyed if(!VideoEncoder.IsValid()) { return; } FScopeLock Lock(&VideoProcessingCS); FTimespan Now = GetMediaTimestamp(); if(bDoFrameSkipping) { PRAGMA_DISABLE_DEPRECATION_WARNINGS uint64 NumExpectedFrames = static_cast(Now.GetTotalSeconds() * VideoConfig.Framerate); PRAGMA_ENABLE_DEPRECATION_WARNINGS UE_LOG(GameplayMediaEncoder, VeryVerbose, TEXT("time %.3f: captured %" UINT64_FMT ", expected %" UINT64_FMT), Now.GetTotalSeconds(), NumCapturedFrames + 1, NumExpectedFrames); if(NumCapturedFrames + 1 > NumExpectedFrames) { UE_LOG(GameplayMediaEncoder, Verbose, TEXT("Framerate control dropped captured frame")); return; } } UpdateVideoConfig(); PRAGMA_DISABLE_DEPRECATION_WARNINGS TSharedPtr InputFrame = ObtainInputFrame(); const int32 FrameId = InputFrame->GetFrameID(); InputFrame->SetTimestampUs(Now.GetTicks()); PRAGMA_ENABLE_DEPRECATION_WARNINGS CopyTexture(FrameBuffer, BackBuffers[InputFrame]); PRAGMA_DISABLE_DEPRECATION_WARNINGS AVEncoder::FVideoEncoder::FEncodeOptions EncodeOptions; VideoEncoder->Encode(InputFrame, EncodeOptions); PRAGMA_ENABLE_DEPRECATION_WARNINGS LastVideoInputTimestamp = Now; NumCapturedFrames++; } PRAGMA_DISABLE_DEPRECATION_WARNINGS TSharedPtr FGameplayMediaEncoder::ObtainInputFrame() PRAGMA_ENABLE_DEPRECATION_WARNINGS { PRAGMA_DISABLE_DEPRECATION_WARNINGS TSharedPtr InputFrame = VideoEncoderInput->ObtainInputFrame(); InputFrame->SetWidth(VideoConfig.Width); InputFrame->SetHeight(VideoConfig.Height); PRAGMA_ENABLE_DEPRECATION_WARNINGS if(!BackBuffers.Contains(InputFrame)) { #if PLATFORM_WINDOWS && PLATFORM_DESKTOP const ERHIInterfaceType RHIType = RHIGetInterfaceType(); const FRHITextureCreateDesc Desc = PRAGMA_DISABLE_DEPRECATION_WARNINGS FRHITextureCreateDesc::Create2D(TEXT("VideoCapturerBackBuffer"), VideoConfig.Width, VideoConfig.Height, PF_B8G8R8A8) PRAGMA_ENABLE_DEPRECATION_WARNINGS .SetFlags(ETextureCreateFlags::Shared | ETextureCreateFlags::RenderTargetable | ETextureCreateFlags::UAV) .SetInitialState(ERHIAccess::CopyDest); if (RHIType == ERHIInterfaceType::D3D11) { FTextureRHIRef Texture = RHICreateTexture(Desc); PRAGMA_DISABLE_DEPRECATION_WARNINGS InputFrame->SetTexture((ID3D11Texture2D*)Texture->GetNativeResource(), [&, InputFrame](ID3D11Texture2D* NativeTexture) { BackBuffers.Remove(InputFrame); }); PRAGMA_ENABLE_DEPRECATION_WARNINGS BackBuffers.Add(InputFrame, Texture); } else if (RHIType == ERHIInterfaceType::D3D12) { FTextureRHIRef Texture = RHICreateTexture(Desc); PRAGMA_DISABLE_DEPRECATION_WARNINGS InputFrame->SetTexture((ID3D12Resource*)Texture->GetNativeResource(), [&, InputFrame](ID3D12Resource* NativeTexture) { BackBuffers.Remove(InputFrame); }); PRAGMA_ENABLE_DEPRECATION_WARNINGS BackBuffers.Add(InputFrame, Texture); } UE_LOG(LogTemp, Log, TEXT("%d backbuffers currently allocated"), BackBuffers.Num()); #else unimplemented(); #endif // PLATFORM_DESKTOP && !PLATFORM_APPLE } return InputFrame; } void FGameplayMediaEncoder::SetVideoBitrate(uint32 Bitrate) { NewVideoBitrate = Bitrate; bChangeBitrate = true; } void FGameplayMediaEncoder::SetVideoFramerate(uint32 Framerate) { NewVideoFramerate = FMath::Clamp(Framerate, MinVideoFPS, MaxVideoFPS); bChangeFramerate = true; } void FGameplayMediaEncoder::UpdateVideoConfig() { if(bChangeBitrate || bChangeFramerate) { PRAGMA_DISABLE_DEPRECATION_WARNINGS auto config = VideoEncoder->GetLayerConfig(0); PRAGMA_ENABLE_DEPRECATION_WARNINGS if(bChangeBitrate) { PRAGMA_DISABLE_DEPRECATION_WARNINGS config.MaxBitrate = MaxVideoBitrate; config.TargetBitrate = NewVideoBitrate; PRAGMA_ENABLE_DEPRECATION_WARNINGS } if(bChangeFramerate) { PRAGMA_DISABLE_DEPRECATION_WARNINGS config.MaxFramerate = NewVideoFramerate; PRAGMA_ENABLE_DEPRECATION_WARNINGS NumCapturedFrames = 0; } PRAGMA_DISABLE_DEPRECATION_WARNINGS VideoEncoder->UpdateLayerConfig(0, config); PRAGMA_ENABLE_DEPRECATION_WARNINGS bChangeFramerate = false; bChangeBitrate = false; } } PRAGMA_DISABLE_DEPRECATION_WARNINGS void FGameplayMediaEncoder::OnEncodedAudioFrame(const AVEncoder::FMediaPacket& Packet) PRAGMA_ENABLE_DEPRECATION_WARNINGS { FScopeLock Lock(&ListenersCS); for(auto&& Listener : Listeners) { Listener->OnMediaSample(Packet); } } PRAGMA_DISABLE_DEPRECATION_WARNINGS void FGameplayMediaEncoder::OnEncodedVideoFrame(uint32 LayerIndex, const TSharedPtr InputFrame, const AVEncoder::FCodecPacket& Packet) PRAGMA_ENABLE_DEPRECATION_WARNINGS { PRAGMA_DISABLE_DEPRECATION_WARNINGS AVEncoder::FMediaPacket packet(AVEncoder::EPacketType::Video); packet.Timestamp = InputFrame->GetTimestampUs(); packet.Duration = 0; // This should probably be 1.0f / fps in ms packet.Data = TArray(Packet.Data.Get(), Packet.DataSize); packet.Video.bKeyFrame = Packet.IsKeyFrame; packet.Video.Width = InputFrame->GetWidth(); packet.Video.Height = InputFrame->GetHeight(); packet.Video.FrameAvgQP = Packet.VideoQP; packet.Video.Framerate = VideoConfig.Framerate; PRAGMA_ENABLE_DEPRECATION_WARNINGS FScopeLock Lock(&ListenersCS); for(auto&& Listener : Listeners) { Listener->OnMediaSample(packet); } PRAGMA_DISABLE_DEPRECATION_WARNINGS InputFrame->Release(); PRAGMA_ENABLE_DEPRECATION_WARNINGS } void FGameplayMediaEncoder::CopyTexture(const FTextureRHIRef& SourceTexture, FTextureRHIRef& DestinationTexture) const { FRHICommandListImmediate& RHICmdList = FRHICommandListImmediate::Get(); if(SourceTexture->GetFormat() == DestinationTexture->GetFormat() && SourceTexture->GetSizeXY() == DestinationTexture->GetSizeXY()) { TransitionAndCopyTexture(RHICmdList, SourceTexture, DestinationTexture, {}); } else // Texture format mismatch, use a shader to do the copy. { IRendererModule* RendererModule = &FModuleManager::GetModuleChecked("Renderer"); // #todo-renderpasses there's no explicit resolve here? Do we need one? FRHIRenderPassInfo RPInfo(DestinationTexture, ERenderTargetActions::Load_Store); RHICmdList.Transition(FRHITransitionInfo(DestinationTexture, ERHIAccess::Unknown, ERHIAccess::RTV)); RHICmdList.BeginRenderPass(RPInfo, TEXT("CopyBackbuffer")); { RHICmdList.SetViewport(0, 0, 0.0f, (float)DestinationTexture->GetSizeX(), (float)DestinationTexture->GetSizeY(), 1.0f); FGraphicsPipelineStateInitializer GraphicsPSOInit; RHICmdList.ApplyCachedRenderTargets(GraphicsPSOInit); GraphicsPSOInit.BlendState = TStaticBlendState<>::GetRHI(); GraphicsPSOInit.RasterizerState = TStaticRasterizerState<>::GetRHI(); GraphicsPSOInit.DepthStencilState = TStaticDepthStencilState::GetRHI(); // New engine version... FGlobalShaderMap* ShaderMap = GetGlobalShaderMap(GMaxRHIFeatureLevel); TShaderMapRef VertexShader(ShaderMap); TShaderMapRef PixelShader(ShaderMap); GraphicsPSOInit.BoundShaderState.VertexDeclarationRHI = GFilterVertexDeclaration.VertexDeclarationRHI; GraphicsPSOInit.BoundShaderState.VertexShaderRHI = VertexShader.GetVertexShader(); GraphicsPSOInit.BoundShaderState.PixelShaderRHI = PixelShader.GetPixelShader(); GraphicsPSOInit.PrimitiveType = PT_TriangleList; SetGraphicsPipelineState(RHICmdList, GraphicsPSOInit, 0); const bool bSameSize = (DestinationTexture->GetDesc().Extent == SourceTexture->GetDesc().Extent); FRHISamplerState* PixelSampler = bSameSize ? TStaticSamplerState::GetRHI() : TStaticSamplerState::GetRHI(); SetShaderParametersLegacyPS(RHICmdList, PixelShader, PixelSampler, SourceTexture); RendererModule->DrawRectangle(RHICmdList, 0, 0, // Dest X, Y (float)DestinationTexture->GetSizeX(), // Dest Width (float)DestinationTexture->GetSizeY(), // Dest Height 0, 0, // Source U, V 1, 1, // Source USize, VSize DestinationTexture->GetSizeXY(), // Target buffer size FIntPoint(1, 1), // Source texture size VertexShader, EDRF_Default); } RHICmdList.EndRenderPass(); RHICmdList.Transition(FRHITransitionInfo(DestinationTexture, ERHIAccess::RTV, ERHIAccess::SRVMask)); } }