record-daemon: initial commit
This commit is contained in:
446
plans/record-daemon-architecture.md
Normal file
446
plans/record-daemon-architecture.md
Normal file
@@ -0,0 +1,446 @@
|
||||
# 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
|
||||
Reference in New Issue
Block a user