Add initial project files and implement basic audio-visual recording functionality

This commit is contained in:
John
2026-03-12 18:53:47 +05:30
parent 0327abb1a0
commit eaa678262c
10 changed files with 153 additions and 105 deletions

View File

@@ -1,8 +1,23 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#include "AudioVideoRecordGameMode.h"
#include "Kismet/GameplayStatics.h"
#include "Components/AudioComponent.h"
AAudioVideoRecordGameMode::AAudioVideoRecordGameMode()
{
// stub
}
void AAudioVideoRecordGameMode::BeginPlay()
{
Super::BeginPlay();
if (BGMSound)
{
BGMAudioComponent = UGameplayStatics::SpawnSound2D(this, BGMSound, BGMVolume);
if (BGMAudioComponent)
{
BGMAudioComponent->bAutoDestroy = false;
}
}
}

View File

@@ -6,6 +6,9 @@
#include "GameFramework/GameModeBase.h"
#include "AudioVideoRecordGameMode.generated.h"
class USoundBase;
class UAudioComponent;
/**
* Simple GameMode for a first person game
*/
@@ -16,6 +19,21 @@ class AAudioVideoRecordGameMode : public AGameModeBase
public:
AAudioVideoRecordGameMode();
/** Background music to play when the level starts. Assign in Blueprint. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Audio")
USoundBase* BGMSound = nullptr;
/** Volume multiplier for BGM (0.0 1.0). */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Audio", meta = (ClampMin = "0.0", ClampMax = "1.0"))
float BGMVolume = 0.5f;
protected:
virtual void BeginPlay() override;
private:
UPROPERTY()
UAudioComponent* BGMAudioComponent = nullptr;
};

View File

