> ## 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.

# Delegates Reference

> Event delegates for reactive programming with the LCK SDK in Unreal Engine.

## What Problem Does This Solve?

Delegates let you react to LCK events in real-time:

* Recording started/stopped
* Save progress updates
* Button interactions
* Camera mode changes
* Audio callbacks

Instead of polling state every frame, you bind handlers that fire when events occur. This keeps your code clean and responsive.

## When to Use This

Reference this when:

* Building custom recording UI
* Handling recording lifecycle events
* Responding to user interactions
* Processing audio data
* Tracking save progress

***

## Delegate Types

LCK uses two kinds of delegates:

| Type                      | Usage                                   | Binding Method                  |
| ------------------------- | --------------------------------------- | ------------------------------- |
| **Dynamic Multicast**     | Blueprint-compatible, multiple bindings | `AddDynamic()`                  |
| **Non-Dynamic Multicast** | C++ only, multiple bindings             | `AddUObject()` or `AddLambda()` |
| **Raw/Lambda**            | C++ only, single or multiple bindings   | `BindLambda()` or `AddLambda()` |

<Warning>
  **Audio delegates are SINGLE delegates** (not multicast). Use `BindLambda()`, not `AddLambda()`. Each bind replaces the previous one.
</Warning>

<Warning>
  **DataModel delegates are non-dynamic multicast.** Most delegates on `ULCKTabletDataModel` (camera mode, mic state, video quality, recording state) use `DECLARE_MULTICAST_DELEGATE`, not `DECLARE_DYNAMIC_MULTICAST_DELEGATE`. Bind with `AddUObject()` or `AddLambda()`, **not** `AddDynamic()`.
</Warning>

***

## Recording Lifecycle Delegates

### FOnRecordingSaveFinished

**What it's for:** Know when recording save completes

```cpp theme={null}
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnRecordingSaveFinished, bool, Success);
```

**Declared on:** `ULCKService`

**Parameters:**

* `Success` (bool) -- True if save succeeded, false on error

**Usage:**

```cpp theme={null}
// Subscribe (dynamic multicast -- use AddDynamic)
Service->OnRecordingSaveFinished.AddDynamic(this, &AMyActor::HandleSaveFinished);

// Handler
UFUNCTION()
void AMyActor::HandleSaveFinished(bool bSuccess)
{
    if (bSuccess)
    {
        ShowNotification(TEXT("Recording saved!"));
        PlaySuccessSound();
    }
    else
    {
        ShowError(TEXT("Failed to save recording"));
    }
}
```

***

### FOnRecordingSaveProgress

**What it's for:** Update progress bar during save

```cpp theme={null}
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnRecordingSaveProgress, float, Progress);
```

**Declared on:** `ULCKService`

**Parameters:**

* `Progress` (float) -- Value from 0.0 (start) to 1.0 (complete)

**Usage:**

```cpp theme={null}
Service->OnRecordingSaveProgress.AddDynamic(this, &AMyUI::UpdateProgressBar);

UFUNCTION()
void AMyUI::UpdateProgressBar(float Progress)
{
    ProgressBar->SetPercent(Progress);
    ProgressText->SetText(FText::AsPercent(Progress));
}
```

***

### FOnRecordingError

**What it's for:** Handle recording errors with context

```cpp theme={null}
DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(
    FOnRecordingError,
    FString, ErrorMessage,
    int32, ErrorCode
);
```

**Declared on:** `ULCKService`

**Parameters:**

* `ErrorMessage` (FString) -- Human-readable error description
* `ErrorCode` (int32) -- Numeric error code (see `ELCKError`)

**Usage:**

```cpp theme={null}
Service->OnRecordingError.AddDynamic(this, &AMyActor::HandleRecordingError);

UFUNCTION()
void AMyActor::HandleRecordingError(FString ErrorMessage, int32 ErrorCode)
{
    UE_LOG(LogLCK, Error, TEXT("Recording error %d: %s"), ErrorCode, *ErrorMessage);

    // Show user-friendly message
    ShowErrorDialog(ErrorMessage);

    // Log to analytics
    Analytics->TrackError(TEXT("Recording"), ErrorCode);
}
```

***

## Camera & Settings Delegates

### FOnTabletCameraModeChanged

**What it's for:** React to camera mode switches

```cpp theme={null}
DECLARE_MULTICAST_DELEGATE_OneParam(FOnTabletCameraModeChanged, UClass*);
```

**Declared on:** `ULCKTabletDataModel`

**Parameters:**

