Skip to main content

What Problem Does This Solve?

ULCKService is your main entry point for LCK functionality:
  • Start/stop recording
  • Configure quality settings
  • Control audio capture (mic, game audio)
  • Switch camera modes
  • Take photos
Instead of interacting with low-level subsystems, you use this high-level service that handles the complexity for you.

When to Use This

Use ULCKService when:
  • Building custom recording UI
  • Implementing record buttons, settings menus
  • Controlling recording from gameplay code
  • Integrating recording into your game flow
Don’t use this directly if: You’re using the default LCK Tablet UI—it already handles everything.

Accessing the Service

From C++

ULCKSubsystem* Subsystem = GetWorld()->GetSubsystem<ULCKSubsystem>();
if (Subsystem)
{
    ULCKService* Service = Subsystem->GetService();
    // Use service...
}

From Blueprint

UFUNCTION(BlueprintCallable, Category = "LCK")
ULCKService* GetLCKService()
{
    if (UWorld* World = GetWorld())
    {
        if (ULCKSubsystem* Subsystem = World->GetSubsystem<ULCKSubsystem>())
        {
            return Subsystem->GetService();
        }
    }
    return nullptr;
}
Blueprint usage: Blueprint Get Service

Recording Methods

StartRecording / StopRecording (Synchronous)

// Start recording
bool bSuccess = Service->StartRecording();
if (!bSuccess)
{
    UE_LOG(LogLCK, Error, TEXT("Failed to start recording"));
}

// Stop recording
bool bSuccess = Service->StopRecording();
Returns: bool — True if operation succeeded
Synchronous methods don’t provide detailed error feedback. Use async methods for better error handling.

// Start with callback
Service->StartRecordingAsync(
    FOnLCKRecorderBoolResult::CreateLambda([this](bool bSuccess)
    {
        if (bSuccess)
        {
            ShowRecordingIndicator();
            UE_LOG(LogLCK, Log, TEXT("Recording started"));
        }
        else
        {
            ShowError(TEXT("Failed to start recording"));
        }
    })
);

// Stop with progress callback
Service->StopRecordingAsync(
    FOnLCKRecorderBoolResult::CreateLambda([this](bool bSuccess)
    {
        if (bSuccess)
        {
            ShowNotification(TEXT("Recording saved!"));
        }
    }),
    FOnLCKRecorderProgress::CreateLambda([this](float Progress)
    {
        // Progress: 0.0 to 1.0
        ProgressBar->SetPercent(Progress);
    })
);
Why async is better:
  • Get detailed success/failure callbacks
  • Track save progress
  • Non-blocking—doesn’t freeze game
  • Better error handling

IsRecording / GetRecordingDuration

// Check if currently recording
bool bIsRecording = Service->IsRecording();

// Get recording duration in seconds
float Duration = Service->GetRecordingDuration();

// Update UI
if (bIsRecording)
{
    FTimespan Time = FTimespan::FromSeconds(Duration);
    TimerText->SetText(FText::Format(
        LOCTEXT("RecordingTime", "{0}:{1:02}"),
        Time.GetMinutes(),
        Time.GetSeconds()
    ));
}

Photo Capture

// Take a photo (synchronous)
Service->TakePhoto();

// Listen for completion
Service->OnPhotoSaved.AddLambda([](const FString& PhotoPath)
{
    UE_LOG(LogLCK, Log, TEXT("Photo saved: %s"), *PhotoPath);
    ShowNotification(FString::Printf(TEXT("Photo saved to %s"), *PhotoPath));
});
Photo format: PNG, saved to platform-specific photos directory

Preview Mode

Preview mode shows the camera output without recording (no file is saved).
// Start preview
Service->StartPreview();

// Stop preview
Service->StopPreview();

// Check if previewing
bool bPreviewing = Service->IsPreviewActive();
When to use preview mode:
  • Let users adjust camera settings before recording
  • Show “what you see is what you get” preview
  • Test camera placement without wasting storage

Quality Configuration

SetQualityProfile

// Change quality preset
Service->SetQualityProfile(ELCKVideoQuality::HD);

// Available presets:
// - ELCKVideoQuality::SD      (720p, 4 Mbps)
// - ELCKVideoQuality::HD      (1080p, 8 Mbps)
// - ELCKVideoQuality::TWO_K   (1440p, 12 Mbps)
// - ELCKVideoQuality::FOUR_K  (2160p, 20 Mbps)
Cannot change quality while recording. Check IsRecording() first.
Quality selection UI example:
UCLASS()
class AQualityMenu : public AActor
{
    GENERATED_BODY()
    
public:
    UFUNCTION()
    void OnQualityButton(ELCKVideoQuality Quality)
    {
        if (Service->IsRecording())
        {
            ShowError(TEXT("Cannot change quality while recording"));
            return;
        }
        
        Service->SetQualityProfile(Quality);
        UpdateUI(Quality);
    }
};

