Skip to content

Commit 8c74c9d

Browse files
author
g
committed
feat: enhance code documentation and add comprehensive unit tests
- Add detailed rustdoc comments to all public APIs and structs - Improve error messages with actionable guidance and context - Optimize release profile with size optimizations and LTO - Add unit tests for command parsing and validation - Update AGENTS.md with official OBS WebSocket spec reference
1 parent c090d0f commit 8c74c9d

File tree

8 files changed

+709
-33
lines changed

8 files changed

+709
-33
lines changed

AGENTS.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
- **obws**: OBS WebSocket client library
66
- **clap**: Command-line argument parsing
77
- **tokio**: Async runtime
8+
- The official obs-websocket spec https://raw.githubusercontent.com/obsproject/obs-websocket/master/docs/generated/protocol.md
89

910
## Project Structure
1011
```

Cargo.lock

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,23 @@ version = "0.20.3"
44
edition = "2021"
55
description = "A minimal command to control obs via obs-websocket"
66
authors = ["Luigi Maselli <luigi@grigio.org>"]
7-
rust-version = "1.88.0"
7+
88
license = "MIT"
99

1010
[[bin]]
1111
name = "obs-cmd"
1212

1313
[dependencies]
14-
tokio = { version = "1.42", features = ["rt-multi-thread", "macros", "time"] }
14+
tokio = { version = "1.42", features = ["rt-multi-thread", "macros", "time"], default-features = false }
1515
obws = "0.14"
1616
clap = { version = "4.5", features = ["derive"] }
1717
url = "2.5"
1818
time = "0.3.37"
1919
thiserror = "2.0"
20+
21+
[profile.release]
22+
opt-level = "z"
23+
lto = true
24+
codegen-units = 1
25+
panic = "abort"
26+
strip = true

src/cli.rs

Lines changed: 57 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,17 @@ use std::path::PathBuf;
33
use std::str::FromStr;
44
use url::Url;
55

6+
/// OBS WebSocket connection configuration.
7+
///
8+
/// This struct represents the connection parameters for connecting
9+
/// to an OBS WebSocket server, typically parsed from a URL string.
610
#[derive(Clone, Debug)]
711
pub struct ObsWebsocket {
12+
/// Hostname or IP address of the OBS WebSocket server
813
pub hostname: String,
14+
/// Port number where OBS WebSocket is listening
915
pub port: u16,
16+
/// Optional password for OBS WebSocket authentication
1017
pub password: Option<String>,
1118
}
1219

@@ -106,19 +113,48 @@ pub enum SceneCollection {
106113
Switch { scene_collection_name: String },
107114
}
108115

116+
/// Command-line interface for obs-cmd.
117+
///
118+
/// This struct defines the main CLI interface using clap for parsing.
119+
/// It supports connecting to OBS WebSocket and executing various commands.
120+
///
121+
/// # Examples
122+
///
123+
/// ```bash
124+
/// # Get OBS version info
125+
/// obs-cmd info
126+
///
127+
/// # Start recording
128+
/// obs-cmd recording start
129+
///
130+
/// # Switch to a scene
131+
/// obs-cmd scene switch "Main Scene"
132+
///
133+
/// # Connect to custom OBS WebSocket
134+
/// obs-cmd --websocket obsws://192.168.1.100:4455/password info
135+
/// ```
109136
#[derive(Parser)]
110137
#[clap(author, version, about, long_about = None)]
111138
pub struct Cli {
139+
/// OBS WebSocket connection URL.
140+
///
141+
/// If not provided, defaults to `obsws://localhost:4455/secret`.
142+
/// Can also be set via OBS_WEBSOCKET_URL environment variable.
112143
#[clap(short, long)]
113-
/// The default websocket URL is `obsws://localhost:4455/secret`
114-
/// if this argument is not provided
115144
pub websocket: Option<ObsWebsocket>,
145+
146+
/// The command to execute on OBS.
116147
#[clap(subcommand)]
117148
pub command: Commands,
118149
}
119150