* `UClass*` -- The new camera mode class (e.g., `ULCKSelfieCameraMode`, `ULCKFirstPersonCameraMode`, `ULCKThirdPersonCameraMode`)

<Warning>
  This is a **non-dynamic** multicast delegate. Use `AddUObject()` or `AddLambda()`, **not** `AddDynamic()`.
</Warning>

**Usage:**

```cpp theme={null}
// Subscribe (non-dynamic -- use AddUObject, not AddDynamic)
DataModel->OnTabletCameraModeChanged.AddUObject(this, &AMyUI::HandleCameraMode);

// Handler (no UFUNCTION() needed for non-dynamic delegates)
void AMyUI::HandleCameraMode(UClass* ModeClass)
{
    if (ModeClass->IsChildOf(ULCKSelfieCameraMode::StaticClass()))
    {
        CameraModeText->SetText(FText::FromString("Selfie"));
    }
    else if (ModeClass->IsChildOf(ULCKFirstPersonCameraMode::StaticClass()))
    {
        CameraModeText->SetText(FText::FromString("First Person"));
    }
    else if (ModeClass->IsChildOf(ULCKThirdPersonCameraMode::StaticClass()))
    {
        CameraModeText->SetText(FText::FromString("Third Person"));
    }
}
```

***

### FOnMicStateChanged

**What it's for:** Update UI when mic state changes

```cpp theme={null}
DECLARE_MULTICAST_DELEGATE_OneParam(FOnMicStateChanged, ELCKMicState);
```

**Declared on:** `ULCKTabletDataModel`

**Parameters:**

* `ELCKMicState` -- New microphone state (On, Off, No\_Access)

<Warning>
  This is a **non-dynamic** multicast delegate. Use `AddUObject()` or `AddLambda()`, **not** `AddDynamic()`.
</Warning>

**Usage:**

```cpp theme={null}
// Subscribe (non-dynamic -- use AddUObject, not AddDynamic)
DataModel->OnMicStateChanged.AddUObject(this, &AMicButton::UpdateIcon);

void AMicButton::UpdateIcon(ELCKMicState NewState)
{
    switch (NewState)
    {
        case ELCKMicState::On:
            Icon->SetBrush(MicOnTexture);
            Icon->SetColorAndOpacity(FLinearColor::White);
            break;
        case ELCKMicState::Off:
            Icon->SetBrush(MicOffTexture);
            Icon->SetColorAndOpacity(FLinearColor::Gray);
            break;
        case ELCKMicState::No_Access:
            Icon->SetBrush(MicBlockedTexture);
            ShowPermissionPrompt();
            break;
    }
}
```

***

### FOnVideoQualityChanged

**What it's for:** React to quality profile changes

```cpp theme={null}
DECLARE_MULTICAST_DELEGATE_OneParam(FOnVideoQualityChanged, ELCKVideoQuality);
```

**Declared on:** `ULCKTabletDataModel`

<Warning>
  This is a **non-dynamic** multicast delegate. Use `AddUObject()` or `AddLambda()`, **not** `AddDynamic()`.
</Warning>

**Usage:**

```cpp theme={null}
DataModel->OnVideoQualityChanged.AddUObject(this, &AMyUI::HandleQualityChanged);

void AMyUI::HandleQualityChanged(ELCKVideoQuality NewQuality)
{
    // Update quality display
}
```

***

### FOnRecordStateChange

**What it's for:** React to recording state changes on the DataModel

```cpp theme={null}
DECLARE_MULTICAST_DELEGATE_OneParam(FOnRecordStateChange, ELCKRecordingState);
```

**Declared on:** `ULCKTabletDataModel`

<Warning>
  This is a **non-dynamic** multicast delegate. Use `AddUObject()` or `AddLambda()`, **not** `AddDynamic()`.
</Warning>

**Usage:**

```cpp theme={null}
DataModel->OnRecordStateChanged.AddUObject(this, &AMyUI::HandleRecordStateChanged);

void AMyUI::HandleRecordStateChanged(ELCKRecordingState NewState)
{
    switch (NewState)
    {
        case ELCKRecordingState::Idle:
            // Ready to record
            break;
        case ELCKRecordingState::Recording:
            // Recording in progress
            break;
        case ELCKRecordingState::Saving:
            // Saving to disk
            break;
    }
}
```

***

### FOnScreenOrientationChanged

**What it's for:** React to screen orientation changes

```cpp theme={null}
DECLARE_MULTICAST_DELEGATE_OneParam(FOnScreenOrientationChanged, ELCKScreenOrientation);
```

**Declared on:** `ULCKTabletDataModel`

