Skip to main content

Overview

LCK UI components automatically reflect the current recording state. This page covers recording state enumeration, visual feedback per state, state transitions, and synchronizing custom UI with LCK state.

ELCKRecordingState

The recording system has six states:
StateDescriptionUI Behavior
IdleReady to recordDefault colors, all buttons enabled
RecordingActively capturingRed record indicator, timer visible
SavingWriting file to diskProgress indicator visible
ProcessingFinalizing videoProgress indicator, buttons disabled
ErrorSomething went wrongError indicator, retry available
PausedRecording pausedPause indicator, resume available
UENUM(BlueprintType)
enum class ELCKRecordingState : uint8
{
    Idle        UMETA(DisplayName = "Idle"),
    Recording   UMETA(DisplayName = "Recording"),
    Saving      UMETA(DisplayName = "Saving"),
    Processing  UMETA(DisplayName = "Processing"),
    Error       UMETA(DisplayName = "Error"),
    Paused      UMETA(DisplayName = "Paused")
};

State Machine

                    StartRecording()
           ┌────────────────────────────┐
           │                            │
           v                            │
     ┌─────────┐                  ┌─────────┐
     │  Idle   │                  │Recording│◄──────┐
     └─────────┘                  └─────────┘       │
           ^                       │      │        │
           │                Pause()│      │Resume()│
           │                       v      │        │
           │                  ┌──────────┐│        │
           │                  │  Paused  │┘        │
           │                  └──────────┘         │
           │                            │          │
           │         StopRecording()    │          │
           │                            v          │
     ┌─────────┐                  ┌──────────┐     │
     │  Error  │<─────────────────│  Saving  │     │
     └─────────┘   Save Failed    └──────────┘     │
           │                            │          │
           │                            v          │
           │                    ┌──────────┐       │
           │                    │Processing│       │
           │                    └──────────┘       │
           │                            │          │
           └────────────────────────────┴──────────┘
                     Complete

Visual Feedback

Default State Colors

StatePrimary ColorRecommended Hex
IdleBlue/Gray#5E45FF / #808080
RecordingRed#FF4444
SavingYellow/Orange#FFAA00
ProcessingYellow/Orange#FFAA00
ErrorRed/Dark#CC0000
PausedGray/Blue#666699

Recording Indicator

During recording, the UI shows a pulsing red record dot, recording duration timer, and microphone level meter.
void ULCKRecordingIndicator::UpdateRecordingState(ELCKRecordingState State)
{
    switch (State)
    {
        case ELCKRecordingState::Idle:
            RecordDot->SetVisibility(ESlateVisibility::Hidden);
            TimerText->SetVisibility(ESlateVisibility::Hidden);
            ProgressBar->SetVisibility(ESlateVisibility::Hidden);
            break;

        case ELCKRecordingState::Recording:
            RecordDot->SetVisibility(ESlateVisibility::Visible);
            TimerText->SetVisibility(ESlateVisibility::Visible);
            StartPulseAnimation();
            break;

        case ELCKRecordingState::Paused:
            RecordDot->SetVisibility(ESlateVisibility::Visible);
            TimerText->SetVisibility(ESlateVisibility::Visible);
            StopPulseAnimation();  // Static indicator when paused
            break;

        case ELCKRecordingState::Saving:
        case ELCKRecordingState::Processing:
            RecordDot->SetVisibility(ESlateVisibility::Hidden);
            ProgressBar->SetVisibility(ESlateVisibility::Visible);
            break;

        case ELCKRecordingState::Error:
            ErrorIcon->SetVisibility(ESlateVisibility::Visible);
            break;
    }
}

Button State Transitions

Cooldown Timer

Buttons have a cooldown to prevent accidental double-presses:
// Default cooldown: 0.25 seconds
static constexpr float ButtonCooldownTime = 0.25f;

void ULCKButton::OnPressed()
{
    if (bIsOnCooldown)
    {
        return;  // Ignore press during cooldown
    }

    // Execute button action
    OnButtonPressed.Broadcast();

    // Start cooldown
    bIsOnCooldown = true;
    GetWorld()->GetTimerManager().SetTimer(
        CooldownHandle,
        this,
        &ULCKButton::EndCooldown,
        ButtonCooldownTime
    );
}

Two-Phase Interaction

