> ## Documentation Index
> Fetch the complete documentation index at: https://docs.liv.tv/llms.txt
> Use this file to discover all available pages before exploring further.

# Best Practices (Unreal)

> Optimization tips and recommended patterns for the LCK SDK in Unreal Engine.

## What Problem Does This Solve?

Following best practices helps you:

* Avoid common integration mistakes
* Optimize recording performance
* Handle errors gracefully
* Write maintainable code
* Deliver smooth user experience

This page collects lessons learned from hundreds of LCK integrations.

## When to Use This

Read this when:

* First-time LCK integration
* Debugging performance issues
* Building custom recording UI
* Optimizing for Quest devices
* Code review / refactoring

***

## Recording

### Quality Guidelines

| Quality | Resolution | Bitrate | FPS | Use Case                           |
| ------- | ---------- | ------- | --- | ---------------------------------- |
| **SD**  | 1280×720   | 4 Mbps  | 30  | Quest 2, performance mode          |
| **HD**  | 1920×1080  | 12 Mbps | 60  | **Standard quality (recommended)** |
| **2K**  | 2560×1440  | 20 Mbps | 60  | Quest 3/Pro, high quality          |
| **4K**  | 3840×2160  | 35 Mbps | 60  | PCVR only, max quality             |

<Info>
  **Quest recommendations:**

  * Quest 2: Use HD (1080p) @ 30fps for best balance
  * Quest 3/Pro: Can handle 2K if game performance allows
  * Avoid 4K on Quest devices (thermal/performance)
</Info>

**File size estimates:**

* SD @ 30fps (default 4 Mbps): \~2 GB per hour
* HD @ 60fps (default 12 Mbps): \~5 GB per hour
* 2K @ 60fps (default 20 Mbps): \~9 GB per hour
* 4K @ 60fps (default 35 Mbps): \~16 GB per hour

***

### Use Async Methods

**Do this:**

```cpp theme={null}
Recorder->StartRecordingAsync(
    FOnLCKRecorderBoolResult::CreateLambda([this](bool bSuccess)
    {
        if (bSuccess)
        {
            ShowRecordingIndicator();
            PlayRecordingSound();
        }
        else
        {
            ShowError(TEXT("Failed to start recording"));
        }
    })
);
```

**Don't do this:**

```cpp theme={null}
// No error feedback, blocks game thread
bool bSuccess = Service->StartRecording();
```

**Why async is better:**

* Non-blocking—doesn't freeze game
* Detailed error callbacks
* Progress tracking for save operations
* Better user experience

***

### Subscribe to Recording Events

**Do this:**

```cpp theme={null}
void ARecordingUI::BeginPlay()
{
    Super::BeginPlay();
    
    // Subscribe to events
    Service->OnRecordingSaveFinished.AddDynamic(this, &ARecordingUI::OnSaveFinished);
    Service->OnRecordingError.AddDynamic(this, &ARecordingUI::OnError);
    Service->OnRecordingSaveProgress.AddDynamic(this, &ARecordingUI::OnProgress);
    
    // Get state changes via DataModel
    DataModel->OnRecordStateChanged.AddUObject(this, &ARecordingUI::OnStateChanged);
}

UFUNCTION()
void ARecordingUI::OnSaveFinished(bool bSuccess)
{
    if (bSuccess)
        ShowNotification(TEXT("Recording saved!"));
}
```

**Don't do this:**

```cpp theme={null}
// Polling state every frame = BAD
void Tick(float DeltaTime)
{
    if (Service->IsRecording() != bWasRecording)
    {
        bWasRecording = Service->IsRecording();
        UpdateUI();
    }
}
```

**Why events are better:**

* React immediately to state changes
* No performance cost of polling
* Cleaner code
* More responsive UI

***

### Validate Before Recording

```cpp theme={null}
bool ARecorder::CanStartRecording()
{
    // 1. Check tracking ID
    ULCKDeveloperSettings* Settings = ULCKDeveloperSettings::Get();
    if (!Settings->IsTrackingIdValid())
    {
        ShowError(TEXT("Recording not configured"));
        return false;
    }
    
    // 2. Check if already recording
    if (Service->IsRecording())
    {
        UE_LOG(LogLCK, Warning, TEXT("Already recording"));
        return false;
    }
    
    // 3. Check storage space
    int64 FreeSpace = FPlatformMisc::GetDiskFreeSpace(FPaths::ProjectSavedDir());
    int64 RequiredSpace = 500 * 1024 * 1024; // 500 MB
    if (FreeSpace < RequiredSpace)
    {
        ShowError(FString::Printf(
            TEXT("Low storage: %d MB free. Need 500 MB minimum."),
            FreeSpace / (1024 * 1024)
        ));
        return false;
    }
    
    // 4. Validate audio config (warnings, not blocking)
    FLCKAudioConfigValidation AudioValidation = Settings->ValidateAudioConfig();
    for (const FString& Warning : AudioValidation.Warnings)
    {
        UE_LOG(LogLCK, Warning, TEXT("Audio: %s"), *Warning);
    }
    
    return true;
}

void ARecorder::StartRecording()
{
    if (!CanStartRecording())
        return;
    
    Recorder->StartRecordingAsync(/* ... */);
}
```

