// Copyright Epic Games, Inc. All Rights Reserved. #include "MovieSceneCaptureDialogModule.h" #include "Dom/JsonValue.h" #include "Dom/JsonObject.h" #include "Layout/Visibility.h" #include "Misc/Attribute.h" #include "Layout/Margin.h" #include "GenericPlatform/GenericApplication.h" #include "Input/Reply.h" #include "UObject/GCObject.h" #include "Widgets/SWidget.h" #include "Misc/Paths.h" #include "Widgets/DeclarativeSyntaxSupport.h" #include "Widgets/SCompoundWidget.h" #include "Widgets/SBoxPanel.h" #include "Styling/CoreStyle.h" #include "Widgets/SWindow.h" #include "Widgets/Text/STextBlock.h" #include "Framework/Docking/TabManager.h" #include "Widgets/Input/NumericTypeInterface.h" #include "FrameNumberDetailsCustomization.h" #include "HAL/IConsoleManager.h" #include "HAL/PlatformProcess.h" #include "HAL/PlatformFileManager.h" #include "HAL/FileManager.h" #include "Misc/CommandLine.h" #include "Misc/StringBuilder.h" #include "Misc/FileHelper.h" #include "Misc/App.h" #include "Serialization/ObjectWriter.h" #include "Serialization/ObjectReader.h" #include "Templates/SubclassOf.h" #include "Engine/GameViewportClient.h" #include "GameFramework/GameModeBase.h" #include "Slate/SceneViewport.h" #include "MovieSceneCapture.h" #include "Serialization/JsonSerializer.h" #include "Framework/Application/SlateApplication.h" #include "Widgets/Layout/SBorder.h" #include "Widgets/Input/SButton.h" #include "Settings/LevelEditorPlaySettings.h" #include "Editor.h" #include "Widgets/Input/SHyperlink.h" #include "Widgets/Images/SThrobber.h" #include "Framework/Notifications/NotificationManager.h" #include "Widgets/Notifications/SNotificationList.h" #include "AudioDevice.h" #include "Widgets/Docking/SDockTab.h" #include "JsonObjectConverter.h" #include "Widgets/Notifications/INotificationWidget.h" #include "PropertyEditorModule.h" #include "IDetailsView.h" #include "FileHelpers.h" #include "ISessionManager.h" #include "ISessionServicesModule.h" #include "ErrorCodes.h" #include "GameFramework/WorldSettings.h" #include "FrameNumberNumericInterface.h" #include "Protocols/AudioCaptureProtocol.h" #define LOCTEXT_NAMESPACE "MovieSceneCaptureDialog" const TCHAR* MovieCaptureSessionName = TEXT("Movie Scene Capture"); DECLARE_DELEGATE_RetVal_OneParam(FText, FOnStartCapture, UMovieSceneCapture*); class SRenderMovieSceneSettings : public SCompoundWidget, public FGCObject { SLATE_BEGIN_ARGS(SRenderMovieSceneSettings) : _InitialObject(nullptr) {} SLATE_EVENT(FOnStartCapture, OnStartCapture) SLATE_ARGUMENT(UMovieSceneCapture*, InitialObject) SLATE_ARGUMENT(TSharedPtr>, NumericTypeInterface) SLATE_END_ARGS() void Construct(const FArguments& InArgs) { FPropertyEditorModule& PropertyEditor = FModuleManager::LoadModuleChecked("PropertyEditor"); FDetailsViewArgs DetailsViewArgs; DetailsViewArgs.bUpdatesFromSelection = false; DetailsViewArgs.bLockable = false; DetailsViewArgs.NameAreaSettings = FDetailsViewArgs::HideNameArea; DetailsViewArgs.ViewIdentifier = "RenderMovieScene"; DetailView = PropertyEditor.CreateDetailView(DetailsViewArgs); DetailView->RegisterInstancedCustomPropertyTypeLayout("FrameNumber", FOnGetPropertyTypeCustomizationInstance::CreateStatic(&FFrameNumberDetailsCustomization::MakeInstance, InArgs._NumericTypeInterface)); OnStartCapture = InArgs._OnStartCapture; ChildSlot [ SNew(SVerticalBox) + SVerticalBox::Slot() [ DetailView.ToSharedRef() ] + SVerticalBox::Slot() .AutoHeight() [ SAssignNew(ErrorText, STextBlock) .Visibility(EVisibility::Hidden) ] + SVerticalBox::Slot() .AutoHeight() .HAlign(HAlign_Right) .Padding(5.f) [ SNew(SButton) .IsEnabled(this, &SRenderMovieSceneSettings::CanStartCapture) .ContentPadding(FMargin(10, 5)) .Text(this, &SRenderMovieSceneSettings::GetStartCaptureText) .OnClicked(this, &SRenderMovieSceneSettings::OnStartClicked) ] ]; MovieSceneCapture = nullptr; if (InArgs._InitialObject) { SetObject(InArgs._InitialObject); } } void SetObject(UMovieSceneCapture* InMovieSceneCapture) { MovieSceneCapture = InMovieSceneCapture; DetailView->SetObject(InMovieSceneCapture); ErrorText->SetText(FText()); ErrorText->SetVisibility(EVisibility::Hidden); } virtual void AddReferencedObjects( FReferenceCollector& Collector ) override { Collector.AddReferencedObject(MovieSceneCapture); } virtual FString GetReferencerName() const override { return TEXT("SRenderMovieSceneSettings"); } private: FReply OnStartClicked() { FText Error; if (OnStartCapture.IsBound()) { Error = OnStartCapture.Execute(MovieSceneCapture); } ErrorText->SetText(Error); ErrorText->SetVisibility(Error.IsEmpty() ? EVisibility::Hidden : EVisibility::Visible); return FReply::Handled(); } FText GetStartCaptureText() const { if (MovieSceneCapture && !MovieSceneCapture->bUseSeparateProcess) { for (const FWorldContext& Context : GEngine->GetWorldContexts()) { if (Context.WorldType == EWorldType::PIE) { return LOCTEXT("ExportExitPIE", "(Exit PIE to start)"); } } } return LOCTEXT("Export", "Capture Movie"); } bool CanStartCapture() const { if (!MovieSceneCapture) { return false; } else if (MovieSceneCapture->bUseSeparateProcess) { return true; } for (const FWorldContext& Context : GEngine->GetWorldContexts()) { if (Context.WorldType == EWorldType::PIE) { return false; } } return true; } TSharedPtr DetailView; TSharedPtr ErrorText; FOnStartCapture OnStartCapture; TObjectPtr MovieSceneCapture; }; DECLARE_DELEGATE_OneParam(FOnCaptureFinished, bool /*bCancelled*/); class SCaptureMovieNotification : public SCompoundWidget, public INotificationWidget { public: SLATE_BEGIN_ARGS(SCaptureMovieNotification){} SLATE_ATTRIBUTE(FCaptureState, CaptureState) SLATE_EVENT(FOnCaptureFinished, OnCaptureFinished) SLATE_EVENT(FSimpleDelegate, OnCancel) SLATE_ARGUMENT(FString, CapturePath) SLATE_END_ARGS() void Construct(const FArguments& InArgs) { CaptureState = InArgs._CaptureState; OnCaptureFinished = InArgs._OnCaptureFinished; OnCancel = InArgs._OnCancel; CachedState = FCaptureState(ECaptureStatus::Pending); FString CapturePath = FPaths::ConvertRelativePathToFull(InArgs._CapturePath); CapturePath.RemoveFromEnd(TEXT("\\")); auto OnBrowseToFolder = [=]{ FString TrimmedPath; if (CapturePath.Split(TEXT("{"), &TrimmedPath, nullptr)) { FPaths::NormalizeDirectoryName(TrimmedPath); FPlatformProcess::ExploreFolder(*TrimmedPath); } else { FPlatformProcess::ExploreFolder(*CapturePath); } }; ChildSlot [ SNew(SBox) .Padding(FMargin(15.0f)) [ SNew(SVerticalBox) + SVerticalBox::Slot() .Padding(FMargin(0,0,0,5.0f)) .HAlign(HAlign_Right) .AutoHeight() [ SNew(SHorizontalBox) + SHorizontalBox::Slot() .VAlign(VAlign_Center) [ SAssignNew(TextBlock, STextBlock) .Font(FCoreStyle::Get().GetFontStyle(TEXT("NotificationList.FontBold"))) .Text(LOCTEXT("RenderingVideo", "Capturing video")) ] + SHorizontalBox::Slot() .AutoWidth() .Padding(FMargin(15.f,0,0,0)) [ SAssignNew(Throbber, SThrobber) ] ] + SVerticalBox::Slot() .AutoHeight() .Padding(FMargin(0,0,0,5.0f)) .HAlign(HAlign_Right) [ SAssignNew(DetailedTextBlock, STextBlock) .Visibility(EVisibility::Collapsed) .Font(FCoreStyle::Get().GetFontStyle(TEXT("NotificationList.FontLight"))) ] + SVerticalBox::Slot() .AutoHeight() .HAlign(HAlign_Right) [ SNew(SHorizontalBox) + SHorizontalBox::Slot() .AutoWidth() .VAlign(VAlign_Center) [ SAssignNew(Hyperlink, SHyperlink) .Text(LOCTEXT("OpenFolder", "Open Capture Folder...")) .OnNavigate_Lambda(OnBrowseToFolder) ] + SHorizontalBox::Slot() .AutoWidth() .Padding(FMargin(5.0f,0,0,0)) .VAlign(VAlign_Center) [ SAssignNew(Button, SButton) .Text(LOCTEXT("StopButton", "Stop Capture")) .OnClicked(this, &SCaptureMovieNotification::ButtonClicked) ] ] ] ]; } virtual void Tick( const FGeometry& AllottedGeometry, const double InCurrentTime, const float InDeltaTime ) { if (State != SNotificationItem::CS_Pending) { return; } FCaptureState StateThisFrame = CaptureState.Get(); if (CachedState.Status != StateThisFrame.Status) { CachedState = StateThisFrame; if (CachedState.Status == ECaptureStatus::Success) { TextBlock->SetText(LOCTEXT("CaptureFinished", "Capture Finished")); OnCaptureFinished.ExecuteIfBound(true); } else if (CachedState.Status == ECaptureStatus::Failure) { TextBlock->SetText(LOCTEXT("CaptureFailed", "Capture Failed")); FText DetailText = CachedState.GetDetailText(); if (!DetailText.IsEmpty()) { DetailedTextBlock->SetText(DetailText); DetailedTextBlock->SetVisibility(EVisibility::Visible); } OnCaptureFinished.ExecuteIfBound(false); } else { ensureMsgf(false, TEXT("Cannot move from a finished to a pending state.")); } } } virtual void OnSetCompletionState(SNotificationItem::ECompletionState InState) { State = InState; if (State != SNotificationItem::CS_Pending) { Throbber->SetVisibility(EVisibility::Collapsed); Button->SetVisibility(EVisibility::Collapsed); } } virtual TSharedRef< SWidget > AsWidget() { return AsShared(); } private: FReply ButtonClicked() { if (State == SNotificationItem::CS_Pending) { OnCancel.ExecuteIfBound(); } return FReply::Handled(); } private: TSharedPtr Button, Throbber, Hyperlink; TSharedPtr TextBlock; TSharedPtr DetailedTextBlock; SNotificationItem::ECompletionState State; FSimpleDelegate OnCancel; FCaptureState CachedState; TAttribute CaptureState; FOnCaptureFinished OnCaptureFinished; }; void FInEditorCapture::Start() { ULevelEditorPlaySettings* PlayInEditorSettings = GetMutableDefault(); bScreenMessagesWereEnabled = GAreScreenMessagesEnabled; GAreScreenMessagesEnabled = false; if (!CaptureObject->Settings.bEnableTextureStreaming) { const int32 UndefinedTexturePoolSize = -1; IConsoleVariable* CVarStreamingPoolSize = IConsoleManager::Get().FindConsoleVariable(TEXT("r.Streaming.PoolSize")); if (CVarStreamingPoolSize) { BackedUpStreamingPoolSize = CVarStreamingPoolSize->GetInt(); CVarStreamingPoolSize->Set(UndefinedTexturePoolSize, ECVF_SetByConsole); } IConsoleVariable* CVarUseFixedPoolSize = IConsoleManager::Get().FindConsoleVariable(TEXT("r.Streaming.UseFixedPoolSize")); if (CVarUseFixedPoolSize) { BackedUpUseFixedPoolSize = CVarUseFixedPoolSize->GetInt(); CVarUseFixedPoolSize->Set(0, ECVF_SetByConsole); } IConsoleVariable* CVarTextureStreaming = IConsoleManager::Get().FindConsoleVariable(TEXT("r.TextureStreaming")); if (CVarTextureStreaming) { BackedUpTextureStreaming = CVarTextureStreaming->GetInt(); CVarTextureStreaming->Set(0, ECVF_SetByConsole); } } FObjectWriter(PlayInEditorSettings, BackedUpPlaySettings); OverridePlaySettings(PlayInEditorSettings); CaptureObject->AddToRoot(); CaptureObject->OnCaptureFinished().AddRaw(this, &FInEditorCapture::OnLevelSequenceFinished); UGameViewportClient::OnViewportCreated().AddRaw(this, &FInEditorCapture::OnPIEViewportStarted); FEditorDelegates::EndPIE.AddRaw(this, &FInEditorCapture::OnEndPIE); FAudioDeviceHandle AudioDevice = GEngine->GetMainAudioDevice(); if (AudioDevice) { TransientPrimaryVolume = AudioDevice->GetTransientPrimaryVolume(); AudioDevice->SetTransientPrimaryVolume(0.0f); } TSharedRef CustomWindow = SNew(SWindow) .Title(LOCTEXT("MovieRenderPreviewTitle", "Movie Render - Preview")) .AutoCenter(EAutoCenter::PrimaryWorkArea) .UseOSWindowBorder(true) .FocusWhenFirstShown(false) .ActivationPolicy(EWindowActivationPolicy::Never) .HasCloseButton(true) .SupportsMaximize(false) .SupportsMinimize(true) .MaxWidth(CaptureObject->GetSettings().Resolution.ResX) .MaxHeight(CaptureObject->GetSettings().Resolution.ResY) .SizingRule(ESizingRule::FixedSize); FSlateApplication::Get().AddWindow(CustomWindow); FRequestPlaySessionParams Params; Params.EditorPlaySettings = PlayInEditorSettings; Params.CustomPIEWindow = CustomWindow; GEditor->RequestPlaySession(Params); } void FInEditorCapture::Cancel() { // If the user cancels through the UI then we request that the editor shut down the PIE instance. // We capture the PIE shutdown request (which calls OnEndPIE) and further process it. This unifies // closing PIE via the close button and the UI into one code path. GEditor->RequestEndPlayMap(); } void FInEditorCapture::OverridePlaySettings(ULevelEditorPlaySettings* PlayInEditorSettings) { const FMovieSceneCaptureSettings& Settings = CaptureObject->GetSettings(); PlayInEditorSettings->NewWindowWidth = Settings.Resolution.ResX; PlayInEditorSettings->NewWindowHeight = Settings.Resolution.ResY; PlayInEditorSettings->CenterNewWindow = false; PlayInEditorSettings->NewWindowPosition = FIntPoint::NoneValue; // It will center PIE to the middle of the screen the first time it is run (until the user drag the window somewhere else) PlayInEditorSettings->LastExecutedPlayModeType = EPlayModeType::PlayMode_InEditorFloating; // Reset everything else PlayInEditorSettings->GameGetsMouseControl = false; PlayInEditorSettings->ShowMouseControlLabel = false; PlayInEditorSettings->ViewportGetsHMDControl = false; PlayInEditorSettings->ShouldMinimizeEditorOnVRPIE = true; PlayInEditorSettings->EnableGameSound = CaptureObject->AudioCaptureProtocolType != UNullAudioCaptureProtocol::StaticClass(); PlayInEditorSettings->bOnlyLoadVisibleLevelsInPIE = false; PlayInEditorSettings->bPreferToStreamLevelsInPIE = false; PlayInEditorSettings->PIEAlwaysOnTop = false; PlayInEditorSettings->DisableStandaloneSound = false; PlayInEditorSettings->AdditionalLaunchParameters = TEXT(""); PlayInEditorSettings->BuildGameBeforeLaunch = EPlayOnBuildMode::PlayOnBuild_Never; PlayInEditorSettings->LaunchConfiguration = EPlayOnLaunchConfiguration::LaunchConfig_Default; PlayInEditorSettings->PackFilesForLaunch = EPlayOnPakFileMode::NoPak; PlayInEditorSettings->SetPlayNetMode(EPlayNetMode::PIE_Standalone); PlayInEditorSettings->SetRunUnderOneProcess(true); PlayInEditorSettings->bLaunchSeparateServer = false; PlayInEditorSettings->SetPlayNumberOfClients(1); } void FInEditorCapture::OnPIEViewportStarted() { for (const FWorldContext& Context : GEngine->GetWorldContexts()) { if (Context.WorldType == EWorldType::PIE) { FSlatePlayInEditorInfo* SlatePlayInEditorSession = GEditor->SlatePlayInEditorMap.Find(Context.ContextHandle); if (SlatePlayInEditorSession) { CapturingFromWorld = Context.World(); TSharedPtr Window = SlatePlayInEditorSession->SlatePlayInEditorWindow.Pin(); const FMovieSceneCaptureSettings& Settings = CaptureObject->GetSettings(); SlatePlayInEditorSession->SlatePlayInEditorWindowViewport->SetViewportSize(Settings.Resolution.ResX,Settings.Resolution.ResY); FVector2D PreviewWindowSize(Settings.Resolution.ResX, Settings.Resolution.ResY); // Keep scaling down the window size while we're bigger than half the desktop width/height { FDisplayMetrics DisplayMetrics; FSlateApplication::Get().GetCachedDisplayMetrics(DisplayMetrics); while(PreviewWindowSize.X >= DisplayMetrics.PrimaryDisplayWidth*.5f || PreviewWindowSize.Y >= DisplayMetrics.PrimaryDisplayHeight*.5f) { PreviewWindowSize *= .5f; } } // Resize and move the window into the desktop a bit FVector2D PreviewWindowPosition(50, 50); Window->ReshapeWindow(PreviewWindowPosition, PreviewWindowSize); if (CaptureObject->Settings.GameModeOverride != nullptr) { CachedGameMode = CapturingFromWorld->GetWorldSettings()->DefaultGameMode; CapturingFromWorld->GetWorldSettings()->DefaultGameMode = CaptureObject->Settings.GameModeOverride; } CachedEngineShowFlags = SlatePlayInEditorSession->SlatePlayInEditorWindowViewport->GetClient()->GetEngineShowFlags(); if (CachedEngineShowFlags && Settings.bUsePathTracer) { CachedPathTracingMode = CachedEngineShowFlags->PathTracing; CachedEngineShowFlags->SetPathTracing(true); } CaptureObject->Initialize(SlatePlayInEditorSession->SlatePlayInEditorWindowViewport, Context.PIEInstance); OnCaptureStarted(); } return; } } UE_LOG(LogTemp, Warning, TEXT("Received PIE Creation callback but failed to find PIE World or missing FSlatePlayInEditorInfo for world.")); } void FInEditorCapture::Shutdown() { FEditorDelegates::EndPIE.RemoveAll(this); UGameViewportClient::OnViewportCreated().RemoveAll(this); CaptureObject->OnCaptureFinished().RemoveAll(this); GAreScreenMessagesEnabled = bScreenMessagesWereEnabled; if (!CaptureObject->Settings.bEnableTextureStreaming) { IConsoleVariable* CVarStreamingPoolSize = IConsoleManager::Get().FindConsoleVariable(TEXT("r.Streaming.PoolSize")); if (CVarStreamingPoolSize) { CVarStreamingPoolSize->Set(BackedUpStreamingPoolSize, ECVF_SetByConsole); } IConsoleVariable* CVarUseFixedPoolSize = IConsoleManager::Get().FindConsoleVariable(TEXT("r.Streaming.UseFixedPoolSize")); if (CVarUseFixedPoolSize) { CVarUseFixedPoolSize->Set(BackedUpUseFixedPoolSize, ECVF_SetByConsole); } IConsoleVariable* CVarTextureStreaming = IConsoleManager::Get().FindConsoleVariable(TEXT("r.TextureStreaming")); if (CVarTextureStreaming) { CVarTextureStreaming->Set(BackedUpTextureStreaming, ECVF_SetByConsole); } } if (CaptureObject->Settings.GameModeOverride != nullptr && CapturingFromWorld != nullptr) { CapturingFromWorld->GetWorldSettings()->DefaultGameMode = CachedGameMode; } if (CachedEngineShowFlags) { CachedEngineShowFlags->SetPathTracing(CachedPathTracingMode); } FObjectReader(GetMutableDefault(), BackedUpPlaySettings); FAudioDeviceHandle AudioDevice = GEngine->GetMainAudioDevice(); if (AudioDevice) { AudioDevice->SetTransientPrimaryVolume(TransientPrimaryVolume); } CaptureObject->Close(); CaptureObject->RemoveFromRoot(); } void FInEditorCapture::OnEndPIE(bool bIsSimulating) { Shutdown(); } void FInEditorCapture::OnLevelSequenceFinished() { Shutdown(); GEditor->RequestEndPlayMap(); } void FInEditorCapture::OnCaptureStarted() { FString CapturePath = CaptureObject->ResolveFileFormat(CaptureObject->Settings.OutputDirectory.Path, FFrameMetrics()); FNotificationInfo Info ( SNew(SCaptureMovieNotification) .CaptureState_Raw(this, &FInEditorCapture::GetCaptureState) .CapturePath(CapturePath) .OnCaptureFinished_Raw(this, &FInEditorCapture::OnCaptureFinished) .OnCancel_Raw(this, &FInEditorCapture::Cancel) ); Info.bFireAndForget = false; Info.ExpireDuration = 5.f; InProgressCaptureNotification = FSlateNotificationManager::Get().AddNotification(Info); InProgressCaptureNotification->SetCompletionState(SNotificationItem::CS_Pending); } FCaptureState FInEditorCapture::GetCaptureState() const { for (const FWorldContext& Context : GEngine->GetWorldContexts()) { if (Context.WorldType == EWorldType::PIE) { return FCaptureState(ECaptureStatus::Pending); } } return FCaptureState(ECaptureStatus::Success); } void FMovieSceneCaptureBase::OnCaptureFinished(bool bSuccess) { if (bSuccess) { InProgressCaptureNotification->SetCompletionState(SNotificationItem::CS_Success); } else { UE_LOG(LogTemp, Error, TEXT("MovieSceneCapture failed to capture.")); InProgressCaptureNotification->SetCompletionState(SNotificationItem::CS_Fail); } InProgressCaptureNotification->ExpireAndFadeout(); InProgressCaptureNotification = nullptr; if (OnFinishedCallback) { OnFinishedCallback(bSuccess); } } void FNewProcessCapture::Start() { // Save out the capture manifest to json FString Filename = FPaths::ProjectSavedDir() / TEXT("MovieSceneCapture/Manifest.json"); TSharedRef Object = MakeShareable(new FJsonObject); if (FJsonObjectConverter::UStructToJsonObject(CaptureObject->GetClass(), CaptureObject, Object, 0, 0)) { TSharedRef RootObject = MakeShareable(new FJsonObject); RootObject->SetField(TEXT("Type"), MakeShareable(new FJsonValueString(CaptureObject->GetClass()->GetPathName()))); RootObject->SetField(TEXT("Data"), MakeShareable(new FJsonValueObject(Object))); TSharedRef AdditionalJson = MakeShareable(new FJsonObject); CaptureObject->SerializeJson(*AdditionalJson); RootObject->SetField(TEXT("AdditionalData"), MakeShareable(new FJsonValueObject(AdditionalJson))); FString Json; TSharedRef > JsonWriter = TJsonWriterFactory<>::Create(&Json, 0); if (FJsonSerializer::Serialize(RootObject, JsonWriter)) { FFileHelper::SaveStringToFile(Json, *Filename); } } else { return; } FString EditorCommandLine = FString::Printf(TEXT("%s -MovieSceneCaptureManifest=\"%s\" -game -NoLoadingScreen -ForceRes -Windowed"), *MapNameToLoad, *Filename); // Spit out any additional, user-supplied command line args if (!CaptureObject->AdditionalCommandLineArguments.IsEmpty()) { EditorCommandLine.AppendChar(' '); EditorCommandLine.Append(CaptureObject->AdditionalCommandLineArguments); } // Spit out any inherited command line args if (!CaptureObject->InheritedCommandLineArguments.IsEmpty()) { EditorCommandLine.AppendChar(' '); EditorCommandLine.Append(CaptureObject->InheritedCommandLineArguments); } // Disable texture streaming if necessary if (!CaptureObject->Settings.bEnableTextureStreaming) { EditorCommandLine.Append(TEXT(" -NoTextureStreaming")); } // Set the game resolution - we always want it windowed EditorCommandLine += FString::Printf(TEXT(" -ResX=%d -ResY=%d -Windowed"), CaptureObject->Settings.Resolution.ResX, CaptureObject->Settings.Resolution.ResY); // Ensure game session is correctly set up EditorCommandLine += FString::Printf(TEXT(" -messaging -SessionName=\"%s\""), MovieCaptureSessionName); TStringBuilder<64> SubprocessCommandLine; FCommandLine::BuildSubprocessCommandLine(ECommandLineArgumentFlags::GameContexts, false /*bOnlyInherited*/, SubprocessCommandLine); FString Params; if (FPaths::IsProjectFilePathSet()) { Params = FString::Printf(TEXT("\"%s\" %s %s"), *FPaths::GetProjectFilePath(), *EditorCommandLine, *SubprocessCommandLine); } else { Params = FString::Printf(TEXT("%s %s %s"), FApp::GetProjectName(), *EditorCommandLine, *SubprocessCommandLine); } FString GamePath = FPlatformProcess::GenerateApplicationPath(FApp::GetName(), FApp::GetBuildConfiguration()); FProcHandle ProcessHandle = FPlatformProcess::CreateProc(*GamePath, *Params, true, false, false, nullptr, 0, nullptr, nullptr); if (ProcessHandle.IsValid()) { if (CaptureObject->bCloseEditorWhenCaptureStarts) { FPlatformMisc::RequestExit(false); return; } SharedProcHandle = MakeShareable(new FProcHandle(ProcessHandle)); OnCaptureStarted(); } else { OnCaptureFinished(false); } } void FNewProcessCapture::Cancel() { // If they cancel the capture via the UI we need to try and find a running session with the right name bool bFoundInstance = false; // Attempt to send a remote command to gracefully terminate the process ISessionServicesModule& SessionServices = FModuleManager::Get().LoadModuleChecked("SessionServices"); TSharedPtr SessionManager = SessionServices.GetSessionManager(); TArray> Sessions; if (SessionManager.IsValid()) { SessionManager->GetSessions(Sessions); } for (const TSharedPtr& Session : Sessions) { if (Session->GetSessionName() == MovieCaptureSessionName) { TArray> Instances; Session->GetInstances(Instances); for (const TSharedPtr& Instance : Instances) { Instance->ExecuteCommand("exit"); bFoundInstance = true; } } } if (!bFoundInstance) { FPlatformProcess::TerminateProc(*SharedProcHandle); } } void FNewProcessCapture::OnCaptureStarted() { FNotificationInfo Info ( SNew(SCaptureMovieNotification) .CaptureState_Raw(this, &FNewProcessCapture::GetCaptureState) .CapturePath(CaptureObject->Settings.OutputDirectory.Path) .OnCaptureFinished_Raw(this, &FNewProcessCapture::OnCaptureFinished) .OnCancel_Raw(this, &FNewProcessCapture::Cancel) ); Info.bFireAndForget = false; Info.ExpireDuration = 5.f; InProgressCaptureNotification = FSlateNotificationManager::Get().AddNotification(Info); InProgressCaptureNotification->SetCompletionState(SNotificationItem::CS_Pending); } FCaptureState FNewProcessCapture::GetCaptureState() const { if (!FPlatformProcess::IsProcRunning(*SharedProcHandle)) { int32 RetCode = 0; FPlatformProcess::GetProcReturnCode(*SharedProcHandle, &RetCode); return FCaptureState(RetCode); } else { return FCaptureState(ECaptureStatus::Pending); } } class FMovieSceneCaptureDialogModule : public IMovieSceneCaptureDialogModule { virtual UWorld* GetCurrentlyRecordingWorld() override { return CurrentCapture.IsValid() ? CurrentCapture->GetWorld() : nullptr; } virtual TSharedPtr GetCurrentCapture() const override { return CurrentCapture; } virtual void StartCapture(UMovieSceneCapture* InCaptureSettings) override { /** Called when the capture object finishes its capture (success or fail). */ auto OnCaptureFinished = [this](bool bSuccess) { UE_LOG(LogTemp, Log, TEXT("Movie Capture finished. Success: %d"), bSuccess); // CurrentCapture has to be null so that StartRecording can be called again which means we can't // broadcast this until after we've nulled out Current Capture. Thus we store the delegate FMovieSceneCaptureBase::FCaptureStateStopped Delegate = CurrentCapture->CaptureStoppedDelegate; CurrentCapture = nullptr; Delegate.Broadcast(bSuccess); }; if (InCaptureSettings->bUseSeparateProcess) { const FString WorldPackageName = GWorld->GetOutermost()->GetName(); FString MapNameToLoad = WorldPackageName; // Allow the game mode to be overridden if (InCaptureSettings->Settings.GameModeOverride != nullptr) { const FString GameModeName = InCaptureSettings->Settings.GameModeOverride->GetPathName(); MapNameToLoad += FString::Printf(TEXT("?game=%s"), *GameModeName); } CurrentCapture = MakeShared(InCaptureSettings, MapNameToLoad, OnCaptureFinished); } else { CurrentCapture = MakeShared(InCaptureSettings, OnCaptureFinished); } UE_LOG(LogTemp, Log, TEXT("Starting movie scene capture...")); CurrentCapture->Start(); } virtual void OpenDialog(const TSharedRef& TabManager, UMovieSceneCapture* CaptureObject, TSharedPtr> InNumericTypeInterface) override { // Ensure the session services module is loaded otherwise we won't necessarily receive status updates from the movie capture session FModuleManager::Get().LoadModuleChecked("SessionServices").GetSessionManager(); TSharedPtr ExistingWindow = CaptureSettingsWindow.Pin(); if (ExistingWindow.IsValid()) { ExistingWindow->BringToFront(); } else { ExistingWindow = SNew(SWindow) .Title( LOCTEXT("RenderMovieSettingsTitle", "Render Movie Settings") ) .HasCloseButton(true) .SupportsMaximize(false) .SupportsMinimize(false) .ClientSize(FVector2D(500, 700)); TSharedPtr OwnerTab = TabManager->GetOwnerTab(); TSharedPtr RootWindow = OwnerTab.IsValid() ? OwnerTab->GetParentWindow() : TSharedPtr(); if(RootWindow.IsValid()) { FSlateApplication::Get().AddWindowAsNativeChild(ExistingWindow.ToSharedRef(), RootWindow.ToSharedRef()); } else { FSlateApplication::Get().AddWindow(ExistingWindow.ToSharedRef()); } } ExistingWindow->SetContent( SNew(SRenderMovieSceneSettings) .InitialObject(CaptureObject) .NumericTypeInterface(InNumericTypeInterface) .OnStartCapture_Raw(this, &FMovieSceneCaptureDialogModule::OnUserRequestStartCapture) ); CaptureSettingsWindow = ExistingWindow; } FText OnUserRequestStartCapture(UMovieSceneCapture* CaptureObject) { if (CurrentCapture.IsValid()) { return LOCTEXT("AlreadyCapturing", "There is already a movie scene capture process open. Please close it and try again."); } FString OutputDirectory = CaptureObject->Settings.OutputDirectory.Path; FPaths::NormalizeFilename(OutputDirectory); // Only validate the directory if it doesn't contain any format specifiers if (!OutputDirectory.Contains(TEXT("{"))) { if (!IFileManager::Get().DirectoryExists(*OutputDirectory)) { if (!IFileManager::Get().MakeDirectory(*OutputDirectory)) { return FText::Format(LOCTEXT( "InvalidDirectory", "Invalid output directory: {0}"), FText::FromString(OutputDirectory) ); } } else if (IFileManager::Get().IsReadOnly(*OutputDirectory)) { return FText::Format(LOCTEXT( "ReadOnlyDirectory", "Read only output directory: {0}"), FText::FromString(OutputDirectory) ); } } // Prompt the user to save their changes so that they'll be in the movie, since we're not saving temporary copies of the level. bool bPromptUserToSave = true; bool bSaveMapPackages = true; bool bSaveContentPackages = true; if( !FEditorFileUtils::SaveDirtyPackages( bPromptUserToSave, bSaveMapPackages, bSaveContentPackages ) ) { return LOCTEXT( "UserCancelled", "Capturing was cancelled from the save dialog." ); } CaptureObject->SaveToConfig(); StartCapture(CaptureObject); // If we managed to get this far, we've done our best to start the capture and don't have a error to report at this time. return FText(); } private: TWeakPtr CaptureSettingsWindow; TSharedPtr CurrentCapture; }; FText FCaptureState::GetDetailText() const { switch (uint32(Code)) { case uint32(EMovieSceneCaptureExitCode::WorldNotFound): { return LOCTEXT("WorldNotFound", "Specified world does not exist. Did you forget to save it?"); } default: { return FText(); } } } IMPLEMENT_MODULE( FMovieSceneCaptureDialogModule, MovieSceneCaptureDialog ) #undef LOCTEXT_NAMESPACE