diff --git a/src/client/general.rs b/src/client/general.rs index 84d5637..ca93ea9 100644 --- a/src/client/general.rs +++ b/src/client/general.rs @@ -121,4 +121,5 @@ impl<'a> General<'a> { } // TODO: Add `ExecuteBatch` request + // TODO: Add `Sleep` request (only useful together with `ExecuteBatch`) } diff --git a/src/client/media_control.rs b/src/client/media_control.rs index 671d921..1875588 100644 --- a/src/client/media_control.rs +++ b/src/client/media_control.rs @@ -15,7 +15,11 @@ impl<'a> MediaControl<'a> { /// /// - `source_name`: Source name. /// - `play_pause`: Whether to pause or play the source. `false` for play, `true` for pause. - pub async fn play_pause_media(&self, source_name: &str, play_pause: bool) -> Result<()> { + pub async fn play_pause_media( + &self, + source_name: &str, + play_pause: Option, + ) -> Result<()> { self.client .send_message(RequestType::PlayPauseMedia { source_name, diff --git a/src/client/mod.rs b/src/client/mod.rs index cc3bdd5..329087d 100644 --- a/src/client/mod.rs +++ b/src/client/mod.rs @@ -41,7 +41,7 @@ pub use self::{ general::General, media_control::MediaControl, outputs::Outputs, profiles::Profiles, recording::Recording, replay_buffer::ReplayBuffer, scene_collections::SceneCollections, scene_items::SceneItems, scenes::Scenes, sources::Sources, streaming::Streaming, - studio_mode::StudioMode, transitions::Transitions, + studio_mode::StudioMode, transitions::Transitions, virtual_cam::VirtualCam, }; mod general; @@ -57,6 +57,7 @@ mod sources; mod streaming; mod studio_mode; mod transitions; +mod virtual_cam; #[derive(Debug, thiserror::Error)] enum InnerError { @@ -121,8 +122,8 @@ where const OBS_STUDIO_VERSION: Comparator = Comparator { op: Op::GreaterEq, - major: 26, - minor: Some(1), + major: 27, + minor: None, patch: None, pre: Prerelease::EMPTY, }; @@ -130,7 +131,7 @@ const OBS_WEBSOCKET_VERSION: Comparator = Comparator { op: Op::Tilde, major: 4, minor: Some(9), - patch: None, + patch: Some(1), pre: Prerelease::EMPTY, }; @@ -468,6 +469,11 @@ impl Client { pub fn transitions(&self) -> Transitions<'_> { Transitions { client: self } } + + /// Access API functions related to the virtual cam. + pub fn virtual_cam(&self) -> VirtualCam<'_> { + VirtualCam { client: self } + } } fn extract_error(value: &mut serde_json::Value) -> Option { @@ -490,23 +496,3 @@ impl Drop for Client { drop(self.disconnect()); } } - -#[cfg(test)] -mod tests { - use semver::Version; - - use super::*; - - #[test] - fn verify_version_req() { - assert!(OBS_STUDIO_VERSION.matches(&Version::new(26, 1, 0))); - assert!(OBS_STUDIO_VERSION.matches(&Version::new(26, 1, 100))); - assert!(OBS_STUDIO_VERSION.matches(&Version::new(26, 100, 100))); - assert!(OBS_STUDIO_VERSION.matches(&Version::new(27, 0, 0))); - - assert!(OBS_WEBSOCKET_VERSION.matches(&Version::new(4, 9, 0))); - assert!(OBS_WEBSOCKET_VERSION.matches(&Version::new(4, 9, 100))); - assert!(!OBS_WEBSOCKET_VERSION.matches(&Version::new(4, 100, 100))); - assert!(!OBS_WEBSOCKET_VERSION.matches(&Version::new(5, 0, 0))); - } -} diff --git a/src/client/sources.rs b/src/client/sources.rs index 1847dc9..a5f8937 100644 --- a/src/client/sources.rs +++ b/src/client/sources.rs @@ -103,6 +103,16 @@ impl<'a> Sources<'a> { .await } + /// Get the source's active status of a specified source (if it is showing in the final mix). + /// + /// - `source_name`: Source name. + pub async fn get_source_active(&self, source_name: &str) -> Result { + self.client + .send_message::(RequestType::GetSourceActive { source_name }) + .await + .map(|sa| sa.source_active) + } + /// Get the audio's active status of a specified source. /// /// - `source_name`: Source name. diff --git a/src/client/virtual_cam.rs b/src/client/virtual_cam.rs new file mode 100644 index 0000000..6b70b70 --- /dev/null +++ b/src/client/virtual_cam.rs @@ -0,0 +1,35 @@ +use super::Client; +use crate::requests::RequestType; +use crate::responses; +use crate::Result; + +/// API functions related to the virtual cam. +pub struct VirtualCam<'a> { + pub(super) client: &'a Client, +} + +impl<'a> VirtualCam<'a> { + /// Get current virtual cam status. + pub async fn get_virtual_cam_status(&self) -> Result { + self.client + .send_message(RequestType::GetVirtualCamStatus) + .await + } + + /// Toggle virtual cam on or off (depending on the current virtual cam state). + pub async fn start_stop_virtual_cam(&self) -> Result<()> { + self.client + .send_message(RequestType::StartStopVirtualCam) + .await + } + + /// Start virtual cam. Will return an `error` if virtual cam is already active. + pub async fn start_virtual_cam(&self) -> Result<()> { + self.client.send_message(RequestType::StartVirtualCam).await + } + + /// Stop virtual cam. Will return an error if virtual cam is not active. + pub async fn stop_virtual_cam(&self) -> Result<()> { + self.client.send_message(RequestType::StopVirtualCam).await + } +} diff --git a/src/common.rs b/src/common.rs index 2b90196..d563c9a 100644 --- a/src/common.rs +++ b/src/common.rs @@ -113,6 +113,31 @@ pub struct Scale { pub x: f64, /// The y-scale factor of the source. pub y: f64, + /// The scale filter of the source. + pub filter: ScaleFilter, +} + +/// Different scaling filters that can be applied to a scene item as part of [`Scale`]. +#[derive(Clone, Copy, Debug, Deserialize)] +pub enum ScaleFilter { + /// Disable any scaling filters. + #[serde(rename = "OBS_SCALE_DISABLE")] + Disable, + /// Nearest neighbor scaling. + #[serde(rename = "OBS_SCALE_POINT")] + Point, + /// Sharpened scaling, 16 samples. + #[serde(rename = "OBS_SCALE_BICUBIC")] + Bicubic, + /// Fast but blurry scaling. + #[serde(rename = "OBS_SCALE_BILINEAR")] + Bilinear, + /// Sharpened scaling, 36 samples. + #[serde(rename = "OBS_SCALE_LANCZOS")] + Lanczos, + /// Weighted sum, 4/6/9 samples. + #[serde(rename = "OBS_SCALE_AREA")] + Area, } /// Response value for diff --git a/src/events.rs b/src/events.rs index f58ae42..86c60eb 100644 --- a/src/events.rs +++ b/src/events.rs @@ -233,6 +233,13 @@ pub enum EventType { /// Current recording resumed. RecordingResumed, // -------------------------------- + // Virtual Cam + // -------------------------------- + /// Virtual cam started successfully. + VirtualCamStarted, + /// Virtual cam stopped successfully. + VirtualCamStopped, + // -------------------------------- // Replay Buffer // -------------------------------- /// A request to start the replay buffer has been issued. @@ -290,6 +297,8 @@ pub enum EventType { source_name: String, /// Source volume. volume: f32, + /// Source volume in Decibel + volume_db: f32, }, /// A source has been muted or unmuted. #[serde(rename_all = "camelCase")] diff --git a/src/requests/mod.rs b/src/requests/mod.rs index dad5ee4..1cc6195 100644 --- a/src/requests/mod.rs +++ b/src/requests/mod.rs @@ -71,7 +71,7 @@ pub(crate) enum RequestType<'a> { /// Source name. source_name: &'a str, /// Whether to pause or play the source. `false` for play, `true` for pause. - play_pause: bool, + play_pause: Option, }, #[serde(rename_all = "camelCase")] RestartMedia { @@ -154,6 +154,11 @@ pub(crate) enum RequestType<'a> { source: &'a str, }, #[serde(rename_all = "camelCase")] + GetSourceActive { + /// Source name. + source_name: &'a str, + }, + #[serde(rename_all = "camelCase")] GetAudioActive { /// Source name. source_name: &'a str, @@ -450,6 +455,13 @@ pub(crate) enum RequestType<'a> { /// manually if you set `release` to false. Defaults to true. release: Option, }, + // -------------------------------- + // Virtual Cam + // -------------------------------- + GetVirtualCamStatus, + StartStopVirtualCam, + StartVirtualCam, + StopVirtualCam, } /// Request information for [`open_projector`](crate::client::General::open_projector). diff --git a/src/responses/mod.rs b/src/responses/mod.rs index b284159..5decc64 100644 --- a/src/responses/mod.rs +++ b/src/responses/mod.rs @@ -288,6 +288,14 @@ pub struct Mute { pub muted: bool, } +/// Response value for [`get_source_active`](crate::client::Sources::get_source_active). +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub(crate) struct SourceActive { + /// Source active status of the source. + pub source_active: bool, +} + /// Response value for [`get_audio_active`](crate::client::Sources::get_audio_active). #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] @@ -1018,3 +1026,15 @@ pub struct Transition { /// Name of the transition. pub name: String, } + +/// Response value for +/// [`get_virtual_cam_status`](crate::client::VirtualCam::get_virtual_cam_status). +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct VirtualCamStatus { + /// Current virtual camera status. + pub is_virtual_cam: bool, + /// Time elapsed since virtual cam started (only present if virtual cam currently active). + #[serde(default, deserialize_with = "crate::de::duration_opt")] + pub virtual_cam_timecode: Option, +} diff --git a/tests/media_control.rs b/tests/media_control.rs index a7e3968..4005cbf 100644 --- a/tests/media_control.rs +++ b/tests/media_control.rs @@ -17,13 +17,13 @@ async fn main() -> Result<()> { pin_mut!(events); - client.play_pause_media(TEST_MEDIA, false).await?; + client.play_pause_media(TEST_MEDIA, Some(false)).await?; wait_for!(events, EventType::MediaPlaying { .. }); client.next_media(TEST_MEDIA).await?; wait_for!(events, EventType::MediaNext { .. }); client.previous_media(TEST_MEDIA).await?; wait_for!(events, EventType::MediaPrevious { .. }); - client.play_pause_media(TEST_MEDIA, true).await?; + client.play_pause_media(TEST_MEDIA, Some(true)).await?; wait_for!(events, EventType::MediaPaused { .. }); let duration = client.get_media_duration(TEST_MEDIA).await?; diff --git a/tests/recording.rs b/tests/recording.rs index 8ae08b7..2391c5f 100644 --- a/tests/recording.rs +++ b/tests/recording.rs @@ -31,11 +31,13 @@ async fn main() -> Result<()> { client.start_recording().await?; wait_for!(events, EventType::RecordingStarted { .. }); - // Pausing doesn't seem to work currently - // client.pause_recording().await?; - // wait_for!(events, EventType::RecordingPaused); - // client.resume_recording().await?; - // wait_for!(events, EventType::RecordingResumed); + time::sleep(Duration::from_secs(1)).await; + client.pause_recording().await?; + wait_for!(events, EventType::RecordingPaused); + time::sleep(Duration::from_secs(1)).await; + client.resume_recording().await?; + wait_for!(events, EventType::RecordingResumed); + time::sleep(Duration::from_secs(1)).await; client.stop_recording().await?; wait_for!(events, EventType::RecordingStopped { .. }); diff --git a/tests/sources.rs b/tests/sources.rs index 74bbe21..88d197b 100644 --- a/tests/sources.rs +++ b/tests/sources.rs @@ -24,6 +24,7 @@ async fn main() -> Result<()> { client.get_sources_list().await?; client.get_sources_types_list().await?; + client.get_source_active(TEST_MEDIA).await?; client.get_audio_active(TEST_MEDIA).await?; client.get_source_default_settings(SOURCE_KIND_VLC).await?; diff --git a/tests/virtual_cam.rs b/tests/virtual_cam.rs new file mode 100644 index 0000000..63b6eb6 --- /dev/null +++ b/tests/virtual_cam.rs @@ -0,0 +1,38 @@ +#![cfg(feature = "test-integration")] + +use std::time::Duration; + +use anyhow::Result; +use futures_util::{pin_mut, StreamExt}; +use obws::events::{Event, EventType}; +use tokio::time; + +#[macro_use] +mod common; + +#[tokio::test] +async fn main() -> Result<()> { + let client = common::new_client().await?; + let events = client.events()?; + let client = client.virtual_cam(); + + pin_mut!(events); + + client.get_virtual_cam_status().await?; + + client.start_stop_virtual_cam().await?; + wait_for!(events, EventType::VirtualCamStarted { .. }); + client.start_stop_virtual_cam().await?; + wait_for!(events, EventType::VirtualCamStopped { .. }); + + // Wait a little more as the virtual cam sometimes doesn't start when started/stopped + // frequently. + time::sleep(Duration::from_secs(1)).await; + + client.start_virtual_cam().await?; + wait_for!(events, EventType::VirtualCamStarted { .. }); + client.stop_virtual_cam().await?; + wait_for!(events, EventType::VirtualCamStopped { .. }); + + Ok(()) +}