Files
UnrealEngine/Engine/Plugins/Media/PixelStreaming2/Source/PixelStreaming2RTC/Private/FreezeFrame.cpp
2025-05-18 13:04:45 +08:00

332 lines
12 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#include "FreezeFrame.h"
#include "ColorConversion.h"
#include "EngineModule.h"
#include "IImageWrapper.h"
#include "IImageWrapperModule.h"
#include "Logging.h"
#include "MediaShaders.h"
#include "Modules/ModuleManager.h"
#include "PixelCaptureBufferI420.h"
#include "PixelCaptureBufferFormat.h"
#include "PixelCaptureOutputFrameI420.h"
#include "PixelCaptureOutputFrameRHI.h"
#include "PixelStreaming2PluginSettings.h"
#include "RenderingThread.h"
#include "RHI.h"
#include "RHICommandList.h"
#include "RHIFwd.h"
#include "ScreenPass.h"
#include "ScreenRendering.h"
#include "TextureResource.h"
#include "UtilsCommon.h"
namespace UE::PixelStreaming2
{
TSharedPtr<FFreezeFrame> FFreezeFrame::Create(TWeakPtr<TThreadSafeMap<FString, TSharedPtr<FPlayerContext>>> InPlayers, TWeakPtr<FVideoCapturer> InVideoCapturer, TWeakPtr<IPixelStreaming2InputHandler> InInputHandler)
{
TSharedPtr<FFreezeFrame> FreezeFrame = TSharedPtr<FFreezeFrame>(new FFreezeFrame(InPlayers, InVideoCapturer, InInputHandler));
return FreezeFrame;
}
FFreezeFrame::FFreezeFrame(TWeakPtr<TThreadSafeMap<FString, TSharedPtr<FPlayerContext>>> WeakPlayers, TWeakPtr<FVideoCapturer> WeakVideoCapturer, TWeakPtr<IPixelStreaming2InputHandler> WeakInputHandler)
: WeakPlayers(WeakPlayers)
, VideoCapturer(WeakVideoCapturer)
, InputHandler(WeakInputHandler)
{
}
FFreezeFrame::~FFreezeFrame()
{
RemoveFreezeFrameBinding();
}
/**
* Add the commands to the RHI command list to copy a texture from source to dest - even if the format is different.
* Assumes SourceTexture is in ERHIAccess::CopySrc and DestTexture is in ERHIAccess::CopyDest
*/
void CopyTexture(FRHICommandList& RHICmdList, FTextureRHIRef SourceTexture, FTextureRHIRef DestTexture)
{
if (SourceTexture->GetDesc().Format == DestTexture->GetDesc().Format
&& SourceTexture->GetDesc().Extent.X == DestTexture->GetDesc().Extent.X
&& SourceTexture->GetDesc().Extent.Y == DestTexture->GetDesc().Extent.Y)
{
RHICmdList.Transition(FRHITransitionInfo(SourceTexture, ERHIAccess::Unknown, ERHIAccess::CopySrc));
RHICmdList.Transition(FRHITransitionInfo(DestTexture, ERHIAccess::Unknown, ERHIAccess::CopyDest));
// source and dest are the same. simple copy
RHICmdList.CopyTexture(SourceTexture, DestTexture, {});
}
else
{
IRendererModule* RendererModule = &FModuleManager::GetModuleChecked<IRendererModule>("Renderer");
RHICmdList.Transition(FRHITransitionInfo(SourceTexture, ERHIAccess::Unknown, ERHIAccess::SRVMask));
RHICmdList.Transition(FRHITransitionInfo(DestTexture, ERHIAccess::Unknown, ERHIAccess::RTV));
// source and destination are different. rendered copy
FRHIRenderPassInfo RPInfo(DestTexture, ERenderTargetActions::Load_Store);
RHICmdList.BeginRenderPass(RPInfo, TEXT("PixelStreaming2::CopyTexture"));
{
FGlobalShaderMap* ShaderMap = GetGlobalShaderMap(GMaxRHIFeatureLevel);
TShaderMapRef<FScreenVS> VertexShader(ShaderMap);
TShaderMapRef<FScreenPS> PixelShader(ShaderMap);
RHICmdList.SetViewport(0, 0, 0.0f, DestTexture->GetDesc().Extent.X, DestTexture->GetDesc().Extent.Y, 1.0f);
FGraphicsPipelineStateInitializer GraphicsPSOInit;
RHICmdList.ApplyCachedRenderTargets(GraphicsPSOInit);
GraphicsPSOInit.BlendState = TStaticBlendState<>::GetRHI();
GraphicsPSOInit.RasterizerState = TStaticRasterizerState<>::GetRHI();
GraphicsPSOInit.DepthStencilState = TStaticDepthStencilState<false, CF_Always>::GetRHI();
GraphicsPSOInit.BoundShaderState.VertexDeclarationRHI = GFilterVertexDeclaration.VertexDeclarationRHI;
GraphicsPSOInit.BoundShaderState.VertexShaderRHI = VertexShader.GetVertexShader();
GraphicsPSOInit.BoundShaderState.PixelShaderRHI = PixelShader.GetPixelShader();
GraphicsPSOInit.PrimitiveType = PT_TriangleList;
SetGraphicsPipelineState(RHICmdList, GraphicsPSOInit, 0);
SetShaderParametersLegacyPS(RHICmdList, PixelShader, TStaticSamplerState<SF_Point>::GetRHI(), SourceTexture);
FIntPoint TargetBufferSize(DestTexture->GetDesc().Extent.X, DestTexture->GetDesc().Extent.Y);
RendererModule->DrawRectangle(RHICmdList, 0, 0, // Dest X, Y
DestTexture->GetDesc().Extent.X, // Dest Width
DestTexture->GetDesc().Extent.Y, // Dest Height
0, 0, // Source U, V
1, 1, // Source USize, VSize
TargetBufferSize, // Target buffer size
FIntPoint(1, 1), // Source texture size
VertexShader, EDRF_Default);
}
RHICmdList.EndRenderPass();
RHICmdList.Transition(FRHITransitionInfo(SourceTexture, ERHIAccess::SRVMask, ERHIAccess::CopySrc));
RHICmdList.Transition(FRHITransitionInfo(DestTexture, ERHIAccess::RTV, ERHIAccess::CopyDest));
}
}
void FFreezeFrame::StartFreeze(UTexture2D* Texture)
{
if (Texture)
{
ENQUEUE_RENDER_COMMAND(ReadSurfaceCommand)
([this, Texture](FRHICommandListImmediate& RHICmdList) {
// A frame is supplied so immediately read its data and send as a JPEG.
FTextureRHIRef TextureRHI = Texture->GetResource() ? Texture->GetResource()->TextureRHI : nullptr;
if (!TextureRHI)
{
UE_LOG(LogPixelStreaming2RTC, Error, TEXT("Attempting freeze frame with texture %s with no texture RHI"), *Texture->GetName());
return;
}
uint32 Width = TextureRHI->GetDesc().Extent.X;
uint32 Height = TextureRHI->GetDesc().Extent.Y;
FRHITextureCreateDesc TextureDesc =
FRHITextureCreateDesc::Create2D(TEXT("PixelStreaming2BlankTexture"), Width, Height, EPixelFormat::PF_B8G8R8A8)
.SetClearValue(FClearValueBinding::None)
.SetFlags(ETextureCreateFlags::RenderTargetable)
.SetInitialState(ERHIAccess::Present)
.DetermineInititialState();
FTextureRHIRef DestTexture = RHICreateTexture(TextureDesc);
// Copy freeze frame texture to empty texture
CopyTexture(RHICmdList, TextureRHI, DestTexture);
TArray<FColor> Data;
FIntRect Rect(0, 0, Width, Height);
// This `ReadSurfaceData` makes a blocking call from CPU -> GPU -> CPU
// WHich is how on the very next line we are able to copy the data out and send it.
RHICmdList.ReadSurfaceData(DestTexture, Rect, Data, FReadSurfaceDataFlags());
SendFreezeFrame(MoveTemp(Data), Rect);
});
}
else
{
// A frame is not supplied, so we need to get it from the video input
// at the next opportunity and send as a JPEG.
SetupFreezeFrameCapture();
}
}
void FFreezeFrame::StopFreeze()
{
TSharedPtr<TThreadSafeMap<FString, TSharedPtr<FPlayerContext>>> Players = WeakPlayers.Pin();
if (!Players)
{
return;
}
TWeakPtr<IPixelStreaming2InputHandler> WeakHandler = InputHandler;
Players->Apply([WeakHandler](FString PlayerId, TSharedPtr<FPlayerContext> Participant) {
if (!Participant->DataTrack)
{
return;
}
if (TSharedPtr<IPixelStreaming2InputHandler> Handler = WeakHandler.Pin())
{
Participant->DataTrack->SendMessage(EPixelStreaming2FromStreamerMessage::UnfreezeFrame);
}
});
CachedJpegBytes.Empty();
}
void FFreezeFrame::SendCachedFreezeFrameTo(FString PlayerId) const
{
TSharedPtr<TThreadSafeMap<FString, TSharedPtr<FPlayerContext>>> Players = WeakPlayers.Pin();
if (!Players)
{
return;
}
if (CachedJpegBytes.Num() > 0)
{
if (TSharedPtr<FPlayerContext> Participant = Players->FindRef(PlayerId); Participant.IsValid())
{
if (!Participant->DataTrack)
{
return;
}
if (TSharedPtr<IPixelStreaming2InputHandler> Handler = InputHandler.Pin())
{
Participant->DataTrack->SendArbitraryData(EPixelStreaming2FromStreamerMessage::FreezeFrame, CachedJpegBytes);
}
}
}
}
void FFreezeFrame::SendFreezeFrame(TArray<FColor> RawData, const FIntRect& Rect)
{
TSharedPtr<TThreadSafeMap<FString, TSharedPtr<FPlayerContext>>> Players = WeakPlayers.Pin();
if (!Players)
{
return;
}
IImageWrapperModule& ImageWrapperModule = FModuleManager::GetModuleChecked<IImageWrapperModule>(TEXT("ImageWrapper"));
TSharedPtr<IImageWrapper> ImageWrapper = ImageWrapperModule.CreateImageWrapper(EImageFormat::JPEG);
bool bSuccess = ImageWrapper->SetRaw(RawData.GetData(), RawData.Num() * sizeof(FColor), Rect.Width(), Rect.Height(), ERGBFormat::BGRA, 8);
if (bSuccess)
{
TWeakPtr<IPixelStreaming2InputHandler> WeakHandler = InputHandler;
// Compress to a JPEG of the maximum possible quality.
const TArray64<uint8>& JpegBytes = ImageWrapper->GetCompressed(100);
Players->Apply([&JpegBytes, WeakHandler](FString PlayerId, TSharedPtr<FPlayerContext> Participant) {
if (!Participant->DataTrack)
{
return;
}
if (TSharedPtr<IPixelStreaming2InputHandler> Handler = WeakHandler.Pin())
{
Participant->DataTrack->SendArbitraryData(EPixelStreaming2FromStreamerMessage::FreezeFrame, JpegBytes);
}
});
CachedJpegBytes = JpegBytes;
}
else
{
UE_LOG(LogPixelStreaming2RTC, Error, TEXT("JPEG image wrapper failed to accept frame data"));
}
}
void FFreezeFrame::SetupFreezeFrameCapture()
{
// Remove any existing binding
RemoveFreezeFrameBinding();
if (TSharedPtr<FVideoCapturer> ConcreteVideoCapturer = VideoCapturer.Pin())
{
OnFrameCapturedForFreezeFrameHandle = ConcreteVideoCapturer->OnFrameCaptured.AddSP(this, &FFreezeFrame::FreezeFrameCapture);
}
}
void FFreezeFrame::RemoveFreezeFrameBinding()
{
if (!OnFrameCapturedForFreezeFrameHandle)
{
return;
}
if (TSharedPtr<FVideoCapturer> ConcreteVideoCapturer = VideoCapturer.Pin())
{
ConcreteVideoCapturer->OnFrameCaptured.Remove(OnFrameCapturedForFreezeFrameHandle.GetValue());
OnFrameCapturedForFreezeFrameHandle.Reset();
}
}
void FFreezeFrame::FreezeFrameCapture()
{
TSharedPtr<FVideoCapturer> Input = VideoCapturer.Pin();
if (!Input)
{
return;
}
// HACK we probably should check if we are outputing a CPU texture rather than checking like this.
EVideoCodec CurrentCodec = GetEnumFromCVar<EVideoCodec>(UPixelStreaming2PluginSettings::CVarEncoderCodec);
if (CurrentCodec == EVideoCodec::VP8 || CurrentCodec == EVideoCodec::VP9)
{
// Request output format is I420 for VPX
TSharedPtr<IPixelCaptureOutputFrame> OutputFrame = Input->RequestFormat(PixelCaptureBufferFormat::FORMAT_I420);
if (OutputFrame)
{
// Can remove binding now we have got the output in the format we need to send a FF
RemoveFreezeFrameBinding();
FPixelCaptureOutputFrameI420* I420Frame = StaticCast<FPixelCaptureOutputFrameI420*>(OutputFrame.Get());
TSharedPtr<FPixelCaptureBufferI420> I420Buffer = I420Frame->GetI420Buffer();
const size_t NumBytes = CalcBufferSizeArgb(I420Frame->GetWidth(), I420Frame->GetHeight());
const uint32_t NumPixels = I420Frame->GetWidth() * I420Frame->GetHeight();
uint8_t* ARGBBuffer = new uint8_t[NumBytes];
ConvertI420ToArgb(I420Buffer->GetDataY(), I420Buffer->GetStrideY(),
I420Buffer->GetDataU(), I420Buffer->GetStrideUV(),
I420Buffer->GetDataV(), I420Buffer->GetStrideUV(),
ARGBBuffer, 0,
I420Buffer->GetWidth(), I420Buffer->GetHeight());
// We assume FColor packing is same ordering as the Buffer are copying from
TArray<FColor> PixelArr((FColor*)ARGBBuffer, NumPixels);
FIntRect Rect(0, 0, I420Frame->GetWidth(), I420Frame->GetHeight());
SendFreezeFrame(MoveTemp(PixelArr), Rect);
}
}
else
{
TSharedPtr<IPixelCaptureOutputFrame> OutputFrame = Input->RequestFormat(PixelCaptureBufferFormat::FORMAT_RHI);
if (OutputFrame)
{
// Can remove binding now we have got the output in the format we need to send a FF
RemoveFreezeFrameBinding();
TWeakPtr<FFreezeFrame> WeakSelf = AsShared();
ENQUEUE_RENDER_COMMAND(ReadSurfaceCommand)
([WeakSelf, OutputFrame](FRHICommandListImmediate& RHICmdList) {
if (auto ThisPtr = WeakSelf.Pin())
{
FPixelCaptureOutputFrameRHI* RHISourceFrame = StaticCast<FPixelCaptureOutputFrameRHI*>(OutputFrame.Get());
// Read the data out of the back buffer and send as a JPEG.
FIntRect Rect(0, 0, RHISourceFrame->GetWidth(), RHISourceFrame->GetHeight());
TArray<FColor> Data;
RHICmdList.ReadSurfaceData(RHISourceFrame->GetFrameTexture(), Rect, Data, FReadSurfaceDataFlags());
ThisPtr->SendFreezeFrame(MoveTemp(Data), Rect);
}
});
}
}
}
} // namespace UE::PixelStreaming2