Parse into more concrete types where possible

pull/1/head
Dominik Nakamura 3 years ago
parent a96db636f6
commit c7ec6fa13c
No known key found for this signature in database
GPG Key ID: E4C6A749B2491910

@ -15,9 +15,12 @@ keywords = ["async", "obs", "obs-websocket", "remote-control", "tokio"]
anyhow = "1.0.36"
async-stream = "0.3.0"
base64 = "0.13.0"
bitflags = "1.2.1"
chrono = { version = "0.4.19", default-features = false, features = ["std"] }
either = { version = "1.6.1", features = ["serde"] }
futures-util = { version = "0.3.8", features = ["sink"] }
log = "0.4.11"
semver = { version = "0.11.0", features = ["serde"] }
serde = { version = "1.0.118", features = ["derive"] }
serde_json = "1.0.60"
serde_with = "1.6.0"

@ -95,7 +95,7 @@ impl Client {
.await;
if let Err(e) = temp {
error!("{:?}", e);
error!("failed handling message: {:?}", e);
}
}
});

@ -1,3 +1,5 @@
use std::path::PathBuf;
use anyhow::Result;
use super::Client;
@ -43,14 +45,14 @@ impl<'a> Recording<'a> {
/// 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<()> {
pub async fn set_recording_folder(&self, rec_folder: PathBuf) -> 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<String> {
pub async fn get_recording_folder(&self) -> Result<PathBuf> {
self.client
.send_message::<responses::RecordingFolder>(RequestType::GetRecordingFolder)
.await

@ -1,7 +1,9 @@
use anyhow::Result;
use chrono::Duration;
use serde::de::DeserializeOwned;
use super::Client;
use crate::common::MonitorType;
use crate::requests::{
AddFilter, MoveFilter, ReorderFilter, RequestType, SourceFilterSettings,
SourceFilterVisibility, SourceScreenshot, SourceSettings, TextFreetype2Properties,
@ -112,7 +114,7 @@ impl<'a> Sources<'a> {
///
/// - `source`: Source name.
/// - `offset`: The desired audio sync offset (in nanoseconds).
pub async fn set_sync_offset(&self, source: String, offset: i64) -> Result<()> {
pub async fn set_sync_offset(&self, source: String, offset: Duration) -> Result<()> {
self.client
.send_message(RequestType::SetSyncOffset { source, offset })
.await
@ -304,7 +306,7 @@ impl<'a> Sources<'a> {
/// 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<String> {
pub async fn get_audio_monitor_type(&self, source_name: String) -> Result<MonitorType> {
self.client
.send_message::<responses::AudioMonitorType>(RequestType::GetAudioMonitorType {
source_name,
@ -321,7 +323,7 @@ impl<'a> Sources<'a> {
pub async fn set_audio_monitor_type(
&self,
source_name: String,
monitor_type: String,
monitor_type: MonitorType,
) -> Result<()> {
self.client
.send_message(RequestType::SetAudioMonitorType {

@ -1,4 +1,5 @@
use anyhow::Result;
use chrono::Duration;
use super::Client;
use crate::requests::RequestType;
@ -36,14 +37,14 @@ impl<'a> Transitions<'a> {
/// 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<()> {
pub async fn set_transition_duration(&self, duration: Duration) -> 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<u64> {
pub async fn get_transition_duration(&self) -> Result<Duration> {
self.client
.send_message::<responses::TransitionDuration>(RequestType::GetTransitionDuration)
.await

@ -1,7 +1,11 @@
//! Common data structures shared between [`requests`](crate::requests),
//! [`responses`](crate::responses) and [`events`](crate::events).
use serde::Deserialize;
use std::convert::TryFrom;
use anyhow::Context;
use bitflags::bitflags;
use serde::{Deserialize, Serialize};
/// Response value for [`get_current_scene`](crate::client::Scenes::get_current_scene) as part of
/// [`CurrentScene`](crate::responses::CurrentScene),
@ -19,7 +23,8 @@ pub struct SceneItem {
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,
#[serde(deserialize_with = "crate::de::bitflags_u8")]
pub alignment: Alignment,
/// The name of this Scene Item.
pub name: String,
/// Scene item ID.
@ -32,8 +37,7 @@ pub struct SceneItem {
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".
/// Source type.
#[serde(rename = "type")]
pub ty: String,
pub volume: f64,
@ -95,7 +99,8 @@ pub struct Position {
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,
#[serde(deserialize_with = "crate::de::bitflags_u8")]
pub alignment: Alignment,
}
/// Response value for
@ -115,13 +120,13 @@ pub struct Scale {
#[derive(Clone, Debug, Deserialize)]
pub struct Crop {
/// The number of pixels cropped off the top of the source before scaling.
pub top: i64,
pub top: u32,
/// The number of pixels cropped off the right of the source before scaling.
pub right: i64,
pub right: u32,
/// The number of pixels cropped off the bottom of the source before scaling.
pub bottom: i64,
pub bottom: u32,
/// The number of pixels cropped off the left of the source before scaling.
pub left: i64,
pub left: u32,
}
/// Response value for
@ -133,11 +138,146 @@ pub struct Bounds {
/// "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,
pub ty: BoundsType,
/// Alignment of the bounding box.
pub alignment: u8,
#[serde(deserialize_with = "crate::de::bitflags_u8")]
pub alignment: Alignment,
/// Width of the bounding box.
pub x: f64,
/// Height of the bounding box.
pub y: f64,
}
/// Monitoring type for audio outputs.
#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum MonitorType {
/// No monitoring.
None,
/// Only monitor but don't output any sounds.
MonitorOnly,
/// Mintor the audio and output it at the same time.
MonitorAndOutput,
}
/// Text alignment used for GDI+ text properties.
#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Align {
/// Align to the left.
Left,
/// Center the text in the middle (horizontally).
Center,
/// Align to the right.
Right,
}
/// Vertical text alignment use for GDI+ text properties.
#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Valign {
/// Align to the top.
Top,
/// Center the text in the middle (vertically).
Center,
/// Align to the bottom.
Bottom,
}
/// The type of streaming for service configurations.
#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum StreamType {
/// Customized RTMP streaming.
RtmpCustom,
/// Common RTMP configuration.
RtmpCommon,
}
bitflags! {
/// Different flags for font display that can be combined together.
pub struct FontFlags: u8 {
/// Make the text appear thicker.
const BOLD = 1;
/// Make the text appear cursive.
const ITALIC = 2;
/// Underline the text with a straight line.
const UNDERLINE = 5;
/// Strikeout the text.
const STRIKEOUT = 8;
}
}
impl TryFrom<u8> for FontFlags {
type Error = anyhow::Error;
fn try_from(value: u8) -> Result<Self, Self::Error> {
Self::from_bits(value).context("value contains unknown flags")
}
}
impl From<FontFlags> for u8 {
fn from(value: FontFlags) -> Self {
value.bits
}
}
bitflags! {
/// Alignment for different items on the scene that is described in two axis. The default is
/// center for both axis.
///
/// For example, only using `LEFT` would arrange the target to the left horzontally and centered
/// vertically. To align to the top right, the alignments can be combined to `LEFT | TOP`.
/// Combining both values for a single axis is invalid, like `LEFT | RIGHT`.
pub struct Alignment: u8 {
/// Align to the left side.
const LEFT = 1;
/// Align to the right side.
const RIGHT = 2;
/// Align to the top.
const TOP = 4;
/// Align to the bottom.
const BOTTOM = 8;
}
}
impl TryFrom<u8> for Alignment {
type Error = anyhow::Error;
fn try_from(value: u8) -> Result<Self, Self::Error> {
Self::from_bits(value).context("value contains unknown flags")
}
}
impl From<Alignment> for u8 {
fn from(value: Alignment) -> Self {
value.bits
}
}
/// Different kinds of bounds that can be applied to different items on the scene as part of the
/// [`Bounds`] type.
#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
pub enum BoundsType {
/// Stretch to bounds.
#[serde(rename = "OBS_BOUNDS_STRETCH")]
Stretch,
/// Scale to inner bounds.
#[serde(rename = "OBS_BOUNDS_SCALE_INNER")]
ScaleInner,
/// Scale to outer bounds.
#[serde(rename = "OBS_BOUNDS_SCALE_OUTER")]
ScaleOuter,
/// Scale to width of bounds.
#[serde(rename = "OBS_BOUNDS_SCALE_TO_WIDTH")]
ScaleToWidth,
/// Scale to height of bounds.
#[serde(rename = "OBS_BOUNDS_SCALE_TO_HEIGHT")]
ScaleToHeight,
/// Maximum size only.
#[serde(rename = "OBS_BOUNDS_MAX_ONLY")]
MaxOnly,
/// No bounds.
#[serde(rename = "OBS_BOUNDS_NONE")]
None,
}

@ -0,0 +1,262 @@
//! Custom deserializers that are used in both the [`events`](crate::events) and
//! [`responses`](crate::responses) modules.
use std::convert::TryFrom;
use std::fmt::{self, Display};
use std::marker::PhantomData;
use anyhow::{Context, Result};
use chrono::Duration;
use serde::de::{Deserializer, Error, Visitor};
pub fn duration<'de, D>(deserializer: D) -> Result<Option<Duration>, D::Error>
where
D: Deserializer<'de>,
{
deserializer.deserialize_option(OptDurationVisitor)
}
struct OptDurationVisitor;
impl<'de> Visitor<'de> for OptDurationVisitor {
type Value = Option<Duration>;
fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str("an optional duration formatted as 'HH:MM:SS.mmm'")
}
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
where
E: Error,
{
let duration = || -> Result<Duration> {
let mut hms = v.splitn(3, ':');
let hours = hms.next().context("hours missing")?.parse()?;
let minutes = hms.next().context("minutes missing")?.parse()?;
let seconds = hms.next().context("seconds missing")?;
let mut sm = seconds.splitn(2, '.');
let seconds = sm.next().context("seconds missing")?.parse()?;
let millis = sm.next().context("milliseconds missing")?.parse()?;
Ok(Duration::hours(hours)
+ Duration::minutes(minutes)
+ Duration::seconds(seconds)
+ Duration::milliseconds(millis))
};
duration().map(Some).map_err(Error::custom)
}
fn visit_none<E>(self) -> Result<Self::Value, E>
where
E: Error,
{
Ok(None)
}
fn visit_some<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
where
D: Deserializer<'de>,
{
deserializer.deserialize_str(Self)
}
}
pub fn duration_millis_opt<'de, D>(deserializer: D) -> Result<Option<Duration>, D::Error>
where
D: Deserializer<'de>,
{
deserializer.deserialize_i64(OptDurationMillisVisitor)
}
struct OptDurationMillisVisitor;
impl<'de> Visitor<'de> for OptDurationMillisVisitor {
type Value = Option<Duration>;
fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str("a duration in milliseconds where -1 means a fixed duration")
}
fn visit_i64<E>(self, v: i64) -> Result<Self::Value, E>
where
E: Error,
{
Ok(if v < 0 {
None
} else {
Some(Duration::milliseconds(v))
})
}
fn visit_u64<E>(self, v: u64) -> Result<Self::Value, E>
where
E: Error,
{
match i64::try_from(v) {
Ok(value) => self.visit_i64(value),
Err(e) => Err(Error::custom(e)),
}
}
fn visit_none<E>(self) -> Result<Self::Value, E>
where
E: Error,
{
Ok(None)
}
fn visit_some<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
where
D: Deserializer<'de>,
{
deserializer.deserialize_i64(Self)
}
}
pub fn duration_millis<'de, D>(deserializer: D) -> Result<Duration, D::Error>
where
D: Deserializer<'de>,
{
deserializer.deserialize_i64(DurationMillisVisitor)
}
struct DurationMillisVisitor;
impl<'de> Visitor<'de> for DurationMillisVisitor {
type Value = Duration;
fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str("a duration in milliseconds")
}
fn visit_i64<E>(self, v: i64) -> Result<Self::Value, E>
where
E: Error,
{
Ok(Duration::milliseconds(v))
}
fn visit_u64<E>(self, v: u64) -> Result<Self::Value, E>
where
E: Error,
{
match i64::try_from(v) {
Ok(value) => self.visit_i64(value),
Err(e) => Err(Error::custom(e)),
}
}
}
pub fn duration_nanos<'de, D>(deserializer: D) -> Result<Duration, D::Error>
where
D: Deserializer<'de>,
{
deserializer.deserialize_i64(DurationNanosVisitor)
}
struct DurationNanosVisitor;
impl<'de> Visitor<'de> for DurationNanosVisitor {
type Value = Duration;
fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str("a duration in nanoseconds")
}
fn visit_i64<E>(self, v: i64) -> Result<Self::Value, E>
where
E: Error,
{
Ok(Duration::nanoseconds(v))
}
fn visit_u64<E>(self, v: u64) -> Result<Self::Value, E>
where
E: Error,
{
match i64::try_from(v) {
Ok(value) => self.visit_i64(value),
Err(e) => Err(Error::custom(e)),
}
}
}
pub fn bitflags_u8<'de, D, T, TE>(deserializer: D) -> Result<T, D::Error>
where
D: Deserializer<'de>,
T: TryFrom<u8, Error = TE>,
TE: Display,
{
deserializer.deserialize_u8(BitflagsU8Visitor { flags: PhantomData })
}
struct BitflagsU8Visitor<T, TE> {
flags: PhantomData<(T, TE)>,
}
impl<'de, T, TE> Visitor<'de> for BitflagsU8Visitor<T, TE>
where
T: TryFrom<u8, Error = TE>,
TE: Display,
{
type Value = T;
fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str("bitflags encoded as u8 integer")
}
fn visit_i64<E>(self, v: i64) -> Result<Self::Value, E>
where
E: Error,
{
u8::try_from(v)
.map_err(|_| Error::custom("value doesn't fit into an u8 integer"))
.and_then(|v| self.visit_u8(v))
}
fn visit_u8<E>(self, v: u8) -> Result<Self::Value, E>
where
E: Error,
{
T::try_from(v).map_err(Error::custom)
}
fn visit_u64<E>(self, v: u64) -> Result<Self::Value, E>
where
E: Error,
{
u8::try_from(v)
.map_err(|_| Error::custom("value doesn't fit into an u8 integer"))
.and_then(|v| self.visit_u8(v))
}
}
#[cfg(test)]
mod tests {
use serde::Deserialize;
use serde_json::json;
use super::*;
#[test]
fn deser_duration() {
#[derive(Debug, PartialEq, Eq, Deserialize)]
struct SimpleDuration {
#[serde(deserialize_with = "duration")]
value: Option<Duration>,
};
let input = json! {{ "value": "02:15:04.310" }};
let expect = SimpleDuration {
value: Some(
Duration::hours(2)
+ Duration::minutes(15)
+ Duration::seconds(4)
+ Duration::milliseconds(310),
),
};
assert_eq!(expect, serde_json::from_value(input).unwrap());
}
}

@ -1,5 +1,6 @@
//! All events that can be received from the API.
use chrono::Duration;
use serde::Deserialize;
use crate::common::{SceneItem, SceneItemTransform};
@ -8,10 +9,12 @@ use crate::common::{SceneItem, SceneItemTransform};
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub struct Event {
#[serde(default, deserialize_with = "crate::de::duration")]
/// Time elapsed between now and stream start (only present if OBS Studio is streaming).
pub stream_timecode: Option<String>,
pub stream_timecode: Option<Duration>,
/// Time elapsed between now and recording start (only present if OBS Studio is recording).
pub rec_timecode: Option<String>,
#[serde(default, deserialize_with = "crate::de::duration")]
pub rec_timecode: Option<Duration>,
/// The type of event.
#[serde(flatten)]
pub ty: EventType,
@ -63,7 +66,8 @@ pub enum EventType {
#[serde(rename_all = "kebab-case")]
TransitionDurationChanged {
/// New transition duration.
new_duration: u64,
#[serde(deserialize_with = "crate::de::duration_millis")]
new_duration: Duration,
},
/// A transition (other than "cut") has begun.
#[serde(rename_all = "kebab-case")]
@ -75,7 +79,8 @@ pub enum EventType {
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,
#[serde(deserialize_with = "crate::de::duration_millis_opt")]
duration: Option<Duration>,
/// Source scene of the transition.
from_scene: String,
/// Destination scene of the transition.
@ -91,7 +96,8 @@ pub enum EventType {
#[serde(rename = "type")]
ty: String,
/// Transition duration (in milliseconds).
duration: u64,
#[serde(deserialize_with = "crate::de::duration_millis")]
duration: Duration,
/// Destination scene of the transition.
to_scene: String,
},
@ -104,7 +110,8 @@ pub enum EventType {
#[serde(rename = "type")]
ty: String,
/// Transition duration (in milliseconds).
duration: u64,
#[serde(deserialize_with = "crate::de::duration_millis")]
duration: Duration,
/// Source scene of the transition.
from_scene: String,
/// Destination scene of the transition.
@ -188,23 +195,11 @@ pub enum EventType {
/// 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,
},
RecordingStarted,
/// 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,
},
RecordingStopping,
/// Recording stopped successfully.
#[serde(rename_all = "camelCase")]
RecordingStopped {
/// Absolute path to the file of the current recording.
recording_filename: String,
},
RecordingStopped,
/// Current recording paused.
RecordingPaused,
/// Current recording resumed.
@ -244,7 +239,7 @@ pub enum EventType {
/// Source name.
source_name: String,
/// Source type. Can be "input", "scene", "transition" or "filter".
source_type: String,
source_type: SourceType,
/// Source kind.
source_kind: String,
/// Source settings.
@ -256,7 +251,7 @@ pub enum EventType {
/// Source name.
source_name: String,
/// Source type. Can be "input", "scene", "transition" or "filter".
source_type: String,
source_type: SourceType,
/// Source kind.
source_kind: String,
},
@ -294,7 +289,8 @@ pub enum EventType {
/// Source name.
source_name: String,
/// Audio sync offset of the source (in nanoseconds).
sync_offset: i64,
#[serde(deserialize_with = "crate::de::duration_nanos")]
sync_offset: Duration,
},
/// Audio mixer routing changed on a source.
#[serde(rename_all = "camelCase")]
@ -314,7 +310,7 @@ pub enum EventType {
/// New source name.
new_name: String,
/// Type of source (input, scene, filter, transition).
source_type: String,
source_type: SourceType,
},
/// A filter was added to a source.
#[serde(rename_all = "camelCase")]
@ -509,3 +505,18 @@ pub struct SourceOrderSceneItem {
/// Scene item unique ID.
pub item_id: i64,
}
/// Part of [`EventType::SourceCreated`], [`EventType::SourceDestroyed`] and
/// [`EventType::SourceRenamed`].
#[derive(Clone, Copy, Debug, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum SourceType {
/// An input source.
Input,
/// A scene.
Scene,
/// Transition between scenes.
Transition,
/// Filter for scene items.
Filter,
}

@ -1,9 +1,11 @@
//! # OBSWS - The obws (obvious) remote control library for OBS
#![deny(missing_docs, rust_2018_idioms, clippy::all)]
#![warn(missing_docs, rust_2018_idioms, clippy::all)]
pub mod client;
pub mod common;
pub mod events;
pub mod requests;
pub mod responses;
mod de;

@ -1,9 +1,16 @@
//! All requests that can be send to the API.
use std::path::PathBuf;
use chrono::Duration;
use either::Either;
use serde::Serialize;
use serde_with::skip_serializing_none;
use crate::common::{Align, Alignment, BoundsType, FontFlags, MonitorType, StreamType, Valign};
mod ser;
#[derive(Serialize)]
#[serde(rename_all = "kebab-case")]
pub(crate) struct Request {
@ -82,7 +89,8 @@ pub(crate) enum RequestType {
/// Source name.
source: String,
/// The desired audio sync offset (in nanoseconds).
offset: i64,
#[serde(serialize_with = "ser::duration_nanos")]
offset: Duration,
},
GetSyncOffset {
/// Source name.
@ -142,7 +150,7 @@ pub(crate) enum RequestType {
/// Source name.
source_name: String,
/// The monitor type to use. Options: `none`, `monitorOnly`, `monitorAndOutput`.
monitor_type: String,
monitor_type: MonitorType,
},
TakeSourceScreenshot(SourceScreenshot),
// --------------------------------
@ -187,7 +195,7 @@ pub(crate) enum RequestType {
#[serde(rename_all = "kebab-case")]
SetRecordingFolder {
/// Path of the recording folder.
rec_folder: String,
rec_folder: PathBuf,
},
GetRecordingFolder,
// --------------------------------
@ -315,7 +323,8 @@ pub(crate) enum RequestType {
},
SetTransitionDuration {
/// Desired duration of the transition (in milliseconds).
duration: u64,
#[serde(serialize_with = "ser::duration_millis")]
duration: Duration,
},
GetTransitionDuration,
}
@ -327,7 +336,7 @@ pub struct Projector {
/// Type of projector: `Preview` (default), `Source`, `Scene`, `StudioProgram`, or `Multiview`
/// (case insensitive).
#[serde(rename = "type")]
pub ty: Option<String>,
pub ty: Option<ProjectorType>,
/// Monitor to open the projector on. If -1 or omitted, opens a window.
pub monitor: Option<i64>,
/// Size and position of the projector window (only if monitor is -1). Encoded in Base64 using
@ -338,6 +347,22 @@ pub struct Projector {
pub name: Option<String>,
}
/// Request information for [`open_projector`](crate::client::General::open_projector) as part of
/// [`Projector`].
#[derive(Clone, Copy, Debug, Serialize)]
pub enum ProjectorType {
/// Open a projector of the preview area.
Preview,
/// Open a projector for a source.
Source,
/// Open a projector for a scene.
Scene,
/// Open a projector of the program pane in studio mode.
StudioProgram,
/// Open a projector in multiview.
Multiview,
}
/// Request information for [`set_volume`](crate::client::Sources::set_volume).
#[skip_serializing_none]
#[derive(Debug, Serialize)]
@ -375,7 +400,7 @@ pub struct TextGdiPlusProperties {
/// Name of the source.
pub source: String,
/// Text Alignment ("left", "center", "right").
pub align: Option<String>,
pub align: Option<Align>,
/// Background color.
pub bk_color: Option<u32>,
/// Background opacity (0-100).
@ -393,7 +418,7 @@ pub struct TextGdiPlusProperties {
/// Extents cy.
pub extents_cy: Option<i64>,
/// File path name.
pub file: Option<String>,
pub file: Option<PathBuf>,
/// Read text from the specified file.
pub read_from_file: Option<bool>,
/// Holds data for the font. Ex:
@ -418,7 +443,7 @@ pub struct TextGdiPlusProperties {
/// Text content to be displayed.
pub text: Option<String>,
/// Text vertical alignment ("top", "center", "bottom").
pub valign: Option<String>,
pub valign: Option<Valign>,
/// Vertical text enabled.
pub vertical: Option<bool>,
/// Visibility of the scene item.
@ -452,7 +477,7 @@ pub struct TextFreetype2Properties {
/// Text content to be displayed.
pub text: Option<String>,
/// File path.
pub text_file: Option<String>,
pub text_file: Option<PathBuf>,
/// Word wrap.
pub word_wrap: Option<bool>,
}
@ -494,7 +519,22 @@ pub struct MoveFilter {
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,
pub movement_type: MovementType,
}
/// Request information for [`move_source_filter`](crate::client::Sources::move_source_filter) as
/// part of [`MoveFilter`].
#[derive(Clone, Copy, Debug, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum MovementType {
/// Move up by one position.
Up,
/// Move down by one position.
Down,
/// Move to the very top.
Top,
/// Move to the very bottom.
Bottom,
}
/// Request information for
@ -538,7 +578,7 @@ pub struct SourceScreenshot {
/// 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<String>,
pub save_to_file_path: Option<PathBuf>,
/// 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,
@ -634,7 +674,8 @@ pub struct SceneTransitionOverride {
/// 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<i64>,
#[serde(serialize_with = "ser::duration_millis_opt")]
pub transition_duration: Option<Duration>,
}
/// Request information for [`set_stream_settings`](crate::client::Streaming::set_stream_settings).
@ -642,17 +683,13 @@ pub struct SceneTransitionOverride {
pub struct SetStreamSettings {
/// The type of streaming service configuration, usually `rtmp_custom` or `rtmp_common`.
#[serde(rename = "type")]
pub ty: String,
pub ty: StreamType,
/// 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
@ -664,7 +701,8 @@ pub struct Font {
/// Font face.
pub face: Option<String>,
/// Font text styling flag. `Bold=1, Italic=2, Bold Italic=3, Underline=5, Strikeout=8`.
pub flags: Option<u8>,
#[serde(serialize_with = "ser::bitflags_u8_opt")]
pub flags: Option<FontFlags>,
/// Font text size.
pub size: Option<u32>,
/// Font Style (unknown function).
@ -698,7 +736,8 @@ pub struct Position {
/// The new y position of the source.
pub y: Option<f64>,
/// The new alignment of the source.
pub alignment: Option<u8>,
#[serde(serialize_with = "ser::bitflags_u8_opt")]
pub alignment: Option<Alignment>,
}
/// Request information for
@ -739,9 +778,10 @@ pub struct Bounds {
/// "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<String>,
pub ty: Option<BoundsType>,
/// The new alignment of the bounding box. (0-2, 4-6, 8-10).
pub alignment: Option<u8>,
#[serde(serialize_with = "ser::bitflags_u8_opt")]
pub alignment: Option<Alignment>,
/// The new width of the bounding box.
pub x: Option<f64>,
/// The new height of the bounding box.
@ -769,7 +809,7 @@ pub struct 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<String>,
ty: Option<StreamType>,
/// 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.
@ -806,5 +846,6 @@ pub struct Transition {
/// Name of the transition.
name: String,
/// Transition duration (in milliseconds).
duration: Option<u64>,
#[serde(serialize_with = "ser::duration_millis_opt")]
duration: Option<Duration>,
}

@ -0,0 +1,42 @@
use chrono::Duration;
use serde::ser::{Error, Serializer};
pub fn duration_millis_opt<S>(value: &Option<Duration>, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
match value {
Some(duration) => serializer.serialize_some(&duration.num_milliseconds()),
None => serializer.serialize_none(),
}
}
pub fn duration_millis<S>(value: &Duration, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_i64(value.num_milliseconds())
}
pub fn duration_nanos<S>(value: &Duration, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
match value.num_nanoseconds() {
Some(nanos) => serializer.serialize_i64(nanos),
None => Err(Error::custom(
"duration is too big to be serialized as nanoseconds",
)),
}
}
pub fn bitflags_u8_opt<S, T>(value: &Option<T>, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
T: Into<u8> + Copy,
{
match value {
Some(flags) => serializer.serialize_some(&(*flags).into()),
None => serializer.serialize_none(),
}
}

@ -1,14 +1,67 @@
use std::fmt;
use std::marker::PhantomData;
use std::iter::FromIterator;
use serde::de::{Deserialize, Deserializer};
use serde::de::{Deserializer, Error, Visitor};
#[allow(dead_code)]
pub fn string_comma_list<'de, D, T>(deserializer: D) -> Result<T, D::Error>
where
D: Deserializer<'de>,
T: FromIterator<String>,
{
let s = <&str>::deserialize(deserializer)?;
deserializer.deserialize_str(StringListVisitor {
sep: ',',
container: PhantomData,
})
}
struct StringListVisitor<T> {
sep: char,
container: PhantomData<T>,
}
impl<'de, T> Visitor<'de> for StringListVisitor<T>
where
T: FromIterator<String>,
{
type Value = T;
fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
formatter,
"a string containing values separated by '{}'",
self.sep
)
}
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
where
E: Error,
{
Ok(v.split(self.sep).map(|s| s.to_owned()).collect())
}
}
#[cfg(test)]
mod tests {
use serde::Deserialize;
use serde_json::json;
use super::*;
#[test]
fn deser_string_comma_list() {
#[derive(Debug, PartialEq, Eq, Deserialize)]
struct SimpleList {
#[serde(deserialize_with = "string_comma_list")]
value: Vec<String>,
}
Ok(s.split(',').map(|s| s.to_owned()).collect())
let input = json! {{ "value": "a,b,c" }};
let expect = SimpleList {
value: vec!["a".to_owned(), "b".to_owned(), "c".to_owned()],
};
assert_eq!(expect, serde_json::from_value(input).unwrap());
}
}

@ -1,8 +1,17 @@
//! All responses that can be received from the API.
use std::collections::HashSet;
use std::path::PathBuf;
use chrono::Duration;
use serde::Deserialize;
use crate::common::{Bounds, Crop, Position, Scale, SceneItem, SceneItemTransform};
pub use semver::Version as SemVerVersion;
use crate::common::{
Align, Bounds, Crop, FontFlags, MonitorType, Position, Scale, SceneItem, SceneItemTransform,
StreamType, Valign,
};
mod de;
@ -30,16 +39,18 @@ 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,
pub obs_websocket_version: SemVerVersion,
/// OBS Studio program version.
pub obs_studio_version: String,
pub obs_studio_version: SemVerVersion,
/// List of available request types, formatted as a comma-separated list string (e.g. :
/// "Method1,Method2,Method3").
pub available_requests: String,
#[serde(deserialize_with = "de::string_comma_list")]
pub available_requests: HashSet<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,
#[serde(deserialize_with = "de::string_comma_list")]
pub supported_image_export_formats: HashSet<String>,
}
/// Response value for [`get_auth_required`](crate::client::General::get_auth_required).
@ -82,15 +93,86 @@ pub struct VideoInfo {
/// Output height.
pub output_height: u64,
/// Scaling method used if output size differs from base size.
pub scale_type: String,
pub scale_type: ScaleType,
/// Frames rendered per second.
pub fps: f64,
/// Video color format.
pub video_format: String,
pub video_format: VideoFormat,
/// Color space for YUV.
pub color_space: String,
pub color_space: ColorSpace,
/// Color range (full or partial).
pub color_range: String,
pub color_range: ColorRange,
}
/// Possible scaling types for the output.
///
/// Response value for [`get_video_info`](crate::client::General::get_video_info) as part of
/// [`VideoInfo`].
#[derive(Clone, Copy, Debug, Deserialize)]
pub enum ScaleType {
/// Fastest, but blurry scaling.
#[serde(rename = "VIDEO_SCALE_BILINEAR")]
Bilinear,
/// Weighted sum, 4/6/9 samples.
#[serde(rename = "VIDEO_SCALE_DEFAULT")]
Area,
/// Sharpened scaling, 16 samples.
#[serde(rename = "VIDEO_SCALE_FAST_BILINEAR")]
Bicubic,
/// Sharpened scaling, 36 samples.
#[serde(rename = "VIDEO_SCALE_BICUBIC")]
Lanczos,
}
/// Supported formats for video output.
///
/// Response value for [`get_video_info`](crate::client::General::get_video_info) as part of
/// [`VideoInfo`].
#[derive(Clone, Copy, Debug, Deserialize)]
pub enum VideoFormat {
/// NV12 format.
#[serde(rename = "VIDEO_FORMAT_NV12")]
Nv12,
/// I420 format.
#[serde(rename = "VIDEO_FORMAT_I420")]
I420,
/// I444 format.
#[serde(rename = "VIDEO_FORMAT_I444")]
I444,
/// RGB format.
#[serde(rename = "VIDEO_FORMAT_RGBA")]
RGB,
}
/// Supported color spaces for video output.
///
/// Response value for [`get_video_info`](crate::client::General::get_video_info) as part of
/// [`VideoInfo`].
#[derive(Clone, Copy, Debug, Deserialize)]
pub enum ColorSpace {
/// 709 color space.
#[serde(rename = "VIDEO_CS_709")]
Cs709,
/// 601 color space.
#[serde(rename = "VIDEO_CS_601")]
Cs601,
/// sRGB color space.
#[serde(rename = "VIDEO_CS_DEFAULT")]
CsSRgb,
}
/// Supported color ranges for video output.
///
/// Response value for [`get_video_info`](crate::client::General::get_video_info) as part of
/// [`VideoInfo`].
#[derive(Clone, Copy, Debug, Deserialize)]
pub enum ColorRange {
/// Partial color range.
#[serde(rename = "VIDEO_RANGE_PARTIAL")]
Partial,
/// Full range.
#[serde(rename = "VIDEO_RANGE_FULL")]
Full,
}
/// Response value for [`get_sources_list`](crate::client::Sources::get_sources_list).
@ -141,7 +223,8 @@ pub struct SyncOffset {
/// Source name.
pub name: String,
/// The audio sync offset (in nanoseconds).
pub offset: i64,
#[serde(deserialize_with = "crate::de::duration_nanos")]
pub offset: Duration,
}
/// Response value for [`get_source_settings`](crate::client::Sources::get_source_settings) and
@ -164,7 +247,7 @@ pub struct TextGdiPlusProperties {
/// Source name.
pub source: String,
/// Text Alignment ("left", "center", "right").
pub align: String,
pub align: Align,
/// Background color.
pub bk_color: u32,
/// Background opacity (0-100).
@ -182,7 +265,7 @@ pub struct TextGdiPlusProperties {
/// Extents cy.
pub extents_cy: i64,
/// File path name.
pub file: String,
pub file: PathBuf,
/// Read text from the specified file.
pub read_from_file: bool,
/// Holds data for the font. Ex:
@ -207,7 +290,7 @@ pub struct TextGdiPlusProperties {
/// Text content to be displayed.
pub text: String,
/// Text vertical alignment ("top", "center", "bottom").
pub valign: String,
pub valign: Valign,
/// Vertical text enabled.
pub vertical: bool,
}
@ -219,27 +302,32 @@ pub struct TextFreetype2Properties {
/// Source name.
pub source: String,
/// Gradient top color.
pub color1: u32,
pub color1: Option<u32>,
/// Gradient bottom color.
pub color2: u32,
pub color2: Option<u32>,
/// Custom width (0 to disable).
pub custom_width: u32,
pub custom_width: Option<u32>,
/// Drop shadow.
#[serde(default)]
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.
#[serde(default)]
pub from_file: bool,
/// Chat log.
#[serde(default)]
pub log_mode: bool,
/// Outline.
#[serde(default)]
pub outline: bool,
/// Text content to be displayed.
pub text: String,
/// File path.
pub text_file: String,
pub text_file: Option<PathBuf>,
/// Word wrap.
#[serde(default)]
pub word_wrap: bool,
}
@ -285,7 +373,7 @@ pub struct SourceFilterInfo<T> {
#[serde(rename_all = "camelCase")]
pub(crate) struct AudioMonitorType {
/// The monitor type in use. Options: `none`, `monitorOnly`, `monitorAndOutput`.
pub monitor_type: String,
pub monitor_type: MonitorType,
}
/// Response value for [`take_source_screenshot`](crate::client::Sources::take_source_screenshot).
@ -301,7 +389,7 @@ pub struct SourceScreenshot {
/// 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<String>,
pub image_file: Option<PathBuf>,
}
/// Response value for [`list_outputs`](crate::client::Outputs::list_outputs).
@ -339,7 +427,7 @@ pub(crate) struct Profiles {
#[serde(rename_all = "kebab-case")]
pub(crate) struct RecordingFolder {
/// Path of the recording folder.
pub rec_folder: String,
pub rec_folder: PathBuf,
}
/// Response value for
@ -457,9 +545,11 @@ pub struct StreamingStatus {
/// Current recording status.
pub recording: bool,
/// Time elapsed since streaming started (only present if currently streaming).
pub stream_timecode: Option<String>,
#[serde(deserialize_with = "crate::de::duration")]
pub stream_timecode: Option<Duration>,
/// Time elapsed since recording started (only present if currently recording).
pub rec_timecode: Option<String>,
#[serde(deserialize_with = "crate::de::duration")]
pub rec_timecode: Option<Duration>,
/// Always false. Retrocompatibility with OBSRemote.
#[serde(default)]
pub preview_only: bool,
@ -471,7 +561,7 @@ pub struct GetStreamSettings {
/// The type of streaming service configuration. Possible values: `rtmp_custom` or
/// `rtmp_common`.
#[serde(rename = "type")]
pub ty: String,
pub ty: StreamType,
/// Stream settings object.
pub settings: StreamSettings,
}
@ -511,7 +601,8 @@ pub struct CurrentTransition {
/// Name of the selected transition.
pub name: String,
/// Transition duration (in milliseconds) if supported by the transition.
pub duration: Option<u64>,
#[serde(deserialize_with = "crate::de::duration_millis_opt")]
pub duration: Option<Duration>,
}
/// Response value for
@ -520,13 +611,10 @@ pub struct CurrentTransition {
#[serde(rename_all = "kebab-case")]
pub(crate) struct TransitionDuration {
/// Duration of the current transition (in milliseconds).
pub transition_duration: u64,
#[serde(deserialize_with = "crate::de::duration_millis")]
pub transition_duration: Duration,
}
// -----------------------------------------------------------------------------
// -----------------------------------------------------------------------------
// -----------------------------------------------------------------------------
/// Response value for [`get_stats`](crate::client::General::get_stats).
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "kebab-case")]
@ -559,8 +647,7 @@ pub struct SourceListItem {
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".
/// Source type.
#[serde(rename = "type")]
pub ty: String,
}
@ -575,13 +662,27 @@ pub struct SourceTypeItem {
pub display_name: String,
/// Type. Value is one of the following: "input", "filter", "transition" or "other".
#[serde(rename = "type")]
pub ty: String,
pub ty: SourceType,
/// Default settings of this source type.
pub default_settings: serde_json::Value,
/// Source type capabilities.
pub caps: Caps,
}
/// Source type as part of [`SourceTypeItem`].
#[derive(Clone, Copy, Debug, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum SourceType {
/// Input source from outside of OBS.
Input,
/// Filter applied to other items.
Filter,
/// Transition when switching scenes.
Transition,
/// Other kinds of sources.
Other,
}
/// Response value for [`get_sources_types_list`](crate::client::Sources::get_sources_types_list) as
/// part of [`SourceTypeItem`].
#[derive(Debug, Deserialize)]
@ -614,7 +715,8 @@ 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,
#[serde(deserialize_with = "crate::de::bitflags_u8")]
pub flags: FontFlags,
/// Font text size.
pub size: u32,
/// Font Style (unknown function).

Loading…
Cancel
Save