***

## Audio

### Audio Source Priority

When multiple audio plugins are enabled, LCK uses priority order:

1. **LCKFMOD** (highest priority)
2. **LCKWwise**
3. **LCKUnrealAudio** (lowest priority, always available)

<Info>
  Only ONE game audio source is active at a time. Microphone and voice chat can run alongside game audio.
</Info>

**Check which is active:**

```cpp theme={null}
ELCKGameAudioType ActiveAudio = Settings->GetActiveGameAudioType();

switch (ActiveAudio)
{
    case ELCKGameAudioType::FMOD:
        UE_LOG(LogLCK, Log, TEXT("Using FMOD for game audio"));
        break;
    case ELCKGameAudioType::Wwise:
        UE_LOG(LogLCK, Log, TEXT("Using Wwise for game audio"));
        break;
    case ELCKGameAudioType::UnrealAudio:
        UE_LOG(LogLCK, Log, TEXT("Using Unreal Audio"));
        break;
}
```

***

### Match Sample Rates

**Do this:**

```cpp theme={null}
// Get Unreal Audio sample rate
int32 SampleRate = ULCKUnrealAudioBPL::GetUnrealAudioSamplerate();

// Configure encoder with matching rate
FLCKRecorderParams Params;
Params.Width = 1920;
Params.Height = 1080;
Params.Framerate = 30;
Params.Samplerate = SampleRate;  // Match!

Recorder->SetupRecorder(Params, CaptureComponent);
```

**Don't do this:**

```cpp theme={null}
// Hardcoded sample rate = audio distortion/sync issues
Params.Samplerate = 48000; // May not match actual audio
```

***

### Thread-Safe Audio Callbacks

<Warning>
  Audio callbacks may come from different threads. If you need to access game thread objects (UObject properties, UI), use `AsyncTask`.
</Warning>

**Do this:**

```cpp theme={null}
AudioSource->OnAudioDataDelegate.BindLambda([this](
    TArrayView<const float> PCM,
    int32 Channels,
    int32 SampleRate,
    ELCKAudioChannel SourceChannel)
{
    // Calculate volume on audio thread (OK)
    float Volume = CalculateRMS(PCM);
    
    // Update UI on game thread
    AsyncTask(ENamedThreads::GameThread, [this, Volume]()
    {
        VolumeIndicator->SetPercent(Volume);
    });
});
```

**Don't do this:**

```cpp theme={null}
AudioSource->OnAudioDataDelegate.BindLambda([this](auto PCM, auto Channels, auto SampleRate, auto Source)
{
    // CRASH: Accessing UObject from non-game thread
    VolumeIndicator->SetPercent(CalculateRMS(PCM));
});
```

***

### Audio Delegate is Single, Not Multicast

<Warning>
  `OnAudioDataDelegate` is a **SINGLE delegate**. Use `BindLambda()`, NOT `AddLambda()`.
</Warning>

**Do this:**

```cpp theme={null}
// BindLambda replaces any existing binding
AudioSource->OnAudioDataDelegate.BindLambda([](auto PCM, auto Channels, auto SampleRate, auto Source)
{
    // Process audio
});
```

**Don't do this:**

```cpp theme={null}
// AddLambda doesn't exist on single delegates
AudioSource->OnAudioDataDelegate.AddLambda([](auto PCM, auto Channels, auto SampleRate, auto Source)
{
    // Compilation error
});
```

***

## UI

### Don't Add Your Own Button Cooldown

LCK buttons include automatic 0.25s cooldown.

**Do this:**

```cpp theme={null}
// Just bind the event, cooldown is automatic
RecordButton->OnTapStarted.AddDynamic(this, &AMyActor::OnRecordPressed);
```

**Don't do this:**

```cpp theme={null}
// Redundant cooldown logic
void OnRecordPressed()
{
    if (FPlatformTime::Seconds() - LastPressTime < 0.25f)
        return; // Unnecessary!
    
    LastPressTime = FPlatformTime::Seconds();
    ToggleRecording();
}
```

***

### Use Showable Groups for Batch Operations

**Do this:**