<Warning>
  This is a **non-dynamic** multicast delegate. Use `AddUObject()` or `AddLambda()`, **not** `AddDynamic()`.
</Warning>

**Usage:**

```cpp theme={null}
DataModel->OnScreenOrientationChanged.AddUObject(this, &AMyUI::HandleOrientationChanged);

void AMyUI::HandleOrientationChanged(ELCKScreenOrientation NewOrientation)
{
    // Update UI layout for new orientation
}
```

***

## UI Interaction Delegates

### FOnTapStarted

**What it's for:** Handle button presses

```cpp theme={null}
DECLARE_DYNAMIC_MULTICAST_DELEGATE(FOnTapStarted);
```

**Usage:**

```cpp theme={null}
RecordButton->OnTapStarted.AddDynamic(this, &AMyActor::HandleRecordButton);

UFUNCTION()
void AMyActor::HandleRecordButton()
{
    if (Service->IsRecording())
    {
        Service->StopRecording();
    }
    else
    {
        Service->StartRecording();
    }
}
```

<Info>
  LCK buttons include automatic 0.25s cooldown. Don't add your own debouncing.
</Info>

***

### FOnStepperValueChanged

**What it's for:** Handle stepper control changes (increment/decrement buttons)

```cpp theme={null}
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnStepperValueChanged, int8, NewValue);
```

**Parameters:**

* `NewValue` (int8) -- Direction: -1 (decrease) or +1 (increase)

**Usage:**

```cpp theme={null}
FOVStepper->OnStepperValueChanged.AddDynamic(this, &ACamera::AdjustFOV);

UFUNCTION()
void ACamera::AdjustFOV(int8 Direction)
{
    CurrentFOV += Direction * 5.0f; // Adjust by 5 degrees
    CurrentFOV = FMath::Clamp(CurrentFOV, 30.0f, 120.0f);
    UpdateCamera();
}
```

***

### FOnPad2DChanged

**What it's for:** Handle 2D directional pad input

```cpp theme={null}
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnPad2DChanged, FIntPoint, NewValue);
```

**Parameters:**

* `NewValue` (FIntPoint) -- Direction as (X, Y) where values are -1, 0, or +1

**Usage:**

```cpp theme={null}
Pad2D->OnPad2DChanged.AddDynamic(this, &ANavigator::HandleDirection);

UFUNCTION()
void ANavigator::HandleDirection(FIntPoint Direction)
{
    // Direction.X: -1 (left), 0 (none), +1 (right)
    // Direction.Y: -1 (down), 0 (none), +1 (up)

    if (Direction.X != 0)
    {
        // Handle horizontal
        ScrollHorizontal(Direction.X);
    }

    if (Direction.Y != 0)
    {
        // Handle vertical
        ScrollVertical(Direction.Y);
    }
}
```

***

## Audio Delegates

### FOnRenderAudioDelegate

**What it's for:** Process raw audio data from audio sources

```cpp theme={null}
DECLARE_MULTICAST_DELEGATE_FourParams(
    FDelegateRenderAudio,
    TArrayView<const float>, // PCM samples (interleaved)
    int32,                    // Channels
    int32,                    // Sample rate (Hz)
    ELCKAudioChannel          // Source channel type
);

typedef FDelegateRenderAudio::FDelegate FOnRenderAudioDelegate;
```

**Parameters:**

* `PCM` (TArrayView\<const float>) -- Interleaved audio samples (32-bit float)
* `Channels` (int32) -- Number of channels (typically 2 for stereo)
* `SampleRate` (int32) -- Audio sample rate in Hz (typically 48000)
* `Source` (ELCKAudioChannel) -- Audio source type (Game, Microphone, VoiceChat)

<Warning>
  **This is a SINGLE delegate** (not multicast). Use `BindLambda()`, not `AddLambda()`. Each bind replaces the previous one.

  Audio callbacks may come from different threads. Use `AsyncTask(ENamedThreads::GameThread, ...)` if accessing game thread objects.
</Warning>

**Usage:**

```cpp theme={null}
// Bind audio callback (replaces any existing binding)
AudioSource->OnAudioDataDelegate.BindLambda([this](
    TArrayView<const float> PCM,
    int32 Channels,
    int32 SampleRate,
    ELCKAudioChannel SourceChannel)
{
    // PCM format: interleaved 32-bit float samples
    // Example: [L, R, L, R, L, R, ...] for stereo

    // Calculate RMS volume
    float Sum = 0.0f;
    for (float Sample : PCM)
    {
        Sum += Sample * Sample;
    }
    float RMS = FMath::Sqrt(Sum / PCM.Num());

    // Update UI on game thread
    AsyncTask(ENamedThreads::GameThread, [this, RMS]()
    {
        UpdateVolumeIndicator(RMS);
    });
});
```