151+
/// Available commands for controlling OBS.
152+
///
153+
/// This enum represents all possible operations that can be performed
154+
/// on OBS Studio via the WebSocket interface.
120155
#[derive(Subcommand)]
121156
pub enum Commands {
157+
/// Get OBS Studio version and information
122158
Info,
123159
#[clap(subcommand)]
124160
Scene(Scene),
@@ -220,10 +256,25 @@ pub enum MediaInput {
220256
},
221257
}
222258

223-
// Parses strings of such format:
224-
// 0:00 -> 0 seconds
225-
// 01:00 -> 1 minute
226-
// 1:00:00 -> 1 hour
259+
/// Parses duration strings in [hh:]mm:ss format.
260+
///
261+
/// This function converts human-readable time strings into Duration objects.
262+
/// Supports both minute:second and hour:minute:second formats.
263+
///
264+
/// # Examples
265+
///
266+
/// * "0:00" -> 0 seconds
267+
/// * "01:00" -> 1 minute
268+
/// * "1:00:00" -> 1 hour
269+
/// * "1:30:45" -> 1 hour, 30 minutes, 45 seconds
270+
///
271+
/// # Arguments
272+
///
273+
/// * `s` - The duration string to parse
274+
///
275+
/// # Returns
276+
///
277+
/// Returns a `time::Duration` on success, or an error string if format is invalid
227278
fn parse_duration(s: &str) -> Result<time::Duration, String> {
228279
let parts = s
229280
.split_terminator(':')

src/connection.rs

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,22 +3,61 @@ use obws::Client;
33
use std::time::Duration;
44
use tokio::time::timeout;
55

6+
/// Configuration for OBS WebSocket connection attempts.
7+
///
8+
/// This struct defines how connection attempts should be handled,
9+
/// including timeouts, retry limits, and delays between attempts.
610
pub struct ConnectionConfig {
11+
/// Maximum duration to wait for a single connection attempt
712
pub timeout_duration: Duration,
13+
/// Maximum number of connection attempts before giving up
814
pub max_retries: u32,
15+
/// Duration to wait between retry attempts
916
pub retry_delay: Duration,
1017
}
1118

1219
impl Default for ConnectionConfig {
1320
fn default() -> Self {
1421
Self {
22+
// 10 second timeout for each connection attempt
1523
timeout_duration: Duration::from_secs(10),
24+
// Try up to 3 times before giving up
1625
max_retries: 3,
26+
// Wait 2 seconds between retry attempts
1727
retry_delay: Duration::from_secs(2),
1828
}
1929
}
2030
}
2131

32+
/// Establishes a WebSocket connection to OBS with retry logic.
33+
///
34+
/// This function attempts to connect to an OBS WebSocket server with the
35+
/// provided configuration. It will retry connection attempts according to
36+
/// the specified config and provide detailed error feedback.
37+
///
38+
/// # Arguments
39+
///
40+
/// * `hostname` - The OBS WebSocket server hostname or IP address
41+
/// * `port` - The port number where OBS WebSocket is listening
42+
/// * `password` - Optional password for OBS WebSocket authentication
43+
/// * `config` - Connection configuration including timeouts and retry settings
44+
///
45+
/// # Returns
46+
///
47+
/// Returns a connected `Client` instance on success, or an error if all
48+
/// connection attempts fail.
49+
///
50+
/// # Examples
51+
///
52+
/// ```rust
53+
/// let config = ConnectionConfig::default();
54+
/// let client = connect_with_retry(
55+
/// "localhost".to_string(),
56+
/// 4455,
57+
/// Some("secret".to_string()),
58+
/// config
59+
/// ).await?;
60+
/// ```
2261
pub async fn connect_with_retry(
2362
hostname: String,
2463
port: u16,
@@ -68,6 +107,19 @@ pub async fn connect_with_retry(
68107
}))
69108
}
70109

