Files
Brandyn / Techy fcc1b09210 init
2026-04-04 15:40:51 -05:00

287 lines
12 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#include "Graph/MovieGraphMP4EncoderNode.h"
#include "Graph/MovieGraphBlueprintLibrary.h"
#include "Graph/MovieGraphConfig.h"
#include "Graph/MovieGraphOCIOHelper.h"
#include "Graph/MovieGraphPipeline.h"
#include "Graph/Nodes/MovieGraphBurnInNode.h"
#include "Graph/Nodes/MovieGraphGlobalOutputSettingNode.h"
#include "ImageWriteTask.h"
#include "MoviePipelineImageQuantization.h"
#include "MoviePipelineTelemetry.h"
#include "MovieRenderPipelineCoreModule.h"
#include "SampleBuffer.h"
#include "Styling/AppStyle.h"
#include UE_INLINE_GENERATED_CPP_BY_NAME(MovieGraphMP4EncoderNode)
UMovieGraphMP4EncoderNode::UMovieGraphMP4EncoderNode()
{
}
#if WITH_EDITOR
FText UMovieGraphMP4EncoderNode::GetNodeTitle(const bool bGetDescriptive) const
{
static const FText NodeName = NSLOCTEXT("MovieGraphNodes", "NodeName_MP4", "H.264 MP4");
return NodeName;
}
FText UMovieGraphMP4EncoderNode::GetMenuCategory() const
{
return NSLOCTEXT("MovieGraphNodes", "WMFNode_Category", "Output Type");
}
FText UMovieGraphMP4EncoderNode::GetKeywords() const
{
static const FText Keywords = NSLOCTEXT("MovieGraphNodes", "MP4_Keywords", "mp4 h264 h265 windows mpeg mov movie video");
return Keywords;
}
FLinearColor UMovieGraphMP4EncoderNode::GetNodeTitleColor() const
{
static const FLinearColor NodeColor = FLinearColor(0.047f, 0.654f, 0.537f);
return NodeColor;
}
FSlateIcon UMovieGraphMP4EncoderNode::GetIconAndTint(FLinearColor& OutColor) const
{
static const FSlateIcon Icon = FSlateIcon(FAppStyle::GetAppStyleSetName(), "LevelEditor.Tabs.Cinematics");
OutColor = FLinearColor::White;
return Icon;
}
#endif // WITH_EDITOR
TUniquePtr<MovieRenderGraph::IVideoCodecWriter> UMovieGraphMP4EncoderNode::Initialize_GameThread(const FMovieGraphVideoNodeInitializationContext& InInitializationContext)
{
bool bIncludeCDOs = true;
constexpr bool bExactMatch = true;
UMovieGraphGlobalOutputSettingNode* OutputSetting =
InInitializationContext.EvaluatedConfig->GetSettingForBranch<UMovieGraphGlobalOutputSettingNode>(GlobalsPinName, bIncludeCDOs, bExactMatch);
bIncludeCDOs = false;
const UMovieGraphMP4EncoderNode* EvaluatedNode = Cast<UMovieGraphMP4EncoderNode>(
InInitializationContext.EvaluatedConfig->GetSettingForBranch(GetClass(), FName(InInitializationContext.PassData->Key.RootBranchName), bIncludeCDOs, bExactMatch));
checkf(EvaluatedNode, TEXT("MP4 Encoder node could not be found in the graph in branch [%s]."), *InInitializationContext.PassData->Key.RootBranchName.ToString());
const FFrameRate SourceFrameRate = InInitializationContext.Pipeline->GetDataSourceInstance()->GetDisplayRate();
const FFrameRate EffectiveFrameRate = UMovieGraphBlueprintLibrary::GetEffectiveFrameRate(OutputSetting, SourceFrameRate);
FMoviePipelineMP4EncoderOptions Options;
Options.OutputFilename = InInitializationContext.FileName;
Options.Width = InInitializationContext.Resolution.X;
Options.Height = InInitializationContext.Resolution.Y;
Options.FrameRate = EffectiveFrameRate;
Options.CommonMeanBitRate = FMath::RoundToInt(EvaluatedNode->AverageBitrateInMbps*1024*1024);
Options.CommonQualityVsSpeed = 100;
Options.CommonConstantRateFactor = EvaluatedNode->ConstantRateFactor;
Options.EncodingRateControl = EvaluatedNode->EncodingRateControl;
Options.bIncludeAudio = EvaluatedNode->bIncludeAudio;
// Optional properties that can be overridden by scripting if needed; not exposed to the UI currently
if (EvaluatedNode->bOverride_MaxBitrateInMbps)
{
Options.CommonMaxBitRate = FMath::RoundToInt(EvaluatedNode->MaxBitrateInMbps*1024*1024);
}
if (EvaluatedNode->bOverride_EncodingProfile)
{
Options.EncodingProfile = EvaluatedNode->EncodingProfile;
}
if (EvaluatedNode->bOverride_EncodingLevel)
{
Options.EncodingLevel = EvaluatedNode->EncodingLevel;
}
TUniquePtr<FMP4CodecWriter> NewWriter = MakeUnique<FMP4CodecWriter>();
NewWriter->Writer = MakeUnique<FMoviePipelineMP4Encoder>(Options);
// If OCIO is enabled, don't do additional color conversion.
NewWriter->bSkipColorConversions = (EvaluatedNode->bOverride_OCIOConfiguration && EvaluatedNode->OCIOConfiguration.bIsEnabled && InInitializationContext.bAllowOCIO);
CachedPipeline = InInitializationContext.Pipeline;
return NewWriter;
}
bool UMovieGraphMP4EncoderNode::Initialize_EncodeThread(MovieRenderGraph::IVideoCodecWriter* InWriter)
{
const FMP4CodecWriter* CodecWriter = static_cast<FMP4CodecWriter*>(InWriter);
if(!CodecWriter->Writer->Initialize())
{
UE_LOG(LogMovieRenderPipeline, Error, TEXT("Failed to initialize Movie Pipeline MP4 writer. An encoder that supports the render resolution and requested MP4 encode options was not found. Try again with a different resolution and/or encoder settings."));
return false;
}
return true;
}
void UMovieGraphMP4EncoderNode::WriteFrame_EncodeThread(MovieRenderGraph::IVideoCodecWriter* InWriter, FImagePixelData* InPixelData, TArray<FMovieGraphPassData>&& InCompositePasses, TObjectPtr<UMovieGraphEvaluatedConfig> InEvaluatedConfig, const FString& InBranchName)
{
FMP4CodecWriter* CodecWriter = static_cast<FMP4CodecWriter*>(InWriter);
if (!CodecWriter->Writer)
{
return;
}
bool bIncludeCDOs = false;
constexpr bool bExactMatch = true;
const UMovieGraphMP4EncoderNode* EvaluatedNode = Cast<UMovieGraphMP4EncoderNode>(
InEvaluatedConfig->GetSettingForBranch(GetClass(), FName(InBranchName), bIncludeCDOs, bExactMatch));
checkf(EvaluatedNode, TEXT("MP4 Encoder node could not be found in the graph in branch [%s]."), *InBranchName);
bIncludeCDOs = true;
const UMovieGraphGlobalOutputSettingNode* OutputSettingNode =
InEvaluatedConfig->GetSettingForBranch<UMovieGraphGlobalOutputSettingNode>(GlobalsPinName, bIncludeCDOs, bExactMatch);
const UE::MovieGraph::FMovieGraphSampleState* GraphPayload = InPixelData->GetPayload<UE::MovieGraph::FMovieGraphSampleState>();
TArray<FPixelPreProcessor> PixelPreProcessors;
// Run OCIO before quantizing. For highest accuracy, OCIO should run on the non-quantized pixel data.
#if WITH_OCIO
if (FMovieGraphOCIOHelper::GenerateOcioPixelPreProcessor(GraphPayload, CachedPipeline.Get(), InEvaluatedConfig, EvaluatedNode->OCIOConfiguration, EvaluatedNode->OCIOContext, PixelPreProcessors))
{
PixelPreProcessors.Pop()(InPixelData);
}
#endif
constexpr int32 TargetBitDepth = 8;
const bool bConvertToSrgb = !CodecWriter->bSkipColorConversions;
const TUniquePtr<FImagePixelData> QuantizedPixelData = UE::MoviePipeline::QuantizeImagePixelDataToBitDepth(InPixelData, TargetBitDepth, nullptr, bConvertToSrgb);
// Do a quick composite of renders/burn-ins.
for (const FMovieGraphPassData& CompositePass : InCompositePasses)
{
// We don't need to copy the data here (even though it's being passed to a async system) because we already made a unique copy of the
// burn in/widget data when we decided to composite it.
switch (QuantizedPixelData->GetType())
{
case EImagePixelType::Color:
PixelPreProcessors.Add(TAsyncCompositeImage<FColor>(CompositePass.Value->MoveImageDataToNew()));
break;
case EImagePixelType::Float16:
PixelPreProcessors.Add(TAsyncCompositeImage<FFloat16Color>(CompositePass.Value->MoveImageDataToNew()));
break;
case EImagePixelType::Float32:
PixelPreProcessors.Add(TAsyncCompositeImage<FLinearColor>(CompositePass.Value->MoveImageDataToNew()));
break;
}
}
// This is done on the current thread for simplicity but the composite itself is parallelized.
FImagePixelData* PixelData = QuantizedPixelData.Get();
for (const FPixelPreProcessor& PreProcessor : PixelPreProcessors)
{
// PreProcessors are assumed to be valid.
PreProcessor(PixelData);
}
const void* Data = nullptr;
int64 DataSize;
QuantizedPixelData->GetRawData(Data, DataSize);
// WriteFrame expects Rec 709 8-bit data.
CodecWriter->Writer->WriteFrame((uint8*)Data);
}
void UMovieGraphMP4EncoderNode::BeginFinalize_EncodeThread(MovieRenderGraph::IVideoCodecWriter* InWriter)
{
// If the writer was not initialized, don't try to finalize anything.
FMP4CodecWriter* CodecWriter = static_cast<FMP4CodecWriter*>(InWriter);
if (!CodecWriter->Writer)
{
return;
}
if (!CodecWriter->Writer->IsInitialized())
{
return;
}
// Nothing to do here if audio isn't being generated. The "invalid shot index" warning below is legitimate *if audio is being rendered*, but if
// no audio is being rendered (eg, with -nosound) then we don't want the warning to show up.
if (!FApp::CanEverRenderAudio())
{
return;
}
const MoviePipeline::FAudioState& AudioData = CachedPipeline->GetAudioRendererInstance()->GetAudioState();
for (const TPair<int32, MovieRenderGraph::IVideoCodecWriter::FLightweightSourceData>& SourceData : CodecWriter->LightweightSourceData)
{
if (!AudioData.FinishedSegments.IsValidIndex(SourceData.Key))
{
UE_LOG(LogMovieRenderPipeline, Warning, TEXT("Invalid shot index was requested for audio data, skipping audio writes."));
continue;
}
// Look up the audio segment for this shot
const MoviePipeline::FAudioState::FAudioSegment& AudioSegment = AudioData.FinishedSegments[SourceData.Key];
// Audio data isn't very sample accurate at this point, so we may have generated slightly more (or less) audio than we expect for
// the number of frames, so we're actually going to trim down the view of data we provide to match the number of frames rendered,
// to avoid any excess audio after the end of the video.
const int32 NumFrames = SourceData.Value.SubmittedFrameCount;
// ToDo: This is possibly dropping fractions of a sample (ie: 1/48,000th) if the audio sample rate can't be evenly divisible by
// the frame rate.
const int32 SamplesPerFrame = AudioSegment.SampleRate * CodecWriter->Writer->GetOptions().FrameRate.AsInterval();
int32 ExpectedSampleCount = NumFrames * SamplesPerFrame * AudioSegment.NumChannels;
ExpectedSampleCount = FMath::Min(ExpectedSampleCount, AudioSegment.SegmentData.Num());
Audio::TSampleBuffer<int16> SampleBuffer = Audio::TSampleBuffer<int16>(AudioSegment.SegmentData.GetData(), ExpectedSampleCount, AudioSegment.NumChannels, AudioSegment.SampleRate);
const TArrayView<int16> SampleData = SampleBuffer.GetArrayView();
CodecWriter->Writer->WriteAudioSample(SampleData);
}
}
void UMovieGraphMP4EncoderNode::Finalize_EncodeThread(MovieRenderGraph::IVideoCodecWriter* InWriter)
{
// Write to disk
const FMP4CodecWriter* CodecWriter = static_cast<FMP4CodecWriter*>(InWriter);
if(!CodecWriter->Writer)
{
return;
}
CodecWriter->Writer->Finalize();
}
const TCHAR* UMovieGraphMP4EncoderNode::GetFilenameExtension() const
{
return TEXT("mp4");
}
bool UMovieGraphMP4EncoderNode::IsAudioSupported() const
{
return true;
}
void UMovieGraphMP4EncoderNode::UpdateTelemetry(FMoviePipelineShotRenderTelemetry* InTelemetry) const
{
InTelemetry->bUsesMP4 = true;
}
void UMovieGraphMP4EncoderNode::InjectNodesPostEvaluation(const FName& InBranchName, UMovieGraphEvaluatedConfig* InEvaluatedConfig, TArray<UMovieGraphSettingNode*>& OutInjectedNodes)
{
if (bEnableBurnIn)
{
UMovieGraphOutputBurnInNode* BurnInNode = NewObject<UMovieGraphOutputBurnInNode>(InEvaluatedConfig);
BurnInNode->bOverride_OutputName = true;
BurnInNode->bOverride_FileNameFormat = true;
BurnInNode->bOverride_OutputRestriction = true;
BurnInNode->bOverride_LayerNameFormat = true;
BurnInNode->bOverride_BurnInClass = true;
BurnInNode->bOverride_bCompositeOntoFinalImage = true;
BurnInNode->OutputName = TEXT("MP4");
BurnInNode->FileNameFormat = BurnInFileNameFormat;
BurnInNode->OutputRestriction = FSoftClassPath(GetClass());
BurnInNode->LayerNameFormat = TEXT(""); // Unused
BurnInNode->BurnInClass = BurnInClass;
BurnInNode->bCompositeOntoFinalImage = bCompositeOntoFinalImage;
OutInjectedNodes.Add(BurnInNode);
}
}