```cpp theme={null}
// Group related UI elements
ULCKShowablesGroup* SettingsGroup = NewObject<ULCKShowablesGroup>(this);
SettingsGroup->Add(FOVButton);
SettingsGroup->Add(DistanceButton);
SettingsGroup->Add(SmoothnessButton);

// Batch show/hide
void ShowSettings()
{
    SettingsGroup->Show();
}

void HideSettings()
{
    SettingsGroup->Hide();
}
```

**Don't do this:**

```cpp theme={null}
// Manually show/hide each element
void ShowSettings()
{
    FOVButton->SetVisibility(true);
    DistanceButton->SetVisibility(true);
    SmoothnessButton->SetVisibility(true);
}
```

***

## Performance

### Optimize Scene Capture Component

```cpp theme={null}
void ARecorder::SetupCaptureComponent(USceneCaptureComponent2D* Capture)
{
    // Disable expensive capture options
    Capture->CaptureSource = ESceneCaptureSource::SCS_FinalColorLDR;
    Capture->bCaptureEveryFrame = false;    // Manual capture
    Capture->bCaptureOnMovement = false;    // Manual capture
    Capture->bAlwaysPersistRenderingState = true;
    
    // Disable post-processing for performance
    Capture->PostProcessSettings.bOverride_AmbientOcclusionIntensity = true;
    Capture->PostProcessSettings.AmbientOcclusionIntensity = 0.0f;
    
    // Disable expensive features
    Capture->ShowFlags.SetTemporalAA(false);
    Capture->ShowFlags.SetMotionBlur(false);
}
```

***

### Unregister Capture Components

**Do this:**

```cpp theme={null}
void ARecorder::BeginDestroy()
{
    Super::BeginDestroy();
    
    if (Service)
    {
        Service->StopRecording();
        Service->UnregisterCaptureComponent(TEXT("MainCapture"));
    }
}
```

**Don't do this:**

```cpp theme={null}
// Memory leak - capture component never cleaned up
void ARecorder::BeginDestroy()
{
    Super::BeginDestroy();
    Service->StopRecording();
}
```

***

### Monitor Frame Times During Recording

```cpp theme={null}
void APerformanceMonitor::Tick(float DeltaTime)
{
    if (!Service || !Service->IsRecording())
        return;
    
    float FrameTimeMs = DeltaTime * 1000.0f;
    
    // Track frame time history
    FrameTimeHistory.Add(FrameTimeMs);
    if (FrameTimeHistory.Num() > 60) // 2 seconds at 30fps
    {
        FrameTimeHistory.RemoveAt(0);
    }
    
    // Calculate average
    float AvgFrameTime = 0.0f;
    for (float Time : FrameTimeHistory)
    {
        AvgFrameTime += Time;
    }
    AvgFrameTime /= FrameTimeHistory.Num();
    
    // Warn if frame time is too high
    float TargetFrameTime = 1000.0f / 30.0f; // 33.3ms for 30fps
    if (AvgFrameTime > TargetFrameTime * 1.2f) // 20% over target
    {
        UE_LOG(LogLCK, Warning, TEXT("Recording impacting performance: %.1fms avg"), 
            AvgFrameTime);
        SuggestLowerQuality();
    }
}
```

***

## Common Pitfalls

### 1. Not Handling Recording State Feedback

**Problem:** No feedback when recording starts/stops/fails

**Solution:** Subscribe to delegates

```cpp theme={null}
Service->OnRecordingSaveFinished.AddDynamic(this, &AMyUI::OnSaveFinished);
Service->OnRecordingError.AddDynamic(this, &AMyUI::OnError);
DataModel->OnRecordStateChanged.AddUObject(this, &AMyUI::OnStateChanged);
```

***

### 2. Mismatched Resolutions

**Problem:** Render target doesn't match recording resolution → distortion

**Solution:** Match exactly

```cpp theme={null}
// Render target
RenderTarget->ResX = 1920;
RenderTarget->ResY = 1080;

// Recording params
FLCKRecorderParams Params;
Params.Width = 1920;  // Must match RenderTarget->ResX
Params.Height = 1080; // Must match RenderTarget->ResY
```

***

### 3. Forgetting to Unregister Capture Component

**Problem:** Memory leak from orphaned capture components

**Solution:** Always unregister on cleanup

```cpp theme={null}
void ARecorder::BeginDestroy()
{
    Super::BeginDestroy();
    
    if (Service)
    {
        Service->UnregisterCaptureComponent(CaptureComponentName);
    }
}
```

***

### 4. Sample Rate Mismatch

**Problem:** Audio distortion or A/V sync issues

**Solution:** Query and match sample rates

```cpp theme={null}
int32 SampleRate = ULCKUnrealAudioBPL::GetUnrealAudioSamplerate();
Params.Samplerate = SampleRate;
```

