From a96db636f670e56206e78deab0bab69a113ca4cd Mon Sep 17 00:00:00 2001 From: Dominik Nakamura Date: Sun, 27 Dec 2020 22:52:56 +0900 Subject: [PATCH] Initial commit --- .gitignore | 4 + Cargo.toml | 32 ++ LICENSE | 19 + README.md | 9 + examples/README.md | 12 + examples/events.rs | 27 ++ examples/iter_scenes.rs | 29 ++ examples/screenshot.rs | 39 ++ examples/simple.rs | 25 + src/client/general.rs | 88 ++++ src/client/mod.rs | 265 +++++++++++ src/client/outputs.rs | 51 ++ src/client/profiles.rs | 37 ++ src/client/recording.rs | 59 +++ src/client/replay_buffer.rs | 44 ++ src/client/scene_collections.rs | 39 ++ src/client/scene_items.rs | 92 ++++ src/client/scenes.rs | 86 ++++ src/client/sources.rs | 348 ++++++++++++++ src/client/streaming.rs | 74 +++ src/client/studio_mode.rs | 67 +++ src/client/transitions.rs | 52 ++ src/common.rs | 143 ++++++ src/events.rs | 511 ++++++++++++++++++++ src/lib.rs | 9 + src/requests.rs | 810 ++++++++++++++++++++++++++++++++ src/responses/de.rs | 14 + src/responses/mod.rs | 751 +++++++++++++++++++++++++++++ 28 files changed, 3736 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.toml create mode 100644 LICENSE create mode 100644 README.md create mode 100644 examples/README.md create mode 100644 examples/events.rs create mode 100644 examples/iter_scenes.rs create mode 100644 examples/screenshot.rs create mode 100644 examples/simple.rs create mode 100644 src/client/general.rs create mode 100644 src/client/mod.rs create mode 100644 src/client/outputs.rs create mode 100644 src/client/profiles.rs create mode 100644 src/client/recording.rs create mode 100644 src/client/replay_buffer.rs create mode 100644 src/client/scene_collections.rs create mode 100644 src/client/scene_items.rs create mode 100644 src/client/scenes.rs create mode 100644 src/client/sources.rs create mode 100644 src/client/streaming.rs create mode 100644 src/client/studio_mode.rs create mode 100644 src/client/transitions.rs create mode 100644 src/common.rs create mode 100644 src/events.rs create mode 100644 src/lib.rs create mode 100644 src/requests.rs create mode 100644 src/responses/de.rs create mode 100644 src/responses/mod.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b2e6735 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +/target + +.env +Cargo.lock diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..7471296 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,32 @@ +[package] +name = "obws" +version = "0.1.0" +authors = ["Dominik Nakamura "] +edition = "2018" +license = "MIT" +readme = "README.md" +description = "The obws (obvious) remote control library for OBS." +homepage = "https://github.com/dnaka91/obws" +repository = "https://github.com/dnaka91/obws" +categories = ["api-bindings", "web-programming"] +keywords = ["async", "obs", "obs-websocket", "remote-control", "tokio"] + +[dependencies] +anyhow = "1.0.36" +async-stream = "0.3.0" +base64 = "0.13.0" +either = { version = "1.6.1", features = ["serde"] } +futures-util = { version = "0.3.8", features = ["sink"] } +log = "0.4.11" +serde = { version = "1.0.118", features = ["derive"] } +serde_json = "1.0.60" +serde_with = "1.6.0" +sha2 = "0.9.2" +tokio = { version = "0.3.6", features = ["net", "sync"] } +tokio-tungstenite = "0.12.0" +tungstenite = { version = "0.11.1", default-features = false } + +[dev-dependencies] +dotenv = "0.15.0" +pretty_env_logger = "0.4.0" +tokio = { version = "0.3.6", features = ["fs", "macros", "rt-multi-thread", "time"] } diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..9a503c6 --- /dev/null +++ b/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2020 Dominik Nakamura + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..8cb0268 --- /dev/null +++ b/README.md @@ -0,0 +1,9 @@ +# OBWS - The obws (obvious) remote control library for OBS + +Library to remote control OBS with the [obs-websocket] plugin. + +[obs-websocket]: https://github.com/Palakis/obs-websocket + +## License + +This project is licensed under [MIT License](LICENSE) (or ). diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 0000000..6c61dcf --- /dev/null +++ b/examples/README.md @@ -0,0 +1,12 @@ +# Examples + +These are several examples that show how to use `obws`. If you require authentication for your OBS +instance, create a `.env` file in the project root and add a `OBS_PASSWORD` entry in it. The +examples will pick up the password and authenticate automatically. + +- `simple` A very basic example showing how to connect, login and print out current version + information and the list of scenes. +- `iter_scenes` Get a list of all scenes and endlessly iterate through them with a small pause + between each change. +- `screenshot` Take a screenshot of the currently visible scene and save it as `screenshot.png`. +- `events` Shows how to get a stream of user events and simply print them out to the terminal. diff --git a/examples/events.rs b/examples/events.rs new file mode 100644 index 0000000..565676f --- /dev/null +++ b/examples/events.rs @@ -0,0 +1,27 @@ +use std::env; + +use anyhow::Result; +use futures_util::{pin_mut, StreamExt}; + +use obws::client::Client; + +#[tokio::main] +async fn main() -> Result<()> { + dotenv::dotenv().ok(); + + env::set_var("RUST_LOG", "obws=debug"); + pretty_env_logger::init(); + + let client = Client::connect("localhost", 4444).await?; + + client.login(env::var("OBS_PASSWORD").ok()).await?; + + let events = client.events(); + pin_mut!(events); + + while let Some(event) = events.next().await { + println!("{:#?}", event); + } + + Ok(()) +} diff --git a/examples/iter_scenes.rs b/examples/iter_scenes.rs new file mode 100644 index 0000000..18f9d3e --- /dev/null +++ b/examples/iter_scenes.rs @@ -0,0 +1,29 @@ +use std::{env, time::Duration}; + +use anyhow::Result; + +use obws::client::Client; + +#[tokio::main] +async fn main() -> Result<()> { + dotenv::dotenv().ok(); + + env::set_var("RUST_LOG", "obws=debug"); + pretty_env_logger::init(); + + let client = Client::connect("localhost", 4444).await?; + + client.login(env::var("OBS_PASSWORD").ok()).await?; + + let scene_list = client.scenes().get_scene_list().await?; + + for scene in scene_list.scenes.iter().cycle() { + client + .scenes() + .set_current_scene(scene.name.clone()) + .await?; + tokio::time::sleep(Duration::from_secs(1)).await; + } + + Ok(()) +} diff --git a/examples/screenshot.rs b/examples/screenshot.rs new file mode 100644 index 0000000..a8c1b1b --- /dev/null +++ b/examples/screenshot.rs @@ -0,0 +1,39 @@ +use std::env; + +use anyhow::Result; +use tokio::fs; + +use obws::{client::Client, requests::SourceScreenshot}; + +#[tokio::main] +async fn main() -> Result<()> { + dotenv::dotenv().ok(); + + env::set_var("RUST_LOG", "obws=debug"); + pretty_env_logger::init(); + + let client = Client::connect("localhost", 4444).await?; + + client.login(env::var("OBS_PASSWORD").ok()).await?; + + let screenshot = client + .sources() + .take_source_screenshot(SourceScreenshot { + source_name: Some("Start".to_owned()), + embed_picture_format: Some("png".to_owned()), + save_to_file_path: None, + file_format: None, + compress_quality: None, + width: None, + height: None, + }) + .await?; + + let image = screenshot.img.unwrap(); + let pos = image.find("base64,").unwrap(); + let image = base64::decode(&image[pos + 7..])?; + + fs::write("screenshot.png", &image).await?; + + Ok(()) +} diff --git a/examples/simple.rs b/examples/simple.rs new file mode 100644 index 0000000..ae4b95f --- /dev/null +++ b/examples/simple.rs @@ -0,0 +1,25 @@ +use std::env; + +use anyhow::Result; + +use obws::client::Client; + +#[tokio::main] +async fn main() -> Result<()> { + dotenv::dotenv().ok(); + + env::set_var("RUST_LOG", "obws=debug"); + pretty_env_logger::init(); + + let client = Client::connect("localhost", 4444).await?; + + let version = client.general().get_version().await?; + println!("{:#?}", version); + + client.login(env::var("OBS_PASSWORD").ok()).await?; + + let scene_list = client.scenes().get_scene_list().await?; + println!("{:#?}", scene_list); + + Ok(()) +} diff --git a/src/client/general.rs b/src/client/general.rs new file mode 100644 index 0000000..139bd74 --- /dev/null +++ b/src/client/general.rs @@ -0,0 +1,88 @@ +use anyhow::Result; +use serde::Serialize; + +use super::Client; +use crate::requests::{Projector, RequestType}; +use crate::responses; + +/// General functions of the API. +pub struct General<'a> { + pub(super) client: &'a Client, +} + +impl<'a> General<'a> { + /// Returns the latest version of the plugin and the API. + pub async fn get_version(&self) -> Result { + self.client.send_message(RequestType::GetVersion).await + } + + /// Tells the client if authentication is required. If so, returns authentication parameters + /// `challenge` and `salt`. + pub async fn get_auth_required(&self) -> Result { + self.client.send_message(RequestType::GetAuthRequired).await + } + + /// Attempt to authenticate the client to the server. + /// + /// - `auth`: Response to the auth challenge. + pub async fn authenticate(&self, auth: String) -> Result<()> { + self.client + .send_message(RequestType::Authenticate { auth }) + .await + } + + /// Set the filename formatting string. + /// + /// - `filename_formatting`: Filename formatting string to set. + pub async fn set_filename_formatting(&self, filename_formatting: String) -> Result<()> { + self.client + .send_message(RequestType::SetFilenameFormatting { + filename_formatting, + }) + .await + } + + /// Get the filename formatting string. + pub async fn get_filename_formatting(&self) -> Result { + self.client + .send_message::(RequestType::GetFilenameFormatting) + .await + .map(|ff| ff.filename_formatting) + } + + /// Get OBS stats (almost the same info as provided in OBS' stats window). + pub async fn get_stats(&self) -> Result { + self.client + .send_message::(RequestType::GetStats) + .await + .map(|s| s.stats) + } + + /// Broadcast custom message to all connected WebSocket clients. + /// + /// - `realm`: Identifier to be choosen by the client. + /// - `data`: User-defined data. + pub async fn broadcast_custom_message(&self, realm: String, data: T) -> Result<()> + where + T: Serialize, + { + self.client + .send_message(RequestType::BroadcastCustomMessage { + realm, + data: serde_json::to_value(&data)?, + }) + .await + } + + /// Get basic OBS video information. + pub async fn get_video_info(&self) -> Result { + self.client.send_message(RequestType::GetVideoInfo).await + } + + /// Open a projector window or create a projector on a monitor. Requires OBS v24.0.4 or newer. + pub async fn open_projector(&self, projector: Projector) -> Result<()> { + self.client + .send_message(RequestType::OpenProjector(projector)) + .await + } +} diff --git a/src/client/mod.rs b/src/client/mod.rs new file mode 100644 index 0000000..5a7feee --- /dev/null +++ b/src/client/mod.rs @@ -0,0 +1,265 @@ +//! The client to the obs-websocket API and main entry point. + +use std::{ + collections::HashMap, + sync::{atomic::AtomicU64, Arc}, +}; + +use anyhow::{bail, Result}; +use futures_util::{ + sink::SinkExt, + stream::{SplitSink, Stream, StreamExt}, +}; +use log::{debug, error, trace}; +use serde::de::DeserializeOwned; +use tokio::{ + net::TcpStream, + sync::{broadcast, oneshot, Mutex}, +}; +use tokio_tungstenite::{tungstenite::Message, WebSocketStream}; + +use crate::{ + events::Event, + requests::{Request, RequestType}, + responses::{AuthRequired, Response}, +}; + +pub use self::{ + general::General, 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, +}; + +mod general; +mod outputs; +mod profiles; +mod recording; +mod replay_buffer; +mod scene_collections; +mod scene_items; +mod scenes; +mod sources; +mod streaming; +mod studio_mode; +mod transitions; + +/// The client is the main entry point to access the obs-websocket API. It allows to call various +/// functions to remote control an OBS instance as well as to listen to events caused by the user +/// by interacting with OBS. +pub struct Client { + write: Mutex, + id_counter: AtomicU64, + receivers: Arc>>>, + event_sender: broadcast::Sender, +} + +type MessageWriter = SplitSink, Message>; + +impl Client { + /// Connect to a obs-websocket instance on the given host and port. + pub async fn connect(host: impl AsRef, port: u16) -> Result { + let (socket, _) = + tokio_tungstenite::connect_async(format!("ws://{}:{}", host.as_ref(), port)).await?; + let (write, mut read) = socket.split(); + let receivers = Arc::new(Mutex::new(HashMap::< + String, + oneshot::Sender, + >::new())); + let receivers2 = Arc::clone(&receivers); + let (event_sender, _) = broadcast::channel(100); + let events_tx = event_sender.clone(); + + tokio::spawn(async move { + while let Some(Ok(msg)) = read.next().await { + trace!("{}", msg); + let temp: Result<()> = async { + let json = serde_json::from_str::(&msg.into_text()?)?; + + if let Some(message_id) = json + .as_object() + .and_then(|obj| obj.get("message-id")) + .and_then(|id| id.as_str()) + { + debug!("got message with id {}", message_id); + if let Some(tx) = receivers2.lock().await.remove(message_id) { + tx.send(json).ok(); + } + } else { + let event = serde_json::from_value(json)?; + events_tx.send(event).ok(); + } + + Ok(()) + } + .await; + + if let Err(e) = temp { + error!("{:?}", e); + } + } + }); + + let write = Mutex::new(write); + let id_counter = AtomicU64::new(1); + + Ok(Self { + write, + id_counter, + receivers, + event_sender, + }) + } + + async fn send_message(&self, req: RequestType) -> Result + where + T: DeserializeOwned, + { + let id = self + .id_counter + .fetch_add(1, std::sync::atomic::Ordering::SeqCst) + .to_string(); + let req = Request { + message_id: id.clone(), + ty: req, + }; + let json = serde_json::to_string(&req)?; + + let (tx, rx) = oneshot::channel(); + self.receivers.lock().await.insert(id, tx); + + debug!("sending message: {}", json); + self.write.lock().await.send(Message::Text(json)).await?; + + let resp = rx.await?; + + if let Some(error) = resp + .as_object() + .and_then(|o| o.get("error")) + .and_then(|e| e.as_str()) + { + bail!("{}", error); + } + + serde_json::from_value::>(resp) + .map(|r| r.details) + .map_err(Into::into) + } + + /// Login to the OBS websocket if an authentication is required. + pub async fn login(&self, password: Option>) -> Result<()> { + let auth_required = self.general().get_auth_required().await?; + + if let AuthRequired { + auth_required: true, + challenge: Some(challenge), + salt: Some(salt), + } = auth_required + { + match password { + Some(password) => { + let auth = Self::create_auth_response(&challenge, &salt, password.as_ref()); + self.general().authenticate(auth).await?; + } + None => bail!("authentication required but no password provided"), + } + } + + Ok(()) + } + + fn create_auth_response(challenge: &str, salt: &str, password: &str) -> String { + use sha2::{Digest, Sha256}; + + let mut hasher = Sha256::new(); + hasher.update(password.as_bytes()); + hasher.update(salt.as_bytes()); + + let mut auth = String::with_capacity(Sha256::output_size() * 4 / 3 + 4); + + base64::encode_config_buf(hasher.finalize_reset(), base64::STANDARD, &mut auth); + + hasher.update(auth.as_bytes()); + hasher.update(challenge.as_bytes()); + auth.clear(); + + base64::encode_config_buf(hasher.finalize(), base64::STANDARD, &mut auth); + + auth + } + + /// Get a stream of events. Each call to this function creates a new listener, therefore it's + /// recommended to keep the stream around and iterate over it. + /// + /// **Note**: To be able to iterate over the stream you have to pin it with + /// [`futures_util::pin_mut`] for example. + pub fn events(&self) -> impl Stream { + let mut receiver = self.event_sender.subscribe(); + + async_stream::stream! { + while let Ok(event) = receiver.recv().await { + yield event; + } + } + } + + /// Access general API functions. + pub fn general(&self) -> General<'_> { + General { client: self } + } + + /// Access API functions related to sources. + pub fn sources(&self) -> Sources<'_> { + Sources { client: self } + } + + /// Access API functions related to outputs. + pub fn outputs(&self) -> Outputs<'_> { + Outputs { client: self } + } + + /// Access API functions related to profiles. + pub fn profiles(&self) -> Profiles<'_> { + Profiles { client: self } + } + + /// Access API functions related to recording. + pub fn recording(&self) -> Recording<'_> { + Recording { client: self } + } + + /// Access API functions related to the replay buffer. + pub fn replay_buffer(&self) -> ReplayBuffer<'_> { + ReplayBuffer { client: self } + } + + /// Access API functions related to scene collections. + pub fn scene_collections(&self) -> SceneCollections<'_> { + SceneCollections { client: self } + } + + /// Access API functions related to scene items. + pub fn scene_items(&self) -> SceneItems<'_> { + SceneItems { client: self } + } + + /// Access API functions related to scenes. + pub fn scenes(&self) -> Scenes<'_> { + Scenes { client: self } + } + + /// Access API functions related to streaming. + pub fn streaming(&self) -> Streaming<'_> { + Streaming { client: self } + } + + /// Access API functions related to the studio mode. + pub fn studio_mode(&self) -> StudioMode<'_> { + StudioMode { client: self } + } + + /// Access API functions related to transitions. + pub fn transitions(&self) -> Transitions<'_> { + Transitions { client: self } + } +} diff --git a/src/client/outputs.rs b/src/client/outputs.rs new file mode 100644 index 0000000..d458a33 --- /dev/null +++ b/src/client/outputs.rs @@ -0,0 +1,51 @@ +use anyhow::Result; + +use super::Client; +use crate::requests::RequestType; +use crate::responses; + +/// API functions related to outputs. +pub struct Outputs<'a> { + pub(super) client: &'a Client, +} + +impl<'a> Outputs<'a> { + /// List existing outputs. + pub async fn list_outputs(&self) -> Result> { + self.client + .send_message::(RequestType::ListOutputs) + .await + .map(|o| o.outputs) + } + + /// Get information about a single output. + /// + /// - `output_name`: Output name. + pub async fn get_output_info(&self, output_name: String) -> Result { + self.client + .send_message::(RequestType::GetOutputInfo { output_name }) + .await + .map(|o| o.output_info) + } + + /// Note: Controlling outputs is an experimental feature of obs-websocket. Some plugins which + /// add outputs to OBS may not function properly when they are controlled in this way. + /// + /// - `output_name`: Output name. + pub async fn start_output(&self, output_name: String) -> Result<()> { + self.client + .send_message(RequestType::StartOutput { output_name }) + .await + } + + /// Note: Controlling outputs is an experimental feature of obs-websocket. Some plugins which + /// add outputs to OBS may not function properly when they are controlled in this way. + /// + /// - `output_name`: Output name. + /// - `force`: Force stop (default: false). + pub async fn stop_output(&self, output_name: String, force: Option) -> Result<()> { + self.client + .send_message(RequestType::StopOutput { output_name, force }) + .await + } +} diff --git a/src/client/profiles.rs b/src/client/profiles.rs new file mode 100644 index 0000000..64634b1 --- /dev/null +++ b/src/client/profiles.rs @@ -0,0 +1,37 @@ +use anyhow::Result; + +use super::Client; +use crate::requests::RequestType; +use crate::responses; + +/// API functions related to profiles. +pub struct Profiles<'a> { + pub(super) client: &'a Client, +} + +impl<'a> Profiles<'a> { + /// Set the currently active profile. + /// + /// - `profile_name`: Name of the desired profile. + pub async fn set_current_profile(&self, profile_name: String) -> Result<()> { + self.client + .send_message(RequestType::SetCurrentProfile { profile_name }) + .await + } + + /// Get the name of the current profile. + pub async fn get_current_profile(&self) -> Result { + self.client + .send_message::(RequestType::GetCurrentProfile) + .await + .map(|cp| cp.profile_name) + } + + /// Get a list of available profiles. + pub async fn list_profiles(&self) -> Result> { + self.client + .send_message::(RequestType::ListProfiles) + .await + .map(|cp| cp.profiles) + } +} diff --git a/src/client/recording.rs b/src/client/recording.rs new file mode 100644 index 0000000..4f2250b --- /dev/null +++ b/src/client/recording.rs @@ -0,0 +1,59 @@ +use anyhow::Result; + +use super::Client; +use crate::requests::RequestType; +use crate::responses; + +/// API functions related to recording. +pub struct Recording<'a> { + pub(super) client: &'a Client, +} + +impl<'a> Recording<'a> { + /// Toggle recording on or off (depending on the current recording state). + pub async fn start_stop_recording(&self) -> Result<()> { + self.client + .send_message(RequestType::StartStopRecording) + .await + } + + /// Start recording. Will return an `error` if recording is already active. + pub async fn start_recording(&self) -> Result<()> { + self.client.send_message(RequestType::StartRecording).await + } + + /// Stop recording. Will return an `error` if recording is not active. + pub async fn stop_recording(&self) -> Result<()> { + self.client.send_message(RequestType::StopRecording).await + } + + /// Pause the current recording. Returns an `error` if recording is not active or already + /// paused. + pub async fn pause_recording(&self) -> Result<()> { + self.client.send_message(RequestType::PauseRecording).await + } + + /// Resume/unpause the current recording (if paused). Returns an error if recording is not + /// active or not paused. + pub async fn resume_recording(&self) -> Result<()> { + self.client.send_message(RequestType::ResumeRecording).await + } + + /// Please note: if this is called while a recording is in progress, the change won't be applied + /// immediately and will be effective on the next recording. + /// + /// - `rec_folder`: Path of the recording folder. + pub async fn set_recording_folder(&self, rec_folder: String) -> Result<()> { + self.client + .send_message(RequestType::SetRecordingFolder { rec_folder }) + .await + } + + /// Get the path of the current recording folder. + pub async fn get_recording_folder(&self) -> Result { + self.client + .send_message::(RequestType::GetRecordingFolder) + .await + .map(|rf| rf.rec_folder) + } +} diff --git a/src/client/replay_buffer.rs b/src/client/replay_buffer.rs new file mode 100644 index 0000000..d0e9aee --- /dev/null +++ b/src/client/replay_buffer.rs @@ -0,0 +1,44 @@ +use anyhow::Result; + +use super::Client; +use crate::requests::RequestType; + +/// API functions related to the replay buffer. +pub struct ReplayBuffer<'a> { + pub(super) client: &'a Client, +} + +impl<'a> ReplayBuffer<'a> { + /// Toggle the Replay Buffer on/off (depending on the current state of the replay buffer). + pub async fn start_stop_replay_buffer(&self) -> Result<()> { + self.client + .send_message(RequestType::StartStopReplayBuffer) + .await + } + + /// Start recording into the Replay Buffer. Will return an `error` if the Replay Buffer is + /// already active or if the "Save Replay Buffer" hotkey is not set in OBS' settings. Setting + /// this hotkey is mandatory, even when triggering saves only through obs-websocket. + pub async fn start_replay_buffer(&self) -> Result<()> { + self.client + .send_message(RequestType::StartReplayBuffer) + .await + } + + /// Stop recording into the Replay Buffer. Will return an `error` if the Replay Buffer is not + /// active. + pub async fn stop_replay_buffer(&self) -> Result<()> { + self.client + .send_message(RequestType::StopReplayBuffer) + .await + } + + /// Flush and save the contents of the Replay Buffer to disk. This is basically the same as + /// triggering the "Save Replay Buffer" hotkey. Will return an `error` if the Replay Buffer is + /// not active. + pub async fn save_replay_buffer(&self) -> Result<()> { + self.client + .send_message(RequestType::SaveReplayBuffer) + .await + } +} diff --git a/src/client/scene_collections.rs b/src/client/scene_collections.rs new file mode 100644 index 0000000..e999ff0 --- /dev/null +++ b/src/client/scene_collections.rs @@ -0,0 +1,39 @@ +use anyhow::Result; + +use super::Client; +use crate::requests::RequestType; +use crate::responses; + +/// API functions related to scene collections. +pub struct SceneCollections<'a> { + pub(super) client: &'a Client, +} + +impl<'a> SceneCollections<'a> { + /// Change the active scene collection. + /// + /// - `sc_name`: Name of the desired scene collection. + pub async fn set_current_scene_collection(&self, sc_name: String) -> Result<()> { + self.client + .send_message(RequestType::SetCurrentSceneCollection { sc_name }) + .await + } + + /// Get the name of the current scene collection. + pub async fn get_current_scene_collection(&self) -> Result { + self.client + .send_message::( + RequestType::GetCurrentSceneCollection, + ) + .await + .map(|csc| csc.sc_name) + } + + /// List available scene collections. + pub async fn list_scene_collections(&self) -> Result> { + self.client + .send_message::(RequestType::ListSceneCollections) + .await + .map(|sc| sc.scene_collections) + } +} diff --git a/src/client/scene_items.rs b/src/client/scene_items.rs new file mode 100644 index 0000000..1bc5569 --- /dev/null +++ b/src/client/scene_items.rs @@ -0,0 +1,92 @@ +use anyhow::Result; +use either::Either; + +use super::Client; +use crate::requests::{ + AddSceneItem, DuplicateSceneItem, RequestType, SceneItemProperties, SceneItemRender, + SceneItemSpecification, +}; +use crate::responses; + +/// API functions related to scene items. +pub struct SceneItems<'a> { + pub(super) client: &'a Client, +} + +impl<'a> SceneItems<'a> { + /// Gets the scene specific properties of the specified source item. Coordinates are relative to + /// the item's parent (the scene or group it belongs to). + /// + /// - `scene_name`: Name of the scene the scene item belongs to. Defaults to the current scene. + /// - `item`: Scene Item name (if this field is a string) or specification (if it is an object). + pub async fn get_scene_item_properties( + &self, + scene_name: Option, + item: Either, + ) -> Result> { + self.client + .send_message(RequestType::GetSceneItemProperties { scene_name, item }) + .await + } + + /// Sets the scene specific properties of a source. Unspecified properties will remain + /// unchanged. Coordinates are relative to the item's parent (the scene or group it belongs to). + pub async fn set_scene_item_properties(&self, properties: SceneItemProperties) -> Result<()> { + self.client + .send_message(RequestType::SetSceneItemProperties(properties)) + .await + } + + /// Reset a scene item. + /// + /// - `scene_name`: Name of the scene the scene item belongs to. Defaults to the current scene. + /// - `item`: Scene Item name (if this field is a string) or specification (if it is an object). + pub async fn reset_scene_item( + &self, + scene_name: Option, + item: Either, + ) -> Result<()> { + self.client + .send_message(RequestType::ResetSceneItem { scene_name, item }) + .await + } + + /// Show or hide a specified source item in a specified scene. + pub async fn set_scene_item_render(&self, scene_item_render: SceneItemRender) -> Result<()> { + self.client + .send_message(RequestType::SetSceneItemRender(scene_item_render)) + .await + } + + /// Deletes a scene item. + /// + /// - `scene`: Name of the scene the scene item belongs to. Defaults to the current scene. + /// - `item`: Scene item to delete. + pub async fn delete_scene_item( + &self, + scene: Option, + item: SceneItemSpecification, + ) -> Result<()> { + self.client + .send_message(RequestType::DeleteSceneItem { scene, item }) + .await + } + + /// Creates a scene item in a scene. In other words, this is how you add a source into a scene. + pub async fn add_scene_item(&self, scene_item: AddSceneItem) -> Result { + self.client + .send_message::(RequestType::AddSceneItem(scene_item)) + .await + .map(|asi| asi.item_id) + } + + /// Duplicates a scene item. + pub async fn duplicate_scene_item( + &self, + scene_item: DuplicateSceneItem, + ) -> Result { + self.client + .send_message(RequestType::DuplicateSceneItem(scene_item)) + .await + } +} diff --git a/src/client/scenes.rs b/src/client/scenes.rs new file mode 100644 index 0000000..7bcfce5 --- /dev/null +++ b/src/client/scenes.rs @@ -0,0 +1,86 @@ +use anyhow::Result; + +use super::Client; +use crate::requests::{RequestType, Scene, SceneTransitionOverride}; +use crate::responses; + +/// API functions related to scenes. +pub struct Scenes<'a> { + pub(super) client: &'a Client, +} + +impl<'a> Scenes<'a> { + /// Switch to the specified scene. + /// + /// - `scene_name`: Name of the scene to switch to. + pub async fn set_current_scene(&self, scene_name: String) -> Result<()> { + self.client + .send_message(RequestType::SetCurrentScene { scene_name }) + .await + } + + /// Get the current scene's name and source items. + pub async fn get_current_scene(&self) -> Result { + self.client.send_message(RequestType::GetCurrentScene).await + } + + /// Get a list of scenes in the currently active profile. + pub async fn get_scene_list(&self) -> Result { + self.client.send_message(RequestType::GetSceneList).await + } + + /// Create a new scene scene. + /// + /// - `scene_name`: Name of the scene to create. + pub async fn create_scene(&self, scene_name: String) -> Result<()> { + self.client + .send_message(RequestType::CreateScene { scene_name }) + .await + } + + /// Changes the order of scene items in the requested scene. + /// + /// - `scene`: Name of the scene to reorder (defaults to current). + /// - `items`: Ordered list of objects with name and/or id specified. Id preferred due to + /// uniqueness per scene + pub async fn reorder_scene_items( + &self, + scene: Option, + items: Vec, + ) -> Result<()> { + self.client + .send_message(RequestType::ReorderSceneItems { scene, items }) + .await + } + + /// Set a scene to use a specific transition override. + pub async fn set_scene_transition_override( + &self, + scene_transition: SceneTransitionOverride, + ) -> Result<()> { + self.client + .send_message(RequestType::SetSceneTransitionOverride(scene_transition)) + .await + } + + /// Remove any transition override on a scene. + /// + /// - `scene_name`: Name of the scene to remove the override from. + pub async fn remove_scene_transition_override(&self, scene_name: String) -> Result<()> { + self.client + .send_message(RequestType::RemoveSceneTransitionOverride { scene_name }) + .await + } + + /// Get the current scene transition override. + /// + /// - `scene_name`: Name of the scene to get the override for. + pub async fn get_scene_transition_override( + &self, + scene_name: String, + ) -> Result { + self.client + .send_message(RequestType::GetSceneTransitionOverride { scene_name }) + .await + } +} diff --git a/src/client/sources.rs b/src/client/sources.rs new file mode 100644 index 0000000..c1f7092 --- /dev/null +++ b/src/client/sources.rs @@ -0,0 +1,348 @@ +use anyhow::Result; +use serde::de::DeserializeOwned; + +use super::Client; +use crate::requests::{ + AddFilter, MoveFilter, ReorderFilter, RequestType, SourceFilterSettings, + SourceFilterVisibility, SourceScreenshot, SourceSettings, TextFreetype2Properties, + TextGdiPlusProperties, Volume, +}; +use crate::responses; + +/// API functions related to sources. +pub struct Sources<'a> { + pub(super) client: &'a Client, +} + +impl<'a> Sources<'a> { + /// List all sources available in the running OBS instance. + pub async fn get_sources_list(&self) -> Result> { + self.client + .send_message::(RequestType::GetSourcesList) + .await + .map(|sl| sl.sources) + } + + /// Get a list of all available sources types. + pub async fn get_sources_types_list(&self) -> Result> { + self.client + .send_message::(RequestType::GetSourceTypesList) + .await + .map(|stl| stl.types) + } + + /// Get the volume of the specified source. Default response uses mul format, NOT SLIDER + /// PERCENTAGE. + /// + /// - `source`: Source name. + /// - `use_decibel`: Output volume in decibels of attenuation instead of amplitude/mul. + pub async fn get_volume( + &self, + source: String, + use_decibel: Option, + ) -> Result { + self.client + .send_message(RequestType::GetVolume { + source, + use_decibel, + }) + .await + } + + /// Set the volume of the specified source. Default request format uses mul, NOT SLIDER + /// PERCENTAGE. + pub async fn set_volume(&self, volume: Volume) -> Result<()> { + self.client + .send_message(RequestType::SetVolume(volume)) + .await + } + + /// Get the mute status of a specified source. + /// + /// - `source`: Source name. + pub async fn get_mute(&self, source: String) -> Result { + self.client + .send_message(RequestType::GetMute { source }) + .await + } + + /// Sets the mute status of a specified source. + /// + /// - `source`: Source name. + /// - `mute`: Desired mute status. + pub async fn set_mute(&self, source: String, mute: bool) -> Result<()> { + self.client + .send_message(RequestType::SetMute { source, mute }) + .await + } + + /// Inverts the mute status of a specified source. + /// + /// - `source`: Source name. + pub async fn toggle_mute(&self, source: String) -> Result<()> { + self.client + .send_message(RequestType::ToggleMute { source }) + .await + } + + /// Get the audio's active status of a specified source. + /// + /// - `source_name`: Source name. + pub async fn get_audio_active(&self, source_name: String) -> Result { + self.client + .send_message::(RequestType::GetAudioActive { source_name }) + .await + .map(|aa| aa.audio_active) + } + + /// Note: If the new name already exists as a source, obs-websocket will return an error. + /// + /// - `source_name`: Source name. + /// - `new_name`: New source name. + pub async fn set_source_name(&self, source_name: String, new_name: String) -> Result<()> { + self.client + .send_message(RequestType::SetSourceName { + source_name, + new_name, + }) + .await + } + + /// Set the audio sync offset of a specified source. + /// + /// - `source`: Source name. + /// - `offset`: The desired audio sync offset (in nanoseconds). + pub async fn set_sync_offset(&self, source: String, offset: i64) -> Result<()> { + self.client + .send_message(RequestType::SetSyncOffset { source, offset }) + .await + } + + /// Get the audio sync offset of a specified source. + /// + /// - `source`: Source name. + pub async fn get_sync_offset(&self, source: String) -> Result { + self.client + .send_message(RequestType::GetSyncOffset { source }) + .await + } + + /// Get settings of the specified source. + /// + /// - `source_name`: Source name. + /// - `source_type`: Type of the specified source. Useful for type-checking if you expect a + /// specific settings schema. + pub async fn get_source_settings( + &self, + source_name: String, + source_type: Option, + ) -> Result> + where + T: DeserializeOwned, + { + self.client + .send_message(RequestType::GetSourceSettings { + source_name, + source_type, + }) + .await + } + + /// Set settings of the specified source. + pub async fn set_source_settings( + &self, + source_settings: SourceSettings, + ) -> Result> + where + T: DeserializeOwned, + { + self.client + .send_message(RequestType::SetSourceSettings(source_settings)) + .await + } + + /// Get the current properties of a Text GDI Plus source. + /// + /// - `source`: Source name. + pub async fn get_text_gdi_plus_properties( + &self, + source: String, + ) -> Result { + self.client + .send_message(RequestType::GetTextGDIPlusProperties { source }) + .await + } + + /// Set the current properties of a Text GDI Plus source. + pub async fn set_text_gdi_plus_properties( + &self, + properties: TextGdiPlusProperties, + ) -> Result<()> { + self.client + .send_message(RequestType::SetTextGDIPlusProperties(Box::new(properties))) + .await + } + + /// Get the current properties of a Text Freetype 2 source. + /// + /// - `source`: Source name. + pub async fn get_text_freetype2_properties( + &self, + source: String, + ) -> Result { + self.client + .send_message(RequestType::GetTextFreetype2Properties { source }) + .await + } + + /// Set the current properties of a Text Freetype 2 source. + pub async fn set_text_freetype2_properties( + &self, + properties: TextFreetype2Properties, + ) -> Result<()> { + self.client + .send_message(RequestType::SetTextFreetype2Properties(properties)) + .await + } + + /// Get configured special sources like Desktop Audio and Mic/Aux sources. + pub async fn get_special_sources(&self) -> Result { + self.client + .send_message(RequestType::GetSpecialSources) + .await + } + + /// List filters applied to a source + /// + /// - `source_name`: Source name. + pub async fn get_source_filters( + &self, + source_name: String, + ) -> Result> { + self.client + .send_message::(RequestType::GetSourceFilters { source_name }) + .await + .map(|sf| sf.filters) + } + + /// List filters applied to a source. + /// + /// - `source_name`: Source name. + /// - `filter_name`: Source filter name. + pub async fn get_source_filter_info( + &self, + source_name: String, + filter_name: String, + ) -> Result> + where + T: DeserializeOwned, + { + self.client + .send_message(RequestType::GetSourceFilterInfo { + source_name, + filter_name, + }) + .await + } + + /// Add a new filter to a source. Available source types along with their settings properties + /// are available from [`get_sources_types_list`](Self::get_sources_types_list). + pub async fn add_filter_to_source(&self, add_filter: AddFilter) -> Result<()> { + self.client + .send_message(RequestType::AddFilterToSource(add_filter)) + .await + } + + /// Remove a filter from a source. + /// + /// - `source_name`: Name of the source from which the specified filter is removed. + /// - `filter_name`: Name of the filter to remove. + pub async fn remove_filter_from_source( + &self, + source_name: String, + filter_name: String, + ) -> Result<()> { + self.client + .send_message(RequestType::RemoveFilterFromSource { + source_name, + filter_name, + }) + .await + } + + /// Move a filter in the chain (absolute index positioning). + pub async fn reorder_source_filter(&self, reorder_filter: ReorderFilter) -> Result<()> { + self.client + .send_message(RequestType::ReorderSourceFilter(reorder_filter)) + .await + } + + /// Move a filter in the chain (relative positioning). + pub async fn move_source_filter(&self, move_filter: MoveFilter) -> Result<()> { + self.client + .send_message(RequestType::MoveSourceFilter(move_filter)) + .await + } + + /// Update settings of a filter. + pub async fn set_source_filter_settings(&self, settings: SourceFilterSettings) -> Result<()> { + self.client + .send_message(RequestType::SetSourceFilterSettings(settings)) + .await + } + + /// Change the visibility/enabled state of a filter. + pub async fn set_source_filter_visibility( + &self, + visibility: SourceFilterVisibility, + ) -> Result<()> { + self.client + .send_message(RequestType::SetSourceFilterVisibility(visibility)) + .await + } + + /// Get the audio monitoring type of the specified source. + /// + /// - `source_name`: Source name. + pub async fn get_audio_monitor_type(&self, source_name: String) -> Result { + self.client + .send_message::(RequestType::GetAudioMonitorType { + source_name, + }) + .await + .map(|amt| amt.monitor_type) + } + + /// Set the audio monitoring type of the specified source. + /// + /// - `source_name`: Source name. + /// - `monitor_type`: The monitor type to use. Options: `none`, `monitorOnly`, + /// `monitorAndOutput`. + pub async fn set_audio_monitor_type( + &self, + source_name: String, + monitor_type: String, + ) -> Result<()> { + self.client + .send_message(RequestType::SetAudioMonitorType { + source_name, + monitor_type, + }) + .await + } + + /// At least [`embed_picture_format`](SourceScreenshot::embed_picture_format) or + /// [`save_to_file_path`](SourceScreenshot::save_to_file_path) must be specified. + /// + /// Clients can specify [`width`](SourceScreenshot::width) and + /// [`height`](SourceScreenshot::height) parameters to receive scaled pictures. Aspect ratio is + /// preserved if only one of these two parameters is specified. + pub async fn take_source_screenshot( + &self, + source_screenshot: SourceScreenshot, + ) -> Result { + self.client + .send_message(RequestType::TakeSourceScreenshot(source_screenshot)) + .await + } +} diff --git a/src/client/streaming.rs b/src/client/streaming.rs new file mode 100644 index 0000000..26071ba --- /dev/null +++ b/src/client/streaming.rs @@ -0,0 +1,74 @@ +use anyhow::Result; + +use super::Client; +use crate::requests::{RequestType, SetStreamSettings, Stream}; +use crate::responses; + +/// API functions related to streaming. +pub struct Streaming<'a> { + pub(super) client: &'a Client, +} + +impl<'a> Streaming<'a> { + /// Get current streaming and recording status. + pub async fn get_streaming_status(&self) -> Result { + self.client + .send_message(RequestType::GetStreamingStatus) + .await + } + + /// Toggle streaming on or off (depending on the current stream state). + pub async fn start_stop_streaming(&self) -> Result<()> { + self.client + .send_message(RequestType::StartStopStreaming) + .await + } + + /// Start streaming. Will return an `error` if streaming is already active. + /// + /// - `stream`: Special stream configuration. Please note: these won't be saved to OBS' + /// configuration. + pub async fn start_streaming(&self, stream: Option) -> Result<()> { + self.client + .send_message(RequestType::StartStreaming { stream }) + .await + } + + /// Stop streaming. Will return an `error` if streaming is not active. + pub async fn stop_streaming(&self) -> Result<()> { + self.client.send_message(RequestType::StopStreaming).await + } + + /// Sets one or more attributes of the current streaming server settings. Any options not passed + /// will remain unchanged. Returns the updated settings in response. If 'type' is different than + /// the current streaming service type, all settings are required. Returns the full settings of + /// the stream (the same as GetStreamSettings). + pub async fn set_stream_settings(&self, settings: SetStreamSettings) -> Result<()> { + self.client + .send_message(RequestType::SetStreamSettings(settings)) + .await + } + + /// Get the current streaming server settings. + pub async fn get_stream_settings(&self) -> Result { + self.client + .send_message(RequestType::GetStreamSettings) + .await + } + + /// Save the current streaming server settings to disk. + pub async fn save_stream_settings(&self) -> Result<()> { + self.client + .send_message(RequestType::SaveStreamSettings) + .await + } + + /// Send the provided text as embedded CEA-608 caption data. + /// + /// - `text`: Captions text. + pub async fn send_captions(&self, text: String) -> Result<()> { + self.client + .send_message(RequestType::SendCaptions { text }) + .await + } +} diff --git a/src/client/studio_mode.rs b/src/client/studio_mode.rs new file mode 100644 index 0000000..5a72eb5 --- /dev/null +++ b/src/client/studio_mode.rs @@ -0,0 +1,67 @@ +use anyhow::Result; + +use super::Client; +use crate::requests::{RequestType, Transition}; +use crate::responses; + +/// API functions related to the studio mode. +pub struct StudioMode<'a> { + pub(super) client: &'a Client, +} + +impl<'a> StudioMode<'a> { + /// Indicates if Studio Mode is currently enabled. + pub async fn get_studio_mode_status(&self) -> Result { + self.client + .send_message::(RequestType::GetStudioModeStatus) + .await + .map(|sms| sms.studio_mode) + } + + /// Get the name of the currently previewed scene and its list of sources. Will return an + /// `error` if Studio Mode is not enabled. + pub async fn get_preview_scene(&self) -> Result { + self.client.send_message(RequestType::GetPreviewScene).await + } + + /// Set the active preview scene. Will return an `error` if Studio Mode is not enabled. + /// + /// - `scene_name`: The name of the scene to preview. + pub async fn set_preview_scene(&self, scene_name: String) -> Result<()> { + self.client + .send_message(RequestType::SetPreviewScene { scene_name }) + .await + } + + /// Transitions the currently previewed scene to the main output. Will return an `error` if + /// Studio Mode is not enabled. + /// + /// - `with_transition`: Change the active transition before switching scenes. Defaults to the + /// active transition. + pub async fn transition_to_program(&self, with_transition: Option) -> Result<()> { + self.client + .send_message(RequestType::TransitionToProgram { with_transition }) + .await + } + + /// Enables Studio Mode. + pub async fn enable_studio_mode(&self) -> Result<()> { + self.client + .send_message(RequestType::EnableStudioMode) + .await + } + + /// Disables Studio Mode. + pub async fn disable_studio_mode(&self) -> Result<()> { + self.client + .send_message(RequestType::DisableStudioMode) + .await + } + + /// Toggles Studio Mode (depending on the current state of studio mode). + pub async fn toggle_studio_mode(&self) -> Result<()> { + self.client + .send_message(RequestType::ToggleStudioMode) + .await + } +} diff --git a/src/client/transitions.rs b/src/client/transitions.rs new file mode 100644 index 0000000..7ce2e2c --- /dev/null +++ b/src/client/transitions.rs @@ -0,0 +1,52 @@ +use anyhow::Result; + +use super::Client; +use crate::requests::RequestType; +use crate::responses; + +/// API functions related to transitions. +pub struct Transitions<'a> { + pub(super) client: &'a Client, +} + +impl<'a> Transitions<'a> { + /// List of all transitions available in the frontend's dropdown menu. + pub async fn get_transition_list(&self) -> Result { + self.client + .send_message(RequestType::GetTransitionList) + .await + } + + /// Get the name of the currently selected transition in the frontend's dropdown menu. + pub async fn get_current_transition(&self) -> Result { + self.client + .send_message(RequestType::GetCurrentTransition) + .await + } + + /// Set the active transition. + /// + /// - `transition_name`: The name of the transition. + pub async fn set_current_transition(&self, transition_name: String) -> Result<()> { + self.client + .send_message(RequestType::SetCurrentTransition { transition_name }) + .await + } + + /// Set the duration of the currently selected transition if supported. + /// + /// - `duration`: Desired duration of the transition (in milliseconds). + pub async fn set_transition_duration(&self, duration: u64) -> Result<()> { + self.client + .send_message(RequestType::SetTransitionDuration { duration }) + .await + } + + /// Get the duration of the currently selected transition if supported. + pub async fn get_transition_duration(&self) -> Result { + self.client + .send_message::(RequestType::GetTransitionDuration) + .await + .map(|td| td.transition_duration) + } +} diff --git a/src/common.rs b/src/common.rs new file mode 100644 index 0000000..741b77c --- /dev/null +++ b/src/common.rs @@ -0,0 +1,143 @@ +//! Common data structures shared between [`requests`](crate::requests), +//! [`responses`](crate::responses) and [`events`](crate::events). + +use serde::Deserialize; + +/// Response value for [`get_current_scene`](crate::client::Scenes::get_current_scene) as part of +/// [`CurrentScene`](crate::responses::CurrentScene), +/// [`get_scene_list`](crate::client::Scenes::get_scene_list) as part of +/// [`Scene`](crate::responses::Scene), +/// [`get_preview_scene`](crate::client::StudioMode::get_preview_scene) as part of +/// [`PreviewScene`](crate::responses::PreviewScene), +/// [`EventType::SwitchScenes`](crate::events::EventType::SwitchScenes), +/// [`EventType::PreviewSceneChanged`](crate::events::EventType::PreviewSceneChanged), +/// and **itself**. +#[allow(missing_docs)] // Docs missing in the obs-websocket spec. +#[derive(Clone, Debug, Deserialize)] +pub struct SceneItem { + pub cy: f64, + pub cx: f64, + /// The point on the source that the item is manipulated from. The sum of 1=Left or 2=Right, and + /// 4=Top or 8=Bottom, or omit to center on that axis. + pub alignment: u8, + /// The name of this Scene Item. + pub name: String, + /// Scene item ID. + pub id: i64, + /// Whether or not this Scene Item is set to "visible". + pub render: bool, + /// Whether or not this Scene Item is muted. + pub muted: bool, + /// Whether or not this Scene Item is locked and can't be moved around + pub locked: bool, + pub source_cx: f64, + pub source_cy: f64, + /// Source type. Value is one of the following: "input", "filter", "transition", "scene" or + /// "unknown". + #[serde(rename = "type")] + pub ty: String, + pub volume: f64, + pub x: f64, + pub y: f64, + /// Name of the item's parent (if this item belongs to a group). + #[serde(rename = "parentGroupName")] + pub parent_group_name: Option, + /// List of children (if this item is a group). + #[serde(rename = "groupChildren", default)] + pub group_children: Vec, +} + +/// Response value for +/// [`get_scene_item_properties`](crate::client::SceneItems::get_scene_item_properties) as part of +/// [`SceneItemProperties`](crate::responses::SceneItemProperties), +/// [`EventType::SceneItemTransformChanged`](crate::events::EventType::SceneItemTransformChanged) +/// and **itself**. +#[derive(Clone, Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SceneItemTransform { + /// Position of the scene item. + pub position: Position, + /// The clockwise rotation of the scene item in degrees around the point of alignment. + pub rotation: f64, + /// Scaling factor of the scene item. + pub scale: Scale, + /// Pixel cropping of the scene item before scaling. + pub crop: Crop, + /// If the scene item is visible. + pub visible: bool, + /// If the scene item is locked in position. + pub locked: bool, + /// Bounding box of the source item. + pub bounds: Bounds, + /// Base width (without scaling) of the source. + pub source_width: u64, + /// Base source (without scaling) of the source. + pub source_height: u64, + /// Scene item width (base source width multiplied by the horizontal scaling factor). + pub width: f64, + /// Scene item height (base source height multiplied by the vertical scaling factor). + pub height: f64, + /// Name of the item's parent (if this item belongs to a group). + pub parent_group_name: Option, + /// List of children (if this item is a group). + #[serde(default)] + pub group_children: Vec, +} + +/// Response value for +/// [`get_scene_item_properties`](crate::client::SceneItems::get_scene_item_properties) as part of +/// [`SceneItemProperties`](crate::responses::SceneItemProperties). +#[derive(Clone, Debug, Deserialize)] +pub struct Position { + /// The x position of the source from the left. + pub x: f64, + /// The y position of the source from the top. + pub y: f64, + /// The point on the source that the item is manipulated from. The sum of 1=Left or 2=Right, and + /// 4=Top or 8=Bottom, or omit to center on that axis. + pub alignment: u8, +} + +/// Response value for +/// [`get_scene_item_properties`](crate::client::SceneItems::get_scene_item_properties) as part of +/// [`SceneItemProperties`](crate::responses::SceneItemProperties) and [`SceneItemTransform`]. +#[derive(Clone, Debug, Deserialize)] +pub struct Scale { + /// The x-scale factor of the source. + pub x: f64, + /// The y-scale factor of the source. + pub y: f64, +} + +/// Response value for +/// [`get_scene_item_properties`](crate::client::SceneItems::get_scene_item_properties) as part of +/// [`SceneItemProperties`](crate::responses::SceneItemProperties) and [`SceneItemTransform`]. +#[derive(Clone, Debug, Deserialize)] +pub struct Crop { + /// The number of pixels cropped off the top of the source before scaling. + pub top: i64, + /// The number of pixels cropped off the right of the source before scaling. + pub right: i64, + /// The number of pixels cropped off the bottom of the source before scaling. + pub bottom: i64, + /// The number of pixels cropped off the left of the source before scaling. + pub left: i64, +} + +/// Response value for +/// [`get_scene_item_properties`](crate::client::SceneItems::get_scene_item_properties) as part of +/// [`SceneItemProperties`](crate::responses::SceneItemProperties) and [`SceneItemTransform`]. +#[derive(Clone, Debug, Deserialize)] +pub struct Bounds { + /// Type of bounding box. Can be "OBS_BOUNDS_STRETCH", "OBS_BOUNDS_SCALE_INNER", + /// "OBS_BOUNDS_SCALE_OUTER", "OBS_BOUNDS_SCALE_TO_WIDTH", "OBS_BOUNDS_SCALE_TO_HEIGHT", + /// "OBS_BOUNDS_MAX_ONLY" or "OBS_BOUNDS_NONE". + #[serde(rename = "type")] + pub ty: String, + /// Alignment of the bounding box. + pub alignment: u8, + /// Width of the bounding box. + pub x: f64, + /// Height of the bounding box. + pub y: f64, +} diff --git a/src/events.rs b/src/events.rs new file mode 100644 index 0000000..4618c60 --- /dev/null +++ b/src/events.rs @@ -0,0 +1,511 @@ +//! All events that can be received from the API. + +use serde::Deserialize; + +use crate::common::{SceneItem, SceneItemTransform}; + +/// Events are sent when a recognized action occurs within OBS. +#[derive(Clone, Debug, Deserialize)] +#[serde(rename_all = "kebab-case")] +pub struct Event { + /// Time elapsed between now and stream start (only present if OBS Studio is streaming). + pub stream_timecode: Option, + /// Time elapsed between now and recording start (only present if OBS Studio is recording). + pub rec_timecode: Option, + /// The type of event. + #[serde(flatten)] + pub ty: EventType, +} + +/// All possible event types that can occur while the user interacts with OBS. +#[derive(Clone, Debug, Deserialize)] +#[serde(tag = "update-type")] +pub enum EventType { + // -------------------------------- + // Scenes + // -------------------------------- + /// Indicates a scene change. + #[serde(rename_all = "kebab-case")] + SwitchScenes { + /// The new scene. + scene_name: String, + /// List of scene items in the new scene. + sources: Vec, + }, + /// Note: This event is not fired when the scenes are reordered. + ScenesChanged, + /// Triggered when switching to another scene collection or when renaming the current scene + /// collection. + #[serde(rename_all = "camelCase")] + SceneCollectionChanged { + /// Name of the new current scene collection. + scene_collection: String, + }, + /// Triggered when a scene collection is created, added, renamed, or removed. + #[serde(rename_all = "camelCase")] + SceneCollectionListChanged { + /// Scene collections list. + scene_collections: Vec, + }, + // -------------------------------- + // Transitions + // -------------------------------- + /// The active transition has been changed. + #[serde(rename_all = "kebab-case")] + SwitchTransition { + /// The name of the new active transition. + transition_name: String, + }, + /// The list of available transitions has been modified. Transitions have been added, removed, + /// or renamed. + TransitionListChanged, + /// The active transition duration has been changed. + #[serde(rename_all = "kebab-case")] + TransitionDurationChanged { + /// New transition duration. + new_duration: u64, + }, + /// A transition (other than "cut") has begun. + #[serde(rename_all = "kebab-case")] + TransitionBegin { + /// Transition name. + name: String, + /// Transition type. + #[serde(rename = "type")] + ty: String, + /// Transition duration (in milliseconds). Will be -1 for any transition with a fixed + /// duration, such as a Stinger, due to limitations of the OBS API. + duration: u64, + /// Source scene of the transition. + from_scene: String, + /// Destination scene of the transition. + to_scene: String, + }, + /// A transition (other than "cut") has ended. Please note that the `from_scene` field is not + /// available in TransitionEnd. + #[serde(rename_all = "kebab-case")] + TransitionEnd { + /// Transition name. + name: String, + /// Transition type. + #[serde(rename = "type")] + ty: String, + /// Transition duration (in milliseconds). + duration: u64, + /// Destination scene of the transition. + to_scene: String, + }, + /// A stinger transition has finished playing its video. + #[serde(rename_all = "kebab-case")] + TransitionVideoEnd { + /// Transition name. + name: String, + /// Transition type. + #[serde(rename = "type")] + ty: String, + /// Transition duration (in milliseconds). + duration: u64, + /// Source scene of the transition. + from_scene: String, + /// Destination scene of the transition. + to_scene: String, + }, + // -------------------------------- + // Profiles + // -------------------------------- + /// Triggered when switching to another profile or when renaming the current profile. + ProfileChanged, + /// Triggered when a profile is created, added, renamed, or removed. + ProfileListChanged, + // -------------------------------- + // Streaming + // -------------------------------- + /// A request to start streaming has been issued. + #[serde(rename_all = "kebab-case")] + StreamStarting { + /// Always false (retrocompatibility). + #[serde(default)] + preview_only: bool, + }, + /// Streaming started successfully. + StreamStarted, + /// A request to stop streaming has been issued. + #[serde(rename_all = "kebab-case")] + StreamStopping { + /// Always false (retrocompatibility). + #[serde(default)] + preview_only: bool, + }, + /// Streaming stopped successfully. + StreamStopped, + /// Emitted every 2 seconds when stream is active. + #[serde(rename_all = "kebab-case")] + StreamStatus { + /// Current streaming state. + streaming: bool, + /// Current recording state. + recording: bool, + /// Replay Buffer status. + replay_buffer_active: bool, + /// Amount of data per second (in bytes) transmitted by the stream encoder. + bytes_per_sec: u64, + /// Amount of data per second (in kilobits) transmitted by the stream encoder. + kbits_per_sec: u64, + /// Percentage of dropped frames. + strain: f64, + /// Total time (in seconds) since the stream started. + total_stream_time: u64, + /// Total number of frames transmitted since the stream started. + num_total_frames: u64, + /// Number of frames dropped by the encoder since the stream started. + num_dropped_frames: u64, + /// Current framerate. + fps: f64, + /// Number of frames rendered. + render_total_frames: u64, + /// Number of frames missed due to rendering lag. + render_missed_frames: u64, + /// Number of frames outputted. + output_total_frames: u64, + /// Number of frames skipped due to encoding lag. + output_skipped_frames: u64, + /// Average frame time (in milliseconds). + average_frame_time: f64, + /// Current CPU usage (percentage). + cpu_usage: f64, + /// Current RAM usage (in megabytes). + memory_usage: f64, + /// Free recording disk space (in megabytes). + free_disk_space: f64, + /// Always false (retrocompatibility). + #[serde(default)] + preview_only: bool, + }, + // -------------------------------- + // Recording + // -------------------------------- + /// Note: `recording_filename` is not provided in this event because this information is not + /// available at the time this event is emitted. + RecordingStarting, + /// Recording started successfully. + #[serde(rename_all = "camelCase")] + RecordingStarted { + /// Absolute path to the file of the current recording. + recording_filename: String, + }, + /// A request to stop recording has been issued. + #[serde(rename_all = "camelCase")] + RecordingStopping { + /// Absolute path to the file of the current recording. + recording_filename: String, + }, + /// Recording stopped successfully. + #[serde(rename_all = "camelCase")] + RecordingStopped { + /// Absolute path to the file of the current recording. + recording_filename: String, + }, + /// Current recording paused. + RecordingPaused, + /// Current recording resumed. + RecordingResumed, + // -------------------------------- + // Replay Buffer + // -------------------------------- + /// A request to start the replay buffer has been issued. + ReplayStarting, + /// Replay Buffer started successfully. + ReplayStarted, + /// A request to stop the replay buffer has been issued. + ReplayStopping, + /// Replay Buffer stopped successfully. + ReplayStopped, + // -------------------------------- + // Other + // -------------------------------- + /// OBS is exiting. + Exiting, + // -------------------------------- + // General + // -------------------------------- + /// A custom broadcast message, sent by the server, requested by one of the websocket clients. + BroadcastCustomMessage { + /// Identifier provided by the sender. + realm: String, + /// User-defined data. + data: serde_json::Value, + }, + // -------------------------------- + // Sources + // -------------------------------- + /// A source has been created. A source can be an input, a scene or a transition. + #[serde(rename_all = "camelCase")] + SourceCreated { + /// Source name. + source_name: String, + /// Source type. Can be "input", "scene", "transition" or "filter". + source_type: String, + /// Source kind. + source_kind: String, + /// Source settings. + source_settings: serde_json::Value, + }, + /// A source has been destroyed/removed. A source can be an input, a scene or a transition. + #[serde(rename_all = "camelCase")] + SourceDestroyed { + /// Source name. + source_name: String, + /// Source type. Can be "input", "scene", "transition" or "filter". + source_type: String, + /// Source kind. + source_kind: String, + }, + /// The volume of a source has changed. + #[serde(rename_all = "camelCase")] + SourceVolumeChanged { + /// Source name. + source_name: String, + /// Source volume. + volume: f32, + }, + /// A source has been muted or unmuted. + #[serde(rename_all = "camelCase")] + SourceMuteStateChanged { + /// Source name. + source_name: String, + /// Mute status of the source. + muted: bool, + }, + /// A source has removed audio. + #[serde(rename_all = "camelCase")] + SourceAudioDeactivated { + /// Source name. + source_name: String, + }, + /// A source has added audio. + #[serde(rename_all = "camelCase")] + SourceAudioActivated { + /// Source name. + source_name: String, + }, + /// The audio sync offset of a source has changed. + #[serde(rename_all = "camelCase")] + SourceAudioSyncOffsetChanged { + /// Source name. + source_name: String, + /// Audio sync offset of the source (in nanoseconds). + sync_offset: i64, + }, + /// Audio mixer routing changed on a source. + #[serde(rename_all = "camelCase")] + SourceAudioMixersChanged { + /// Source name. + source_name: String, + /// Routing status of the source for each audio mixer (array of 6 values). + mixers: [AudioMixer; 6], + /// Raw mixer flags (little-endian, one bit per mixer) as an hexadecimal value. + hex_mixers_value: String, + }, + /// A source has been renamed. + #[serde(rename_all = "camelCase")] + SourceRenamed { + /// Previous source name. + previous_name: String, + /// New source name. + new_name: String, + /// Type of source (input, scene, filter, transition). + source_type: String, + }, + /// A filter was added to a source. + #[serde(rename_all = "camelCase")] + SourceFilterAdded { + /// Source name. + source_name: String, + /// Filter name. + filter_name: String, + /// Filter type. + filter_type: String, + /// Filter settings. + filter_settings: serde_json::Value, + }, + /// A filter was removed from a source. + #[serde(rename_all = "camelCase")] + SourceFilterRemoved { + /// Source name. + source_name: String, + /// Filter name. + filter_name: String, + /// Filter type. + filter_type: String, + }, + /// The visibility/enabled state of a filter changed. + #[serde(rename_all = "camelCase")] + SourceFilterVisibilityChanged { + /// Source name. + source_name: String, + /// Filter name. + filter_name: String, + /// New filter state. + filter_enabled: bool, + }, + /// Filters in a source have been reordered. + #[serde(rename_all = "camelCase")] + SourceFiltersReordered { + /// Source name. + source_name: String, + /// Ordered Filters list. + filters: Vec, + }, + // -------------------------------- + // Scene Items + // -------------------------------- + /// Scene items within a scene have been reordered. + #[serde(rename_all = "kebab-case")] + SourceOrderChanged { + /// Name of the scene where items have been reordered. + scene_name: String, + /// Ordered list of scene items. + scene_items: Vec, + }, + /// A scene item has been added to a scene. + #[serde(rename_all = "kebab-case")] + SceneItemAdded { + /// Name of the scene. + scene_name: String, + /// Name of the item added to the scene. + item_name: String, + /// Scene item ID. + item_id: i64, + }, + /// A scene item has been removed from a scene. + #[serde(rename_all = "kebab-case")] + SceneItemRemoved { + /// Name of the scene. + scene_name: String, + /// Name of the item removed from the scene. + item_name: String, + /// Scene item ID. + item_id: i64, + }, + /// A scene item's visibility has been toggled. + #[serde(rename_all = "kebab-case")] + SceneItemVisibilityChanged { + /// Name of the scene. + scene_name: String, + /// Name of the item in the scene. + item_name: String, + /// Scene item ID. + item_id: i64, + /// New visibility state of the item. + item_visible: bool, + }, + /// A scene item's locked status has been toggled. + #[serde(rename_all = "kebab-case")] + SceneItemLockChanged { + /// Name of the scene. + scene_name: String, + /// Name of the item in the scene. + item_name: String, + /// Scene item ID. + item_id: i64, + /// New locked state of the item. + item_locked: bool, + }, + /// A scene item's transform has been changed. + #[serde(rename_all = "kebab-case")] + SceneItemTransformChanged { + /// Name of the scene. + scene_name: String, + /// Name of the item in the scene. + item_name: String, + /// Scene item ID. + item_id: i64, + /// Scene item transform properties. + transform: SceneItemTransform, + }, + /// A scene item is selected. + #[serde(rename_all = "kebab-case")] + SceneItemSelected { + /// Name of the scene. + scene_name: String, + /// Name of the item in the scene. + item_name: String, + /// ID of the item in the scene. + item_id: i64, + }, + /// A scene item is deselected. + #[serde(rename_all = "kebab-case")] + SceneItemDeselected { + /// Name of the scene. + scene_name: String, + /// Name of the item in the scene. + item_name: String, + /// ID of the item in the scene. + item_id: i64, + }, + // -------------------------------- + // Studio Mode + // -------------------------------- + /// The selected preview scene has changed (only available in Studio Mode). + #[serde(rename_all = "kebab-case")] + PreviewSceneChanged { + /// Name of the scene being previewed. + scene_name: String, + /// List of sources composing the scene. + soruces: Vec, + }, + /// Studio Mode has been enabled or disabled. + #[serde(rename_all = "kebab-case")] + StudioModeSwitched { + /// The new enabled state of Studio Mode. + new_state: bool, + }, + /// Fallback value for any unknown event type. + #[serde(other)] + Unknown, +} + +/// Part of [`EventType::SceneCollectionListChanged`]. +#[derive(Clone, Debug, Deserialize)] +pub struct SceneCollection { + /// Scene collection name. + pub name: String, +} + +/// Part of [`EventType::ProfileListChanged`]. +#[derive(Clone, Debug, Deserialize)] +pub struct Profile { + /// Profile name. + pub name: String, +} + +/// Part of [`EventType::SourceAudioMixersChanged`]. +#[derive(Clone, Debug, Deserialize)] +pub struct AudioMixer { + /// Mixer number. + pub id: i64, + /// Routing status. + pub enabled: bool, +} + +/// Part of [`EventType::SourceFiltersReordered`]. +#[derive(Clone, Debug, Deserialize)] +pub struct SourceFilter { + /// Filter name. + pub name: String, + /// Filter type. + #[serde(rename = "type")] + pub ty: String, + /// Filter visibility status. + pub enabled: bool, +} + +/// Part of [`EventType::SourceOrderChanged`]. +#[derive(Clone, Debug, Deserialize)] +#[serde(rename_all = "kebab-case")] +pub struct SourceOrderSceneItem { + /// Item source name. + pub source_name: String, + /// Scene item unique ID. + pub item_id: i64, +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..3fa2c8f --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,9 @@ +//! # OBSWS - The obws (obvious) remote control library for OBS + +#![deny(missing_docs, rust_2018_idioms, clippy::all)] + +pub mod client; +pub mod common; +pub mod events; +pub mod requests; +pub mod responses; diff --git a/src/requests.rs b/src/requests.rs new file mode 100644 index 0000000..ff6be98 --- /dev/null +++ b/src/requests.rs @@ -0,0 +1,810 @@ +//! All requests that can be send to the API. + +use either::Either; +use serde::Serialize; +use serde_with::skip_serializing_none; + +#[derive(Serialize)] +#[serde(rename_all = "kebab-case")] +pub(crate) struct Request { + pub message_id: String, + #[serde(flatten)] + pub ty: RequestType, +} + +#[derive(Serialize)] +#[serde(tag = "request-type")] +pub(crate) enum RequestType { + // -------------------------------- + // General + // -------------------------------- + GetVersion, + GetAuthRequired, + Authenticate { + /// Response to the auth challenge. + auth: String, + }, + #[serde(rename_all = "kebab-case")] + SetFilenameFormatting { + /// Filename formatting string to set. + filename_formatting: String, + }, + GetFilenameFormatting, + GetStats, + BroadcastCustomMessage { + /// Identifier to be choosen by the client. + realm: String, + /// User-defined data. + data: serde_json::Value, + }, + GetVideoInfo, + OpenProjector(Projector), + // -------------------------------- + // Sources + // -------------------------------- + GetSourcesList, + GetSourceTypesList, + #[serde(rename_all = "camelCase")] + GetVolume { + /// Source name. + source: String, + /// Output volume in decibels of attenuation instead of amplitude/mul. + use_decibel: Option, + }, + SetVolume(Volume), + GetMute { + /// Source name. + source: String, + }, + SetMute { + /// Source name. + source: String, + /// Desired mute status. + mute: bool, + }, + ToggleMute { + /// Source name. + source: String, + }, + #[serde(rename_all = "camelCase")] + GetAudioActive { + /// Source name. + source_name: String, + }, + #[serde(rename_all = "camelCase")] + SetSourceName { + /// Source name. + source_name: String, + /// New source name. + new_name: String, + }, + SetSyncOffset { + /// Source name. + source: String, + /// The desired audio sync offset (in nanoseconds). + offset: i64, + }, + GetSyncOffset { + /// Source name. + source: String, + }, + #[serde(rename_all = "camelCase")] + GetSourceSettings { + /// Source name. + source_name: String, + /// Type of the specified source. Useful for type-checking if you expect a specific settings + /// schema. + source_type: Option, + }, + SetSourceSettings(SourceSettings), + GetTextGDIPlusProperties { + /// Source name. + source: String, + }, + SetTextGDIPlusProperties(Box), + GetTextFreetype2Properties { + /// Source name. + source: String, + }, + SetTextFreetype2Properties(TextFreetype2Properties), + GetSpecialSources, + #[serde(rename_all = "camelCase")] + GetSourceFilters { + /// Source name. + source_name: String, + }, + #[serde(rename_all = "camelCase")] + GetSourceFilterInfo { + /// Source name. + source_name: String, + /// Source filter name. + filter_name: String, + }, + AddFilterToSource(AddFilter), + #[serde(rename_all = "camelCase")] + RemoveFilterFromSource { + /// Name of the source from which the specified filter is removed. + source_name: String, + /// Name of the filter to remove. + filter_name: String, + }, + ReorderSourceFilter(ReorderFilter), + MoveSourceFilter(MoveFilter), + SetSourceFilterSettings(SourceFilterSettings), + SetSourceFilterVisibility(SourceFilterVisibility), + #[serde(rename_all = "camelCase")] + GetAudioMonitorType { + /// Source name. + source_name: String, + }, + #[serde(rename_all = "camelCase")] + SetAudioMonitorType { + /// Source name. + source_name: String, + /// The monitor type to use. Options: `none`, `monitorOnly`, `monitorAndOutput`. + monitor_type: String, + }, + TakeSourceScreenshot(SourceScreenshot), + // -------------------------------- + // Outputs + // -------------------------------- + ListOutputs, + #[serde(rename_all = "camelCase")] + GetOutputInfo { + /// Output name. + output_name: String, + }, + #[serde(rename_all = "camelCase")] + StartOutput { + /// Output name. + output_name: String, + }, + #[serde(rename_all = "camelCase")] + StopOutput { + /// Output name. + output_name: String, + /// Force stop (default: false). + force: Option, + }, + // -------------------------------- + // Profiles + // -------------------------------- + #[serde(rename_all = "kebab-case")] + SetCurrentProfile { + /// Name of the desired profile. + profile_name: String, + }, + GetCurrentProfile, + ListProfiles, + // -------------------------------- + // Recording + // -------------------------------- + StartStopRecording, + StartRecording, + StopRecording, + PauseRecording, + ResumeRecording, + #[serde(rename_all = "kebab-case")] + SetRecordingFolder { + /// Path of the recording folder. + rec_folder: String, + }, + GetRecordingFolder, + // -------------------------------- + // Replay Buffer + // -------------------------------- + StartStopReplayBuffer, + StartReplayBuffer, + StopReplayBuffer, + SaveReplayBuffer, + // -------------------------------- + // Scene Collections + // -------------------------------- + #[serde(rename_all = "kebab-case")] + SetCurrentSceneCollection { + /// Name of the desired scene collection. + sc_name: String, + }, + GetCurrentSceneCollection, + ListSceneCollections, + // -------------------------------- + // Scene Items + // -------------------------------- + #[serde(rename_all = "kebab-case")] + GetSceneItemProperties { + /// Name of the scene the scene item belongs to. Defaults to the current scene. + scene_name: Option, + /// Scene Item name (if this field is a string) or specification (if it is an object). + #[serde(with = "either::serde_untagged")] + item: Either, + }, + SetSceneItemProperties(SceneItemProperties), + #[serde(rename_all = "kebab-case")] + ResetSceneItem { + /// Name of the scene the scene item belongs to. Defaults to the current scene. + scene_name: Option, + /// Scene Item name (if this field is a string) or specification (if it is an object). + #[serde(with = "either::serde_untagged")] + item: Either, + }, + SetSceneItemRender(SceneItemRender), + DeleteSceneItem { + /// Name of the scene the scene item belongs to. Defaults to the current scene. + scene: Option, + /// Scene item to delete. + item: SceneItemSpecification, // TODO: fields are actually not optional + }, + AddSceneItem(AddSceneItem), + DuplicateSceneItem(DuplicateSceneItem), + // -------------------------------- + // Scenes + // -------------------------------- + #[serde(rename_all = "kebab-case")] + SetCurrentScene { + /// Name of the scene to switch to. + scene_name: String, + }, + GetCurrentScene, + GetSceneList, + #[serde(rename_all = "camelCase")] + CreateScene { + /// Name of the scene to create. + scene_name: String, + }, + ReorderSceneItems { + /// Name of the scene to reorder (defaults to current). + scene: Option, + /// Ordered list of objects with name and/or id specified. Id preferred due to uniqueness + /// per scene. + items: Vec, + }, + SetSceneTransitionOverride(SceneTransitionOverride), + #[serde(rename_all = "camelCase")] + RemoveSceneTransitionOverride { + /// Name of the scene to remove the override from. + scene_name: String, + }, + #[serde(rename_all = "camelCase")] + GetSceneTransitionOverride { + /// Name of the scene to get the override for. + scene_name: String, + }, + // -------------------------------- + // Streaming + // -------------------------------- + GetStreamingStatus, + StartStopStreaming, + StartStreaming { + /// Special stream configuration. Please note: these won't be saved to OBS' configuration. + stream: Option, + }, + StopStreaming, + SetStreamSettings(SetStreamSettings), + GetStreamSettings, + SaveStreamSettings, + SendCaptions { + /// Captions text. + text: String, + }, + // -------------------------------- + // Studio Mode + // -------------------------------- + GetStudioModeStatus, + GetPreviewScene, + #[serde(rename_all = "kebab-case")] + SetPreviewScene { + /// The name of the scene to preview. + scene_name: String, + }, + TransitionToProgram { + /// Change the active transition before switching scenes. Defaults to the active transition. + with_transition: Option, + }, + EnableStudioMode, + DisableStudioMode, + ToggleStudioMode, + // -------------------------------- + // Transitions + // -------------------------------- + GetTransitionList, + GetCurrentTransition, + #[serde(rename_all = "kebab-case")] + SetCurrentTransition { + /// The name of the transition. + transition_name: String, + }, + SetTransitionDuration { + /// Desired duration of the transition (in milliseconds). + duration: u64, + }, + GetTransitionDuration, +} + +/// Request information for [`open_projector`](crate::client::General::open_projector). +#[skip_serializing_none] +#[derive(Debug, Serialize)] +pub struct Projector { + /// Type of projector: `Preview` (default), `Source`, `Scene`, `StudioProgram`, or `Multiview` + /// (case insensitive). + #[serde(rename = "type")] + pub ty: Option, + /// Monitor to open the projector on. If -1 or omitted, opens a window. + pub monitor: Option, + /// Size and position of the projector window (only if monitor is -1). Encoded in Base64 using + /// [Qt's geometry encoding](https://doc.qt.io/qt-5/qwidget.html#saveGeometry). Corresponds to + /// OBS's saved projectors. + pub geometry: Option, + /// Name of the source or scene to be displayed (ignored for other projector types). + pub name: Option, +} + +/// Request information for [`set_volume`](crate::client::Sources::set_volume). +#[skip_serializing_none] +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct Volume { + /// Source name. + pub source: String, + /// Desired volume. Must be between `0.0` and `20.0` for mul, and under 26.0 for dB. OBS will + /// interpret dB values under -100.0 as Inf. Note: The OBS volume sliders only reach a maximum + /// of 1.0mul/0.0dB, however OBS actually supports larger values. + pub volume: f64, + /// Interperet `volume` data as decibels instead of amplitude/mul. + pub use_decibel: Option, +} + +/// Request information for [`set_source_settings`](crate::client::Sources::set_source_settings). +#[skip_serializing_none] +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct SourceSettings { + /// Source name. + pub source_name: String, + /// Type of the specified source. Useful for type-checking to avoid settings a set of settings + /// incompatible with the actual source's type. + pub source_type: Option, + /// Source settings (varies between source types, may require some probing around). + pub source_settings: serde_json::Value, +} + +/// Request information for +/// [`set_text_gdi_plus_properties`](crate::client::Sources::set_text_gdi_plus_properties). +#[skip_serializing_none] +#[derive(Debug, Serialize)] +pub struct TextGdiPlusProperties { + /// Name of the source. + pub source: String, + /// Text Alignment ("left", "center", "right"). + pub align: Option, + /// Background color. + pub bk_color: Option, + /// Background opacity (0-100). + pub bk_opacity: Option, + /// Chat log. + pub chatlog: Option, + /// Chat log lines. + pub chatlog_lines: Option, + /// Text color. + pub color: Option, + /// Extents wrap. + pub extents: Option, + /// Extents cx. + pub extents_cx: Option, + /// Extents cy. + pub extents_cy: Option, + /// File path name. + pub file: Option, + /// Read text from the specified file. + pub read_from_file: Option, + /// Holds data for the font. Ex: + /// `"font": { "face": "Arial", "flags": 0, "size": 150, "style": "" }`. + pub font: Option, + /// Gradient enabled. + pub gradient: Option, + /// Gradient color. + pub gradient_color: Option, + /// Gradient direction. + pub gradient_dir: Option, + /// Gradient opacity (0-100). + pub gradient_opacity: Option, + /// Outline. + pub outline: Option, + /// Outline color. + pub outline_color: Option, + /// Outline size. + pub outline_size: Option, + /// Outline opacity (0-100). + pub outline_opacity: Option, + /// Text content to be displayed. + pub text: Option, + /// Text vertical alignment ("top", "center", "bottom"). + pub valign: Option, + /// Vertical text enabled. + pub vertical: Option, + /// Visibility of the scene item. + pub render: Option, +} + +/// Request information for +/// [`set_text_freetype2_properties`](crate::client::Sources::set_text_freetype2_properties). +#[skip_serializing_none] +#[derive(Debug, Serialize)] +pub struct TextFreetype2Properties { + /// Source name. + pub source: String, + /// Gradient top color. + pub color1: Option, + /// Gradient bottom color. + pub color2: Option, + /// Custom width (0 to disable). + pub custom_width: Option, + /// Drop shadow. + pub drop_shadow: Option, + /// Holds data for the font. Ex: + /// `"font": { "face": "Arial", "flags": 0, "size": 150, "style": "" }`. + pub font: Option, + /// Read text from the specified file. + pub from_file: Option, + /// Chat log. + pub log_mode: Option, + /// Outline. + pub outline: Option, + /// Text content to be displayed. + pub text: Option, + /// File path. + pub text_file: Option, + /// Word wrap. + pub word_wrap: Option, +} + +/// Request information for [`add_filter_to_source`](crate::client::Sources::add_filter_to_source). +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct AddFilter { + /// Name of the source on which the filter is added. + pub source_name: String, + /// Name of the new filter. + pub filter_name: String, + /// Filter type. + pub filter_type: String, + /// Filter settings. + pub filter_settings: serde_json::Value, +} + +/// Request information for +/// [`reorder_source_filter`](crate::client::Sources::reorder_source_filter). +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ReorderFilter { + /// Name of the source to which the filter belongs. + pub source_name: String, + /// Name of the filter to reorder. + pub filter_name: String, + /// Desired position of the filter in the chain. + pub new_index: u32, +} + +/// Request information for [`move_source_filter`](crate::client::Sources::move_source_filter). +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct MoveFilter { + /// Name of the source to which the filter belongs. + pub source_name: String, + /// Name of the filter to reorder. + pub filter_name: String, + /// How to move the filter around in the source's filter chain. Either "up", "down", "top" or + /// "bottom". + pub movement_type: String, +} + +/// Request information for +/// [`set_source_filter_settings`](crate::client::Sources::set_source_filter_settings). +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct SourceFilterSettings { + /// Name of the source to which the filter belongs. + pub source_name: String, + /// Name of the filter to reconfigure. + pub filter_name: String, + /// New settings. These will be merged to the current filter settings. + pub filter_settings: serde_json::Value, +} + +/// Request information for +/// [`set_source_filter_visibility`](crate::client::Sources::set_source_filter_visibility). +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct SourceFilterVisibility { + /// Source name. + pub source_name: String, + /// Source filter name. + pub filter_name: String, + /// New filter state. + pub filter_enabled: bool, +} + +/// Request information for +/// [`take_source_screenshot`](crate::client::Sources::take_source_screenshot). +#[skip_serializing_none] +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct SourceScreenshot { + /// Source name. Note that, since scenes are also sources, you can also provide a scene name. If + /// not provided, the currently active scene is used. + pub source_name: Option, + /// Format of the Data URI encoded picture. Can be "png", "jpg", "jpeg" or "bmp" (or any other + /// value supported by Qt's Image module). + pub embed_picture_format: Option, + /// Full file path (file extension included) where the captured image is to be saved. Can be in + /// a format different from [`embed_picture_format`](SourceScreenshot::embed_picture_format). + /// Can be a relative path. + pub save_to_file_path: Option, + /// Format to save the image file as (one of the values provided in the + /// [`supported_image_export_formats`](crate::responses::Version::supported_image_export_formats) + /// response field of [`get_version`](crate::client::General::get_version)). If not specified, + /// tries to guess based on file extension. + pub file_format: Option, + /// Compression ratio between -1 and 100 to write the image with. -1 is automatic, 1 is smallest + /// file/most compression, 100 is largest file/least compression. Varies with image type. + pub compress_quality: Option, + /// Screenshot width. Defaults to the source's base width. + pub width: Option, + /// Screenshot height. Defaults to the source's base height. + pub height: Option, +} + +/// Request information for +/// [`set_scene_item_properties`](crate::client::SceneItems::set_scene_item_properties). +#[skip_serializing_none] +#[derive(Debug, Serialize)] +#[serde(rename_all = "kebab-case")] +pub struct SceneItemProperties { + /// Name of the scene the source item belongs to. Defaults to the current scene. + pub scene_name: Option, + /// Scene Item name (if this field is a string) or specification (if it is an object). + #[serde(with = "either::serde_untagged")] + pub item: Either, + /// Position of the scene item. + pub position: Option, + /// The new clockwise rotation of the item in degrees. + pub rotation: Option, + /// Scaling factor of the scene item. + pub scale: Option, + /// Pixel cropping of the scene item before scaling. + pub crop: Option, + /// The new visibility of the source. 'true' shows source, 'false' hides source. + pub visible: Option, + /// The new locked status of the source. 'true' keeps it in its current position, 'false' allows + /// movement. + pub locked: Option, + /// Bounding box of the scene item. + pub bounds: Option, +} + +/// Request information for +/// [`set_scene_item_render`](crate::client::SceneItems::set_scene_item_render). +#[skip_serializing_none] +#[derive(Debug, Serialize)] +#[serde(rename_all = "kebab-case")] +pub struct SceneItemRender { + /// Name of the scene the scene item belongs to. Defaults to the currently active scene. + pub scene_name: Option, + /// Scene Item name. + pub source: String, + /// true = shown ; false = hidden. + pub render: bool, +} + +/// Request information for [`add_scene_item`](crate::client::SceneItems::add_scene_item). +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct AddSceneItem { + /// Name of the scene to create the scene item in. + pub scene_name: String, + /// Name of the source to be added. + pub source_name: String, + /// Whether to make the sceneitem visible on creation or not. Default `true`. + pub set_visible: bool, +} + +/// Request information for +/// [`duplicate_scene_item`](crate::client::SceneItems::duplicate_scene_item). +#[skip_serializing_none] +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct DuplicateSceneItem { + /// Name of the scene to copy the item from. Defaults to the current scene. + pub from_scene: Option, + /// Name of the scene to create the item in. Defaults to the current scene. + pub to_scene: Option, + /// Scene Item to duplicate from the source scene. + pub item: SceneItemSpecification, // TODO: fields are actually not optional +} + +/// Request information for +/// [`set_scene_transition_override`](crate::client::Scenes::set_scene_transition_override). +#[skip_serializing_none] +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct SceneTransitionOverride { + /// Name of the scene to switch to. + pub scene_name: String, + /// Name of the transition to use. + pub transition_name: String, + /// Duration in milliseconds of the transition if transition is not fixed. Defaults to the + /// current duration specified in the UI if there is no current override and this value is not + /// given. + pub transition_duration: Option, +} + +/// Request information for [`set_stream_settings`](crate::client::Streaming::set_stream_settings). +#[derive(Debug, Serialize)] +pub struct SetStreamSettings { + /// The type of streaming service configuration, usually `rtmp_custom` or `rtmp_common`. + #[serde(rename = "type")] + pub ty: String, + /// The actual settings of the stream. + pub settings: StreamSettings, + /// Persist the settings to disk. + pub save: bool, +} + +// ----------------------------------------------------------------------------- +// ----------------------------------------------------------------------------- +// ----------------------------------------------------------------------------- + +/// Request information for +/// [`set_text_gdi_plus_properties`](crate::client::Sources::set_text_gdi_plus_properties) as part +/// of [`TextGdiPlusProperties`] and +/// [`set_text_freetype2_properties`](crate::client::Sources::set_text_freetype2_properties) as part +/// of [`TextFreetype2Properties`]. +#[skip_serializing_none] +#[derive(Debug, Serialize)] +pub struct Font { + /// Font face. + pub face: Option, + /// Font text styling flag. `Bold=1, Italic=2, Bold Italic=3, Underline=5, Strikeout=8`. + pub flags: Option, + /// Font text size. + pub size: Option, + /// Font Style (unknown function). + pub style: Option, +} + +/// Request information for +/// [`get_scene_item_properties`](crate::client::SceneItems::get_scene_item_properties), +/// [`set_scene_item_properties`](crate::client::SceneItems::set_scene_item_properties) as part of +/// [`SceneItemProperties`], [`reset_scene_item`](crate::client::SceneItems::reset_scene_item), +/// [`delete_scene_item`](crate::client::SceneItems::delete_scene_item) and +/// [`duplicate_scene_item`](crate::client::SceneItems::duplicate_scene_item) as part of +/// [`DuplicateSceneItem`]. +#[skip_serializing_none] +#[derive(Debug, Serialize)] +pub struct SceneItemSpecification { + /// Scene Item name. + pub name: Option, + /// Scene Item ID. + pub id: Option, +} + +/// Request information for +/// [`set_scene_item_properties`](crate::client::SceneItems::set_scene_item_properties) as part of +/// [`SceneItemProperties`]. +#[skip_serializing_none] +#[derive(Debug, Serialize)] +pub struct Position { + /// The new x position of the source. + pub x: Option, + /// The new y position of the source. + pub y: Option, + /// The new alignment of the source. + pub alignment: Option, +} + +/// Request information for +/// [`set_scene_item_properties`](crate::client::SceneItems::set_scene_item_properties) as part of +/// [`SceneItemProperties`]. +#[skip_serializing_none] +#[derive(Debug, Serialize)] +pub struct Scale { + /// The new x scale of the item. + pub x: Option, + /// The new y scale of the item. + pub y: Option, +} + +/// Request information for +/// [`set_scene_item_properties`](crate::client::SceneItems::set_scene_item_properties) as part of +/// [`SceneItemProperties`]. +#[skip_serializing_none] +#[derive(Debug, Serialize)] +pub struct Crop { + /// The new amount of pixels cropped off the top of the source before scaling. + pub top: Option, + /// The new amount of pixels cropped off the bottom of the source before scaling. + pub bottom: Option, + /// The new amount of pixels cropped off the left of the source before scaling. + pub left: Option, + /// The new amount of pixels cropped off the right of the source before scaling. + pub right: Option, +} + +/// Request information for +/// [`set_scene_item_properties`](crate::client::SceneItems::set_scene_item_properties) as part of +/// [`SceneItemProperties`]. +#[skip_serializing_none] +#[derive(Debug, Serialize)] +pub struct Bounds { + /// The new bounds type of the source. Can be "OBS_BOUNDS_STRETCH", "OBS_BOUNDS_SCALE_INNER", + /// "OBS_BOUNDS_SCALE_OUTER", "OBS_BOUNDS_SCALE_TO_WIDTH", "OBS_BOUNDS_SCALE_TO_HEIGHT", + /// "OBS_BOUNDS_MAX_ONLY" or "OBS_BOUNDS_NONE". + #[serde(rename = "type")] + pub ty: Option, + /// The new alignment of the bounding box. (0-2, 4-6, 8-10). + pub alignment: Option, + /// The new width of the bounding box. + pub x: Option, + /// The new height of the bounding box. + pub y: Option, +} + +/// Request information for +/// [`reorder_scene_items`](crate::client::Scenes::reorder_scene_items) as part of +/// [`ReorderLineItems`](RequestType::ReorderSceneItems). +#[skip_serializing_none] +#[derive(Debug, Serialize)] +pub struct Scene { + /// Id of a specific scene item. Unique on a scene by scene basis. + id: Option, + /// Name of a scene item. Sufficiently unique if no scene items share sources within the scene. + name: Option, +} + +/// Request information for [`start_streaming`](crate::client::Streaming::start_streaming). +#[skip_serializing_none] +#[derive(Debug, Serialize)] +pub struct Stream { + /// If specified ensures the type of stream matches the given type (usually 'rtmp_custom' or + /// 'rtmp_common'). If the currently configured stream type does not match the given stream + /// type, all settings must be specified in the `settings` object or an error will occur when + /// starting the stream. + #[serde(rename = "type")] + ty: Option, + /// Adds the given object parameters as encoded query string parameters to the 'key' of the RTMP + /// stream. Used to pass data to the RTMP service about the streaming. May be any String, + /// Numeric, or Boolean field. + metadata: Option, + /// Settings for the stream. + settings: Option, +} + +/// Request information for [`start_streaming`](crate::client::Streaming::start_streaming) as part +/// of [`Stream`] and [`set_stream_settings`](crate::client::Streaming::set_stream_settings) as part +/// of [`SetStreamSettings`]. +#[skip_serializing_none] +#[derive(Debug, Serialize)] +pub struct StreamSettings { + /// The publish URL. + server: Option, + /// The publish key of the stream. + key: Option, + /// Indicates whether authentication should be used when connecting to the streaming server. + use_auth: Option, + /// If authentication is enabled, the username for the streaming server. Ignored if + /// [`use_auth`](Self::use_auth) is not set to `true`. + username: Option, + /// If authentication is enabled, the password for the streaming server. Ignored if + /// [`use_auth`](Self::use_auth) is not set to `true`. + password: Option, +} + +/// Request information for +/// [`transition_to_program`](crate::client::StudioMode::transition_to_program). +#[skip_serializing_none] +#[derive(Debug, Serialize)] +pub struct Transition { + /// Name of the transition. + name: String, + /// Transition duration (in milliseconds). + duration: Option, +} diff --git a/src/responses/de.rs b/src/responses/de.rs new file mode 100644 index 0000000..033c6ad --- /dev/null +++ b/src/responses/de.rs @@ -0,0 +1,14 @@ +use std::iter::FromIterator; + +use serde::de::{Deserialize, Deserializer}; + +#[allow(dead_code)] +pub fn string_comma_list<'de, D, T>(deserializer: D) -> Result +where + D: Deserializer<'de>, + T: FromIterator, +{ + let s = <&str>::deserialize(deserializer)?; + + Ok(s.split(',').map(|s| s.to_owned()).collect()) +} diff --git a/src/responses/mod.rs b/src/responses/mod.rs new file mode 100644 index 0000000..a16f463 --- /dev/null +++ b/src/responses/mod.rs @@ -0,0 +1,751 @@ +//! All responses that can be received from the API. + +use serde::Deserialize; + +use crate::common::{Bounds, Crop, Position, Scale, SceneItem, SceneItemTransform}; + +mod de; + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "kebab-case")] +pub(crate) struct Response { + pub message_id: String, + pub status: Status, + pub error: Option, + #[serde(flatten)] + pub details: T, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "kebab-case")] +pub(crate) enum Status { + Ok, + Error, +} + +/// Response value for [`get_version`](crate::client::General::get_version). +#[derive(Debug, Deserialize)] +#[serde(rename_all = "kebab-case")] +pub struct Version { + /// OBSRemote compatible API version. Fixed to 1.1 for retrocompatibility. + pub version: f64, + /// obs-websocket plugin version. + pub obs_websocket_version: String, + /// OBS Studio program version. + pub obs_studio_version: String, + /// List of available request types, formatted as a comma-separated list string (e.g. : + /// "Method1,Method2,Method3"). + pub available_requests: String, + /// List of supported formats for features that use image export (like the + /// [`TakeSourceScreenshot`](crate::requests::RequestType::TakeSourceScreenshot) request type) + /// formatted as a comma-separated list string. + pub supported_image_export_formats: String, +} + +/// Response value for [`get_auth_required`](crate::client::General::get_auth_required). +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct AuthRequired { + /// Indicates whether authentication is required. + pub auth_required: bool, + /// A random string that will be used to generate the auth response. + pub challenge: Option, + /// Applied to the password when generating the auth response. + pub salt: Option, +} + +/// Response value for [`get_filename_formatting`](crate::client::General::get_filename_formatting). +#[derive(Debug, Deserialize)] +#[serde(rename_all = "kebab-case")] +pub(crate) struct FilenameFormatting { + /// Current filename formatting string. + pub filename_formatting: String, +} + +/// Response value for [`get_stats`](crate::client::General::get_stats). +#[derive(Debug, Deserialize)] +pub(crate) struct Stats { + /// See [`ObsStats`]. + pub stats: ObsStats, +} + +/// Response value for [`get_video_info`](crate::client::General::get_video_info). +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct VideoInfo { + /// Base (canvas) width. + pub base_width: u64, + /// Base (canvas) height. + pub base_height: u64, + /// Output width. + pub output_width: u64, + /// Output height. + pub output_height: u64, + /// Scaling method used if output size differs from base size. + pub scale_type: String, + /// Frames rendered per second. + pub fps: f64, + /// Video color format. + pub video_format: String, + /// Color space for YUV. + pub color_space: String, + /// Color range (full or partial). + pub color_range: String, +} + +/// Response value for [`get_sources_list`](crate::client::Sources::get_sources_list). +#[derive(Debug, Deserialize)] +pub(crate) struct SourcesList { + /// Array of sources. + pub sources: Vec, +} + +/// Response value for [`get_sources_types_list`](crate::client::Sources::get_sources_types_list). +#[derive(Debug, Deserialize)] +pub(crate) struct SourceTypesList { + /// Array of source types. + pub types: Vec, +} + +/// Response value for [`get_volume`](crate::client::Sources::get_volume). +#[derive(Debug, Deserialize)] +pub struct Volume { + /// Source name. + pub name: String, + /// Volume of the source. Between `0.0` and `20.0` if using mul, under `26.0` if using dB. + pub volume: f64, + /// Indicates whether the source is muted. + pub muted: bool, +} + +/// Response value for [`get_mute`](crate::client::Sources::get_mute). +#[derive(Debug, Deserialize)] +pub struct Mute { + /// Source name. + pub name: String, + /// Mute status of the source. + pub muted: bool, +} + +/// Response value for [`get_audio_active`](crate::client::Sources::get_audio_active). +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub(crate) struct AudioActive { + /// Audio active status of the source. + pub audio_active: bool, +} + +/// Response value for [`get_sync_offset`](crate::client::Sources::get_sync_offset). +#[derive(Debug, Deserialize)] +pub struct SyncOffset { + /// Source name. + pub name: String, + /// The audio sync offset (in nanoseconds). + pub offset: i64, +} + +/// Response value for [`get_source_settings`](crate::client::Sources::get_source_settings) and +/// [`set_source_settings`](crate::client::Sources::set_source_settings). +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SourceSettings { + /// Source name. + pub source_name: String, + /// Type of the specified source. + pub source_type: String, + /// Source settings (varies between source types, may require some probing around). + pub source_settings: T, +} + +/// Response value for +/// [`get_text_gdi_plus_properties`](crate::client::Sources::get_text_gdi_plus_properties). +#[derive(Debug, Deserialize)] +pub struct TextGdiPlusProperties { + /// Source name. + pub source: String, + /// Text Alignment ("left", "center", "right"). + pub align: String, + /// Background color. + pub bk_color: u32, + /// Background opacity (0-100). + pub bk_opacity: u8, + /// Chat log. + pub chatlog: bool, + /// Chat log lines. + pub chatlog_lines: u64, + /// Text color. + pub color: u32, + /// Extents wrap. + pub extents: bool, + /// Extents cx. + pub extents_cx: i64, + /// Extents cy. + pub extents_cy: i64, + /// File path name. + pub file: String, + /// Read text from the specified file. + pub read_from_file: bool, + /// Holds data for the font. Ex: + /// `"font": { "face": "Arial", "flags": 0, "size": 150, "style": "" }`. + pub font: Font, + /// Gradient enabled. + pub gradient: bool, + /// Gradient color. + pub gradient_color: u32, + /// Gradient direction. + pub gradient_dir: f32, + /// Gradient opacity (0-100). + pub gradient_opacity: u8, + /// Outline. + pub outline: bool, + /// Outline color. + pub outline_color: u32, + /// Outline size. + pub outline_size: u64, + /// Outline opacity (0-100). + pub outline_opacity: u8, + /// Text content to be displayed. + pub text: String, + /// Text vertical alignment ("top", "center", "bottom"). + pub valign: String, + /// Vertical text enabled. + pub vertical: bool, +} + +/// Response value for +/// [`get_text_freetype2_properties`](crate::client::Sources::get_text_freetype2_properties). +#[derive(Debug, Deserialize)] +pub struct TextFreetype2Properties { + /// Source name. + pub source: String, + /// Gradient top color. + pub color1: u32, + /// Gradient bottom color. + pub color2: u32, + /// Custom width (0 to disable). + pub custom_width: u32, + /// Drop shadow. + pub drop_shadow: bool, + /// Holds data for the font. Ex: + /// `"font": { "face": "Arial", "flags": 0, "size": 150, "style": "" }`. + pub font: Font, + /// Read text from the specified file. + pub from_file: bool, + /// Chat log. + pub log_mode: bool, + /// Outline. + pub outline: bool, + /// Text content to be displayed. + pub text: String, + /// File path. + pub text_file: String, + /// Word wrap. + pub word_wrap: bool, +} + +/// Response value for [`get_special_sources`](crate::client::Sources::get_special_sources). +#[derive(Debug, Deserialize)] +#[serde(rename_all = "kebab-case")] +pub struct SpecialSources { + /// Name of the first Desktop Audio capture source. + pub desktop_1: Option, + /// Name of the second Desktop Audio capture source. + pub desktop_2: Option, + /// Name of the first Mic/Aux input source. + pub mic_1: Option, + /// Name of the second Mic/Aux input source. + pub mic_2: Option, + /// Name of the third Mic/Aux input source. + pub mic_3: Option, +} + +/// Response value for [`get_source_filters`](crate::client::Sources::get_source_filters). +#[derive(Debug, Deserialize)] +pub(crate) struct SourceFilters { + /// List of filters for the specified source. + pub filters: Vec, +} + +/// Response value for [`get_source_filter_info`](crate::client::Sources::get_source_filter_info). +#[derive(Debug, Deserialize)] +pub struct SourceFilterInfo { + /// Filter status (enabled or not). + pub enabled: bool, + /// Filter type. + #[serde(rename = "type")] + pub ty: String, + /// Filter name. + pub name: String, + /// Filter settings. + pub settings: T, +} + +/// Response value for [`get_audio_monitor_type`](crate::client::Sources::get_audio_monitor_type). +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub(crate) struct AudioMonitorType { + /// The monitor type in use. Options: `none`, `monitorOnly`, `monitorAndOutput`. + pub monitor_type: String, +} + +/// Response value for [`take_source_screenshot`](crate::client::Sources::take_source_screenshot). +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SourceScreenshot { + /// Source name. + pub source_name: String, + /// Image Data URI (if + /// [`embed_picture_format`](crate::requests::SourceScreenshot::embed_picture_format) was + /// specified in the request). + pub img: Option, + /// Absolute path to the saved image file (if + /// [`save_to_file_path`](crate::requests::SourceScreenshot::save_to_file_path) was specified in + /// the request). + pub image_file: Option, +} + +/// Response value for [`list_outputs`](crate::client::Outputs::list_outputs). +#[derive(Debug, Deserialize)] +pub(crate) struct Outputs { + /// Outputs list. + pub outputs: Vec, +} + +/// Response value for [`get_output_info`](crate::client::Outputs::get_output_info). +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub(crate) struct OutputInfo { + /// Output info. + pub output_info: Output, +} + +/// Response value for [`get_current_profile`](crate::client::Profiles::get_current_profile). +#[derive(Debug, Deserialize)] +#[serde(rename_all = "kebab-case")] +pub(crate) struct CurrentProfile { + /// Name of the currently active profile. + pub profile_name: String, +} + +/// Response value for [`list_profiles`](crate::client::Profiles::list_profiles). +#[derive(Debug, Deserialize)] +pub(crate) struct Profiles { + /// List of available profiles. + pub profiles: Vec, +} + +/// Response value for [`get_recording_folder`](crate::client::Recording::get_recording_folder). +#[derive(Debug, Deserialize)] +#[serde(rename_all = "kebab-case")] +pub(crate) struct RecordingFolder { + /// Path of the recording folder. + pub rec_folder: String, +} + +/// Response value for +/// [`get_current_scene_collection`](crate::client::SceneCollections::get_current_scene_collection). +#[serde(rename_all = "kebab-case")] +#[derive(Debug, Deserialize)] +pub(crate) struct CurrentSceneCollection { + /// Name of the currently active scene collection. + pub sc_name: String, +} + +/// Response value for +/// [`list_scene_collections`](crate::client::SceneCollections::list_scene_collections). +#[derive(Debug, Deserialize)] +#[serde(rename_all = "kebab-case")] +pub(crate) struct SceneCollections { + /// Scene collections list. + pub scene_collections: Vec, +} + +/// Response value for +/// [`get_scene_item_properties`](crate::client::SceneItems::get_scene_item_properties). +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SceneItemProperties { + /// Scene Item name. + pub name: String, + /// Scene Item ID. + pub item_id: i64, + /// Position of the source. + pub position: Position, + /// The clockwise rotation of the item in degrees around the point of alignment. + pub rotation: f64, + /// Scaling factor of the source. + pub scale: Scale, + /// Pixel cropping of the source before scaling. + pub crop: Crop, + /// If the source is visible. + pub visible: bool, + /// If the source is muted. + pub muted: bool, + /// If the source's transform is locked. + pub locked: bool, + /// Bounding box of the source. + pub bounds: Bounds, + /// Base width (without scaling) of the source. + pub source_width: u32, + /// Base source (without scaling) of the source. + pub source_height: u32, + /// Scene item width (base source width multiplied by the horizontal scaling factor). + pub width: f64, + /// Scene item height (base source height multiplied by the vertical scaling factor). + pub height: f64, + // pub alignment: u8, + /// Name of the item's parent (if this item belongs to a group). + pub parent_group_name: Option, + /// List of children (if this item is a group). + #[serde(default)] + pub group_children: Vec, +} + +/// Response value for [`add_scene_item`](crate::client::SceneItems::add_scene_item). +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub(crate) struct AddSceneItem { + /// Numerical ID of the created scene item. + pub item_id: i64, +} + +/// Response value for [`duplicate_scene_item`](crate::client::SceneItems::duplicate_scene_item). +#[derive(Debug, Deserialize)] +pub struct DuplicateSceneItem { + /// Name of the scene where the new item was created. + pub scene: String, + /// New item info. + pub item: SceneItemSpecification, +} + +/// Response value for [`get_current_scene`](crate::client::Scenes::get_current_scene). +#[derive(Debug, Deserialize)] +pub struct CurrentScene { + /// Name of the currently active scene. + pub name: String, + /// Ordered list of the current scene's source items. + pub sources: Vec, +} + +/// Response value for [`get_scene_list`](crate::client::Scenes::get_scene_list). +#[derive(Debug, Deserialize)] +#[serde(rename_all = "kebab-case")] +pub struct SceneList { + /// Name of the currently active scene. + pub current_scene: String, + /// Ordered list of the current profile's scenes. + pub scenes: Vec, +} + +/// Response value for +/// [`get_scene_transition_override`](crate::client::Scenes::get_scene_transition_override). +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SceneTransitionOverride { + /// Name of the current overriding transition. Empty string if no override is set. + pub transition_name: String, + /// Transition duration. `-1` if no override is set. + pub transition_duration: i64, +} + +/// Response value for [`get_streaming_status`](crate::client::Streaming::get_streaming_status). +#[derive(Debug, Deserialize)] +#[serde(rename_all = "kebab-case")] +pub struct StreamingStatus { + /// Current streaming status. + pub streaming: bool, + /// Current recording status. + pub recording: bool, + /// Time elapsed since streaming started (only present if currently streaming). + pub stream_timecode: Option, + /// Time elapsed since recording started (only present if currently recording). + pub rec_timecode: Option, + /// Always false. Retrocompatibility with OBSRemote. + #[serde(default)] + pub preview_only: bool, +} + +/// Response value for [`get_stream_settings`](crate::client::Streaming::get_stream_settings). +#[derive(Debug, Deserialize)] +pub struct GetStreamSettings { + /// The type of streaming service configuration. Possible values: `rtmp_custom` or + /// `rtmp_common`. + #[serde(rename = "type")] + pub ty: String, + /// Stream settings object. + pub settings: StreamSettings, +} + +/// Response value for +/// [`get_studio_mode_status`](crate::client::StudioMode::get_studio_mode_status). +#[derive(Debug, Deserialize)] +#[serde(rename_all = "kebab-case")] +pub(crate) struct StudioModeStatus { + /// Indicates if Studio Mode is enabled. + pub studio_mode: bool, +} + +/// Response value for [`get_preview_scene`](crate::client::StudioMode::get_preview_scene). +#[derive(Debug, Deserialize)] +pub struct PreviewScene { + /// The name of the active preview scene. + pub name: String, + /// Array of scene items of the active preview scene. + pub sources: Vec, +} + +/// Response value for [`get_transition_list`](crate::client::Transitions::get_transition_list). +#[derive(Debug, Deserialize)] +#[serde(rename_all = "kebab-case")] +pub struct TransitionList { + /// Name of the currently active transition. + pub current_transition: String, + /// List of transitions. + pub transitions: Vec, +} + +/// Response value for +/// [`get_current_transition`](crate::client::Transitions::get_current_transition). +#[derive(Debug, Deserialize)] +pub struct CurrentTransition { + /// Name of the selected transition. + pub name: String, + /// Transition duration (in milliseconds) if supported by the transition. + pub duration: Option, +} + +/// Response value for +/// [`get_transition_duration`](crate::client::Transitions::get_transition_duration). +#[derive(Debug, Deserialize)] +#[serde(rename_all = "kebab-case")] +pub(crate) struct TransitionDuration { + /// Duration of the current transition (in milliseconds). + pub transition_duration: u64, +} + +// ----------------------------------------------------------------------------- +// ----------------------------------------------------------------------------- +// ----------------------------------------------------------------------------- + +/// Response value for [`get_stats`](crate::client::General::get_stats). +#[derive(Clone, Debug, Deserialize)] +#[serde(rename_all = "kebab-case")] +pub struct ObsStats { + /// Current framerate. + pub fps: f64, + /// Number of frames rendered. + pub render_total_frames: u64, + /// Number of frames missed due to rendering lag. + pub render_missed_frames: u64, + /// Number of frames outputted. + pub output_total_frames: u64, + /// Number of frames skipped due to encoding lag. + pub output_skipped_frames: u64, + /// Average frame render time (in milliseconds). + pub average_frame_time: f64, + /// Current CPU usage (percentage). + pub cpu_usage: f64, + /// Current RAM usage (in megabytes). + pub memory_usage: f64, + /// Free recording disk space (in megabytes) + pub free_disk_space: f64, +} + +/// Response value for [`get_sources_list`](crate::client::Sources::get_sources_list). +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SourceListItem { + /// Unique source name. + pub name: String, + /// Non-unique source internal type (a.k.a kind). + pub type_id: String, + /// Source type. Value is one of the following: "input", "filter", "transition", "scene" or + /// "unknown". + #[serde(rename = "type")] + pub ty: String, +} + +/// Response value for [`get_sources_types_list`](crate::client::Sources::get_sources_types_list). +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SourceTypeItem { + /// Non-unique internal source type ID. + pub type_id: String, + /// Display name of the source type. + pub display_name: String, + /// Type. Value is one of the following: "input", "filter", "transition" or "other". + #[serde(rename = "type")] + pub ty: String, + /// Default settings of this source type. + pub default_settings: serde_json::Value, + /// Source type capabilities. + pub caps: Caps, +} + +/// Response value for [`get_sources_types_list`](crate::client::Sources::get_sources_types_list) as +/// part of [`SourceTypeItem`]. +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Caps { + /// True if source of this type provide frames asynchronously. + pub is_async: bool, + /// True if sources of this type provide video. + pub has_video: bool, + /// True if sources of this type provide audio. + pub has_audio: bool, + /// True if interaction with this sources of this type is possible. + pub can_interact: bool, + /// True if sources of this type composite one or more sub-sources. + pub is_composite: bool, + /// True if sources of this type should not be fully duplicated. + pub do_not_duplicate: bool, + /// True if sources of this type may cause a feedback loop if it's audio is monitored and + /// shouldn't be. + pub do_not_self_monitor: bool, +} + +/// Response value for +/// [`get_text_gdi_plus_properties`](crate::client::Sources::get_text_gdi_plus_properties) as part +/// of [`TextGdiPlusProperties`] and +/// [`get_text_freetype2_properties`](crate::client::Sources::get_text_freetype2_properties) as part +/// of [`TextFreetype2Properties`]. +#[derive(Debug, Deserialize)] +pub struct Font { + /// Font face. + pub face: String, + /// Font text styling flag. `Bold=1, Italic=2, Bold Italic=3, Underline=5, Strikeout=8`. + pub flags: u8, + /// Font text size. + pub size: u32, + /// Font Style (unknown function). + pub style: String, +} + +/// Response value for [`get_source_filters`](crate::client::Sources::get_source_filters). +#[derive(Debug, Deserialize)] +pub struct SourceFilter { + /// Filter status (enabled or not). + pub enabled: bool, + /// Filter type. + #[serde(rename = "type")] + pub ty: String, + /// Filter name. + pub name: String, + /// Filter settings. + pub settings: serde_json::Value, +} + +/// Response value for [`list_outputs`](crate::client::Outputs::list_outputs) and +/// [`get_output_info`](crate::client::Outputs::get_output_info). +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Output { + /// Output name. + pub name: String, + /// Output type/kind. + #[serde(rename = "type")] + pub ty: String, + /// Video output width. + pub width: u32, + /// Video output height. + pub height: u32, + /// Output flags. + pub flags: OutputFlags, + /// Output settings. + pub settings: serde_json::Value, + /// Output status (active or not). + pub active: bool, + /// Output reconnection status (reconnecting or not). + pub reconnecting: bool, + /// Output congestion. + pub congestion: f64, + /// Number of frames sent. + pub total_frames: u64, + /// Number of frames dropped. + pub dropped_frames: u64, + /// Total bytes sent. + pub total_bytes: u64, +} + +/// Response value for [`list_outputs`](crate::client::Outputs::list_outputs) and +/// [`get_output_info`](crate::client::Outputs::get_output_info) as part of [`Output`]. +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct OutputFlags { + /// Raw flags value. + pub raw_value: u64, + /// Output uses audio. + pub audio: bool, + /// Output uses video. + pub video: bool, + /// Output is encoded. + pub encoded: bool, + /// Output uses several audio tracks. + pub multi_track: bool, + /// Output uses a service. + pub service: bool, +} + +/// Response value for [`list_profiles`](crate::client::Profiles::list_profiles). +#[derive(Debug, Deserialize)] +#[serde(rename_all = "kebab-case")] +pub struct Profile { + /// Profile name. + pub profile_name: String, +} + +/// Response value for +/// [`list_scene_collections`](crate::client::SceneCollections::list_scene_collections). +#[derive(Debug, Deserialize)] +#[serde(rename_all = "kebab-case")] +pub struct SceneCollection { + /// Scene collection name. + pub sc_name: String, +} + +/// Response value for [`duplicate_scene_item`](crate::client::SceneItems::duplicate_scene_item) as +/// part of [`DuplicateSceneItem`]. +#[derive(Debug, Deserialize)] +pub struct SceneItemSpecification { + /// New item ID. + pub id: i64, + /// New item name. + pub name: String, +} + +/// Response value for [`get_scene_list`](crate::client::Scenes::get_scene_list) as part of +/// [`SceneList`]. +// TODO: actually the same as `CurrentScene`. +#[derive(Clone, Debug, Deserialize)] +pub struct Scene { + /// Name of the scene. + pub name: String, + /// Ordered list of the scene's source items. + pub sources: Vec, +} + +/// Response value for [`get_stream_settings`](crate::client::Streaming::get_stream_settings) as +/// part of [`GetStreamSettings`]. +#[derive(Debug, Deserialize)] +pub struct StreamSettings { + /// The publish URL. + pub server: String, + /// The publish key of the stream. + pub key: String, + /// Indicates whether authentication should be used when connecting to the streaming server. + pub use_auth: bool, + /// The username to use when accessing the streaming server. Only present if + /// [`use_auth`](Self::use_auth) is `true`. + pub username: Option, + /// The password to use when accessing the streaming server. Only present if + /// [`use_auth`](Self::use_auth) is `true`. + pub password: Option, +} + +/// Response value for [`get_transition_list`](crate::client::Transitions::get_transition_list) as +/// part of [`TransitionList`]. +#[derive(Debug, Deserialize)] +pub struct Transition { + /// Name of the transition. + pub name: String, +}