Files
leaguerecorder/plans/record-daemon-architecture.md

12 KiB

Record Daemon Architecture

Overview

The record-daemon is a high-performance Rust daemon that automatically records League of Legends matches using libobs, captures game events via the League Client API (LQP), and exposes configuration via a Unix socket IPC for the Tauri app.

High-Level Architecture

flowchart TB
    subgraph Daemon[Record Daemon]
        direction TB
        Main[Main Event Loop]
        State[State Machine]
        
        subgraph Core[Core Modules]
            Config[Configuration Manager]
            LQP[LQP Client]
            OBS[libobs Recording Engine]
            IPC[IPC Server]
            Timeline[Event Timeline Store]
        end
    end
    
    subgraph External[External Systems]
        LC[League Client]
        Game[League of Legends Game]
        Tauri[Tauri App]
        FS[File System]
    end
    
    LQP -->|WebSocket| LC
    OBS -->|Game Capture| Game
    IPC -->|Unix Socket| Tauri
    Config -->|JSON/TOML| FS
    Timeline -->|SQLite/JSON| FS
    OBS -->|Video Files| FS
    
    Main --> State
    State --> Config
    State --> LQP
    State --> OBS
    State --> IPC
    State --> Timeline

Module Structure

record-daemon/
├── Cargo.toml
├── src/
│   ├── main.rs              # Entry point, daemon setup
│   ├── lib.rs               # Library exports
│   ├── config/
│   │   ├── mod.rs           # Config module exports
│   │   ├── settings.rs      # User-configurable settings
│   │   ├── presets.rs       # Encoding presets (quality/performance)
│   │   └── persistence.rs   # Config file I/O
│   ├── lqp/
│   │   ├── mod.rs           # LQP module exports
│   │   ├── client.rs        # LQP WebSocket client
│   │   ├── events.rs        # Game event types and parsing
│   │   └── auth.rs          # LQP authentication (port/password from lockfile)
│   ├── recording/
│   │   ├── mod.rs           # Recording module exports
│   │   ├── obs_context.rs   # libobs initialization and context
│   │   ├── capture.rs       # Game capture source setup
│   │   ├── encoder.rs       # Video encoder configuration
│   │   └── output.rs        # File output configuration
│   ├── ipc/
│   │   ├── mod.rs           # IPC module exports
│   │   ├── server.rs        # Unix socket server
│   │   ├── protocol.rs      # Request/response protocol
│   │   └── handlers.rs      # Command handlers
│   ├── timeline/
│   │   ├── mod.rs           # Timeline module exports
│   │   ├── store.rs         # Event storage backend
│   │   └── mapper.rs        # Event-to-video timestamp mapping
│   ├── state/
│   │   ├── mod.rs           # State module exports
│   │   ├── machine.rs       # Daemon state machine
│   │   └── transitions.rs   # State transition logic
│   └── error.rs             # Error types and handling
└── tests/
    └── ...

State Machine

The daemon operates as a finite state machine with the following states:

stateDiagram-v2
    [*] --> Idle: Daemon Start
    
    Idle --> Monitoring: League Client Detected
    Monitoring --> Idle: League Client Closed
    
    Monitoring --> Recording: Match Started
    Recording --> Monitoring: Match Ended
    
    Recording --> Recording: Event Received
    
    Idle --> Error: Fatal Error
    Monitoring --> Error: Fatal Error
    Recording --> Error: Fatal Error
    
    Error --> Idle: Recovery

State Descriptions

State Description
Idle Daemon is running but League Client is not detected
Monitoring League Client is running, waiting for match to start
Recording Active recording in progress, capturing events
Error Recoverable error state, attempting recovery

Data Flow

Match Recording Flow

sequenceDiagram
    participant D as Daemon
    participant LC as League Client
    participant OBS as libobs
    participant TL as Timeline
    participant FS as FileSystem
    
    Note over D: State: Monitoring
    LC->>D: MatchFound event via LQP
    D->>OBS: Initialize recording context
    D->>FS: Create output file
    
    Note over D: State: Recording
    LC->>D: GameStart event
    D->>OBS: Start recording
    D->>TL: Record start timestamp
    
    loop Game Events
        LC->>D: Kill/Death/Objective events
        D->>TL: Store event with timestamp
    end
    
    LC->>D: GameEnd event
    D->>OBS: Stop recording
    D->>TL: Finalize timeline
    D->>FS: Save timeline metadata
    
    Note over D: State: Monitoring

IPC Communication Flow

sequenceDiagram
    participant T as Tauri App
    participant IPC as IPC Server
    participant D as Daemon Core
    participant C as Config Manager
    
    T->>IPC: Connect via Unix Socket
    T->>IPC: GetSettings request
    IPC->>D: Handle request
    D->>C: Read current settings
    C-->>D: Settings data
    D-->>IPC: Settings response
    IPC-->>T: JSON response
    
    T->>IPC: UpdateSettings request
    IPC->>D: Handle request
    D->>C: Validate and save
    C-->>D: Success
    D-->>IPC: Confirmation
    IPC-->>T: Success response

Key Components

1. Configuration Module

Handles user-configurable settings with hot-reload support.

// Key configuration structures
struct Settings {
    output_path: PathBuf,
    encoder_preset: EncoderPreset,
    frame_rate: u32,
    quality: QualityLevel,
    audio_capture: AudioSettings,
}

enum EncoderPreset {
    Nvenc { bitrate: u32, cq_level: u32 },
    Amf { bitrate: u32 },
    X264 { preset: String, bitrate: u32 },
}

enum QualityLevel {
    Low,    // 720p30, lower bitrate
    Medium, // 1080p30, moderate bitrate
    High,   // 1080p60, high bitrate
    Ultra,  // 1440p60, max quality
}