***

### 5. Using AddLambda for Audio Delegate

**Problem:** `OnAudioDataDelegate` is single, not multicast

**Solution:** Use `BindLambda()` instead

```cpp theme={null}
// Correct
AudioSource->OnAudioDataDelegate.BindLambda([](auto PCM, auto Channels, auto SampleRate, auto Source) {
    // ...
});

// Wrong - compilation error
AudioSource->OnAudioDataDelegate.AddLambda([](auto PCM, auto Channels, auto SampleRate, auto Source) {
    // ...
});
```

***

## Platform Checklist

### Android (Quest)

<Check>**Vulkan enabled** in Project Settings → Android</Check>
<Check>**RECORD\_AUDIO permission** in AndroidManifest.xml</Check>
<Check>**WRITE\_EXTERNAL\_STORAGE permission** (API \< 29)</Check>
<Check>**LCKOboe plugin enabled** for low-latency mic</Check>
<Check>**LCKVulkan loads at EarliestPossible** (don't change!)</Check>

**AndroidManifest.xml:**

```xml theme={null}
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" 
                 android:maxSdkVersion="28" />
```

***

### Windows (PCVR)

<Check>**Media Foundation available** (Windows 10+)</Check>
<Check>**DirectX 11 compatible GPU**</Check>
<Check>**H.264 hardware encoding support**</Check>

***

## Debugging

### Enable Verbose Logging

```ini theme={null}
; DefaultEngine.ini
[Core.Log]
LogLCK=VeryVerbose
LogLCKEncoding=VeryVerbose
LogLCKAudio=VeryVerbose
LogLCKUI=Verbose
LogLCKTablet=Verbose
```

**What you'll see:**

```
LogLCK: Recording started
LogLCKEncoding: Encoder initialized: 1920x1080 @ 60fps, 12 Mbps
LogLCKAudio: Audio source registered: UnrealAudio
LogLCKEncoding: Frame 0 encoded (8.2ms)
LogLCKEncoding: Frame 30 encoded (7.9ms)
LogLCK: Recording stopped
LogLCKEncoding: Finalizing video file...
LogLCK: Recording saved: /Game/Movies/recording_001.mp4
```

***

### Common Log Messages

| Log Message               | Meaning                       | Action                      |
| ------------------------- | ----------------------------- | --------------------------- |
| `Recording started`       | Recording began successfully  | -                           |
| `Encoder initialized`     | Platform encoder ready        | -                           |
| `Audio source registered` | Audio capture active          | -                           |
| `Invalid Tracking ID`     | Tracking ID missing/invalid   | Add ID from dashboard       |
| `Encoder not available`   | Platform encoder failed       | Check platform requirements |
| `Permission denied`       | Missing permissions (Android) | Request permissions         |

***

## Quick Reference

**Start recording:**

```cpp theme={null}
Recorder->StartRecordingAsync(FOnLCKRecorderBoolResult::CreateLambda([](bool bSuccess) {
    // Handle result
}));
```

**Stop recording:**

```cpp theme={null}
Recorder->StopRecordingAsync(
    FOnLCKRecorderBoolResult::CreateLambda([](bool bSuccess) { /* Done */ }),
    FOnLCKRecorderProgress::CreateLambda([](float Progress) { /* 0.0-1.0 */ })
);
```

**Check state:**

```cpp theme={null}
bool bRecording = Service->IsRecording();
float Duration = Service->GetCurrentRecordingDuration();
```

**Subscribe to events:**

```cpp theme={null}
Service->OnRecordingSaveFinished.AddDynamic(this, &AMyActor::OnSaveFinished);
Service->OnRecordingError.AddDynamic(this, &AMyActor::OnError);
DataModel->OnRecordStateChanged.AddUObject(this, &AMyActor::OnStateChanged);
```

***

## Key Takeaways

<Check>**Use async methods** for better error handling</Check>
<Check>**Subscribe to events** instead of polling state</Check>
<Check>**Validate before recording** (tracking ID, storage, state)</Check>
<Check>**Match sample rates** to avoid audio issues</Check>
<Check>**Audio callbacks on any thread** — use AsyncTask for UI updates</Check>
<Check>**Single delegate for audio** — use BindLambda, not AddLambda</Check>
<Check>**Unregister captures** to prevent memory leaks</Check>
<Check>**HD @ 30fps for Quest 2** — higher quality impacts performance</Check>

***

## Related

* [Error Codes](/api-reference/unreal/errors) — Error handling patterns
* [Delegates Reference](/api-reference/unreal/delegates) — All event delegates
* [Architecture](/api-reference/unreal/architecture) — System design
* [Device Overrides](/api-reference/unreal/device-overrides) — Platform-specific settings
