447 lines
12 KiB
Markdown
447 lines
12 KiB
Markdown
# 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
|
|
|
|
```mermaid
|
|
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:
|
|
|
|
```mermaid
|
|
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
|
|
|
|
```mermaid
|
|
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
|
|
|
|
```mermaid
|
|
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.
|
|
|
|
```rust
|
|
// 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.
|
|
|
|
```rust
|
|
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.
|
|
|
|
```rust
|
|
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.
|
|
|
|
```rust
|
|
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.
|
|
|
|
```rust
|
|
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
|
|
|
|
```toml
|
|
[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:
|
|
|
|
```json
|
|
{
|
|
"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
|
|
|
|
```rust
|
|
#[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
|