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
  • 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:
  • Pulsing red record dot
  • Recording duration timer
  • 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:
  1. Overlap Begin - Hand enters button collision
  2. 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
DataModel->OnRecordingStateChanged.AddDynamic(
    this,
    &UMyComponent::HandleStateChange
);

void UMyComponent::HandleStateChange(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;
    }
}

Reactive Properties

The data model uses reactive properties that broadcast on change:
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(
    FOnRecordingStateChanged,
    ELCKRecordingState, NewState
);

UPROPERTY(BlueprintAssignable)
FOnRecordingStateChanged OnRecordingStateChanged;

UPROPERTY(BlueprintAssignable)
FOnCameraModeChanged OnCameraModeChanged;

UPROPERTY(BlueprintAssignable)
FOnQualityChanged OnQualityChanged;

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->OnRecordingStateChanged.AddDynamic(
                this, &UCustomRecordingUI::OnStateChanged
            );
        }
    }

    UFUNCTION()
    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
  • 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