Add initial project files and implement basic audio-visual recording functionality
This commit is contained in:
1
Build/Mac/AudioVideoRecord.PackageVersionCounter
Normal file
1
Build/Mac/AudioVideoRecord.PackageVersionCounter
Normal file
@@ -0,0 +1 @@
|
|||||||
|
0.3
|
||||||
15
Build/Mac/Resources/Info.Template.plist
Normal file
15
Build/Mac/Resources/Info.Template.plist
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>NSAppTransportSecurity</key>
|
||||||
|
<dict>
|
||||||
|
<key>NSAllowsArbitraryLoads</key>
|
||||||
|
<true/>
|
||||||
|
</dict>
|
||||||
|
<key>NSHighResolutionCapable</key>
|
||||||
|
<false/>
|
||||||
|
<key>ITSAppUsesNonExemptEncryption</key>
|
||||||
|
<false/>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
8
Build/Mac/Resources/Sandbox.NoNet.entitlements
Normal file
8
Build/Mac/Resources/Sandbox.NoNet.entitlements
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>com.apple.security.app-sandbox</key>
|
||||||
|
<true/>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
12
Build/Mac/Resources/Sandbox.Server.entitlements
Normal file
12
Build/Mac/Resources/Sandbox.Server.entitlements
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>com.apple.security.app-sandbox</key>
|
||||||
|
<true/>
|
||||||
|
<key>com.apple.security.network.client</key>
|
||||||
|
<true/>
|
||||||
|
<key>com.apple.security.network.server</key>
|
||||||
|
<true/>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
@@ -1,8 +1,23 @@
|
|||||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||||
|
|
||||||
#include "AudioVideoRecordGameMode.h"
|
#include "AudioVideoRecordGameMode.h"
|
||||||
|
#include "Kismet/GameplayStatics.h"
|
||||||
|
#include "Components/AudioComponent.h"
|
||||||
|
|
||||||
AAudioVideoRecordGameMode::AAudioVideoRecordGameMode()
|
AAudioVideoRecordGameMode::AAudioVideoRecordGameMode()
|
||||||
{
|
{
|
||||||
// stub
|
}
|
||||||
|
|
||||||
|
void AAudioVideoRecordGameMode::BeginPlay()
|
||||||
|
{
|
||||||
|
Super::BeginPlay();
|
||||||
|
|
||||||
|
if (BGMSound)
|
||||||
|
{
|
||||||
|
BGMAudioComponent = UGameplayStatics::SpawnSound2D(this, BGMSound, BGMVolume);
|
||||||
|
if (BGMAudioComponent)
|
||||||
|
{
|
||||||
|
BGMAudioComponent->bAutoDestroy = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,9 @@
|
|||||||
#include "GameFramework/GameModeBase.h"
|
#include "GameFramework/GameModeBase.h"
|
||||||
#include "AudioVideoRecordGameMode.generated.h"
|
#include "AudioVideoRecordGameMode.generated.h"
|
||||||
|
|
||||||
|
class USoundBase;
|
||||||
|
class UAudioComponent;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Simple GameMode for a first person game
|
* Simple GameMode for a first person game
|
||||||
*/
|
*/
|
||||||
@@ -16,6 +19,21 @@ class AAudioVideoRecordGameMode : public AGameModeBase
|
|||||||
|
|
||||||
public:
|
public:
|
||||||
AAudioVideoRecordGameMode();
|
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;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -118,81 +118,17 @@ void USimpleRecorder::StartRecording()
|
|||||||
|
|
||||||
InitOutputPaths();
|
InitOutputPaths();
|
||||||
bIsRecording = true;
|
bIsRecording = true;
|
||||||
|
bFFmpegStarted = false;
|
||||||
// ─── 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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
UE_LOG(LogAudioVideoRecord, Log, TEXT("SimpleRecorder: Starting recording..."));
|
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(" Video → %s"), *VideoFilePath);
|
||||||
UE_LOG(LogAudioVideoRecord, Log, TEXT(" Audio → %s"), *AudioFilePath);
|
UE_LOG(LogAudioVideoRecord, Log, TEXT(" Audio → %s"), *AudioFilePath);
|
||||||
UE_LOG(LogAudioVideoRecord, Log, TEXT(" Final → %s"), *FinalFilePath);
|
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 ──────────────────────────────────
|
// ─── 1. Register back-buffer delegate (video frames) ────────────
|
||||||
{
|
// FFmpeg is NOT started here — it launches on the first frame
|
||||||
FString FFmpeg = GetFFmpegExecutable();
|
// so we can read the real back-buffer dimensions.
|
||||||
|
|
||||||
// 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) ────────────
|
|
||||||
if (FSlateApplication::IsInitialized())
|
if (FSlateApplication::IsInitialized())
|
||||||
{
|
{
|
||||||
BackBufferDelegateHandle =
|
BackBufferDelegateHandle =
|
||||||
@@ -200,7 +136,7 @@ void USimpleRecorder::StartRecording()
|
|||||||
.AddUObject(this, &USimpleRecorder::OnBackBufferReady);
|
.AddUObject(this, &USimpleRecorder::OnBackBufferReady);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── 3. Start audio submix recording ────────────────────────────
|
// ─── 2. Start audio submix recording ────────────────────────────
|
||||||
{
|
{
|
||||||
// Clear any leftover audio data
|
// Clear any leftover audio data
|
||||||
FScopeLock Lock(&AudioBufferCritSection);
|
FScopeLock Lock(&AudioBufferCritSection);
|
||||||
@@ -302,32 +238,93 @@ void USimpleRecorder::StopRecording()
|
|||||||
UE_LOG(LogAudioVideoRecord, Log, TEXT("SimpleRecorder: Recording stopped. All files saved."));
|
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
|
// OnBackBufferReady — called every presented frame on the RENDER thread
|
||||||
// =====================================================================
|
// =====================================================================
|
||||||
void USimpleRecorder::OnBackBufferReady(SWindow& SlateWindow, const FTextureRHIRef& BackBuffer)
|
void USimpleRecorder::OnBackBufferReady(SWindow& SlateWindow, const FTextureRHIRef& BackBuffer)
|
||||||
{
|
{
|
||||||
// Safety: if we already stopped, do nothing
|
if (!bIsRecording)
|
||||||
if (!bIsRecording || !FFmpegVideoPipe)
|
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// We're on the render thread here.
|
|
||||||
// Read the back-buffer pixels into a CPU-side array.
|
|
||||||
|
|
||||||
FRHICommandListImmediate& RHICmdList = FRHICommandListImmediate::Get();
|
FRHICommandListImmediate& RHICmdList = FRHICommandListImmediate::Get();
|
||||||
|
|
||||||
// Determine actual back-buffer size (may differ from our target)
|
|
||||||
const FIntPoint BBSize = BackBuffer->GetSizeXY();
|
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 ReadW = FMath::Min(CaptureWidth, (int32)BBSize.X);
|
||||||
const int32 ReadH = FMath::Min(CaptureHeight, (int32)BBSize.Y);
|
const int32 ReadH = FMath::Min(CaptureHeight, (int32)BBSize.Y);
|
||||||
|
|
||||||
FIntRect ReadRect(0, 0, ReadW, ReadH);
|
FIntRect ReadRect(0, 0, ReadW, ReadH);
|
||||||
|
|
||||||
// ReadSurfaceData puts pixels into a TArray<FColor> (BGRA, 8-bit per channel)
|
|
||||||
TArray<FColor> Pixels;
|
TArray<FColor> Pixels;
|
||||||
Pixels.SetNumUninitialized(ReadW * ReadH);
|
Pixels.SetNumUninitialized(ReadW * ReadH);
|
||||||
|
|
||||||
@@ -338,30 +335,7 @@ void USimpleRecorder::OnBackBufferReady(SWindow& SlateWindow, const FTextureRHIR
|
|||||||
FReadSurfaceDataFlags(RCM_UNorm)
|
FReadSurfaceDataFlags(RCM_UNorm)
|
||||||
);
|
);
|
||||||
|
|
||||||
// If the back-buffer is smaller than our target, we still write the full
|
fwrite(Pixels.GetData(), sizeof(FColor), Pixels.Num(), FFmpegVideoPipe);
|
||||||
// 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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// =====================================================================
|
// =====================================================================
|
||||||
|
|||||||
@@ -96,6 +96,9 @@ private:
|
|||||||
/** Called every frame on the render thread when the back buffer is ready. */
|
/** Called every frame on the render thread when the back buffer is ready. */
|
||||||
void OnBackBufferReady(SWindow& SlateWindow, const FTextureRHIRef& BackBuffer);
|
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. */
|
/** Writes the captured audio buffer to a .wav file. */
|
||||||
void SaveAudioToWav();
|
void SaveAudioToWav();
|
||||||
|
|
||||||
@@ -108,6 +111,7 @@ private:
|
|||||||
// Video
|
// Video
|
||||||
FDelegateHandle BackBufferDelegateHandle;
|
FDelegateHandle BackBufferDelegateHandle;
|
||||||
FILE* FFmpegVideoPipe = nullptr;
|
FILE* FFmpegVideoPipe = nullptr;
|
||||||
|
bool bFFmpegStarted = false;
|
||||||
|
|
||||||
// Audio — accumulated raw PCM data
|
// Audio — accumulated raw PCM data
|
||||||
TArray<float> AudioBuffer; // interleaved float samples
|
TArray<float> AudioBuffer; // interleaved float samples
|
||||||
|
|||||||
1
build-mac.sh
Executable file
1
build-mac.sh
Executable file
@@ -0,0 +1 @@
|
|||||||
|
"/Users/Shared/Epic Games/UE_5.6/Engine/Build/BatchFiles/Mac/Build.sh" AudioVideoRecordEditor Mac Development "/Users/johnjayasinghs/projects/axial/AudioVideoRecord/AudioVideoRecord.uproject"
|
||||||
Reference in New Issue
Block a user