@@ -118,81 +118,17 @@ void USimpleRecorder::StartRecording()
InitOutputPaths();
bIsRecording = true;
// ─── 0. Auto-detect viewport size if not explicitly set ─────────
if (bAutoDetectResolution)
{
if (GEngine && GEngine->GameViewport)
{
FVector2D ViewportSize;
GEngine->GameViewport->GetViewportSize(ViewportSize);
if (ViewportSize.X > 0 && ViewportSize.Y > 0)
{
CaptureWidth = FMath::RoundToInt32(ViewportSize.X);
CaptureHeight = FMath::RoundToInt32(ViewportSize.Y);
// Ensure even dimensions (required by most video encoders)
CaptureWidth = CaptureWidth & ~1;
CaptureHeight = CaptureHeight & ~1;
}
}
}
bFFmpegStarted = false;
UE_LOG(LogAudioVideoRecord, Log, TEXT("SimpleRecorder: Starting recording..."));
UE_LOG(LogAudioVideoRecord, Log, TEXT(" Resolution: %dx%d @ %d FPS"), CaptureWidth, CaptureHeight, CaptureFPS);
UE_LOG(LogAudioVideoRecord, Log, TEXT(" Video → %s"), *VideoFilePath);
UE_LOG(LogAudioVideoRecord, Log, TEXT(" Audio → %s"), *AudioFilePath);
UE_LOG(LogAudioVideoRecord, Log, TEXT(" Final → %s"), *FinalFilePath);
UE_LOG(LogAudioVideoRecord, Log, TEXT(" FFmpeg pipe will start on first frame (actual back-buffer size)."));
// ─── 1. Open FFmpeg video pipe ──────────────────────────────────
{
FString FFmpeg = GetFFmpegExecutable();
// Build command:
// ffmpeg -y -f rawvideo -pix_fmt bgra
// -video_size WxH -framerate FPS
// -i - (stdin)
// -c:v <encoder> ... -b:v BITRATE
// output.mp4
//
// Encoder selection per platform:
// Mac: h264_videotoolbox (Apple HW encoder)
// Windows: h264_nvenc / h264_amf / h264_qsv (detected at runtime)
// Linux: libx264 (software fallback)
#if PLATFORM_MAC
FString EncoderFlags = FString::Printf(TEXT("-c:v h264_videotoolbox -b:v %s"), *VideoBitrate);
#elif PLATFORM_WINDOWS
FString EncoderFlags = FString::Printf(TEXT("-c:v h264_nvenc -preset p5 -b:v %s"), *VideoBitrate);
#else
FString EncoderFlags = FString::Printf(TEXT("-c:v libx264 -preset medium -b:v %s"), *VideoBitrate);
#endif
FString Cmd = FString::Printf(
TEXT("\"%s\" -y -f rawvideo -pix_fmt bgra -video_size %dx%d -framerate %d ")
TEXT("-i - %s \"%s\""),
*FFmpeg,
CaptureWidth, CaptureHeight, CaptureFPS,
*EncoderFlags,
*VideoFilePath
);
UE_LOG(LogAudioVideoRecord, Log, TEXT(" FFmpeg cmd: %s"), *Cmd);
// popen opens a pipe — we write raw BGRA bytes into FFmpeg's stdin
#if PLATFORM_WINDOWS
FFmpegVideoPipe = _popen(TCHAR_TO_ANSI(*Cmd), "wb");
#else
FFmpegVideoPipe = popen(TCHAR_TO_ANSI(*Cmd), "w");
#endif
if (!FFmpegVideoPipe)
{
UE_LOG(LogAudioVideoRecord, Error,
TEXT("SimpleRecorder: Failed to open FFmpeg pipe! Make sure ffmpeg.exe is on your PATH."));
bIsRecording = false;
return;
}
}
// ─── 2. Register back-buffer delegate (video frames) ────────────
// ─── 1. Register back-buffer delegate (video frames) ────────────
// FFmpeg is NOT started here — it launches on the first frame
// so we can read the real back-buffer dimensions.
if (FSlateApplication::IsInitialized())
{
BackBufferDelegateHandle =
@@ -200,7 +136,7 @@ void USimpleRecorder::StartRecording()
.AddUObject(this, &USimpleRecorder::OnBackBufferReady);
}
// ─── 3. Start audio submix recording ────────────────────────────
// ─── 2. Start audio submix recording ────────────────────────────
{
// Clear any leftover audio data
FScopeLock Lock(&AudioBufferCritSection);
@@ -302,32 +238,93 @@ void USimpleRecorder::StopRecording()
UE_LOG(LogAudioVideoRecord, Log, TEXT("SimpleRecorder: Recording stopped. All files saved."));
}
// =====================================================================
// LaunchFFmpegVideoPipe — called once from first OnBackBufferReady
// =====================================================================
bool USimpleRecorder::LaunchFFmpegVideoPipe()
{
FString FFmpeg = GetFFmpegExecutable();
#if PLATFORM_MAC
FString EncoderFlags = FString::Printf(TEXT("-c:v h264_videotoolbox -b:v %s"), *VideoBitrate);
#elif PLATFORM_WINDOWS
FString EncoderFlags = FString::Printf(TEXT("-c:v h264_nvenc -preset p5 -b:v %s"), *VideoBitrate);
#else
FString EncoderFlags = FString::Printf(TEXT("-c:v libx264 -preset medium -b:v %s"), *VideoBitrate);
#endif
// -movflags frag_keyframe+empty_moov writes fragmented MP4:
// each keyframe starts a new self-contained fragment, so the file
// is playable even if the process crashes mid-recording.
FString Cmd = FString::Printf(
TEXT("\"%s\" -y -f rawvideo -pix_fmt bgra -video_size %dx%d -framerate %d ")
TEXT("-i - %s -movflags frag_keyframe+empty_moov \"%s\""),
*FFmpeg,
CaptureWidth, CaptureHeight, CaptureFPS,
*EncoderFlags,
*VideoFilePath
);
UE_LOG(LogAudioVideoRecord, Log, TEXT(" Resolution: %dx%d @ %d FPS"), CaptureWidth, CaptureHeight, CaptureFPS);
UE_LOG(LogAudioVideoRecord, Log, TEXT(" FFmpeg cmd: %s"), *Cmd);
#if PLATFORM_WINDOWS
FFmpegVideoPipe = _popen(TCHAR_TO_ANSI(*Cmd), "wb");
#else
FFmpegVideoPipe = popen(TCHAR_TO_ANSI(*Cmd), "w");
#endif
if (!FFmpegVideoPipe)
{
UE_LOG(LogAudioVideoRecord, Error,
TEXT("SimpleRecorder: Failed to open FFmpeg pipe!"));
return false;
}
return true;
}
// =====================================================================
// OnBackBufferReady — called every presented frame on the RENDER thread
// =====================================================================
void USimpleRecorder::OnBackBufferReady(SWindow& SlateWindow, const FTextureRHIRef& BackBuffer)
{
// Safety: if we already stopped, do nothing
if (!bIsRecording || !FFmpegVideoPipe)
if (!bIsRecording)
{
return;
}
// We're on the render thread here.
// Read the back-buffer pixels into a CPU-side array.
FRHICommandListImmediate& RHICmdList = FRHICommandListImmediate::Get();
// Determine actual back-buffer size (may differ from our target)
const FIntPoint BBSize = BackBuffer->GetSizeXY();
// We need a rect matching our capture resolution, clamped to back-buffer
// ─── First frame: read actual back-buffer size and start FFmpeg ──
if (!bFFmpegStarted)
{
// Use real back-buffer dimensions (not the DPI-scaled viewport size)
CaptureWidth = (int32)BBSize.X & ~1; // ensure even
CaptureHeight = (int32)BBSize.Y & ~1;
if (CaptureWidth <= 0 || CaptureHeight <= 0)
{
return; // back buffer not ready yet, try next frame
}
if (!LaunchFFmpegVideoPipe())
{
bIsRecording = false;
return;
}
bFFmpegStarted = true;
}
if (!FFmpegVideoPipe)
{
return;
}
// Read the full back-buffer at CaptureWidth x CaptureHeight
const int32 ReadW = FMath::Min(CaptureWidth, (int32)BBSize.X);
const int32 ReadH = FMath::Min(CaptureHeight, (int32)BBSize.Y);
FIntRect ReadRect(0, 0, ReadW, ReadH);
// ReadSurfaceData puts pixels into a TArray<FColor> (BGRA, 8-bit per channel)
TArray<FColor> Pixels;
Pixels.SetNumUninitialized(ReadW * ReadH);
@@ -338,30 +335,7 @@ void USimpleRecorder::OnBackBufferReady(SWindow& SlateWindow, const FTextureRHIR
FReadSurfaceDataFlags(RCM_UNorm)
);
// If the back-buffer is smaller than our target, we still write the full
// frame size so FFmpeg doesn't choke. Pad with black if needed.
if (ReadW == CaptureWidth && ReadH == CaptureHeight)
{
// Fast path — dimensions match exactly
fwrite(Pixels.GetData(), sizeof(FColor), Pixels.Num(), FFmpegVideoPipe);
}
else
{
// Slow path — need to pad to CaptureWidth x CaptureHeight
// Allocate a black frame
TArray<FColor> PaddedFrame;
PaddedFrame.SetNumZeroed(CaptureWidth * CaptureHeight);
for (int32 Row = 0; Row < ReadH; ++Row)
{
FMemory::Memcpy(
&PaddedFrame[Row * CaptureWidth],
&Pixels[Row * ReadW],
ReadW * sizeof(FColor));
}
fwrite(PaddedFrame.GetData(), sizeof(FColor), PaddedFrame.Num(), FFmpegVideoPipe);
}
fwrite(Pixels.GetData(), sizeof(FColor), Pixels.Num(), FFmpegVideoPipe);
}
// =====================================================================

View File

@@ -96,6 +96,9 @@ private:
/** Called every frame on the render thread when the back buffer is ready. */
void OnBackBufferReady(SWindow& SlateWindow, const FTextureRHIRef& BackBuffer);
/** Launches the FFmpeg video pipe with the actual back-buffer dimensions. */
bool LaunchFFmpegVideoPipe();
/** Writes the captured audio buffer to a .wav file. */
void SaveAudioToWav();
@@ -108,6 +111,7 @@ private:
// Video
FDelegateHandle BackBufferDelegateHandle;
FILE* FFmpegVideoPipe = nullptr;
bool bFFmpegStarted = false;
// Audio — accumulated raw PCM data
TArray<float> AudioBuffer; // interleaved float samples