2. LQP Client

Connects to the League Client via WebSocket using credentials from the lockfile.

struct LqpClient {
    port: u16,
    password: String,
    websocket: Option<WebSocket>,
}

// Key events to capture
enum GameEvent {
    MatchFound(MatchInfo),
    GameStart(GameStartInfo),
    Kill(KillEvent),
    Death(DeathEvent),
    Objective(ObjectiveEvent),
    GameEnd(GameEndInfo),
}

3. libobs Recording Engine

Manages OBS context, capture sources, and encoding.

struct ObsRecordingEngine {
    context: ObsContext,
    video_encoder: Box<dyn VideoEncoder>,
    audio_encoder: Box<dyn AudioEncoder>,
    output: Box<dyn Output>,
    active: bool,
}

impl ObsRecordingEngine {
    fn initialize(config: &Settings) -> Result<Self>;
    fn start_recording(&mut self, output_path: &Path) -> Result<()>;
    fn stop_recording(&mut self) -> Result<RecordingResult>;
    fn get_current_timestamp(&self) -> Duration;
}

4. IPC Server

Unix socket server for communication with Tauri app.

struct IpcServer {
    socket_path: PathBuf,
    listener: UnixListener,
    handlers: HashMap<IpcCommand, CommandHandler>,
}

enum IpcCommand {
    GetSettings,
    UpdateSettings,
    GetStatus,
    GetRecordings,
    GetTimeline { recording_id: Uuid },
    StartRecording,
    StopRecording,
}

5. Event Timeline Store

Stores game events mapped to video timestamps.

struct TimelineStore {
    backend: StorageBackend,
}

struct RecordingMetadata {
    id: Uuid,
    game_id: u64,
    start_time: DateTime<Utc>,
    end_time: Option<DateTime<Utc>>,
    output_file: PathBuf,
    events: Vec<TimestampedEvent>,
}

struct TimestampedEvent {
    video_timestamp: Duration,  // Offset from recording start
    game_timestamp: Duration,   // Game time
    event: GameEvent,
}

Performance Considerations

Memory Management

  • Use Arc<RwLock<T>> for shared state between modules
  • Pre-allocate buffers for video encoding
  • Use crossbeam channels for inter-thread communication
  • Implement backpressure on event channels

CPU Efficiency

  • Run libobs on dedicated thread
  • Use async/await with tokio for I/O operations
  • Minimize allocations in hot paths
  • Use parking_lot locks for better performance

GPU Acceleration

  • Prioritize NVENC/AMF hardware encoding
  • Fall back to software encoding only if unavailable
  • Configure GPU-based video capture

Disk I/O

  • Use buffered writes for video output
  • Store timeline data in memory during recording
  • Flush to disk only on recording end
  • Consider SSD for output directory

Dependencies

[dependencies]
# Async runtime
tokio = { version = "1", features = ["full"] }

# Serialization
serde = { version = "1", features = ["derive"] }
serde_json = "1"
toml = "0.8"

# libobs bindings
libobs-rs = "0.1"  # or appropriate version

# WebSocket for LQP
tokio-tungstenite = "0.21"

# HTTP client for LQP REST API
reqwest = { version = "0.11", features = ["json", "rustls-tls"] }

# Async utilities
futures = "0.3"
async-trait = "0.1"

# Error handling
thiserror = "1"
anyhow = "1"

# Logging
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }

# UUID for recording IDs
uuid = { version = "1", features = ["v4", "serde"] }

# Date/time
chrono = { version = "0.4", features = ["serde"] }

# Locking primitives
parking_lot = "0.12"

# Crossbeam channels
crossbeam = "0.8"

# Unix socket IPC
tokio-util = { version = "0.7", features = ["codec"] }

# Configuration directories
directories = "5"

# CLI (optional, for debugging)
clap = { version = "4", features = ["derive"] }

IPC Protocol Specification

Message Format

All messages are JSON-encoded with the following structure:

{
  "type": "request|response|notification",
  "id": "uuid-v4",
  "command": "CommandName",
  "payload": { ... }
}

Commands

Command Direction Payload Description
GetSettings Request → {} Get current settings
GetSettings ← Response Settings Current settings object
UpdateSettings Request → Settings Update settings
UpdateSettings ← Response { "success": true } Confirmation
GetStatus Request → {} Get daemon status
GetStatus ← Response DaemonStatus Current state and info
GetRecordings Request → {} List all recordings
GetRecordings ← Response [RecordingMetadata] Recording list
GetTimeline Request → { "recording_id": "uuid" } Get specific timeline
GetTimeline ← Response Timeline Event timeline
RecordingStarted ← Notification RecordingInfo Recording started
RecordingStopped ← Notification RecordingResult Recording ended
GameEvent ← Notification GameEvent Live game event

Error Handling Strategy

#[derive(Debug, thiserror::Error)]
pub enum DaemonError {
    #[error("LQP connection failed: {0}")]
    LqpConnection(#[from] LqpError),
    
    #[error("Recording error: {0}")]
    Recording(#[from] RecordingError),
    
    #[error("Configuration error: {0}")]
    Config(#[from] ConfigError),
    
    #[error("IPC error: {0}")]
    Ipc(#[from] IpcError),
    
    #[error("IO error: {0}")]
    Io(#[from] std::io::Error),
}

Next Steps

  1. Set up Cargo.toml with all required dependencies
  2. Implement configuration module first (needed by all other modules)
  3. Implement LQP client for game detection and event capture
  4. Implement libobs recording engine
  5. Implement IPC server
  6. Implement timeline storage
  7. Wire everything together in the main daemon loop
  8. Add comprehensive logging and error handling
  9. Write tests and documentation