record-daemon: fix obs recording

This commit is contained in:
2026-03-20 20:34:44 +01:00
parent dbb224e118
commit 1166424c29
12 changed files with 3717 additions and 515 deletions
+99 -127
View File
@@ -1,11 +1,13 @@
//! Game capture source configuration.
//! Game capture source configuration using libobs-simple.
//!
//! This module provides capture sources for recording game footage,
//! including game capture, window capture, and monitor capture.
use serde::{Deserialize, Serialize};
use tracing::{debug, info};
use crate::error::Result;
/// Game capture source for recording game footage.
///
/// Uses OBS game capture for efficient GPU-based capture.
#[derive(Debug, Clone)]
pub struct GameCapture {
/// Source name.
@@ -18,6 +20,8 @@ pub struct GameCapture {
pub mode: CaptureMode,
/// Whether to capture cursor.
pub capture_cursor: bool,
/// Window class (Windows only).
pub window_class: Option<String>,
}
impl Default for GameCapture {
@@ -26,8 +30,9 @@ impl Default for GameCapture {
name: "Game Capture".to_string(),
window: None,
process_name: Some("League of Legends.exe".to_string()),
mode: CaptureMode::Any,
mode: CaptureMode::Process,
capture_cursor: false,
window_class: Some("RiotWindowClass".to_string()),
}
}
}
@@ -41,6 +46,18 @@ impl GameCapture {
}
}
/// Create a game capture configured for League of Legends.
pub fn for_league_of_legends() -> Self {
Self {
name: "League of Legends Capture".to_string(),
window: Some("League of Legends (TM) Client".to_string()),
process_name: Some("League of Legends.exe".to_string()),
mode: CaptureMode::Window,
capture_cursor: false,
window_class: Some("RiotWindowClass".to_string()),
}
}
/// Set the window to capture.
pub fn with_window(mut self, window: &str) -> Self {
self.window = Some(window.to_string());
@@ -55,105 +72,56 @@ impl GameCapture {
self
}
/// Set the window class (Windows only).
pub fn with_window_class(mut self, class: &str) -> Self {
self.window_class = Some(class.to_string());
self
}
/// Set whether to capture the cursor.
pub fn with_cursor(mut self, capture: bool) -> Self {
self.capture_cursor = capture;
self
}
/// Create the OBS source.
///
/// Note: This would create the actual obs_source_t in libobs.
pub fn create_source(&self) -> Result<CaptureSource> {
info!("Creating game capture source: {}", self.name);
// Note: Actual libobs source creation would happen here
// obs_source_create("game_capture", name, settings, nullptr)
let source = CaptureSource {
name: self.name.clone(),
source_type: SourceType::GameCapture,
active: false,
};
debug!("Game capture source created");
Ok(source)
/// Get the window string for OBS in the format "Title:Class:Executable".
pub fn window_string(&self) -> Option<String> {
match (&self.window, &self.window_class, &self.process_name) {
(Some(window), Some(class), Some(process)) => {
Some(format!("{}:{}:{}", window, class, process))
}
(Some(window), None, Some(process)) => Some(format!("{}::{}", window, process)),
(Some(window), Some(class), None) => Some(format!("{}:{}:", window, class)),
_ => None,
}
}
}
/// Capture mode.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
#[derive(Default)]
pub enum CaptureMode {
/// Capture any fullscreen application.
Any,
/// Capture a specific window.
Window,
/// Capture a specific process.
#[default]
Process,
}
/// Capture source abstraction.
#[derive(Debug, Clone)]
pub struct CaptureSource {
/// Source name.
pub name: String,
/// Source type.
pub source_type: SourceType,
/// Whether the source is active.
pub active: bool,
}
impl CaptureSource {
/// Check if the source is active.
pub fn is_active(&self) -> bool {
self.active
}
/// Activate the source.
pub fn activate(&mut self) -> Result<()> {
if self.active {
return Ok(());
}
debug!("Activating capture source: {}", self.name);
// Note: Actual activation would involve obs_source_set_enabled
self.active = true;
Ok(())
}
/// Deactivate the source.
pub fn deactivate(&mut self) -> Result<()> {
if !self.active {
return Ok(());
}
debug!("Deactivating capture source: {}", self.name);
// Note: Actual deactivation would involve obs_source_set_enabled
self.active = false;
Ok(())
}
}
/// Source type.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SourceType {
/// Game capture source.
GameCapture,
/// Window capture source.
WindowCapture,
/// Monitor capture source.
MonitorCapture,
}
/// Window capture source (alternative to game capture).
///
/// Uses OBS window capture which works with more applications
/// but may have slightly higher overhead than game capture.
#[derive(Debug, Clone)]
pub struct WindowCapture {
/// Source name.
pub name: String,
/// Window title.
pub window_title: String,
/// Window class (X11).
/// Window class (X11/Windows).
pub window_class: Option<String>,
/// Whether to capture cursor.
pub capture_cursor: bool,
@@ -170,35 +138,28 @@ impl WindowCapture {
}
}
/// Set the window class (for X11).
/// Set the window class (for X11/Windows).
pub fn with_class(mut self, class: &str) -> Self {
self.window_class = Some(class.to_string());
self
}
/// Create the OBS source.
pub fn create_source(&self) -> Result<CaptureSource> {
info!(
"Creating window capture source: {} ({})",
self.name, self.window_title
);
let source = CaptureSource {
name: self.name.clone(),
source_type: SourceType::WindowCapture,
active: false,
};
Ok(source)
/// Set whether to capture the cursor.
pub fn with_cursor(mut self, capture: bool) -> Self {
self.capture_cursor = capture;
self
}
}
/// Monitor capture source (fallback).
///
/// Captures an entire monitor. Useful as a fallback when
/// game capture or window capture don't work.
#[derive(Debug, Clone)]
pub struct MonitorCapture {
/// Source name.
pub name: String,
/// Monitor index.
/// Monitor index (0-based).
pub monitor: u32,
/// Whether to capture cursor.
pub capture_cursor: bool,
@@ -214,39 +175,39 @@ impl MonitorCapture {
}
}
/// Create the OBS source.
pub fn create_source(&self) -> Result<CaptureSource> {
info!(
"Creating monitor capture source: {} (monitor {})",
self.name, self.monitor
);
let source = CaptureSource {
name: self.name.clone(),
source_type: SourceType::MonitorCapture,
active: false,
};
Ok(source)
/// Set whether to capture the cursor.
pub fn with_cursor(mut self, capture: bool) -> Self {
self.capture_cursor = capture;
self
}
}
/// Find the League of Legends game window.
pub fn find_league_window() -> Option<String> {
// Note: Actual window finding would use platform-specific APIs
// On Linux: X11/Wayland
// On Windows: Win32 API
/// Capture source type.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SourceType {
/// Game capture source.
GameCapture,
/// Window capture source.
WindowCapture,
/// Monitor capture source.
MonitorCapture,
}
#[cfg(target_os = "linux")]
/// Find the League of Legends game window.
///
/// Returns the window title if found, or a default if not found.
pub fn find_league_window() -> Option<String> {
#[cfg(target_os = "windows")]
{
// Would use x11rb or similar to find window
// On Windows, we can use the Win32 API to find the window
// For now, return the expected window title
Some("League of Legends (TM) Client".to_string())
}
#[cfg(target_os = "windows")]
#[cfg(target_os = "linux")]
{
// Would use FindWindowW
// On Linux, we would use X11 or Wayland APIs
// For now, return the expected window title
Some("League of Legends (TM) Client".to_string())
}
@@ -254,6 +215,11 @@ pub fn find_league_window() -> Option<String> {
None
}
/// Get the default capture configuration for League of Legends.
pub fn league_capture_config() -> GameCapture {
GameCapture::for_league_of_legends()
}
#[cfg(test)]
mod tests {
use super::*;
@@ -262,7 +228,7 @@ mod tests {
fn test_game_capture_creation() {
let capture = GameCapture::new("Test Capture");
assert_eq!(capture.name, "Test Capture");
assert_eq!(capture.mode, CaptureMode::Any);
assert_eq!(capture.mode, CaptureMode::Process);
}
#[test]
@@ -277,17 +243,23 @@ mod tests {
}
#[test]
fn test_capture_source_activation() {
let mut source = CaptureSource {
name: "Test".to_string(),
source_type: SourceType::GameCapture,
active: false,
};
fn test_league_capture_config() {
let capture = league_capture_config();
assert_eq!(capture.name, "League of Legends Capture");
assert_eq!(
capture.process_name,
Some("League of Legends.exe".to_string())
);
assert_eq!(capture.window_class, Some("RiotWindowClass".to_string()));
}
assert!(!source.is_active());
source.activate().unwrap();
assert!(source.is_active());
source.deactivate().unwrap();
assert!(!source.is_active());
#[test]
fn test_window_string() {
let capture = GameCapture::for_league_of_legends();
let window_str = capture.window_string();
assert!(window_str.is_some());
let s = window_str.unwrap();
assert!(s.contains("League of Legends"));
assert!(s.contains("RiotWindowClass"));
}
}
+222 -52
View File
@@ -1,4 +1,7 @@
//! Video and audio encoder configuration.
//!
//! This module provides encoder configuration types that integrate with
//! libobs-simple for hardware and software encoding.
use serde::{Deserialize, Serialize};
@@ -17,6 +20,22 @@ pub struct EncoderConfig {
pub settings: EncoderSettings,
}
impl Default for EncoderConfig {
fn default() -> Self {
Self {
encoder_id: "jim_nvenc".to_string(),
bitrate: 6000,
keyframe_interval: 2,
settings: EncoderSettings::Nvenc {
cq_level: 20,
two_pass: true,
preset: "p4".to_string(),
rate_control: NvencRateControl::Vbr,
},
}
}
}
/// Encoder-specific settings.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "lowercase")]
@@ -133,21 +152,56 @@ impl EncoderConfig {
EncoderSettings::Nvenc { .. } | EncoderSettings::Amf { .. }
)
}
}
/// Video encoder trait for abstraction over different encoders.
pub trait VideoEncoder {
/// Get the encoder ID.
fn id(&self) -> &str;
/// Check if this is an NVIDIA encoder.
pub fn is_nvenc(&self) -> bool {
matches!(self.settings, EncoderSettings::Nvenc { .. })
}
/// Get the current bitrate.
fn bitrate(&self) -> u32;
/// Check if this is an AMD encoder.
pub fn is_amf(&self) -> bool {
matches!(self.settings, EncoderSettings::Amf { .. })
}
/// Update the bitrate.
fn set_bitrate(&mut self, bitrate: u32);
/// Check if this is a software encoder.
pub fn is_software(&self) -> bool {
matches!(self.settings, EncoderSettings::X264 { .. })
}
/// Get encoder-specific settings as JSON.
fn settings_json(&self) -> serde_json::Value;
/// Get the hardware codec type for libobs-simple.
pub fn hardware_codec(&self) -> Option<libobs_simple::output::simple::HardwareCodec> {
use libobs_simple::output::simple::HardwareCodec;
match &self.settings {
EncoderSettings::Nvenc { .. } => Some(HardwareCodec::H264),
EncoderSettings::Amf { .. } => Some(HardwareCodec::H264),
EncoderSettings::X264 { .. } => None,
}
}
/// Get the hardware preset for libobs-simple.
pub fn hardware_preset(&self) -> libobs_simple::output::simple::HardwarePreset {
use libobs_simple::output::simple::HardwarePreset;
match &self.settings {
EncoderSettings::Nvenc { preset, .. } => match preset.as_str() {
"p1" => HardwarePreset::Speed,
"p2" => HardwarePreset::Speed,
"p3" => HardwarePreset::Balanced,
"p4" => HardwarePreset::Balanced,
"p5" => HardwarePreset::Quality,
"p6" => HardwarePreset::Quality,
"p7" => HardwarePreset::Quality,
_ => HardwarePreset::Balanced,
},
EncoderSettings::Amf { quality, .. } => match quality {
AmfQuality::Speed => HardwarePreset::Speed,
AmfQuality::Balanced => HardwarePreset::Balanced,
AmfQuality::Quality => HardwarePreset::Quality,
},
EncoderSettings::X264 { .. } => HardwarePreset::Balanced,
}
}
}
/// Audio encoder configuration.
@@ -174,49 +228,14 @@ impl Default for AudioEncoderConfig {
}
}
/// Audio encoder trait.
pub trait AudioEncoder {
/// Get the encoder ID.
fn id(&self) -> &str;
/// Get the current bitrate.
fn bitrate(&self) -> u32;
/// Update the bitrate.
fn set_bitrate(&mut self, bitrate: u32);
}
/// Detect available hardware encoders.
pub fn detect_hardware_encoders() -> Vec<EncoderCapability> {
let mut capabilities = Vec::new();
// Note: Actual detection would query the system for GPU availability
// For now, we check environment variables and common indicators
#[cfg(target_os = "linux")]
{
// Check for NVIDIA
if std::path::Path::new("/dev/nvidia0").exists() {
capabilities.push(EncoderCapability::Nvenc);
}
// Check for AMD
if std::path::Path::new("/sys/class/drm").exists() {
// Would check for AMD GPU
// capabilities.push(EncoderCapability::Amf);
impl AudioEncoderConfig {
/// Create a new audio encoder config with the specified bitrate.
pub fn with_bitrate(bitrate: u32) -> Self {
Self {
bitrate,
..Default::default()
}
}
#[cfg(target_os = "windows")]
{
// On Windows, would use DXGI to detect GPUs
// For now, assume software encoding
}
// Always available
capabilities.push(EncoderCapability::Software);
capabilities
}
/// Encoder capability.
@@ -259,6 +278,142 @@ impl EncoderCapability {
},
}
}
/// Get a human-readable name for this encoder capability.
pub fn name(&self) -> &'static str {
match self {
EncoderCapability::Nvenc => "NVIDIA NVENC",
EncoderCapability::Amf => "AMD AMF",
EncoderCapability::QuickSync => "Intel QuickSync",
EncoderCapability::Software => "Software (x264)",
}
}
}
/// Detect available hardware encoders.
///
/// This function checks the system for available GPU encoders.
pub fn detect_hardware_encoders() -> Vec<EncoderCapability> {
use std::io::Write;
use tracing::info;
info!("[ENCODER_DETECT] Starting hardware encoder detection...");
std::io::stderr().flush().ok();
let mut capabilities = Vec::new();
#[cfg(target_os = "linux")]
{
// Check for NVIDIA
if std::path::Path::new("/dev/nvidia0").exists() {
info!("[ENCODER_DETECT] Found NVIDIA device");
capabilities.push(EncoderCapability::Nvenc);
}
// Check for AMD
if std::path::Path::new("/sys/class/drm").exists() {
// Would check for AMD GPU
// capabilities.push(EncoderCapability::Amf);
}
}
#[cfg(target_os = "windows")]
{
// On Windows, check for NVIDIA first
// Try to load nvenc DLL
info!("[ENCODER_DETECT] Checking for NVENC...");
std::io::stderr().flush().ok();
if is_nvenc_available() {
info!("[ENCODER_DETECT] NVENC available");
capabilities.push(EncoderCapability::Nvenc);
} else {
info!("[ENCODER_DETECT] NVENC not available");
}
// Check for AMD AMF
info!("[ENCODER_DETECT] Checking for AMF...");
std::io::stderr().flush().ok();
if is_amf_available() {
info!("[ENCODER_DETECT] AMF available");
capabilities.push(EncoderCapability::Amf);
} else {
info!("[ENCODER_DETECT] AMF not available");
}
// Check for Intel QuickSync
info!("[ENCODER_DETECT] Checking for QuickSync...");
std::io::stderr().flush().ok();
if is_quicksync_available() {
info!("[ENCODER_DETECT] QuickSync available");
capabilities.push(EncoderCapability::QuickSync);
} else {
info!("[ENCODER_DETECT] QuickSync not available");
}
}
// Always available
info!("[ENCODER_DETECT] Software encoder always available");
capabilities.push(EncoderCapability::Software);
info!("[ENCODER_DETECT] Detected encoders: {:?}", capabilities);
std::io::stderr().flush().ok();
capabilities
}
/// Check if NVIDIA NVENC is available.
#[cfg(target_os = "windows")]
fn is_nvenc_available() -> bool {
// Check for NVENC DLL
std::path::Path::new("C:\\Windows\\System32\\nvEncMFTH264.dll").exists()
|| std::path::Path::new("C:\\Windows\\System32\\nvEncMFTH265.dll").exists()
}
#[cfg(not(target_os = "windows"))]
fn is_nvenc_available() -> bool {
false
}
/// Check if AMD AMF is available.
#[cfg(target_os = "windows")]
fn is_amf_available() -> bool {
// Check for AMF runtime
std::path::Path::new("C:\\Windows\\System32\\amdocl64.dll").exists()
}
#[cfg(not(target_os = "windows"))]
fn is_amf_available() -> bool {
false
}
/// Check if Intel QuickSync is available.
#[cfg(target_os = "windows")]
fn is_quicksync_available() -> bool {
// Check for Intel Media SDK
std::path::Path::new("C:\\Windows\\System32\\mfx64.dll").exists()
}
#[cfg(not(target_os = "windows"))]
fn is_quicksync_available() -> bool {
false
}
/// Get the best available encoder capability.
pub fn best_available_encoder() -> EncoderCapability {
let capabilities = detect_hardware_encoders();
// Prefer hardware encoders
for cap in &capabilities {
if matches!(
cap,
EncoderCapability::Nvenc | EncoderCapability::Amf | EncoderCapability::QuickSync
) {
return *cap;
}
}
// Fall back to software
EncoderCapability::Software
}
#[cfg(test)]
@@ -278,6 +433,7 @@ mod tests {
assert_eq!(config.encoder_id, "jim_nvenc");
assert_eq!(config.bitrate, 8000);
assert!(config.is_hardware());
assert!(config.is_nvenc());
}
#[test]
@@ -292,6 +448,7 @@ mod tests {
assert_eq!(config.encoder_id, "x264");
assert!(!config.is_hardware());
assert!(config.is_software());
}
#[test]
@@ -300,4 +457,17 @@ mod tests {
assert!(!capabilities.is_empty());
assert!(capabilities.contains(&EncoderCapability::Software));
}
#[test]
fn test_best_available_encoder() {
let best = best_available_encoder();
// Should always return something
assert!(matches!(
best,
EncoderCapability::Nvenc
| EncoderCapability::Amf
| EncoderCapability::QuickSync
| EncoderCapability::Software
));
}
}
+17 -6
View File
@@ -8,8 +8,8 @@ pub mod encoder;
mod obs_context;
mod output;
pub use capture::{CaptureSource, GameCapture};
pub use encoder::{AudioEncoder, EncoderConfig, VideoEncoder};
pub use capture::{CaptureMode, GameCapture, MonitorCapture, SourceType, WindowCapture};
pub use encoder::{AudioEncoderConfig, EncoderCapability, EncoderConfig, EncoderSettings};
pub use obs_context::{ObsContext, ObsContextBuilder};
pub use output::{OutputConfig, RecordingOutput, RecordingResult};
@@ -17,7 +17,7 @@ use std::sync::Arc;
use chrono::{DateTime, Utc};
use parking_lot::RwLock;
use tracing::{info, warn};
use tracing::{error, info, warn};
use crate::config::Settings;
use crate::error::{RecordingError, Result};
@@ -92,10 +92,17 @@ impl RecordingEngine {
/// * `game_id` - Optional game ID for the recording.
/// * `champion` - Optional champion name for the filename.
pub fn start_recording(&mut self, game_id: Option<u64>, champion: Option<&str>) -> Result<()> {
info!(
"RecordingEngine::start_recording called - game_id: {:?}, champion: {:?}",
game_id, champion
);
if self.is_recording {
warn!("Already recording, returning error");
return Err(RecordingError::AlreadyRecording.into());
}
info!("Reading settings...");
let settings = self.settings.read().clone();
// Generate output filename
@@ -105,10 +112,14 @@ impl RecordingEngine {
info!("Starting recording to: {:?}", output_path);
// Start the recording
let context = self.context.as_mut().ok_or(RecordingError::ObsInitError(
"OBS not initialized".to_string(),
))?;
let context = self.context.as_mut().ok_or_else(|| {
error!("OBS not initialized when start_recording was called");
RecordingError::ObsInitError("OBS not initialized".to_string())
})?;
info!("Calling OBS context start_recording...");
context.start_recording(&output_path)?;
info!("OBS context start_recording returned successfully");
self.current_output = Some(RecordingOutput {
path: output_path,
+711 -188
View File
@@ -1,82 +1,25 @@
//! OBS context initialization and management.
//!
//! This module handles the lifecycle of the OBS library context.
//! This module handles the lifecycle of the OBS library context using libobs-simple.
use std::path::Path;
use tracing::{debug, info, warn};
use libobs_simple::output::simple::{
HardwareCodec, HardwarePreset, OutputFormat, SimpleOutputBuilder,
};
use libobs_simple::wrapper::data::output::ObsOutputTrait;
use libobs_simple::wrapper::data::video::ObsVideoInfoBuilder;
use libobs_simple::wrapper::data::ObsObjectBuilder;
use libobs_simple::wrapper::scenes::SceneItemExtSceneTrait;
use libobs_simple::wrapper::utils::{ObsPath, ObsString};
use libobs_wrapper::context::ObsContext as LibObsContext;
use libobs_wrapper::utils::StartupInfo;
use tracing::{error, info, warn};
use super::encoder::{best_available_encoder, EncoderCapability};
use crate::config::{AudioSettings, VideoSettings};
use crate::error::{RecordingError, Result};
/// OBS video settings for initialization.
#[derive(Debug, Clone)]
pub struct ObsVideoInfo {
/// Graphics adapter index (-1 for default).
pub adapter: i32,
/// Output resolution width.
pub output_width: u32,
/// Output resolution height.
pub output_height: u32,
/// Frames per second numerator.
pub fps_num: u32,
/// Frames per second denominator.
pub fps_den: u32,
/// Base resolution width.
pub base_width: u32,
/// Base resolution height.
pub base_height: u32,
/// Output format.
pub output_format: ObsVideoFormat,
}
impl Default for ObsVideoInfo {
fn default() -> Self {
Self {
adapter: -1,
output_width: 1920,
output_height: 1080,
fps_num: 60,
fps_den: 1,
base_width: 1920,
base_height: 1080,
output_format: ObsVideoFormat::Nv12,
}
}
}
impl ObsVideoInfo {
/// Create video info from settings.
pub fn from_settings(settings: &VideoSettings) -> Self {
let (width, height) = settings.quality.resolution();
let fps = settings.frame_rate;
Self {
adapter: -1,
output_width: width,
output_height: height,
fps_num: fps,
fps_den: 1,
base_width: width,
base_height: height,
output_format: ObsVideoFormat::Nv12,
}
}
}
/// Video output format.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ObsVideoFormat {
/// NV12 format (common for hardware encoders).
Nv12,
/// I420 format.
I420,
/// I444 format.
I444,
/// RGBA format.
Rgba,
}
/// Builder for OBS context.
pub struct ObsContextBuilder {
video_settings: Option<VideoSettings>,
@@ -126,20 +69,17 @@ impl ObsContextBuilder {
.map_err(|e| RecordingError::OutputDirError(e.to_string()))?;
}
let video_info = ObsVideoInfo::from_settings(&video_settings);
let context = ObsContext {
video_info,
video_settings,
audio_settings,
output_dir,
initialized: false,
context: None,
output: None,
recording: false,
current_output: None,
encoder_capability: None,
};
// Note: Actual libobs initialization would happen here
// For now, we create a stub that can be extended with actual libobs bindings
Ok(context)
}
}
@@ -156,23 +96,27 @@ impl Default for ObsContextBuilder {
/// recording functionality.
pub struct ObsContext {
/// Video configuration.
video_info: ObsVideoInfo,
video_settings: VideoSettings,
/// Audio configuration.
audio_settings: AudioSettings,
/// Output directory for recordings.
output_dir: std::path::PathBuf,
/// Whether OBS has been initialized.
initialized: bool,
/// The underlying libobs context.
context: Option<LibObsContext>,
/// The current output.
output: Option<libobs_simple::wrapper::data::output::ObsOutputRef>,
/// Whether currently recording.
recording: bool,
/// Current output path (if recording).
current_output: Option<std::path::PathBuf>,
/// Detected encoder capability.
encoder_capability: Option<EncoderCapability>,
}
impl ObsContext {
/// Check if OBS is initialized.
pub fn is_initialized(&self) -> bool {
self.initialized
self.context.is_some()
}
/// Check if currently recording.
@@ -180,31 +124,695 @@ impl ObsContext {
self.recording
}
/// Get the video info.
pub fn video_info(&self) -> &ObsVideoInfo {
&self.video_info
/// Get the video settings.
pub fn video_settings(&self) -> &VideoSettings {
&self.video_settings
}
/// Initialize OBS.
fn initialize(&mut self) -> Result<()> {
if self.context.is_some() {
info!("OBS context already initialized");
return Ok(());
}
info!("[OBS_INIT] Starting OBS context initialization...");
use std::io::Write;
std::io::stderr().flush().ok();
// Pre-flight checks for OBS
self.preflight_checks()?;
// Detect best available encoder
info!("[OBS_INIT] Detecting best available encoder...");
std::io::stderr().flush().ok();
let encoder = best_available_encoder();
info!("[OBS_INIT] Detected encoder: {}", encoder.name());
std::io::stderr().flush().ok();
self.encoder_capability = Some(encoder);
let (width, height) = self.video_settings.quality.resolution();
let fps = self.video_settings.frame_rate;
info!(
"[OBS_INIT] OBS video config: {}x{} @ {}fps",
width, height, fps
);
std::io::stderr().flush().ok();
// Create startup info with video configuration
info!("[OBS_INIT] Creating OBS video info builder...");
std::io::stderr().flush().ok();
let video_info = ObsVideoInfoBuilder::new()
.fps_num(fps)
.fps_den(1)
.base_width(width)
.base_height(height)
.output_width(width)
.output_height(height)
.build();
info!("[OBS_INIT] OBS video info built successfully");
std::io::stderr().flush().ok();
info!("[OBS_INIT] Building OBS context with LibObsContext::builder()...");
std::io::stderr().flush().ok();
let compat = LibObsContext::check_version_compatibility();
if !compat {
error!("OBS version compatibility is wrong. This will not go well...");
}
//FIXME: this is crashing here.
// LibObsContext::new makes the whole application crash, even with default options
let info = StartupInfo::default().set_video_info(video_info);
let context_result =
std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| LibObsContext::new(info)));
let context = match context_result {
Ok(Ok(ctx)) => {
info!("[OBS_INIT] OBS context created successfully");
std::io::stderr().flush().ok();
ctx
}
Ok(Err(e)) => {
error!("[OBS_INIT] Failed to create OBS context: {:?}", e);
std::io::stderr().flush().ok();
return Err(RecordingError::ObsInitError(format!(
"Failed to create OBS context: {:?}",
e
))
.into());
}
Err(panic_info) => {
error!(
"[OBS_INIT] PANIC during OBS context creation: {:?}",
panic_info
);
std::io::stderr().flush().ok();
eprintln!(
"[OBS_INIT] PANIC during OBS context creation: {:?}",
panic_info
);
return Err(RecordingError::ObsInitError(
"Panic during OBS context creation".to_string(),
)
.into());
}
};
if self.audio_settings.enabled {
info!(
"[OBS_INIT] OBS audio config: {} channels @ {}Hz",
self.audio_settings.channels, self.audio_settings.sample_rate
);
}
self.context = Some(context);
info!("[OBS_INIT] OBS context initialized successfully");
std::io::stderr().flush().ok();
Ok(())
}
/// Pre-flight checks for OBS initialization.
fn preflight_checks(&self) -> Result<()> {
use std::io::Write;
info!("[PREFLIGHT] Running OBS pre-flight checks...");
std::io::stderr().flush().ok();
// Check for OBS installation directory
// libobs-bootstrapper typically extracts OBS to a specific location
let obs_paths = self.get_obs_search_paths();
info!("[PREFLIGHT] OBS search paths: {:?}", obs_paths);
std::io::stderr().flush().ok();
let mut obs_found = false;
for path in &obs_paths {
if path.exists() {
info!("[PREFLIGHT] Found OBS at: {:?}", path);
std::io::stderr().flush().ok();
// Check for plugins directory
let plugins_path = path.join("obs-plugins");
if plugins_path.exists() {
info!("[PREFLIGHT] Found OBS plugins at: {:?}", plugins_path);
std::io::stderr().flush().ok();
// Check for 64-bit plugins
let plugins_64 = plugins_path.join("64bit");
if plugins_64.exists() {
info!("[PREFLIGHT] Found 64-bit plugins at: {:?}", plugins_64);
std::io::stderr().flush().ok();
// List available plugins
if let Ok(entries) = std::fs::read_dir(&plugins_64) {
let plugins: Vec<String> = entries
.filter_map(|e| e.ok())
.filter_map(|e| e.file_name().to_str().map(|s| s.to_string()))
.collect();
info!("[PREFLIGHT] Available plugins: {:?}", plugins);
std::io::stderr().flush().ok();
}
}
}
obs_found = true;
break;
}
}
if !obs_found {
warn!(
"[PREFLIGHT] OBS installation not found in standard paths - this may cause issues"
);
std::io::stderr().flush().ok();
}
// Check for display (required for capture)
#[cfg(target_os = "windows")]
{
info!("[PREFLIGHT] Checking for display availability...");
std::io::stderr().flush().ok();
// On Windows, check if we have a display
use std::ptr;
unsafe {
let dc = winapi::um::winuser::GetDC(ptr::null_mut());
if dc.is_null() {
warn!("[PREFLIGHT] No display context available - capture may fail");
} else {
info!("[PREFLIGHT] Display context available");
winapi::um::winuser::ReleaseDC(ptr::null_mut(), dc);
}
}
std::io::stderr().flush().ok();
}
info!("[PREFLIGHT] Pre-flight checks completed");
std::io::stderr().flush().ok();
Ok(())
}
/// Get OBS search paths based on platform.
fn get_obs_search_paths(&self) -> Vec<std::path::PathBuf> {
let mut paths = Vec::new();
// Current directory
paths.push(std::env::current_dir().unwrap_or_default().join("obs"));
// libobs-bootstrapper default locations
#[cfg(target_os = "windows")]
{
// Check AppData
if let Some(app_data) = std::env::var_os("LOCALAPPDATA") {
paths.push(std::path::PathBuf::from(app_data).join("obs-studio"));
}
if let Some(app_data) = std::env::var_os("APPDATA") {
paths.push(std::path::PathBuf::from(app_data).join("obs-studio"));
}
// Program Files
paths.push(std::path::PathBuf::from("C:\\Program Files\\obs-studio"));
paths.push(std::path::PathBuf::from(
"C:\\Program Files (x86)\\obs-studio",
));
}
#[cfg(target_os = "linux")]
{
paths.push(std::path::PathBuf::from("/usr/share/obs"));
paths.push(std::path::PathBuf::from("/usr/local/share/obs"));
if let Some(home) = std::env::var_os("HOME") {
paths.push(std::path::PathBuf::from(home).join(".local/share/obs"));
}
}
paths
}
/// Start recording to the specified output path.
pub fn start_recording(&mut self, output_path: &Path) -> Result<()> {
use std::io::Write;
info!(
"[START_REC] start_recording called with path: {:?}",
output_path
);
std::io::stderr().flush().ok();
if self.recording {
warn!("[START_REC] Already recording, returning error");
return Err(RecordingError::AlreadyRecording.into());
}
if !self.initialized {
// Initialize on first use
if self.context.is_none() {
info!("[START_REC] OBS context not initialized, initializing now...");
std::io::stderr().flush().ok();
self.initialize()?;
info!("[START_REC] OBS initialization complete");
std::io::stderr().flush().ok();
}
info!("Starting OBS recording to: {:?}", output_path);
let context = self.context.as_ref().ok_or_else(|| {
error!("[START_REC] OBS not initialized after initialize()");
std::io::stderr().flush().ok();
RecordingError::ObsInitError("OBS not initialized".to_string())
})?;
// Note: Actual libobs recording start would happen here
// This is a stub implementation
info!("[START_REC] Starting OBS recording to: {:?}", output_path);
std::io::stderr().flush().ok();
// Get bitrate from encoder preset
let bitrate = self.video_settings.encoder_preset.effective_bitrate();
info!("[START_REC] Using bitrate: {} kbps", bitrate);
std::io::stderr().flush().ok();
// Create output path
let path_str = output_path.to_string_lossy();
let obs_path = ObsPath::new(&path_str);
// Get detected encoder capability
let encoder = self
.encoder_capability
.unwrap_or_else(best_available_encoder);
info!("[START_REC] Using encoder: {}", encoder.name());
std::io::stderr().flush().ok();
// Build the output based on encoder capability
info!("[START_REC] Creating SimpleOutputBuilder...");
std::io::stderr().flush().ok();
let output_result = match encoder {
EncoderCapability::Nvenc => {
info!("Building NVENC output...");
SimpleOutputBuilder::new(context.clone(), ObsString::from("output"), obs_path)
.video_bitrate(bitrate)
.audio_bitrate(self.audio_settings.bitrate)
.hardware_encoder(HardwareCodec::H264, HardwarePreset::Quality)
.format(OutputFormat::Mpeg4)
.build()
}
EncoderCapability::Amf => {
info!("Building AMF output...");
SimpleOutputBuilder::new(context.clone(), ObsString::from("output"), obs_path)
.video_bitrate(bitrate)
.audio_bitrate(self.audio_settings.bitrate)
.hardware_encoder(HardwareCodec::H264, HardwarePreset::Balanced)
.format(OutputFormat::Mpeg4)
.build()
}
EncoderCapability::QuickSync => {
info!("Building QuickSync output...");
SimpleOutputBuilder::new(context.clone(), ObsString::from("output"), obs_path)
.video_bitrate(bitrate)
.audio_bitrate(self.audio_settings.bitrate)
.hardware_encoder(HardwareCodec::H264, HardwarePreset::Balanced)
.format(OutputFormat::Mpeg4)
.build()
}
EncoderCapability::Software => {
info!("Building software (x264) output...");
SimpleOutputBuilder::new(context.clone(), ObsString::from("output"), obs_path)
.video_bitrate(bitrate)
.audio_bitrate(self.audio_settings.bitrate)
.format(OutputFormat::Mpeg4)
.build()
}
};
info!("[START_REC] Output build complete, checking for errors...");
std::io::stderr().flush().ok();
let output = output_result.map_err(|e| {
error!("[START_REC] Failed to create output: {:?}", e);
std::io::stderr().flush().ok();
RecordingError::StartError(format!("Failed to create output: {:?}", e))
})?;
info!("[START_REC] Output created successfully, setting up game capture...");
std::io::stderr().flush().ok();
// Set up game capture source
self.setup_game_capture()?;
info!("[START_REC] Game capture set up, starting output...");
std::io::stderr().flush().ok();
// Start the output - wrap in catch_unwind as this may crash in native code
let start_result =
std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| output.start()));
match start_result {
Ok(Ok(())) => {
info!("[START_REC] Output started successfully");
std::io::stderr().flush().ok();
}
Ok(Err(e)) => {
error!("[START_REC] Failed to start output: {:?}", e);
std::io::stderr().flush().ok();
return Err(
RecordingError::StartError(format!("Failed to start output: {:?}", e)).into(),
);
}
Err(panic_info) => {
error!("[START_REC] PANIC starting output: {:?}", panic_info);
std::io::stderr().flush().ok();
eprintln!("[START_REC] PANIC starting output: {:?}", panic_info);
return Err(RecordingError::StartError("Panic starting output".to_string()).into());
}
}
self.output = Some(output);
self.current_output = Some(output_path.to_path_buf());
self.recording = true;
debug!("OBS recording started");
info!("[START_REC] OBS recording started successfully");
std::io::stderr().flush().ok();
Ok(())
}
/// Set up capture source with fallback from game capture to monitor capture.
fn setup_game_capture(&mut self) -> Result<()> {
use std::io::Write;
info!("[CAPTURE] Setting up capture source...");
std::io::stderr().flush().ok();
// Try game capture first, fall back to monitor capture
match self.try_game_capture() {
Ok(()) => {
info!("[CAPTURE] Game capture set up successfully");
std::io::stderr().flush().ok();
Ok(())
}
Err(e) => {
warn!(
"[CAPTURE] Game capture failed: {}, falling back to monitor capture",
e
);
std::io::stderr().flush().ok();
self.setup_monitor_capture()
}
}
}
/// Try to set up game capture source.
fn try_game_capture(&mut self) -> Result<()> {
use libobs_simple::sources::windows::{GameCaptureSourceBuilder, ObsGameCaptureMode};
use libobs_simple::sources::ObsSourceBuilder;
use std::io::Write;
info!("[GAME_CAPTURE] Attempting game capture setup...");
std::io::stderr().flush().ok();
let context = self.context.as_mut().ok_or_else(|| {
error!("[GAME_CAPTURE] OBS not initialized in setup_game_capture");
std::io::stderr().flush().ok();
RecordingError::ObsInitError("OBS not initialized".to_string())
})?;
// Create a scene
info!("[GAME_CAPTURE] Creating scene 'main'...");
std::io::stderr().flush().ok();
let mut scene = context.scene("main", None).map_err(|e| {
error!("[GAME_CAPTURE] Failed to create scene: {:?}", e);
std::io::stderr().flush().ok();
RecordingError::StartError(format!("Failed to create scene: {:?}", e))
})?;
info!("[GAME_CAPTURE] Scene created successfully");
std::io::stderr().flush().ok();
// Build game capture source
info!("[GAME_CAPTURE] Getting OBS runtime...");
std::io::stderr().flush().ok();
let runtime = context.runtime();
info!("[GAME_CAPTURE] Creating game capture source builder...");
std::io::stderr().flush().ok();
// Wrap game capture builder in catch_unwind as it may crash in native code
let builder_result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
GameCaptureSourceBuilder::new("game_capture", runtime.clone())
}));
let builder = match builder_result {
Ok(Ok(b)) => {
info!("[GAME_CAPTURE] Game capture builder created");
std::io::stderr().flush().ok();
b
}
Ok(Err(e)) => {
error!(
"[GAME_CAPTURE] Failed to create game capture builder: {:?}",
e
);
std::io::stderr().flush().ok();
return Err(RecordingError::StartError(format!(
"Failed to create game capture builder: {:?}",
e
))
.into());
}
Err(panic_info) => {
error!(
"[GAME_CAPTURE] PANIC creating game capture builder: {:?}",
panic_info
);
std::io::stderr().flush().ok();
eprintln!(
"[GAME_CAPTURE] PANIC creating game capture builder: {:?}",
panic_info
);
return Err(RecordingError::StartError(
"Panic creating game capture builder".to_string(),
)
.into());
}
};
info!("[GAME_CAPTURE] Configuring game capture for League of Legends...");
std::io::stderr().flush().ok();
// Use "Any" mode to capture any fullscreen application
// This is the most reliable mode for games like League of Legends
info!("[GAME_CAPTURE] Using 'Any' mode to capture fullscreen games...");
std::io::stderr().flush().ok();
// Wrap source build in catch_unwind
let source_result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
builder.set_capture_mode(ObsGameCaptureMode::Any).build()
}));
let source = match source_result {
Ok(Ok(s)) => {
info!("[GAME_CAPTURE] Game capture source created");
std::io::stderr().flush().ok();
s
}
Ok(Err(e)) => {
error!(
"[GAME_CAPTURE] Failed to create game capture source: {:?}",
e
);
std::io::stderr().flush().ok();
return Err(RecordingError::StartError(format!(
"Failed to create game capture source: {:?}",
e
))
.into());
}
Err(panic_info) => {
error!(
"[GAME_CAPTURE] PANIC creating game capture source: {:?}",
panic_info
);
std::io::stderr().flush().ok();
eprintln!(
"[GAME_CAPTURE] PANIC creating game capture source: {:?}",
panic_info
);
return Err(RecordingError::StartError(
"Panic creating game capture source".to_string(),
)
.into());
}
};
// Add source to scene using add_source
info!("[GAME_CAPTURE] Adding source to scene...");
std::io::stderr().flush().ok();
scene.add_source(source).map_err(|e| {
error!("[GAME_CAPTURE] Failed to add source to scene: {:?}", e);
std::io::stderr().flush().ok();
RecordingError::StartError(format!("Failed to add source to scene: {:?}", e))
})?;
info!("[GAME_CAPTURE] Source added to scene");
std::io::stderr().flush().ok();
// Set the scene as active
info!("[GAME_CAPTURE] Setting scene as active on channel 0...");
std::io::stderr().flush().ok();
scene.set_to_channel(0).map_err(|e| {
error!("[GAME_CAPTURE] Failed to set scene: {:?}", e);
RecordingError::StartError(format!("Failed to set scene: {:?}", e))
})?;
info!("[GAME_CAPTURE] Game capture source configured successfully");
std::io::stderr().flush().ok();
Ok(())
}
/// Set up monitor capture as fallback.
fn setup_monitor_capture(&mut self) -> Result<()> {
use libobs_simple::sources::windows::MonitorCaptureSourceBuilder;
use libobs_simple::sources::ObsSourceBuilder;
use std::io::Write;
info!("[MONITOR_CAPTURE] Setting up monitor capture as fallback...");
std::io::stderr().flush().ok();
let context = self.context.as_mut().ok_or_else(|| {
error!("[MONITOR_CAPTURE] OBS not initialized");
std::io::stderr().flush().ok();
RecordingError::ObsInitError("OBS not initialized".to_string())
})?;
// Create a scene
info!("[MONITOR_CAPTURE] Creating scene 'main'...");
std::io::stderr().flush().ok();
let mut scene = context.scene("main", None).map_err(|e| {
error!("[MONITOR_CAPTURE] Failed to create scene: {:?}", e);
std::io::stderr().flush().ok();
RecordingError::StartError(format!("Failed to create scene: {:?}", e))
})?;
info!("[MONITOR_CAPTURE] Scene created successfully");
std::io::stderr().flush().ok();
// Get monitor info
info!("[MONITOR_CAPTURE] Detecting monitors...");
std::io::stderr().flush().ok();
let monitors = display_info::DisplayInfo::all().map_err(|e| {
error!("[MONITOR_CAPTURE] Failed to get display info: {:?}", e);
std::io::stderr().flush().ok();
RecordingError::StartError(format!("Failed to get display info: {:?}", e))
})?;
if monitors.is_empty() {
error!("[MONITOR_CAPTURE] No monitors detected");
std::io::stderr().flush().ok();
return Err(RecordingError::StartError("No monitors detected".to_string()).into());
}
// Use the primary monitor (first in the list)
let primary_monitor = &monitors[0];
info!(
"[MONITOR_CAPTURE] Using monitor: {}x{} at ({}, {})",
primary_monitor.width, primary_monitor.height, primary_monitor.x, primary_monitor.y
);
std::io::stderr().flush().ok();
// Build monitor capture source
info!("[MONITOR_CAPTURE] Creating monitor capture source builder...");
std::io::stderr().flush().ok();
let builder_result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
MonitorCaptureSourceBuilder::new("monitor_capture", context.runtime().clone())
}));
let builder = match builder_result {
Ok(Ok(b)) => {
info!("[MONITOR_CAPTURE] Monitor capture builder created");
std::io::stderr().flush().ok();
b
}
Ok(Err(e)) => {
error!(
"[MONITOR_CAPTURE] Failed to create monitor capture builder: {:?}",
e
);
std::io::stderr().flush().ok();
return Err(RecordingError::StartError(format!(
"Failed to create monitor capture builder: {:?}",
e
))
.into());
}
Err(panic_info) => {
error!(
"[MONITOR_CAPTURE] PANIC creating monitor capture builder: {:?}",
panic_info
);
std::io::stderr().flush().ok();
eprintln!(
"[MONITOR_CAPTURE] PANIC creating monitor capture builder: {:?}",
panic_info
);
return Err(RecordingError::StartError(
"Panic creating monitor capture builder".to_string(),
)
.into());
}
};
// Build the source
let source_result =
std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| builder.build()));
let source = match source_result {
Ok(Ok(s)) => {
info!("[MONITOR_CAPTURE] Monitor capture source created");
std::io::stderr().flush().ok();
s
}
Ok(Err(e)) => {
error!(
"[MONITOR_CAPTURE] Failed to create monitor capture source: {:?}",
e
);
std::io::stderr().flush().ok();
return Err(RecordingError::StartError(format!(
"Failed to create monitor capture source: {:?}",
e
))
.into());
}
Err(panic_info) => {
error!(
"[MONITOR_CAPTURE] PANIC creating monitor capture source: {:?}",
panic_info
);
std::io::stderr().flush().ok();
eprintln!(
"[MONITOR_CAPTURE] PANIC creating monitor capture source: {:?}",
panic_info
);
return Err(RecordingError::StartError(
"Panic creating monitor capture source".to_string(),
)
.into());
}
};
// Add source to scene
info!("[MONITOR_CAPTURE] Adding source to scene...");
std::io::stderr().flush().ok();
scene.add_source(source).map_err(|e| {
error!("[MONITOR_CAPTURE] Failed to add source to scene: {:?}", e);
std::io::stderr().flush().ok();
RecordingError::StartError(format!("Failed to add source to scene: {:?}", e))
})?;
info!("[MONITOR_CAPTURE] Source added to scene");
std::io::stderr().flush().ok();
// Set the scene as active
info!("[MONITOR_CAPTURE] Setting scene as active on channel 0...");
std::io::stderr().flush().ok();
scene.set_to_channel(0).map_err(|e| {
error!("[MONITOR_CAPTURE] Failed to set scene: {:?}", e);
RecordingError::StartError(format!("Failed to set scene: {:?}", e))
})?;
info!("[MONITOR_CAPTURE] Monitor capture source configured successfully");
std::io::stderr().flush().ok();
Ok(())
}
@@ -216,49 +824,16 @@ impl ObsContext {
info!("Stopping OBS recording");
// Note: Actual libobs recording stop would happen here
// This is a stub implementation
if let Some(mut output) = self.output.take() {
output.stop().map_err(|e| {
RecordingError::StopError(format!("Failed to stop output: {:?}", e))
})?;
}
self.recording = false;
self.current_output = None;
debug!("OBS recording stopped");
Ok(())
}
/// Initialize OBS.
fn initialize(&mut self) -> Result<()> {
if self.initialized {
return Ok(());
}
info!("Initializing OBS context");
// Note: Actual libobs initialization would happen here
// This would involve:
// 1. obs_startup()
// 2. obs_reset_video()
// 3. obs_reset_audio()
// 4. Loading modules (obs-ffmpeg, etc.)
// 5. Creating scene and source
// For now, we simulate initialization
debug!(
"OBS video config: {}x{} @ {}fps",
self.video_info.output_width,
self.video_info.output_height,
self.video_info.fps_num / self.video_info.fps_den
);
if self.audio_settings.enabled {
debug!(
"OBS audio config: {} channels @ {}Hz",
self.audio_settings.channels, self.audio_settings.sample_rate
);
}
self.initialized = true;
info!("OBS context initialized successfully");
info!("OBS recording stopped successfully");
Ok(())
}
@@ -268,13 +843,14 @@ impl ObsContext {
self.stop_recording()?;
}
if self.initialized {
if self.output.is_some() {
self.output = None;
}
if self.context.is_some() {
info!("Shutting down OBS context");
// Note: Actual libobs shutdown would happen here
// obs_shutdown()
self.initialized = false;
// The LibObsContext handles cleanup on drop
self.context = None;
}
Ok(())
@@ -289,49 +865,6 @@ impl Drop for ObsContext {
}
}
// Note: When actual libobs bindings are available, we would add
// FFI bindings here. For now, this provides the interface that
// will be implemented with real libobs calls.
/// Stub module for libobs FFI bindings.
///
/// When actual bindings are available, this would contain:
/// - obs_startup
/// - obs_shutdown
/// - obs_reset_video
/// - obs_reset_audio
/// - obs_scene_create
/// - obs_source_create
/// - obs_output_create
/// - obs_encoder_create
/// etc.
pub mod ffi {
//! libobs FFI bindings (stub).
//!
//! This module will contain the actual FFI bindings to libobs.
//! Currently using stubs until libobs-rs or similar bindings are integrated.
/// Placeholder for obs_video_info struct.
#[repr(C)]
pub struct ObsVideoInfo {
pub adapter: i32,
pub output_width: u32,
pub output_height: u32,
pub fps_num: u32,
pub fps_den: u32,
pub base_width: u32,
pub base_height: u32,
pub output_format: u32,
}
/// Placeholder for obs_audio_info struct.
#[repr(C)]
pub struct ObsAudioInfo {
pub samples_per_sec: u32,
pub speakers: u32,
}
}
#[cfg(test)]
mod tests {
use super::*;
@@ -347,14 +880,4 @@ mod tests {
assert!(!context.is_initialized());
assert!(!context.is_recording());
}
#[test]
fn test_video_info_from_settings() {
let settings = VideoSettings::default();
let info = ObsVideoInfo::from_settings(&settings);
assert_eq!(info.output_width, 1920);
assert_eq!(info.output_height, 1080);
assert_eq!(info.fps_num, 60);
}
}