110+
/// Checks the health of an existing OBS WebSocket connection.
111+
///
112+
/// This function verifies that the connection to OBS is still active
113+
/// and responsive by attempting to retrieve the OBS version.
114+
///
115+
/// # Arguments
116+
///
117+
/// * `client` - The OBS WebSocket client to check
118+
///
119+
/// # Returns
120+
///
121+
/// Returns `Ok(())` if the connection is healthy, or an error if the
122+
/// connection is unresponsive or broken.
71123
pub async fn check_connection_health(client: &Client) -> Result<()> {
72124
timeout(Duration::from_secs(5), client.general().version())
73125
.await

src/error.rs

Lines changed: 24 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,61 +1,62 @@
11
use thiserror::Error;
22

3+
/// Error types for obs-cmd operations.
4+
///
5+
/// This enum represents all possible errors that can occur during
6+
/// OBS WebSocket operations, connection handling, and command execution.
7+
/// Each error variant provides detailed, actionable error messages.
38
#[derive(Error, Debug)]
49
#[allow(clippy::result_large_err)]
510
pub enum ObsCmdError {
6-
#[error("WebSocket connection error: {0}")]
11+
#[error("WebSocket connection failed: {0}. Ensure OBS is running with WebSocket server enabled")]
712
ConnectionError(#[from] obws::error::Error),
813

9-
#[error("URL parsing error: {0}")]
14+
#[error("Invalid URL format: {0}. Use format: obsws://hostname:port/password")]
1015
UrlParseError(#[from] url::ParseError),
1116

12-
#[error("Environment variable error: {0}")]
17+
#[error("Environment variable error: {0}. Set OBS_WEBSOCKET_URL environment variable")]
1318
EnvError(#[from] std::env::VarError),
1419

15-
#[allow(dead_code)]
16-
#[error("Command execution failed: {message}")]
17-
CommandError { message: String },
1820

19-
#[error("Invalid audio command: {command}")]
21+
22+
#[error("Invalid audio command '{command}'. Valid commands are: mute, unmute, toggle, status")]
2023
InvalidAudioCommand { command: String },
2124

22-
#[error("Invalid filter command: {command}")]
25+
#[error("Invalid filter command '{command}'. Valid commands are: enable, disable, toggle")]
2326
InvalidFilterCommand { command: String },
2427

25-
#[error("Invalid scene item command: {command}")]
28+
#[error("Invalid scene item command '{command}'. Valid commands are: enable, disable, toggle")]
2629
InvalidSceneItemCommand { command: String },
2730

28-
#[error("Monitor not available: index {index} out of range")]
31+
#[error("Monitor index {index} is not available. Check available monitors with OBS")]
2932
MonitorNotAvailable { index: u32 },
3033

31-
#[error("No monitor list received from OBS")]
34+
#[error("Unable to retrieve monitor list from OBS. Ensure OBS is running and WebSocket is connected")]
3235
NoMonitorList,
3336

34-
#[error("Recording is not active")]
37+
#[error("Recording is not currently active. Start recording first")]
3538
RecordingNotActive,
3639

37-
#[error("Recording is paused")]
40+
#[error("Recording is currently paused. Use resume command to continue")]
3841
RecordingPaused,
3942

40-
#[error("No last replay found")]
43+
#[error("No replay buffer recording found. Start replay buffer first")]
4144
NoLastReplay,
4245

43-
#[allow(dead_code)]
44-
#[error("OBS operation failed: {0}")]
45-
ObsOperationError(String),
4646

47-
#[allow(dead_code)]
48-
#[error("Invalid URL format: {0}")]
49-
InvalidUrlFormat(String),
5047

51-
#[error("Connection timeout after {timeout} seconds")]
48+
#[error("Connection timed out after {timeout} seconds. Check OBS is running and WebSocket is enabled")]
5249
ConnectionTimeout { timeout: u64 },
5350

54-
#[error("All {attempts} connection attempts failed")]
51+
#[error("Failed to connect after {attempts} attempts. Verify OBS WebSocket settings and network connectivity")]
5552
AllConnectionAttemptsFailed { attempts: u32 },
5653

57-
#[error("WebSocket URL parsing failed: {0}")]
54+
#[error("Invalid WebSocket URL format: {0}. Expected format: obsws://hostname:port/password")]
5855
WebSocketUrlParseError(String),
5956
}
6057

58+
/// Result type alias for obs-cmd operations.
59+
///
60+
/// This is a convenience alias for `std::result::Result<T, ObsCmdError>`
61+
/// to simplify error handling throughout the application.
6162
pub type Result<T> = std::result::Result<T, ObsCmdError>;

0 commit comments

Comments
 (0)