LCK buttons use a two-phase interaction model: Overlap Begin (hand enters button collision) and Overlap End (hand exits button collision).
void ULCKButton::OnOverlapBegin(AActor* OtherActor)
{
    // Validate it's a hand
    if (!OtherActor->ActorHasTag(TEXT("Hand")))
    {
        return;
    }

    // Check touch direction (dot product validation)
    FVector TouchDirection = GetTouchDirection(OtherActor);
    if (FVector::DotProduct(TouchDirection, GetForwardVector()) > 0.5f)
    {
        // Valid front-facing touch
        SetState(ELCKButtonState::Pressed);
        OnPressed();
    }
}

Subscribing to State Changes

ULCKTabletDataModel

The tablet data model provides state change delegates:
// Get data model
ULCKTabletDataModel* DataModel = Tablet->GetDataModel();

// Subscribe to state changes (non-dynamic delegate — use AddLambda or AddUObject)
DataModel->OnRecordStateChanged.AddLambda([this](ELCKRecordingState NewState)
{
    switch (NewState)
    {
        case ELCKRecordingState::Recording:
            // Show recording UI
            break;
        case ELCKRecordingState::Saving:
        case ELCKRecordingState::Processing:
            // Show progress bar
            break;
        case ELCKRecordingState::Paused:
            // Show pause indicator
            break;
        case ELCKRecordingState::Error:
            // Show error message
            break;
        case ELCKRecordingState::Idle:
            // Reset to default state
            break;
    }
});

// Or bind to a UObject member function
DataModel->OnRecordStateChanged.AddUObject(this, &UMyComponent::HandleStateChange);

Reactive Properties

The data model uses non-dynamic multicast delegates that broadcast on change:
DECLARE_MULTICAST_DELEGATE_OneParam(FOnRecordStateChage, ELCKRecordingState);
DECLARE_MULTICAST_DELEGATE_OneParam(FOnMicStateChanged, ELCKMicState);
DECLARE_MULTICAST_DELEGATE_OneParam(FOnScreenOrientationChanged, ELCKScreenOrientation);
DECLARE_MULTICAST_DELEGATE_OneParam(FOnVideoQualityChanged, ELCKVideoQuality);
These are non-dynamic delegates. Use AddLambda() or AddUObject() to subscribe — NOT AddDynamic().

Synchronizing Custom UI

Example: Custom Recording Indicator

UCLASS()
class UCustomRecordingUI : public UUserWidget
{
    GENERATED_BODY()

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

        // Find tablet and subscribe
        ALCKTablet* Tablet = FindLCKTablet();
        if (Tablet)
        {
            ULCKTabletDataModel* DataModel = Tablet->GetDataModel();
            DataModel->OnRecordStateChanged.AddUObject(
                this, &UCustomRecordingUI::OnStateChanged
            );
        }
    }

    void OnStateChanged(ELCKRecordingState NewState)
    {
        // Update custom UI
        RecordingIcon->SetBrushFromTexture(
            GetIconForState(NewState)
        );

        StatusText->SetText(
            GetTextForState(NewState)
        );
    }

private:
    UPROPERTY(meta = (BindWidget))
    UImage* RecordingIcon;

    UPROPERTY(meta = (BindWidget))
    UTextBlock* StatusText;
};

Example: Duration Timer

void URecordingTimer::Tick(float DeltaTime)
{
    if (ULCKService* Service = GetLCKService())
    {
        if (Service->IsRecording())
        {
            float Duration = Service->GetRecordingDuration();

            // Format as MM:SS
            int32 Minutes = FMath::FloorToInt(Duration / 60.0f);
            int32 Seconds = FMath::FloorToInt(FMath::Fmod(Duration, 60.0f));

            TimerText->SetText(FText::FromString(
                FString::Printf(TEXT("%02d:%02d"), Minutes, Seconds)
            ));
        }
    }
}

State Persistence

Recording state does NOT persist across sessions. On game start, state is always Idle. Settings that DO persist: last used camera mode, last used quality profile, and audio configuration.
// Load saved settings on startup
void ALCKTablet::BeginPlay()
{
    Super::BeginPlay();

    // Recording state always starts Idle
    DataModel->SetRecordingState(ELCKRecordingState::Idle);

    // But camera mode is loaded from saved settings
    DataModel->SetCameraMode(LoadSavedCameraMode());
}

Error Handling

Error State Recovery

void ULCKTabletDataModel::HandleRecordingError(ELCKError Error)
{
    // Transition to error state
    SetRecordingState(ELCKRecordingState::Error);

    // Broadcast error for UI to display
    OnRecordingError.Broadcast(Error);

    // After timeout, return to Idle
    FTimerHandle Handle;
    GetWorld()->GetTimerManager().SetTimer(
        Handle,
        [this]() { SetRecordingState(ELCKRecordingState::Idle); },
        3.0f,   // 3 second error display
        false
    );
}

See Also