diff --git a/.DS_Store b/.DS_Store
index d02cf09..f607a0f 100644
Binary files a/.DS_Store and b/.DS_Store differ
diff --git a/Build/Mac/AudioVideoRecord.PackageVersionCounter b/Build/Mac/AudioVideoRecord.PackageVersionCounter
new file mode 100644
index 0000000..be58634
--- /dev/null
+++ b/Build/Mac/AudioVideoRecord.PackageVersionCounter
@@ -0,0 +1 @@
+0.3
diff --git a/Build/Mac/Resources/Info.Template.plist b/Build/Mac/Resources/Info.Template.plist
new file mode 100644
index 0000000..0d46cad
--- /dev/null
+++ b/Build/Mac/Resources/Info.Template.plist
@@ -0,0 +1,15 @@
+
+
+
+
+ NSAppTransportSecurity
+
+ NSAllowsArbitraryLoads
+
+
+ NSHighResolutionCapable
+
+ ITSAppUsesNonExemptEncryption
+
+
+
diff --git a/Build/Mac/Resources/Sandbox.NoNet.entitlements b/Build/Mac/Resources/Sandbox.NoNet.entitlements
new file mode 100644
index 0000000..852fa1a
--- /dev/null
+++ b/Build/Mac/Resources/Sandbox.NoNet.entitlements
@@ -0,0 +1,8 @@
+
+
+
+
+ com.apple.security.app-sandbox
+
+
+
diff --git a/Build/Mac/Resources/Sandbox.Server.entitlements b/Build/Mac/Resources/Sandbox.Server.entitlements
new file mode 100644
index 0000000..7a2230d
--- /dev/null
+++ b/Build/Mac/Resources/Sandbox.Server.entitlements
@@ -0,0 +1,12 @@
+
+
+
+
+ com.apple.security.app-sandbox
+
+ com.apple.security.network.client
+
+ com.apple.security.network.server
+
+
+
diff --git a/Source/AudioVideoRecord/AudioVideoRecordGameMode.cpp b/Source/AudioVideoRecord/AudioVideoRecordGameMode.cpp
index fb772d5..1612c04 100644
--- a/Source/AudioVideoRecord/AudioVideoRecordGameMode.cpp
+++ b/Source/AudioVideoRecord/AudioVideoRecordGameMode.cpp
@@ -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;
+ }
+ }
}
diff --git a/Source/AudioVideoRecord/AudioVideoRecordGameMode.h b/Source/AudioVideoRecord/AudioVideoRecordGameMode.h
index 9749052..8384058 100644
--- a/Source/AudioVideoRecord/AudioVideoRecordGameMode.h
+++ b/Source/AudioVideoRecord/AudioVideoRecordGameMode.h
@@ -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;
};
diff --git a/Source/AudioVideoRecord/SimpleRecorder.cpp b/Source/AudioVideoRecord/SimpleRecorder.cpp
index d029f2d..51e1ad6 100644
--- a/Source/AudioVideoRecord/SimpleRecorder.cpp
+++ b/Source/AudioVideoRecord/SimpleRecorder.cpp
@@ -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 ... -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 (BGRA, 8-bit per channel)
TArray 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 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);
}
// =====================================================================
diff --git a/Source/AudioVideoRecord/SimpleRecorder.h b/Source/AudioVideoRecord/SimpleRecorder.h
index 13f89b2..d0aea85 100644
--- a/Source/AudioVideoRecord/SimpleRecorder.h
+++ b/Source/AudioVideoRecord/SimpleRecorder.h
@@ -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 AudioBuffer; // interleaved float samples
diff --git a/build-mac.sh b/build-mac.sh
new file mode 100755
index 0000000..a6973c7
--- /dev/null
+++ b/build-mac.sh
@@ -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"
\ No newline at end of file