**Fire the delegate (from audio source implementation):**

```cpp theme={null}
// Inside ILCKAudioSource::Render() or similar
TArray<float> PCMData = GetAudioBuffer();
OnAudioDataDelegate.ExecuteIfBound(PCMData, 2, 48000, ELCKAudioChannel::Game);
```

***

## Async Operation Delegates

### FOnLCKRecorderBoolResult

**What it's for:** Handle async operation results

```cpp theme={null}
DECLARE_DELEGATE_OneParam(FOnLCKRecorderBoolResult, bool /*bSuccess*/);
```

**Usage:**

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

***

### FOnLCKRecorderProgress

**What it's for:** Track save/encoding progress

```cpp theme={null}
DECLARE_DELEGATE_OneParam(FOnLCKRecorderProgress, float /*Progress*/);
```

**Usage:**

```cpp theme={null}
Recorder->StopRecordingAsync(
    FOnLCKRecorderBoolResult::CreateLambda([this](bool bSuccess)
    {
        if (bSuccess)
        {
            ShowSuccess(TEXT("Recording saved"));
        }
    }),
    FOnLCKRecorderProgress::CreateLambda([this](float Progress)
    {
        ProgressBar->SetPercent(Progress);
        StatusText->SetText(FText::Format(
            LOCTEXT("SavingProgress", "Saving... {0}%"),
            FText::AsPercent(Progress)
        ));
    })
);
```

***

## Binding Patterns

### Dynamic Delegates (Blueprint-Compatible)

Used for `FOnRecordingSaveFinished`, `FOnRecordingSaveProgress`, `FOnRecordingError`, and UI delegates like `FOnTapStarted`.

```cpp theme={null}
// Header
UCLASS()
class AMyActor : public AActor
{
    GENERATED_BODY()

    UFUNCTION()
    void HandleButtonTap();
};

// Implementation
void AMyActor::BeginPlay()
{
    Super::BeginPlay();
    Button->OnTapStarted.AddDynamic(this, &AMyActor::HandleButtonTap);
}

void AMyActor::HandleButtonTap()
{
    // Handle tap
}
```

***

### Non-Dynamic Multicast (C++ Only)

Used for DataModel delegates like `FOnTabletCameraModeChanged`, `FOnMicStateChanged`, `FOnVideoQualityChanged`, `FOnRecordStateChange`.

```cpp theme={null}
// Header -- no UFUNCTION() needed
UCLASS()
class AMyActor : public AActor
{
    GENERATED_BODY()

    void HandleCameraMode(UClass* ModeClass);
    void HandleMicState(ELCKMicState NewState);
};

// Implementation
void AMyActor::BeginPlay()
{
    Super::BeginPlay();

    ULCKTabletDataModel* DataModel = Tablet->GetDataModel();

    // Use AddUObject for member functions
    DataModel->OnTabletCameraModeChanged.AddUObject(this, &AMyActor::HandleCameraMode);
    DataModel->OnMicStateChanged.AddUObject(this, &AMyActor::HandleMicState);

    // Or use AddLambda for inline handlers
    DataModel->OnVideoQualityChanged.AddLambda([this](ELCKVideoQuality NewQuality)
    {
        UpdateQualityDisplay(NewQuality);
    });
}
```

***

### Lambda Binding (C++ Only)

```cpp theme={null}
// Non-dynamic multicast delegate (DataModel events)
DataModel->OnTabletCameraModeChanged.AddLambda([this](UClass* ModeClass)
{
    UpdateCameraModeUI(ModeClass);
});

// Dynamic multicast delegate (UI events -- use AddDynamic, not AddLambda)
Button->OnTapStarted.AddDynamic(this, &AMyActor::OnRecordButtonTapped);

// Single delegate (audio)
AudioSource->OnAudioDataDelegate.BindLambda([this](auto PCM, auto Channels, auto SampleRate, auto Source)
{
    ProcessAudio(PCM);
});
```

***

### Removing Bindings

```cpp theme={null}
// Dynamic delegate
Button->OnTapStarted.RemoveDynamic(this, &AMyActor::HandleButtonTap);

// Non-dynamic delegate with handle
FDelegateHandle Handle = DataModel->OnMicStateChanged.AddUObject(this, &AMyActor::HandleMicState);
DataModel->OnMicStateChanged.Remove(Handle);

// Lambda with handle
FDelegateHandle LambdaHandle = Delegate.AddLambda([](){ /* ... */ });
Delegate.Remove(LambdaHandle);

// Clear all
Delegate.Clear();
```