GetCurrentQuality

ELCKVideoQuality Current = Service->GetCurrentQuality();

switch (Current)
{
    case ELCKVideoQuality::SD:
        QualityText->SetText(FText::FromString("SD (720p)"));
        break;
    case ELCKVideoQuality::HD:
        QualityText->SetText(FText::FromString("HD (1080p)"));
        break;
    // ...
}

Orientation

// Set orientation
Service->SetOrientation(ELCKScreenOrientation::Portrait);  // 9:16
Service->SetOrientation(ELCKScreenOrientation::Landscape); // 16:9

// Get current
ELCKScreenOrientation Current = Service->GetOrientation();
Use cases:
  • Landscape: YouTube, traditional video
  • Portrait: TikTok, Instagram Stories
Cannot change orientation while recording.

Camera Modes

SetCameraMode

// Switch camera mode
Service->SetCameraMode(ELCKCameraMode::Selfie);
Service->SetCameraMode(ELCKCameraMode::FirstPerson);
Service->SetCameraMode(ELCKCameraMode::ThirdPerson);
Camera mode descriptions:
ModeDescriptionUse Case
SelfieFront/back facing camera on tabletVlog-style, show player’s face
FirstPersonPOV from HMD positionGameplay perspective
ThirdPersonOrbital camera following playerCinematic, show player character
Example: Camera mode switcher
void ACameraSwitcher::OnModeButton(int32 ModeIndex)
{
    switch (ModeIndex)
    {
        case 0:
            Service->SetCameraMode(ELCKCameraMode::Selfie);
            ModeText->SetText(FText::FromString("Selfie"));
            break;
        case 1:
            Service->SetCameraMode(ELCKCameraMode::FirstPerson);
            ModeText->SetText(FText::FromString("First Person"));
            break;
        case 2:
            Service->SetCameraMode(ELCKCameraMode::ThirdPerson);
            ModeText->SetText(FText::FromString("Third Person"));
            break;
    }
}

Audio Control

Microphone

// Enable/disable microphone
Service->SetMicrophoneEnabled(true);

// Get current state
bool bMicEnabled = Service->IsMicrophoneEnabled();

// Get volume level (0.0 to 1.0)
float MicLevel = Service->GetMicrophoneVolume();
Volume indicator example:
void AMicIndicator::Tick(float DeltaTime)
{
    Super::Tick(DeltaTime);
    
    float Volume = Service->GetMicrophoneVolume();
    VolumeBar->SetPercent(Volume);
    
    // Visual feedback
    if (Volume > 0.7f)
    {
        VolumeBar->SetFillColorAndOpacity(FLinearColor::Red);
    }
    else if (Volume > 0.3f)
    {
        VolumeBar->SetFillColorAndOpacity(FLinearColor::Yellow);
    }
    else
    {
        VolumeBar->SetFillColorAndOpacity(FLinearColor::Green);
    }
}

Game Audio

// Enable/disable game audio capture
Service->SetGameAudioEnabled(true);

// Get current state
bool bGameAudioEnabled = Service->IsGameAudioEnabled();
Audio source priority: FMOD > Wwise > Unreal Audio (only one active at a time)

State Tracking via Data Model

ULCKService does not expose state change delegates directly. Use ULCKTabletDataModel for state notifications.

Get the Data Model

void UMyComponent::BeginPlay()
{
    Super::BeginPlay();
    
    // Find tablet in world
    ALCKTablet* Tablet = Cast<ALCKTablet>(
        UGameplayStatics::GetActorOfClass(GetWorld(), ALCKTablet::StaticClass())
    );
    
    if (Tablet)
    {
        ULCKTabletDataModel* DataModel = Tablet->GetDataModel();
        
        // Subscribe to state changes
        DataModel->OnRecordingStateChanged.AddDynamic(
            this, &UMyComponent::HandleStateChanged
        );
    }
}

Available Data Model Events

