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