***

## Complete Example: Recording UI

```cpp theme={null}
UCLASS()
class ARecordingUI : public AActor
{
    GENERATED_BODY()

protected:
    virtual void BeginPlay() override
    {
        Super::BeginPlay();

        ULCKService* Service = GetLCKService();
        ALCKTablet* Tablet = FindTablet();

        if (!Service || !Tablet) return;

        ULCKTabletDataModel* DataModel = Tablet->GetDataModel();

        // Recording lifecycle (dynamic multicast -- use AddDynamic)
        Service->OnRecordingSaveFinished.AddDynamic(this, &ARecordingUI::OnSaveFinished);
        Service->OnRecordingSaveProgress.AddDynamic(this, &ARecordingUI::OnSaveProgress);
        Service->OnRecordingError.AddDynamic(this, &ARecordingUI::OnError);

        // State changes (non-dynamic multicast -- use AddUObject)
        DataModel->OnRecordStateChanged.AddUObject(this, &ARecordingUI::OnStateChanged);
        DataModel->OnMicStateChanged.AddUObject(this, &ARecordingUI::OnMicChanged);

        // UI interactions (dynamic multicast -- use AddDynamic)
        RecordButton->OnTapStarted.AddDynamic(this, &ARecordingUI::OnRecordButton);
        MicButton->OnTapStarted.AddDynamic(this, &ARecordingUI::OnMicButton);
    }

    UFUNCTION()
    void OnRecordButton()
    {
        if (Service->IsRecording())
            Service->StopRecording();
        else
            Service->StartRecording();
    }

    UFUNCTION()
    void OnMicButton()
    {
        bool bCurrentState = Service->IsMicrophoneEnabled();
        Service->SetMicrophoneEnabled(!bCurrentState);
    }

    // Non-dynamic delegate handler (no UFUNCTION needed)
    void OnStateChanged(ELCKRecordingState NewState)
    {
        switch (NewState)
        {
            case ELCKRecordingState::Recording:
                RecordButton->SetText(FText::FromString("Stop"));
                RecordingIndicator->SetVisibility(ESlateVisibility::Visible);
                break;
            case ELCKRecordingState::Idle:
                RecordButton->SetText(FText::FromString("Record"));
                RecordingIndicator->SetVisibility(ESlateVisibility::Hidden);
                break;
            case ELCKRecordingState::Saving:
                ProgressPanel->SetVisibility(ESlateVisibility::Visible);
                break;
        }
    }

    UFUNCTION()
    void OnSaveProgress(float Progress)
    {
        ProgressBar->SetPercent(Progress);
    }

    UFUNCTION()
    void OnSaveFinished(bool bSuccess)
    {
        ProgressPanel->SetVisibility(ESlateVisibility::Hidden);

        if (bSuccess)
            ShowNotification(TEXT("Recording saved!"));
    }

    UFUNCTION()
    void OnError(FString ErrorMessage, int32 ErrorCode)
    {
        ShowErrorDialog(ErrorMessage);
    }

    // Non-dynamic delegate handler (no UFUNCTION needed)
    void OnMicChanged(ELCKMicState NewState)
    {
        switch (NewState)
        {
            case ELCKMicState::On:
                MicIcon->SetBrush(MicOnTexture);
                break;
            case ELCKMicState::Off:
                MicIcon->SetBrush(MicOffTexture);
                break;
            case ELCKMicState::No_Access:
                ShowPermissionDialog();
                break;
        }
    }
};
```

***

## Key Takeaways

<Check>
  **Use delegates for events** -- Don't poll state every frame
</Check>

<Check>
  **Dynamic vs non-dynamic matters** -- DataModel delegates use AddUObject/AddLambda, Service delegates use AddDynamic
</Check>

<Check>
  **Audio delegates are SINGLE** -- Use BindLambda, not AddLambda
</Check>

<Check>
  **Thread safety matters** -- Audio callbacks may come from any thread
</Check>

<Check>
  **Remove bindings on cleanup** -- Prevent dangling pointers
</Check>

<Check>
  **Dynamic delegates for Blueprint** -- Use UFUNCTION handlers with AddDynamic
</Check>

***

## Related

* [Error Codes](/api-reference/unreal/errors) -- Error handling patterns
* [Service Interface](/api-reference/unreal/service-interface) -- ULCKService API reference
* [Architecture](/api-reference/unreal/architecture) -- How delegates flow through the system