EventTypeDescription
OnRecordingStateChangedFOnRecordingStateChangedRecording state changed (Idle, Recording, Saving)
OnCameraModeChangedFOnCameraModeChangedCamera mode changed
OnMicStateChangedFOnMicStateChangedMicrophone state changed (On, Off, No_Access)
OnQualityChangedFOnQualityChangedQuality profile changed
Example: State-driven UI
UFUNCTION()
void URecordingUI::HandleStateChanged(ELCKRecordingState NewState)
{
    switch (NewState)
    {
        case ELCKRecordingState::Idle:
            RecordButton->SetText(FText::FromString("Start Recording"));
            RecordButton->SetIsEnabled(true);
            ProgressPanel->SetVisibility(ESlateVisibility::Collapsed);
            break;
            
        case ELCKRecordingState::Recording:
            RecordButton->SetText(FText::FromString("Stop Recording"));
            RecordButton->SetIsEnabled(true);
            RecordingIndicator->SetVisibility(ESlateVisibility::Visible);
            break;
            
        case ELCKRecordingState::Saving:
            RecordButton->SetIsEnabled(false);
            ProgressPanel->SetVisibility(ESlateVisibility::Visible);
            StatusText->SetText(FText::FromString("Saving..."));
            break;
            
        case ELCKRecordingState::Error:
            RecordButton->SetIsEnabled(true);
            ShowErrorDialog();
            break;
    }
}

Complete Example: Recording Manager

UCLASS()
class URecordingManager : public UActorComponent
{
    GENERATED_BODY()
    
protected:
    UPROPERTY()
    ULCKService* Service;
    
    UPROPERTY()
    ULCKTabletDataModel* DataModel;
    
public:
    virtual void BeginPlay() override
    {
        Super::BeginPlay();
        
        // Get service
        Service = GetLCKService();
        if (!Service)
        {
            UE_LOG(LogLCK, Error, TEXT("LCK Service not available"));
            return;
        }
        
        // Get data model
        ALCKTablet* Tablet = FindTablet();
        if (Tablet)
        {
            DataModel = Tablet->GetDataModel();
            DataModel->OnRecordingStateChanged.AddDynamic(
                this, &URecordingManager::OnStateChanged
            );
        }
        
        // Subscribe to events
        Service->OnRecordingSaveFinished.AddDynamic(
            this, &URecordingManager::OnSaveFinished
        );
        Service->OnRecordingError.AddDynamic(
            this, &URecordingManager::OnError
        );
    }
    
    UFUNCTION(BlueprintCallable)
    void ToggleRecording()
    {
        if (Service->IsRecording())
        {
            StopRecording();
        }
        else
        {
            StartRecording();
        }
    }
    
    void StartRecording()
    {
        if (Service->IsRecording())
        {
            UE_LOG(LogLCK, Warning, TEXT("Already recording"));
            return;
        }
        
        Service->StartRecordingAsync(
            FOnLCKRecorderBoolResult::CreateLambda([this](bool bSuccess)
            {
                if (bSuccess)
                {
                    OnRecordingStarted();
                }
                else
                {
                    OnRecordingFailed();
                }
            })
        );
    }
    
    void StopRecording()
    {
        if (!Service->IsRecording())
        {
            return;
        }
        
        Service->StopRecordingAsync(
            FOnLCKRecorderBoolResult::CreateLambda([this](bool bSuccess)
            {
                OnRecordingStopped(bSuccess);
            }),
            FOnLCKRecorderProgress::CreateLambda([this](float Progress)
            {
                OnSaveProgress(Progress);
            })
        );
    }
    
    UFUNCTION()
    void OnStateChanged(ELCKRecordingState NewState)
    {
        // Update UI based on state
        BroadcastStateChange(NewState);
    }
    
    UFUNCTION()
    void OnSaveFinished(bool bSuccess)
    {
        if (bSuccess)
        {
            ShowNotification(TEXT("Recording saved!"));
            PlaySuccessSound();
        }
        else
        {
            ShowError(TEXT("Failed to save recording"));
        }
    }
    
    UFUNCTION()
    void OnError(FString ErrorMessage, int32 ErrorCode)
    {
        UE_LOG(LogLCK, Error, TEXT("Recording error %d: %s"), ErrorCode, *ErrorMessage);
        ShowErrorDialog(ErrorMessage);
    }
    
private:
    void OnRecordingStarted() { /* Broadcast event */ }
    void OnRecordingFailed() { /* Show error */ }
    void OnRecordingStopped(bool bSuccess) { /* Handle completion */ }
    void OnSaveProgress(float Progress) { /* Update progress bar */ }
    
    ULCKService* GetLCKService()
    {
        if (UWorld* World = GetWorld())
        {
            if (ULCKSubsystem* Subsystem = World->GetSubsystem<ULCKSubsystem>())
            {
                return Subsystem->GetService();
            }
        }
        return nullptr;
    }
    
    ALCKTablet* FindTablet()
    {
        return Cast<ALCKTablet>(
            UGameplayStatics::GetActorOfClass(GetWorld(), ALCKTablet::StaticClass())
        );
    }
};

Key Takeaways

Get via ULCKSubsystem — Don’t create or cache manually
Use async methods — Better error handling and progress tracking
Check IsRecording() — Before changing settings or starting
Use DataModel for state events — Service doesn’t expose state delegates
Don’t modify during recording — Quality, orientation locked while active