// 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 UMovieGraphMP4EncoderNode::Initialize_GameThread(const FMovieGraphVideoNodeInitializationContext& InInitializationContext) { bool bIncludeCDOs = true; constexpr bool bExactMatch = true; UMovieGraphGlobalOutputSettingNode* OutputSetting = InInitializationContext.EvaluatedConfig->GetSettingForBranch(GlobalsPinName, bIncludeCDOs, bExactMatch); bIncludeCDOs = false; const UMovieGraphMP4EncoderNode* EvaluatedNode = Cast( 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 NewWriter = MakeUnique(); NewWriter->Writer = MakeUnique(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(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&& InCompositePasses, TObjectPtr InEvaluatedConfig, const FString& InBranchName) { FMP4CodecWriter* CodecWriter = static_cast(InWriter); if (!CodecWriter->Writer) { return; } bool bIncludeCDOs = false; constexpr bool bExactMatch = true; const UMovieGraphMP4EncoderNode* EvaluatedNode = Cast( 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(GlobalsPinName, bIncludeCDOs, bExactMatch); const UE::MovieGraph::FMovieGraphSampleState* GraphPayload = InPixelData->GetPayload(); TArray 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 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(CompositePass.Value->MoveImageDataToNew())); break; case EImagePixelType::Float16: PixelPreProcessors.Add(TAsyncCompositeImage(CompositePass.Value->MoveImageDataToNew())); break; case EImagePixelType::Float32: PixelPreProcessors.Add(TAsyncCompositeImage(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(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& 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 SampleBuffer = Audio::TSampleBuffer(AudioSegment.SegmentData.GetData(), ExpectedSampleCount, AudioSegment.NumChannels, AudioSegment.SampleRate); const TArrayView SampleData = SampleBuffer.GetArrayView(); CodecWriter->Writer->WriteAudioSample(SampleData); } } void UMovieGraphMP4EncoderNode::Finalize_EncodeThread(MovieRenderGraph::IVideoCodecWriter* InWriter) { // Write to disk const FMP4CodecWriter* CodecWriter = static_cast(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& OutInjectedNodes) { if (bEnableBurnIn) { UMovieGraphOutputBurnInNode* BurnInNode = NewObject(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); } }